|
| 1 | +# Security checklists: |
| 2 | +# https://aquilax.ai/blog/github-actions-security-hardening |
| 3 | +# https://www.wiz.io/blog/github-actions-security-guide |
| 4 | + |
| 5 | +name: Deploy |
| 6 | + |
| 7 | +on: |
| 8 | + workflow_dispatch: |
| 9 | + inputs: |
| 10 | + run_id: |
| 11 | + description: "Test and build workflow run ID to deploy" |
| 12 | + required: true |
| 13 | + # zizmor: ignore[dangerous-triggers] Only triggered via default branch |
| 14 | + workflow_run: |
| 15 | + workflows: |
| 16 | + - Test and build |
| 17 | + branches: |
| 18 | + - main |
| 19 | + types: |
| 20 | + - completed |
| 21 | + |
| 22 | +permissions: |
| 23 | + contents: read |
| 24 | + |
| 25 | +concurrency: |
| 26 | + group: ci-${{ github.workflow }}-${{ github.ref }} |
| 27 | + |
| 28 | +jobs: |
| 29 | + validate: |
| 30 | + runs-on: ubuntu-24.04 |
| 31 | + timeout-minutes: 2 |
| 32 | + if: github.event_name == 'workflow_dispatch' || github.event.workflow_run.conclusion == 'success' |
| 33 | + outputs: |
| 34 | + build_run_id: ${{ steps.resolve-run-id.outputs.run_id }} |
| 35 | + build_skipped: ${{ steps.validate-run-id.outputs.build_skipped }} |
| 36 | + build_head_sha: ${{ steps.validate-run-id.outputs.build_head_sha }} |
| 37 | + build_run_number: ${{ steps.validate-run-id.outputs.build_run_number }} |
| 38 | + build_url: ${{ steps.validate-run-id.outputs.build_url }} |
| 39 | + steps: |
| 40 | + - name: Resolve run ID |
| 41 | + id: resolve-run-id |
| 42 | + run: echo "run_id=$RESULT" >> "$GITHUB_OUTPUT" |
| 43 | + env: |
| 44 | + RESULT: ${{ github.event_name == 'workflow_run' && github.event.workflow_run.id || github.event.inputs.run_id }} |
| 45 | + |
| 46 | + - name: Validate run ID |
| 47 | + id: validate-run-id |
| 48 | + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 |
| 49 | + env: |
| 50 | + RUN_ID: ${{ steps.resolve-run-id.outputs.run_id }} |
| 51 | + EXPECTED_WORKFLOW_NAME: Test and build |
| 52 | + EXPECTED_BRANCH: main |
| 53 | + EXPECTED_JOB_NAME: apiserver-build |
| 54 | + with: |
| 55 | + script: | |
| 56 | + const runId = Number(process.env.RUN_ID); |
| 57 | + if (!Number.isInteger(runId) || runId <= 0) { |
| 58 | + core.setFailed(`Invalid run_id: ${process.env.RUN_ID}`); |
| 59 | + return; |
| 60 | + } |
| 61 | +
|
| 62 | + const { owner, repo } = context.repo; |
| 63 | + const run = ( |
| 64 | + await github.rest.actions.getWorkflowRun({ |
| 65 | + owner, |
| 66 | + repo, |
| 67 | + run_id: runId |
| 68 | + }) |
| 69 | + ).data; |
| 70 | +
|
| 71 | + if (run.name !== process.env.EXPECTED_WORKFLOW_NAME) { |
| 72 | + core.setFailed( |
| 73 | + `run_id ${runId} is for workflow '${run.name}', expected '${process.env.EXPECTED_WORKFLOW_NAME}'` |
| 74 | + ); |
| 75 | + return; |
| 76 | + } |
| 77 | +
|
| 78 | + if (run.head_branch !== process.env.EXPECTED_BRANCH) { |
| 79 | + core.setFailed( |
| 80 | + `run_id ${runId} is on branch '${run.head_branch}', expected '${process.env.EXPECTED_BRANCH}'` |
| 81 | + ); |
| 82 | + return; |
| 83 | + } |
| 84 | +
|
| 85 | + if (run.conclusion !== 'success') { |
| 86 | + core.setFailed( |
| 87 | + `run_id ${runId} has conclusion '${run.conclusion}', expected 'success'` |
| 88 | + ); |
| 89 | + return; |
| 90 | + } |
| 91 | +
|
| 92 | + if (run.head_repository?.full_name !== `${owner}/${repo}`) { |
| 93 | + core.setFailed( |
| 94 | + `run_id ${runId} belongs to '${run.head_repository?.full_name}', expected '${owner}/${repo}'` |
| 95 | + ); |
| 96 | + return; |
| 97 | + } |
| 98 | +
|
| 99 | + const jobs = await github.paginate(github.rest.actions.listJobsForWorkflowRun, { |
| 100 | + owner, |
| 101 | + repo, |
| 102 | + run_id: runId, |
| 103 | + per_page: 100, |
| 104 | + }); |
| 105 | +
|
| 106 | + const buildJob = jobs.find((job) => job.name === process.env.EXPECTED_JOB_NAME); |
| 107 | + if (!buildJob) { |
| 108 | + core.setFailed( |
| 109 | + `run_id ${runId} does not contain expected job '${process.env.EXPECTED_JOB_NAME}'` |
| 110 | + ); |
| 111 | + return; |
| 112 | + } |
| 113 | +
|
| 114 | + if (buildJob.conclusion === 'skipped') { |
| 115 | + core.setOutput('build_skipped', 'true'); |
| 116 | + core.info(`job '${process.env.EXPECTED_JOB_NAME}' in run_id ${runId} was skipped; nothing to deploy`); |
| 117 | + return; |
| 118 | + } |
| 119 | +
|
| 120 | + if (buildJob.conclusion !== 'success') { |
| 121 | + core.setFailed( |
| 122 | + `job '${process.env.EXPECTED_JOB_NAME}' in run_id ${runId} has conclusion '${buildJob.conclusion}', expected 'success'` |
| 123 | + ); |
| 124 | + return; |
| 125 | + } |
| 126 | +
|
| 127 | + core.setOutput('build_skipped', 'false'); |
| 128 | + core.setOutput('build_head_sha', run.head_sha); |
| 129 | + core.setOutput('build_run_number', String(run.run_number)); |
| 130 | + core.setOutput('build_url', run.html_url); |
| 131 | +
|
| 132 | + deploy-apiserver: |
| 133 | + runs-on: ubuntu-24.04 |
| 134 | + timeout-minutes: 15 |
| 135 | + needs: validate |
| 136 | + environment: deploy |
| 137 | + if: needs.validate.outputs.build_skipped == 'false' |
| 138 | + permissions: |
| 139 | + contents: write |
| 140 | + id-token: write |
| 141 | + steps: |
| 142 | + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 |
| 143 | + with: |
| 144 | + persist-credentials: true |
| 145 | + ref: ${{ needs.validate.outputs.build_head_sha }} |
| 146 | + |
| 147 | + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 |
| 148 | + with: |
| 149 | + name: apiserver |
| 150 | + run-id: ${{ needs.validate.outputs.build_run_id }} |
| 151 | + github-token: ${{ secrets.GITHUB_TOKEN }} |
| 152 | + |
| 153 | + - name: Create tag |
| 154 | + run: | |
| 155 | + { |
| 156 | + git show -s --format=%B "$BUILD_HEAD_SHA" |
| 157 | + echo |
| 158 | + echo "Deploy run: $GITHUB_RUN_ID" |
| 159 | + echo "Build run: $BUILD_RUN_ID" |
| 160 | + echo "Build number: $BUILD_RUN_NUMBER" |
| 161 | + echo "Build URL: $BUILD_URL" |
| 162 | + echo "Build SHA: $BUILD_HEAD_SHA" |
| 163 | + } > tag-message.txt |
| 164 | +
|
| 165 | + git config user.name "github-actions[bot]" |
| 166 | + git config user.email "github-actions[bot]@users.noreply.github.com" |
| 167 | + git tag -a apiserver-"$GITHUB_RUN_NUMBER" -F tag-message.txt "$BUILD_HEAD_SHA" |
| 168 | + env: |
| 169 | + BUILD_RUN_ID: ${{ needs.validate.outputs.build_run_id }} |
| 170 | + BUILD_RUN_NUMBER: ${{ needs.validate.outputs.build_run_number }} |
| 171 | + BUILD_URL: ${{ needs.validate.outputs.build_url }} |
| 172 | + BUILD_HEAD_SHA: ${{ needs.validate.outputs.build_head_sha }} |
| 173 | + |
| 174 | + - name: Push tag |
| 175 | + run: git push origin apiserver-"$GITHUB_RUN_NUMBER" |
| 176 | + |
| 177 | + - name: Create release |
| 178 | + run: > |
| 179 | + gh release create |
| 180 | + apiserver-"$GITHUB_RUN_NUMBER" |
| 181 | + apiserver-*.tar.zst |
| 182 | + --title "apiserver v$GITHUB_RUN_NUMBER" |
| 183 | + --notes-from-tag |
| 184 | + --verify-tag |
| 185 | + env: |
| 186 | + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} |
| 187 | + |
| 188 | + - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 |
| 189 | + id: get-id-token |
| 190 | + with: |
| 191 | + script: | |
| 192 | + const fs = require('fs'); |
| 193 | + const token = await core.getIDToken('backend.fullstaqruby.org'); |
| 194 | + fs.writeFileSync( |
| 195 | + process.env.GITHUB_OUTPUT, |
| 196 | + `id_token<<EOF\n${token}\nEOF\n`, |
| 197 | + { flag: 'a' } |
| 198 | + ); |
| 199 | +
|
| 200 | + - name: Deploy |
| 201 | + run: > |
| 202 | + curl -fL --no-progress-meter -X POST -H "Authorization: Bearer $TOKEN" |
| 203 | + https://apt.fullstaqruby.org/admin/upgrade_apiserver |
| 204 | + env: |
| 205 | + TOKEN: ${{ steps.get-id-token.outputs.id_token }} |
0 commit comments