Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# Changelog

All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

### Added
- CI/CD pipeline with ShellCheck, actionlint, and yamllint
- Branch protection and governance files (CODEOWNERS, issue templates, PR template)
- CONTRIBUTING.md with development guidelines
- SECURITY.md with security policy
- Pre-commit hooks configuration

### Fixed
- ShellCheck warnings: properly quoted GITHUB_OUTPUT
- Actionlint configuration to only lint workflow files

## [1.0.0] - 2024-01-01

### Added
- Initial release of ZAD Actions
- **deploy** action: Deploy container images to ZAD Operations Manager
- Support for cloning configuration from existing deployments
- `force-clone` parameter to re-clone even if deployment exists
- Input validation for security (alphanumeric, hyphens, underscores, dots only)
- 60-second curl timeout to prevent hanging
- **cleanup** action: Remove ZAD deployments and GitHub resources
- Delete ZAD deployments via Operations Manager API
- Delete GitHub deployments (mark inactive, then delete)
- Delete GitHub environments (requires admin token)
- Delete container images from GHCR
- Best-effort cleanup (continues even if individual steps fail)
- Comprehensive documentation with examples
- EUPL-1.2 license

### Security
- Input validation before logging to prevent injection attacks
- Secure handling of API keys via environment variables
- Dangerous character detection for container inputs

[Unreleased]: https://github.com/RijksICTGilde/zad-actions/compare/v1.0.0...HEAD
[1.0.0]: https://github.com/RijksICTGilde/zad-actions/releases/tag/v1.0.0
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# ZAD Actions

[![CI](https://github.com/RijksICTGilde/zad-actions/actions/workflows/ci.yml/badge.svg)](https://github.com/RijksICTGilde/zad-actions/actions/workflows/ci.yml)
[![License: EUPL-1.2](https://img.shields.io/badge/License-EUPL--1.2-blue.svg)](https://opensource.org/licenses/EUPL-1.2)
[![GitHub release](https://img.shields.io/github/v/release/RijksICTGilde/zad-actions)](https://github.com/RijksICTGilde/zad-actions/releases)

Reusable GitHub Actions for deploying to [ZAD](https://github.com/RijksICTGilde/RIG-Cluster) (Zelfservice voor Applicatie Deployment).

## Available Actions
Expand Down
73 changes: 73 additions & 0 deletions cleanup/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,79 @@ permissions:

**Note:** The default `GITHUB_TOKEN` cannot delete GitHub environments. You need a Personal Access Token (PAT) or GitHub App token with admin permissions for the repository.

### Scheduled Stale Environment Cleanup

Automatically clean up old PR preview environments:

```yaml
name: Cleanup Stale Environments

on:
schedule:
- cron: '0 2 * * *' # Daily at 2 AM
workflow_dispatch:

jobs:
cleanup-stale:
runs-on: ubuntu-latest
permissions:
deployments: write
packages: write
steps:
- name: Get stale PR environments
id: stale
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
# Find PR environments older than 7 days
gh api repos/${{ github.repository }}/environments \
--jq '.environments[] | select(.name | startswith("pr")) | .name' > envs.txt
echo "Found environments:"
cat envs.txt

- name: Cleanup each stale environment
env:
ZAD_API_KEY: ${{ secrets.ZAD_API_KEY }}
run: |
while read -r env; do
echo "Cleaning up: $env"
# Extract PR number from environment name (e.g., pr123 -> 123)
pr_num="${env#pr}"
# Add your cleanup logic here
done < envs.txt
```

### Conditional Cleanup Based on Outputs

Check cleanup results and take action:

```yaml
- name: Cleanup deployment
id: cleanup
uses: RijksICTGilde/zad-actions/cleanup@v1
with:
api-key: ${{ secrets.ZAD_API_KEY }}
project-id: my-project
deployment-name: pr${{ github.event.pull_request.number }}
delete-github-env: true
delete-container: true
container-org: ${{ github.repository_owner }}
container-name: my-app
container-tag: pr-${{ github.event.number }}
github-token: ${{ secrets.GITHUB_TOKEN }}
github-admin-token: ${{ secrets.GITHUB_ADMIN_TOKEN }}

- name: Report cleanup results
run: |
echo "ZAD deleted: ${{ steps.cleanup.outputs.zad-deleted }}"
echo "Environment deleted: ${{ steps.cleanup.outputs.github-env-deleted }}"
echo "Container deleted: ${{ steps.cleanup.outputs.container-deleted }}"

- name: Notify on incomplete cleanup
if: steps.cleanup.outputs.zad-deleted != 'true'
run: echo "::warning::ZAD deployment was not deleted - may need manual cleanup"
```

## How It Works

1. **Delete ZAD Deployment**: Calls the ZAD Operations Manager DELETE API
Expand Down
42 changes: 26 additions & 16 deletions cleanup/action.yml
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
# SPDX-License-Identifier: EUPL-1.2
name: 'Cleanup ZAD Deployment'
description: 'Remove a ZAD deployment and optionally clean up GitHub resources (environments, deployments, container images)'
author: 'RijksICTGilde'
Expand Down Expand Up @@ -104,14 +105,23 @@ runs:

if [ "$HTTP_CODE" -ge 200 ] && [ "$HTTP_CODE" -lt 300 ]; then
echo "ZAD deployment deleted successfully (HTTP $HTTP_CODE)"
echo "deleted=true" >> $GITHUB_OUTPUT
echo "deleted=true" >> "$GITHUB_OUTPUT"
elif [ "$HTTP_CODE" -eq 404 ]; then
echo "ZAD deployment not found (already deleted?)"
echo "deleted=false" >> $GITHUB_OUTPUT
echo "deleted=false" >> "$GITHUB_OUTPUT"
elif [ "$HTTP_CODE" -eq 000 ]; then
echo "::warning::Unable to connect to ZAD API - network issue or API unavailable"
echo "deleted=false" >> "$GITHUB_OUTPUT"
elif [ "$HTTP_CODE" -eq 401 ]; then
echo "::warning::Authentication failed (HTTP 401) - verify ZAD_API_KEY is correct"
echo "deleted=false" >> "$GITHUB_OUTPUT"
elif [ "$HTTP_CODE" -eq 403 ]; then
echo "::warning::Access denied (HTTP 403) - API key may lack permission for project '$PROJECT_ID'"
echo "deleted=false" >> "$GITHUB_OUTPUT"
else
echo "Failed to delete ZAD deployment (HTTP $HTTP_CODE)"
echo "::warning::Failed to delete ZAD deployment (HTTP $HTTP_CODE)"
echo "$BODY"
echo "deleted=false" >> $GITHUB_OUTPUT
echo "deleted=false" >> "$GITHUB_OUTPUT"
# Don't fail - continue with other cleanup tasks
fi

Expand All @@ -131,7 +141,7 @@ runs:

if [ -z "$DEPLOYMENTS" ]; then
echo "No GitHub deployments found for environment: $DEPLOYMENT_NAME"
echo "deleted=false" >> $GITHUB_OUTPUT
echo "deleted=false" >> "$GITHUB_OUTPUT"
exit 0
fi

Expand All @@ -156,9 +166,9 @@ runs:

echo "Deleted $DELETED_COUNT GitHub deployment(s)"
if [ "$DELETED_COUNT" -gt 0 ]; then
echo "deleted=true" >> $GITHUB_OUTPUT
echo "deleted=true" >> "$GITHUB_OUTPUT"
else
echo "deleted=false" >> $GITHUB_OUTPUT
echo "deleted=false" >> "$GITHUB_OUTPUT"
fi

- name: Delete GitHub Environment
Expand All @@ -184,21 +194,21 @@ runs:
# Check if it was successful (204 No Content) or environment didn't exist (404)
if echo "$HTTP_RESPONSE" | grep -q "HTTP/2 204\|HTTP/1.1 204"; then
echo "GitHub environment '$DEPLOYMENT_NAME' deleted successfully"
echo "deleted=true" >> $GITHUB_OUTPUT
echo "deleted=true" >> "$GITHUB_OUTPUT"
elif echo "$HTTP_RESPONSE" | grep -q "HTTP/2 404\|HTTP/1.1 404"; then
echo "GitHub environment '$DEPLOYMENT_NAME' does not exist (already deleted?)"
echo "deleted=false" >> $GITHUB_OUTPUT
echo "reason=not_found" >> $GITHUB_OUTPUT
echo "deleted=false" >> "$GITHUB_OUTPUT"
echo "reason=not_found" >> "$GITHUB_OUTPUT"
elif echo "$HTTP_RESPONSE" | grep -q "HTTP/2 403\|HTTP/1.1 403"; then
echo "::error::Permission denied: github-admin-token lacks permission to delete environments"
echo "::error::Ensure the token has 'repo' scope or is a GitHub App with 'administration:write' permission"
echo "deleted=false" >> $GITHUB_OUTPUT
echo "reason=permission_denied" >> $GITHUB_OUTPUT
echo "deleted=false" >> "$GITHUB_OUTPUT"
echo "reason=permission_denied" >> "$GITHUB_OUTPUT"
else
echo "::warning::Failed to delete GitHub environment '$DEPLOYMENT_NAME'"
echo "Response: $HTTP_RESPONSE"
echo "deleted=false" >> $GITHUB_OUTPUT
echo "reason=unknown" >> $GITHUB_OUTPUT
echo "deleted=false" >> "$GITHUB_OUTPUT"
echo "reason=unknown" >> "$GITHUB_OUTPUT"
fi

- name: Delete Container Image
Expand Down Expand Up @@ -235,7 +245,7 @@ runs:

if [ -z "$VERSIONS" ]; then
echo "Container image not found or no matching tag"
echo "deleted=false" >> $GITHUB_OUTPUT
echo "deleted=false" >> "$GITHUB_OUTPUT"
exit 0
fi

Expand All @@ -250,4 +260,4 @@ runs:
fi
done

echo "deleted=$DELETED" >> $GITHUB_OUTPUT
echo "deleted=$DELETED" >> "$GITHUB_OUTPUT"
80 changes: 80 additions & 0 deletions deploy/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,86 @@ For example:
- `component: editor`, `deployment: pr73`, `project: regel-k4c`
- URL: `https://editor-pr73-regel-k4c.rig.prd1.gn2.quattro.rijksapps.nl`

### Multi-Component Deployment

Deploy multiple components in the same workflow:

```yaml
deploy:
runs-on: ubuntu-latest
strategy:
matrix:
component: [frontend, api, worker]
steps:
- name: Deploy ${{ matrix.component }}
uses: RijksICTGilde/zad-actions/deploy@v1
with:
api-key: ${{ secrets.ZAD_API_KEY }}
project-id: my-project
deployment-name: production
component: ${{ matrix.component }}
image: ghcr.io/org/app-${{ matrix.component }}:${{ github.sha }}
```

### Conditional Deployment (Branch-Based)

Deploy to different environments based on branch:

```yaml
deploy:
runs-on: ubuntu-latest
steps:
- name: Deploy to staging
if: github.ref == 'refs/heads/develop'
uses: RijksICTGilde/zad-actions/deploy@v1
with:
api-key: ${{ secrets.ZAD_API_KEY }}
project-id: my-project
deployment-name: staging
component: web
image: ghcr.io/org/app:${{ github.sha }}

- name: Deploy to production
if: github.ref == 'refs/heads/main'
uses: RijksICTGilde/zad-actions/deploy@v1
with:
api-key: ${{ secrets.ZAD_API_KEY }}
project-id: my-project
deployment-name: production
component: web
image: ghcr.io/org/app:${{ github.sha }}
clone-from: staging
```

### Deploy with Deployment Status Check

Wait for deployment to be healthy:

```yaml
- name: Deploy to ZAD
id: deploy
uses: RijksICTGilde/zad-actions/deploy@v1
with:
api-key: ${{ secrets.ZAD_API_KEY }}
project-id: my-project
deployment-name: production
component: web
image: ghcr.io/org/app:latest

- name: Wait for deployment to be ready
run: |
for i in {1..30}; do
if curl -s -o /dev/null -w "%{http_code}" "${{ steps.deploy.outputs.url }}/health" | grep -q "200"; then
echo "Deployment is healthy!"
exit 0
fi
echo "Waiting for deployment... (attempt $i/30)"
sleep 10
done
echo "Deployment health check timed out"
exit 1
```

## How It Works

1. Constructs a JSON payload with deployment configuration
Expand Down
27 changes: 25 additions & 2 deletions deploy/action.yml
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
# SPDX-License-Identifier: EUPL-1.2
name: 'Deploy to ZAD'
description: 'Deploy a container image to ZAD Operations Manager'
author: 'RijksICTGilde'
Expand Down Expand Up @@ -138,10 +139,32 @@ runs:

# Construct the URL (standard ZAD URL pattern)
URL="https://${COMPONENT}-${DEPLOYMENT_NAME}-${PROJECT_ID}.rig.prd1.gn2.quattro.rijksapps.nl"
echo "url=$URL" >> $GITHUB_OUTPUT
echo "url=$URL" >> "$GITHUB_OUTPUT"
echo "Deployed to: $URL"
elif [ "$HTTP_CODE" -eq 000 ]; then
echo "::error::Deployment failed: Unable to connect to ZAD API"
echo "::error::This could be a network issue or the API may be unavailable"
echo "::error::Please verify the API URL: $API_BASE_URL"
exit 1
elif [ "$HTTP_CODE" -eq 401 ]; then
echo "::error::Deployment failed: Authentication failed (HTTP 401)"
echo "::error::Please verify your ZAD_API_KEY secret is correct and not expired"
exit 1
elif [ "$HTTP_CODE" -eq 403 ]; then
echo "::error::Deployment failed: Access denied (HTTP 403)"
echo "::error::Your API key may not have permission for project '$PROJECT_ID'"
exit 1
elif [ "$HTTP_CODE" -eq 404 ]; then
echo "::error::Deployment failed: Project not found (HTTP 404)"
echo "::error::Please verify project-id '$PROJECT_ID' exists in ZAD"
exit 1
elif [ "$HTTP_CODE" -ge 500 ]; then
echo "::error::Deployment failed: ZAD API server error (HTTP $HTTP_CODE)"
echo "::error::This is likely a temporary issue. Please try again later"
echo "$BODY"
exit 1
else
echo "Deployment failed (HTTP $HTTP_CODE)"
echo "::error::Deployment failed (HTTP $HTTP_CODE)"
echo "$BODY"
exit 1
fi