diff --git a/README.md b/README.md index c58ff83..80a1979 100644 --- a/README.md +++ b/README.md @@ -106,51 +106,17 @@ jobs: clusters/customization/prod/mothership/diablo-redbook/diablo-redbook-helm.yaml spec.template.spec.containers.redbook.image ``` -### Build, Push, Deploy and Track Deployment +### Deployment tracking annotations -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 +Whenever the action updates a GitOps file, it stamps the following annotations onto the manifest's `metadata.annotations`: -The environment name is derived from the mops file path (e.g. `kubernetes/namespaces//prod/de1/...` becomes `prod-de1`). +| Annotation | Value | +|------------|-------| +| `deploy.staffbase.com/repositoryFullName` | The source repository in `owner/repo` form (`$GITHUB_REPOSITORY`) | +| `deploy.staffbase.com/commitSha` | The commit SHA being deployed (`$GITHUB_SHA`) | +| `deploy.staffbase.com/version` | The deployed image tag — `dev-` on `dev`, `main-` on `main`/`master`, the tag name on tag pushes | -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 -``` +These keys mirror the [Swarmia Deployment API](https://help.swarmia.com/settings/organization/configuring-deployments-in-swarmia) field names and are read by `flux-deployment-reporter` to report deployments to Swarmia once Flux finishes reconciling. ## Inputs @@ -179,8 +145,6 @@ 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 @@ -188,7 +152,6 @@ jobs: |-----------------|---------------------| | `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 6ef9d11..2a9952e 100644 --- a/action.yml +++ b/action.yml @@ -95,13 +95,6 @@ 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: @@ -110,9 +103,6 @@ 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.create_deployments.outputs.deployment_ids || steps.update_image.outputs.deployment_ids }} runs: using: "composite" @@ -187,109 +177,6 @@ runs: token: ${{ inputs.gitops-token }} path: .github/${{ inputs.gitops-repository }} - - name: Create GitHub Deployments - id: create_deployments - if: inputs.create-deployment == 'true' && inputs.github-token != '' && inputs.gitops-token != '' - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 - env: - INPUT_GITOPS_DEV: ${{ inputs.gitops-dev }} - INPUT_GITOPS_STAGE: ${{ inputs.gitops-stage }} - INPUT_GITOPS_PROD: ${{ inputs.gitops-prod }} - DOCKER_IMAGE: ${{ inputs.docker-registry }}/${{ inputs.docker-image }} - DOCKER_TAG: ${{ steps.preparation.outputs.tag }} - with: - github-token: ${{ inputs.github-token }} - script: | - const ref = process.env.GITHUB_REF; - const image = process.env.DOCKER_IMAGE; - const tag = process.env.DOCKER_TAG; - - let fileList = ''; - if ((ref === 'refs/heads/master' || ref === 'refs/heads/main') && process.env.INPUT_GITOPS_STAGE) { - fileList = process.env.INPUT_GITOPS_STAGE; - } else if (ref === 'refs/heads/dev' && process.env.INPUT_GITOPS_DEV) { - fileList = process.env.INPUT_GITOPS_DEV; - } else if (ref.startsWith('refs/tags/') && process.env.INPUT_GITOPS_PROD) { - fileList = process.env.INPUT_GITOPS_PROD; - } - - if (!fileList) { - core.setOutput('deployment_ids', '{}'); - return; - } - - // Derive unique environments from file paths - // Path format: kubernetes/namespaces////.yaml - const environments = [...new Set( - fileList.split('\n') - .map(line => line.trim()) - .filter(line => line.length > 0) - .map(line => { - const filePath = line.split(/\s+/)[0]; - const parts = filePath.split('/'); - return `${parts[3]}-${parts[4]}`; - }) - )]; - - // Deactivate stale in-progress/queued/pending deployments for an environment - async function deactivateStaleDeployments(environment) { - const { data: deployments } = await github.rest.repos.listDeployments({ - ...context.repo, - environment, - per_page: 30 - }); - - for (const dep of deployments) { - const { data: statuses } = await github.rest.repos.listDeploymentStatuses({ - ...context.repo, - deployment_id: dep.id, - per_page: 1 - }); - - const latestState = statuses[0]?.state; - if (['in_progress', 'queued', 'pending'].includes(latestState)) { - core.info(`Marking stale deployment ${dep.id} (${latestState}) as inactive`); - await github.rest.repos.createDeploymentStatus({ - ...context.repo, - deployment_id: dep.id, - state: 'inactive', - description: 'Superseded by newer deployment' - }); - } - } - } - - const ids = {}; - for (const env of environments) { - try { - await deactivateStaleDeployments(env); - - const { data: deployment } = await github.rest.repos.createDeployment({ - ...context.repo, - ref: context.sha, - environment: env, - auto_merge: false, - required_contexts: [], - payload: { image, tag }, - description: `Deploy ${image}:${tag} to ${env}` - }); - - await github.rest.repos.createDeploymentStatus({ - ...context.repo, - deployment_id: deployment.id, - state: 'in_progress', - description: 'Updating GitOps repository' - }); - - core.info(`Created deployment ${deployment.id} for environment ${env}`); - ids[env] = String(deployment.id); - } catch (error) { - core.warning(`Failed to create GitHub Deployment for ${env}: ${error.message}`); - } - } - - core.setOutput('deployment_ids', JSON.stringify(ids)); - - name: Update Docker Image in Repository id: update_image if: inputs.gitops-token != '' @@ -300,8 +187,6 @@ runs: INPUT_DOCKER_IMAGE: ${{ inputs.docker-image }} INPUT_TAG: ${{ steps.preparation.outputs.tag }} INPUT_PUSH: ${{ steps.preparation.outputs.push }} - INPUT_CREATE_DEPLOYMENT: ${{ inputs.create-deployment }} - INPUT_DEPLOYMENT_IDS: ${{ steps.create_deployments.outputs.deployment_ids }} INPUT_GITOPS_USER: ${{ inputs.gitops-user }} INPUT_GITOPS_EMAIL: ${{ inputs.gitops-email }} INPUT_GITOPS_TOKEN: ${{ inputs.gitops-token }} diff --git a/scripts/lib/gitops-functions.sh b/scripts/lib/gitops-functions.sh index 3a2f7a9..9f88ce3 100755 --- a/scripts/lib/gitops-functions.sh +++ b/scripts/lib/gitops-functions.sh @@ -4,7 +4,6 @@ # # Expected env vars (set by caller): # INPUT_DOCKER_REGISTRY, INPUT_DOCKER_IMAGE, INPUT_TAG, INPUT_PUSH, -# INPUT_CREATE_DEPLOYMENT, INPUT_DEPLOYMENT_IDS, # INPUT_GITOPS_USER, INPUT_GITOPS_TOKEN, # INPUT_GITOPS_ORGANIZATION, INPUT_GITOPS_REPOSITORY, # GITHUB_REPOSITORY, GITHUB_SHA, IMAGE @@ -29,16 +28,6 @@ commit_changes() { fi } -# Derives the environment identifier from a mops file path. -# Expected path format: kubernetes/namespaces////.yaml -derive_environment() { - local file_path="$1" - local env cluster - env=$(echo "$file_path" | cut -d'/' -f4) - cluster=$(echo "$file_path" | cut -d'/' -f5) - echo "${env}-${cluster}" -} - update_file() { local file="$1" local field="$2" @@ -49,24 +38,10 @@ update_file() { echo "Run update ${file} ${field} ${image}" yq -i ."${field}"=\""${image}"\" "${file}" - if [[ "${INPUT_CREATE_DEPLOYMENT}" == "true" ]]; then - local deploy_env - deploy_env=$(derive_environment "${file}") - - 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}" - - # Write deployment-id annotation if available from the create_deployments step - local deploy_id="" - if [[ -n "${INPUT_DEPLOYMENT_IDS:-}" && "${INPUT_DEPLOYMENT_IDS}" != "{}" ]]; then - deploy_id=$(echo "${INPUT_DEPLOYMENT_IDS}" | jq -r --arg env "$deploy_env" '.[$env] // empty') - fi - - if [[ -n "$deploy_id" ]]; then - yq -i '.metadata.annotations["deploy.staffbase.com/deployment-id"] = "'"${deploy_id}"'"' "${file}" - fi - fi + echo "Writing deployment annotations to ${file}" + yq -i '.metadata.annotations["deploy.staffbase.com/repositoryFullName"] = "'"${GITHUB_REPOSITORY}"'"' "${file}" + yq -i '.metadata.annotations["deploy.staffbase.com/commitSha"] = "'"${GITHUB_SHA}"'"' "${file}" + yq -i '.metadata.annotations["deploy.staffbase.com/version"] = "'"${INPUT_TAG}"'"' "${file}" } process_file_updates() { diff --git a/scripts/update-gitops.sh b/scripts/update-gitops.sh index bbb85ab..f9037c7 100755 --- a/scripts/update-gitops.sh +++ b/scripts/update-gitops.sh @@ -5,10 +5,9 @@ # # Required env vars: GITHUB_REF, GITHUB_SHA, GITHUB_REPOSITORY, # INPUT_DOCKER_REGISTRY, INPUT_DOCKER_IMAGE, INPUT_TAG, INPUT_PUSH, -# INPUT_CREATE_DEPLOYMENT, INPUT_GITOPS_USER, INPUT_GITOPS_EMAIL, +# INPUT_GITOPS_USER, INPUT_GITOPS_EMAIL, # INPUT_GITOPS_TOKEN, INPUT_GITOPS_ORGANIZATION, INPUT_GITOPS_REPOSITORY -# Optional env vars: INPUT_GITOPS_DEV, INPUT_GITOPS_STAGE, INPUT_GITOPS_PROD, -# INPUT_DEPLOYMENT_IDS +# Optional env vars: INPUT_GITOPS_DEV, INPUT_GITOPS_STAGE, INPUT_GITOPS_PROD SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" # shellcheck source=lib/common.sh diff --git a/tests/lib-gitops-functions.bats b/tests/lib-gitops-functions.bats index 6e47903..aafa1bd 100644 --- a/tests/lib-gitops-functions.bats +++ b/tests/lib-gitops-functions.bats @@ -13,8 +13,6 @@ setup() { export INPUT_DOCKER_IMAGE="my-service" export INPUT_TAG="main-abcdef12" export INPUT_PUSH="true" - export INPUT_CREATE_DEPLOYMENT="false" - export INPUT_DEPLOYMENT_IDS="{}" export INPUT_GITOPS_USER="Staffbot" export INPUT_GITOPS_TOKEN="fake-token" export INPUT_GITOPS_ORGANIZATION="Staffbase" @@ -44,20 +42,6 @@ esac GIT_MOCK chmod +x "${TEST_TEMP_DIR}/mocks/git" - # Create mock jq that passes through - cat > "${TEST_TEMP_DIR}/mocks/jq" << 'JQ_MOCK' -#!/usr/bin/env bash -# Use real jq if available, otherwise simple passthrough -if command -v /usr/bin/jq &>/dev/null; then - /usr/bin/jq "$@" -elif command -v /opt/homebrew/bin/jq &>/dev/null; then - /opt/homebrew/bin/jq "$@" -else - cat -fi -JQ_MOCK - chmod +x "${TEST_TEMP_DIR}/mocks/jq" - export PATH="${TEST_TEMP_DIR}/mocks:$PATH" } @@ -65,26 +49,6 @@ teardown() { teardown_common } -# --- derive_environment --- - -@test "derive_environment extracts env and cluster from standard mops path" { - run derive_environment "kubernetes/namespaces/my-service/prod/de1/deployment.yaml" - assert_success - assert_output "prod-de1" -} - -@test "derive_environment handles stage environment" { - run derive_environment "kubernetes/namespaces/my-service/stage/us1/deployment.yaml" - assert_success - assert_output "stage-us1" -} - -@test "derive_environment handles dev environment" { - run derive_environment "kubernetes/namespaces/my-service/dev/de1/deployment.yaml" - assert_success - assert_output "dev-de1" -} - # --- update_file --- @test "update_file calls yq to check and update field" { @@ -94,33 +58,31 @@ teardown() { grep -q 'yq -i' "${TEST_TEMP_DIR}/yq_calls.log" } -@test "update_file writes deployment annotations when create-deployment is true" { - export INPUT_CREATE_DEPLOYMENT="true" - update_file "kubernetes/namespaces/svc/prod/de1/deployment.yaml" "spec.image" "$IMAGE" - grep -q 'deploy.staffbase.com/repo' "${TEST_TEMP_DIR}/yq_calls.log" - grep -q 'deploy.staffbase.com/sha' "${TEST_TEMP_DIR}/yq_calls.log" +@test "update_file always writes deploy.staffbase.com/repositoryFullName annotation" { + update_file "deployment.yaml" "spec.image" "$IMAGE" + grep -q 'deploy.staffbase.com/repositoryFullName' "${TEST_TEMP_DIR}/yq_calls.log" } -@test "update_file skips annotations when create-deployment is false" { - export INPUT_CREATE_DEPLOYMENT="false" +@test "update_file always writes deploy.staffbase.com/commitSha annotation" { update_file "deployment.yaml" "spec.image" "$IMAGE" - ! grep -q 'deploy.staffbase.com' "${TEST_TEMP_DIR}/yq_calls.log" 2>/dev/null || true + grep -q 'deploy.staffbase.com/commitSha' "${TEST_TEMP_DIR}/yq_calls.log" } -@test "update_file writes deployment-id annotation when deployment ID is available" { - export INPUT_CREATE_DEPLOYMENT="true" - export INPUT_DEPLOYMENT_IDS='{"prod-de1":"12345"}' - update_file "kubernetes/namespaces/svc/prod/de1/deployment.yaml" "spec.image" "$IMAGE" - grep -q 'deploy.staffbase.com/deployment-id' "${TEST_TEMP_DIR}/yq_calls.log" +@test "update_file writes deploy.staffbase.com/version annotation with INPUT_TAG" { + update_file "deployment.yaml" "spec.image" "$IMAGE" + grep -q "deploy.staffbase.com/version.*${INPUT_TAG}" "${TEST_TEMP_DIR}/yq_calls.log" } -@test "update_file skips deployment-id annotation when no matching deployment ID" { - export INPUT_CREATE_DEPLOYMENT="true" - export INPUT_DEPLOYMENT_IDS='{"stage-us1":"99999"}' - update_file "kubernetes/namespaces/svc/prod/de1/deployment.yaml" "spec.image" "$IMAGE" +@test "update_file does not write deploy.staffbase.com/deployment-id annotation" { + update_file "deployment.yaml" "spec.image" "$IMAGE" ! grep -q 'deployment-id' "${TEST_TEMP_DIR}/yq_calls.log" } +@test "update_file does not write legacy /repo or /sha annotation keys" { + update_file "deployment.yaml" "spec.image" "$IMAGE" + ! grep -qE 'deploy\.staffbase\.com/(repo|sha)"' "${TEST_TEMP_DIR}/yq_calls.log" +} + # --- commit_changes --- @test "commit_changes commits and pushes when push is true" { diff --git a/tests/update-gitops.bats b/tests/update-gitops.bats index 6cafd96..8f135df 100644 --- a/tests/update-gitops.bats +++ b/tests/update-gitops.bats @@ -12,8 +12,6 @@ setup() { export INPUT_DOCKER_IMAGE="my-service" export INPUT_TAG="main-abcdef12" export INPUT_PUSH="true" - export INPUT_CREATE_DEPLOYMENT="false" - export INPUT_DEPLOYMENT_IDS="{}" export INPUT_GITOPS_USER="Staffbot" export INPUT_GITOPS_EMAIL="staffbot@staffbase.com" export INPUT_GITOPS_TOKEN="fake-token"