diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..a485214 --- /dev/null +++ b/CHANGELOG.md @@ -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 diff --git a/README.md b/README.md index 2ac4acd..65a4473 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/cleanup/README.md b/cleanup/README.md index 9c1849b..4e426df 100644 --- a/cleanup/README.md +++ b/cleanup/README.md @@ -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 diff --git a/cleanup/action.yml b/cleanup/action.yml index 695b8b1..51971ab 100644 --- a/cleanup/action.yml +++ b/cleanup/action.yml @@ -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' @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -250,4 +260,4 @@ runs: fi done - echo "deleted=$DELETED" >> $GITHUB_OUTPUT + echo "deleted=$DELETED" >> "$GITHUB_OUTPUT" diff --git a/deploy/README.md b/deploy/README.md index e964f35..3a406e5 100644 --- a/deploy/README.md +++ b/deploy/README.md @@ -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 diff --git a/deploy/action.yml b/deploy/action.yml index b8511b8..f86ef0a 100644 --- a/deploy/action.yml +++ b/deploy/action.yml @@ -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' @@ -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