diff --git a/README.md b/README.md index 3da5042..c58ff83 100644 --- a/README.md +++ b/README.md @@ -30,14 +30,14 @@ jobs: ci-cd: name: Build, Push and Deploy - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v6 - name: GitOps (build, push and deploy a new Docker image) - uses: Staffbase/gitops-github-action@v7.0 + uses: Staffbase/gitops-github-action@v7.1 with: docker-username: ${{ vars.HARBOR_USERNAME }} docker-password: ${{ secrets.HARBOR_PASSWORD }} @@ -62,14 +62,14 @@ jobs: ci-cd: name: Build and Push - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v6 - name: GitOps (build and push a new Docker image) - uses: Staffbase/gitops-github-action@v7.0 + uses: Staffbase/gitops-github-action@v7.1 with: docker-username: ${{ vars.HARBOR_USERNAME }} docker-password: ${{ secrets.HARBOR_PASSWORD }} @@ -87,14 +87,14 @@ jobs: ci-cd: name: Deploy - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v6 - name: GitOps (deploy a new Docker image) - uses: Staffbase/gitops-github-action@v7.0 + uses: Staffbase/gitops-github-action@v7.1 with: docker-image: private/diablo-redbook gitops-token: ${{ secrets.GITOPS_TOKEN }} @@ -106,6 +106,52 @@ jobs: clusters/customization/prod/mothership/diablo-redbook/diablo-redbook-helm.yaml spec.template.spec.containers.redbook.image ``` +### Build, Push, Deploy and Track Deployment + +When `create-deployment` is set to `true`, the action will: +1. Create a GitHub Deployment on the source repository for each target environment +2. Set the deployment status to `in_progress` +3. Write deployment tracking annotations (`deploy.staffbase.com/repo`, `deploy.staffbase.com/sha`, `deploy.staffbase.com/deployment-id`) to the Application CR in the mops overlay + +The environment name is derived from the mops file path (e.g. `kubernetes/namespaces//prod/de1/...` becomes `prod-de1`). + +The calling workflow must grant the `deployments: write` permission: + +```yaml +name: CD + +on: [ push ] + +permissions: + deployments: write + +jobs: + ci-cd: + name: Build, Push and Deploy + + runs-on: ubuntu-24.04 + + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: GitOps (build, push, deploy and track) + uses: Staffbase/gitops-github-action@v7.1 + with: + docker-username: ${{ vars.HARBOR_USERNAME }} + docker-password: ${{ secrets.HARBOR_PASSWORD }} + docker-image: private/diablo-redbook + gitops-token: ${{ secrets.GITOPS_TOKEN }} + create-deployment: true + github-token: ${{ github.token }} + gitops-dev: |- + clusters/customization/dev/mothership/diablo-redbook/diablo-redbook-helm.yaml spec.template.spec.containers.redbook.image + gitops-stage: |- + clusters/customization/stage/mothership/diablo-redbook/diablo-redbook-helm.yaml spec.template.spec.containers.redbook.image + gitops-prod: |- + clusters/customization/prod/mothership/diablo-redbook/diablo-redbook-helm.yaml spec.template.spec.containers.redbook.image +``` + ## Inputs | Name | Description | Default | @@ -133,13 +179,16 @@ jobs: | `gitops-stage` | Files which should be updated by the GitHub Action for STAGE, must be relative to the root of the GitOps repository | | | `gitops-prod` | Files which should be updated by the GitHub Action for PROD, must be relative to the root of the GitOps repository | | | `working-directory` | The directory in which the GitOps action should be executed. The docker-file variable should be relative to working directory. | `.` | +| `create-deployment` | Create GitHub Deployments on the source repository and write tracking annotations to the GitOps CRs | `false` | +| `github-token` | GitHub Token for creating deployments (requires `deployments: write` permission). Required when `create-deployment` is `true`. | | ## Outputs | Name | Description | |-----------------|---------------------| -| `docker-digest` | Digest of the image | -| `docker-tag` | Tag of the image | +| `docker-digest` | Digest of the image | +| `docker-tag` | Tag of the image | +| `deployment-id` | JSON map of environment to GitHub Deployment ID (set when `create-deployment` is `true`) | ## Contributing diff --git a/action.yml b/action.yml index 2700d37..31dcc00 100644 --- a/action.yml +++ b/action.yml @@ -95,6 +95,13 @@ inputs: description: 'The path relative to the repo root dir in which the GitOps action should be executed.' required: false default: '.' + create-deployment: + description: 'Create GitHub Deployments on the source repository and write tracking annotations to the GitOps CRs' + required: false + default: 'false' + github-token: + description: 'GitHub Token for creating deployments (requires deployments: write permission). Required when create-deployment is true.' + required: false outputs: docker-tag: @@ -103,6 +110,9 @@ outputs: docker-digest: description: 'Docker digest' value: ${{ steps.docker_build.outputs.digest || steps.docker_retag.outputs.digest }} + deployment-id: + description: 'JSON map of environment to GitHub Deployment ID (only set when create-deployment is true)' + value: ${{ steps.update_image.outputs.deployment_ids }} runs: using: "composite" @@ -283,10 +293,16 @@ runs: path: .github/${{ inputs.gitops-repository }} - name: Update Docker Image in Repository + id: update_image if: inputs.gitops-token != '' working-directory: .github/${{ inputs.gitops-repository }} shell: bash run: | + IMAGE="${{ inputs.docker-registry }}/${{ inputs.docker-image }}:${{ steps.preparation.outputs.tag }}" + CREATE_DEPLOYMENT="${{ inputs.create-deployment }}" + GITHUB_TOKEN_INPUT="${{ inputs.github-token }}" + DEPLOYMENT_IDS='{}' + push_to_gitops_repo () { # In case there was another push in the meantime, we pull it again git pull --rebase https://${{ inputs.gitops-user }}:${{ inputs.gitops-token }}@github.com/${{ inputs.gitops-organization }}/${{ inputs.gitops-repository }}.git @@ -310,6 +326,88 @@ runs: fi } + derive_environment () { + local file_path="$1" + # Path: kubernetes/namespaces////.yaml + local env=$(echo "$file_path" | cut -d'/' -f4) + local cluster=$(echo "$file_path" | cut -d'/' -f5) + echo "${env}-${cluster}" + } + + create_deployment () { + local environment="$1" + local image="$2" + local tag="$3" + + RESPONSE=$(curl -s -w "\n%{http_code}" \ + -X POST \ + -H "Accept: application/vnd.github+json" \ + -H "Authorization: Bearer ${GITHUB_TOKEN_INPUT}" \ + -H "X-GitHub-Api-Version: 2022-11-28" \ + "https://api.github.com/repos/${GITHUB_REPOSITORY}/deployments" \ + -d "{ + \"ref\": \"${GITHUB_SHA}\", + \"environment\": \"${environment}\", + \"auto_merge\": false, + \"required_contexts\": [], + \"payload\": { + \"image\": \"${image}\", + \"tag\": \"${tag}\" + }, + \"description\": \"Deploy ${image}:${tag} to ${environment}\" + }") + + HTTP_CODE=$(echo "$RESPONSE" | tail -1) + BODY=$(echo "$RESPONSE" | sed '$d') + + if [[ "$HTTP_CODE" -lt 200 || "$HTTP_CODE" -ge 300 ]]; then + echo "::warning::Failed to create GitHub Deployment for ${environment} (HTTP ${HTTP_CODE}): ${BODY}" >&2 + return + fi + + local deployment_id=$(echo "$BODY" | jq -r '.id') + echo "Created deployment ${deployment_id} for environment ${environment}" >&2 + + # Set initial status to in_progress + curl -s \ + -X POST \ + -H "Accept: application/vnd.github+json" \ + -H "Authorization: Bearer ${GITHUB_TOKEN_INPUT}" \ + -H "X-GitHub-Api-Version: 2022-11-28" \ + "https://api.github.com/repos/${GITHUB_REPOSITORY}/deployments/${deployment_id}/statuses" \ + -d "{\"state\": \"in_progress\", \"description\": \"Updating GitOps repository\"}" > /dev/null + + echo "$deployment_id" + } + + update_file () { + local file="$1" + local field="$2" + local image="$3" + + echo "Check if path ${file} ${field} exists and get old current version" + yq -e ."${field}" "${file}" + echo "Run update ${file} ${field} ${image}" + yq -i ."${field}"=\""${image}"\" "${file}" + + if [[ "$CREATE_DEPLOYMENT" == "true" ]]; then + local deploy_env=$(derive_environment "${file}") + local deploy_id="" + + if [[ -n "$GITHUB_TOKEN_INPUT" ]]; then + deploy_id=$(create_deployment "${deploy_env}" "${{ inputs.docker-registry }}/${{ inputs.docker-image }}" "${{ steps.preparation.outputs.tag }}") + fi + + echo "Writing deployment annotations to ${file}" + yq -i '.metadata.annotations["deploy.staffbase.com/repo"] = "'"${GITHUB_REPOSITORY}"'"' "${file}" + yq -i '.metadata.annotations["deploy.staffbase.com/sha"] = "'"${GITHUB_SHA}"'"' "${file}" + if [[ -n "$deploy_id" ]]; then + yq -i '.metadata.annotations["deploy.staffbase.com/deployment-id"] = "'"${deploy_id}"'"' "${file}" + DEPLOYMENT_IDS=$(echo "$DEPLOYMENT_IDS" | jq -c --arg env "$deploy_env" --arg id "$deploy_id" '. + {($env): $id}') + fi + fi + } + # configure git user git config --global user.email "${{ inputs.gitops-email }}" && git config --global user.name "${{ inputs.gitops-user }}" @@ -317,10 +415,7 @@ runs: echo "Run update for STAGE" while IFS= read -r line; do array=($line) - echo "Check if path $line exists and get old current version" - yq -e .${array[1]} ${array[0]} - echo "Run update $line ${{ inputs.docker-registry }}/${{ inputs.docker-image }}:${{ steps.preparation.outputs.tag }}" - yq -i .${array[1]}=\"${{ inputs.docker-registry }}/${{ inputs.docker-image }}:${{ steps.preparation.outputs.tag }}\" ${array[0]} + update_file "${array[0]}" "${array[1]}" "$IMAGE" done <<< "${{ inputs.gitops-stage }}" commit_changes @@ -328,10 +423,7 @@ runs: echo "Run update for DEV" while IFS= read -r line; do array=($line) - echo "Check if path $line exists and get old current version" - yq -e .${array[1]} ${array[0]} - echo "Run update $line ${{ inputs.docker-registry }}/${{ inputs.docker-image }}:${{ steps.preparation.outputs.tag }}" - yq -i .${array[1]}=\"${{ inputs.docker-registry }}/${{ inputs.docker-image }}:${{ steps.preparation.outputs.tag }}\" ${array[0]} + update_file "${array[0]}" "${array[1]}" "$IMAGE" done <<< "${{ inputs.gitops-dev }}" commit_changes @@ -339,10 +431,7 @@ runs: echo "Run update for PROD" while IFS= read -r line; do array=($line) - echo "Check if path $line exists and get old current version" - yq -e .${array[1]} ${array[0]} - echo "Run update $line ${{ inputs.docker-registry }}/${{ inputs.docker-image }}:${{ steps.preparation.outputs.tag }}" - yq -i .${array[1]}=\"${{ inputs.docker-registry }}/${{ inputs.docker-image }}:${{ steps.preparation.outputs.tag }}\" ${array[0]} + update_file "${array[0]}" "${array[1]}" "$IMAGE" done <<< "${{ inputs.gitops-prod }}" commit_changes @@ -350,13 +439,12 @@ runs: echo "Simulate update for DEV" while IFS= read -r line; do array=($line) - echo "Check if path $line exists and get old current version" - yq -e .${array[1]} ${array[0]} - echo "Run update $line ${{ inputs.docker-registry }}/${{ inputs.docker-image }}:${{ steps.preparation.outputs.tag }}" - yq -i .${array[1]}=\"${{ inputs.docker-registry }}/${{ inputs.docker-image }}:${{ steps.preparation.outputs.tag }}\" ${array[0]} + update_file "${array[0]}" "${array[1]}" "$IMAGE" done <<< "${{ inputs.gitops-dev }}" fi + echo "deployment_ids=$DEPLOYMENT_IDS" >> $GITHUB_OUTPUT + - name: Emit Image Build Event to Upwind.io env: UPWIND_CLIENT_SECRET: ${{ inputs.upwind-client-secret }}