From 83d12232db26d28ab8f1b74e32719f89ff467a17 Mon Sep 17 00:00:00 2001 From: Pierre Demessence Date: Fri, 3 Apr 2026 06:02:13 +0200 Subject: [PATCH 1/2] ci(workflows): split build-and-deploy into reusable build and deploy workflows - Add build.yml: reusable workflow for Docker image build + push to GHCR - Add deploy.yml: reusable workflow for k8s manifest image tag update - Add pr-build.yml: /build PR comment trigger (build only, no deploy) - Update deploy-to-dev.yml: use build -> deploy pipeline - Update deploy-prod.yml: use build -> deploy pipeline - Update pr-deploy-instructions.yml: full manifest PR workflow steps - Delete build-and-deploy.yml: replaced by build.yml + deploy.yml --- .github/workflows/build.yml | 65 ++++++++++ .github/workflows/deploy-prod.yml | 16 ++- .github/workflows/deploy-to-dev.yml | 21 +++- .../{build-and-deploy.yml => deploy.yml} | 73 ++++-------- .github/workflows/pr-build.yml | 112 ++++++++++++++++++ .github/workflows/pr-deploy-instructions.yml | 11 +- 6 files changed, 235 insertions(+), 63 deletions(-) create mode 100644 .github/workflows/build.yml rename .github/workflows/{build-and-deploy.yml => deploy.yml} (51%) create mode 100644 .github/workflows/pr-build.yml diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 00000000..18479e82 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,65 @@ +name: Build + +on: + workflow_call: + inputs: + checkout_ref: + description: "Git ref to checkout and build from" + required: true + type: string + image_name: + description: "GHCR image name (without registry prefix)" + required: true + type: string + outputs: + short_sha: + description: "Short SHA of the built commit" + value: ${{ jobs.build.outputs.short_sha }} + image_ref: + description: "Full image reference with tag (ghcr.io/owner/name:sha)" + value: ${{ jobs.build.outputs.image_ref }} + +permissions: + contents: read + packages: write + +jobs: + build: + name: Build & Push Image + runs-on: ubuntu-latest + outputs: + short_sha: ${{ steps.sha.outputs.short }} + image_ref: ghcr.io/${{ steps.sha.outputs.owner }}/${{ steps.sha.outputs.image }}:${{ steps.sha.outputs.short }} + steps: + - name: Checkout source + uses: actions/checkout@v4 + with: + ref: ${{ inputs.checkout_ref }} + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Login to GHCR + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract short SHA and image prefix + id: sha + run: | + echo "short=$(git rev-parse --short=7 HEAD)" >> "$GITHUB_OUTPUT" + echo "owner=$(echo '${{ github.repository_owner }}' | tr '[:upper:]' '[:lower:]')" >> "$GITHUB_OUTPUT" + echo "image=$(echo '${{ inputs.image_name }}' | tr '[:upper:]' '[:lower:]')" >> "$GITHUB_OUTPUT" + + - name: Build and push + uses: docker/build-push-action@v6 + with: + context: . + push: true + tags: | + ghcr.io/${{ steps.sha.outputs.owner }}/${{ steps.sha.outputs.image }}:${{ steps.sha.outputs.short }} + ghcr.io/${{ steps.sha.outputs.owner }}/${{ steps.sha.outputs.image }}:latest + cache-from: type=gha + cache-to: type=gha,mode=max diff --git a/.github/workflows/deploy-prod.yml b/.github/workflows/deploy-prod.yml index 0f7b0020..3e91f0f3 100644 --- a/.github/workflows/deploy-prod.yml +++ b/.github/workflows/deploy-prod.yml @@ -14,16 +14,26 @@ concurrency: cancel-in-progress: false jobs: + build: + uses: ./.github/workflows/build.yml + with: + checkout_ref: master + image_name: udc-bot + permissions: + contents: read + packages: write + deploy: - uses: ./.github/workflows/build-and-deploy.yml + needs: build + uses: ./.github/workflows/deploy.yml with: image_name: udc-bot + short_sha: ${{ needs.build.outputs.short_sha }} manifest_path: k8s/prod/bot.yaml - checkout_ref: master + manifest_ref: master commit_message: "chore(k8s): update prod image to {sha}" secrets: APP_ID: ${{ secrets.APP_ID }} APP_PRIVATE_KEY: ${{ secrets.APP_PRIVATE_KEY }} permissions: contents: write - packages: write diff --git a/.github/workflows/deploy-to-dev.yml b/.github/workflows/deploy-to-dev.yml index d9bae26f..fef04006 100644 --- a/.github/workflows/deploy-to-dev.yml +++ b/.github/workflows/deploy-to-dev.yml @@ -70,13 +70,23 @@ jobs: core.setOutput('is_pr', 'false'); } - deploy: + build: needs: resolve - uses: ./.github/workflows/build-and-deploy.yml + uses: ./.github/workflows/build.yml + with: + checkout_ref: ${{ needs.resolve.outputs.checkout_ref }} + image_name: udc-bot-dev + permissions: + contents: read + packages: write + + deploy: + needs: [resolve, build] + uses: ./.github/workflows/deploy.yml with: image_name: udc-bot-dev + short_sha: ${{ needs.build.outputs.short_sha }} manifest_path: k8s/dev/bot.yaml - checkout_ref: ${{ needs.resolve.outputs.checkout_ref }} manifest_ref: master commit_message: ${{ needs.resolve.outputs.commit_message }} secrets: @@ -84,10 +94,9 @@ jobs: APP_PRIVATE_KEY: ${{ secrets.APP_PRIVATE_KEY }} permissions: contents: write - packages: write notify: - needs: [resolve, deploy] + needs: [resolve, build, deploy] if: always() && needs.resolve.outputs.is_pr == 'true' name: Notify PR runs-on: ubuntu-latest @@ -97,7 +106,7 @@ jobs: uses: actions/github-script@v7 env: PR_NUMBER: ${{ inputs.pr_number }} - SHORT_SHA: ${{ needs.resolve.outputs.short_sha }} + SHORT_SHA: ${{ needs.build.outputs.short_sha }} REPO_OWNER: ${{ github.repository_owner }} with: script: | diff --git a/.github/workflows/build-and-deploy.yml b/.github/workflows/deploy.yml similarity index 51% rename from .github/workflows/build-and-deploy.yml rename to .github/workflows/deploy.yml index 93961934..a717b429 100644 --- a/.github/workflows/build-and-deploy.yml +++ b/.github/workflows/deploy.yml @@ -1,4 +1,4 @@ -name: Build & Deploy +name: Deploy on: workflow_call: @@ -7,20 +7,20 @@ on: description: "GHCR image name (without registry prefix)" required: true type: string - manifest_path: - description: "Path to the k8s manifest to update" + short_sha: + description: "Short SHA to use as the image tag" required: true type: string - checkout_ref: - description: "Git ref to checkout and build from" + manifest_path: + description: "Path to the k8s manifest to update" required: true type: string manifest_ref: - description: "Branch where the manifest lives (for commit+push)" - required: false + description: "Branch where the manifest lives (for checkout + commit)" + required: true type: string commit_message: - description: "Commit message for the manifest update" + description: "Commit message template ({sha} will be replaced with short SHA)" required: true type: string secrets: @@ -33,48 +33,12 @@ on: permissions: contents: write - packages: write jobs: - build-and-deploy: - name: Build & Deploy + deploy: + name: Update Manifest runs-on: ubuntu-latest - outputs: - short_sha: ${{ steps.sha.outputs.short }} steps: - - name: Checkout source - uses: actions/checkout@v4 - with: - ref: ${{ inputs.checkout_ref }} - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Login to GHCR - uses: docker/login-action@v3 - with: - registry: ghcr.io - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Extract short SHA and image prefix - id: sha - run: | - echo "short=$(git rev-parse --short=7 HEAD)" >> "$GITHUB_OUTPUT" - echo "owner=$(echo '${{ github.repository_owner }}' | tr '[:upper:]' '[:lower:]')" >> "$GITHUB_OUTPUT" - echo "image=$(echo '${{ inputs.image_name }}' | tr '[:upper:]' '[:lower:]')" >> "$GITHUB_OUTPUT" - - - name: Build and push - uses: docker/build-push-action@v6 - with: - context: . - push: true - tags: | - ghcr.io/${{ steps.sha.outputs.owner }}/${{ steps.sha.outputs.image }}:${{ steps.sha.outputs.short }} - ghcr.io/${{ steps.sha.outputs.owner }}/${{ steps.sha.outputs.image }}:latest - cache-from: type=gha - cache-to: type=gha,mode=max - - name: Generate GitHub App token id: app-token uses: actions/create-github-app-token@v1 @@ -83,7 +47,6 @@ jobs: private-key: ${{ secrets.APP_PRIVATE_KEY }} - name: Checkout manifest branch - if: inputs.manifest_ref != '' uses: actions/checkout@v4 with: ref: ${{ inputs.manifest_ref }} @@ -91,17 +54,23 @@ jobs: - name: Update manifest image tag env: - OWNER: ${{ steps.sha.outputs.owner }} - IMAGE: ${{ steps.sha.outputs.image }} - SHORT_SHA: ${{ steps.sha.outputs.short }} + OWNER: ${{ github.repository_owner }} + IMAGE: ${{ inputs.image_name }} + SHORT_SHA: ${{ inputs.short_sha }} MANIFEST_PATH: ${{ inputs.manifest_path }} run: | - sed -i "s|image: ghcr.io/${OWNER}/${IMAGE}:.*|image: ghcr.io/${OWNER}/${IMAGE}:${SHORT_SHA}|" "${MANIFEST_PATH}" + OWNER_LC=$(echo "${OWNER}" | tr '[:upper:]' '[:lower:]') + IMAGE_LC=$(echo "${IMAGE}" | tr '[:upper:]' '[:lower:]') + sed -i "s|image: ghcr.io/${OWNER_LC}/${IMAGE_LC}:.*|image: ghcr.io/${OWNER_LC}/${IMAGE_LC}:${SHORT_SHA}|" "${MANIFEST_PATH}" + if ! grep -q "image: ghcr.io/${OWNER_LC}/${IMAGE_LC}:${SHORT_SHA}" "${MANIFEST_PATH}"; then + echo "::error::Failed to update image tag in ${MANIFEST_PATH}" + exit 1 + fi - name: Commit and push manifest update env: COMMIT_MSG: ${{ inputs.commit_message }} - SHORT_SHA: ${{ steps.sha.outputs.short }} + SHORT_SHA: ${{ inputs.short_sha }} MANIFEST_PATH: ${{ inputs.manifest_path }} GH_TOKEN: ${{ steps.app-token.outputs.token }} run: | diff --git a/.github/workflows/pr-build.yml b/.github/workflows/pr-build.yml new file mode 100644 index 00000000..803665b5 --- /dev/null +++ b/.github/workflows/pr-build.yml @@ -0,0 +1,112 @@ +name: PR Build on Comment + +on: + issue_comment: + types: [created] + +permissions: + contents: read + packages: write + pull-requests: write + issues: write + +jobs: + check: + name: Validate trigger + if: | + github.event.issue.pull_request && + contains(github.event.comment.body, '/build') && + github.event.comment.author_association != 'NONE' && + github.event.comment.author_association != 'FIRST_TIMER' && + github.event.comment.author_association != 'FIRST_TIME_CONTRIBUTOR' + runs-on: ubuntu-latest + outputs: + head_sha: ${{ steps.pr.outputs.head_sha }} + pr_number: ${{ steps.pr.outputs.pr_number }} + steps: + - name: Get PR details + id: pr + uses: actions/github-script@v7 + with: + script: | + const { data: pr } = await github.rest.pulls.get({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: context.issue.number, + }); + if (pr.state !== 'open') { + core.setFailed(`PR #${context.issue.number} is not open`); + return; + } + core.setOutput('head_sha', pr.head.sha); + core.setOutput('pr_number', context.issue.number); + + - name: React to comment + uses: actions/github-script@v7 + with: + script: | + await github.rest.reactions.createForIssueComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: context.payload.comment.id, + content: 'rocket', + }); + + build: + needs: check + uses: ./.github/workflows/build.yml + with: + checkout_ref: ${{ needs.check.outputs.head_sha }} + image_name: udc-bot-dev + permissions: + contents: read + packages: write + + notify: + needs: [check, build] + if: always() + name: Post result + runs-on: ubuntu-latest + permissions: + pull-requests: write + issues: write + steps: + - name: Post success comment + if: needs.build.result == 'success' + uses: actions/github-script@v7 + env: + PR_NUMBER: ${{ needs.check.outputs.pr_number }} + IMAGE_REF: ${{ needs.build.outputs.image_ref }} + SHORT_SHA: ${{ needs.build.outputs.short_sha }} + with: + script: | + const { PR_NUMBER, IMAGE_REF, SHORT_SHA } = process.env; + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: Number(PR_NUMBER), + body: [ + '🐳 **Build complete!**', + '', + `Image: \`${IMAGE_REF}\``, + '', + 'To deploy with manifest changes:', + `1. Update \`k8s/dev/bot.yaml\` image tag to \`${SHORT_SHA}\` on your branch`, + '2. Switch ArgoCD dev target revision to your branch', + ].join('\n'), + }); + + - name: Post failure comment + if: needs.build.result == 'failure' + uses: actions/github-script@v7 + env: + PR_NUMBER: ${{ needs.check.outputs.pr_number }} + with: + script: | + const { PR_NUMBER } = process.env; + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: Number(PR_NUMBER), + body: `❌ Build failed. Check the [workflow run](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}) for details.`, + }); diff --git a/.github/workflows/pr-deploy-instructions.yml b/.github/workflows/pr-deploy-instructions.yml index 1f51669d..445dfc15 100644 --- a/.github/workflows/pr-deploy-instructions.yml +++ b/.github/workflows/pr-deploy-instructions.yml @@ -24,8 +24,15 @@ jobs: body: [ '### 🚀 Deploy this PR', '', - `To deploy this branch to the **dev** environment, go to the [Deploy to Dev](${context.payload.repository.html_url}/actions/workflows/deploy-to-dev.yml) workflow and click **Run workflow** with this PR number.`, + '#### Code-only changes', + `1. Make sure the **udc-bot-dev** ArgoCD app target revision is set to \`master\`.`, + `2. Go to the [Deploy to Dev](${context.payload.repository.html_url}/actions/workflows/deploy-to-dev.yml) workflow and click **Run workflow** with this PR number.`, '', - 'This will build a Docker image from your branch, push it to GHCR, and trigger an ArgoCD sync.', + '#### Manifest changes (e.g. new services in `k8s/dev/`)', + '1. Comment `/build` on this PR to build the Docker image (without deploying to master).', + '2. Once the build succeeds, update the image tag in `k8s/dev/bot.yaml` on your branch with the SHA from the build comment, then push.', + '3. In ArgoCD, switch the **udc-bot-dev** app target revision from `master` to your branch (App Details → Source → Edit → Save).', + '4. ArgoCD will sync your manifest changes + new image. Test your changes.', + '5. When done, switch ArgoCD target revision back to `master`.', ].join('\n'), }); From 8e223392e11185f8c611ba3e27cf3855b31bd7f7 Mon Sep 17 00:00:00 2001 From: Pierre Demessence Date: Fri, 3 Apr 2026 06:33:51 +0200 Subject: [PATCH 2/2] Update .github/workflows/pr-build.yml Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .github/workflows/pr-build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pr-build.yml b/.github/workflows/pr-build.yml index 803665b5..76953246 100644 --- a/.github/workflows/pr-build.yml +++ b/.github/workflows/pr-build.yml @@ -64,7 +64,7 @@ jobs: notify: needs: [check, build] - if: always() + if: needs.check.result != 'skipped' && always() name: Post result runs-on: ubuntu-latest permissions: