name: Release # Runs after a successful Deploy. When the top of CHANGELOG.md names a version # that has no Release yet, this tags it, publishes a GitHub Release with the # notes, and — for minor/major bumps only — invokes the release-notifier Lambda # to announce it in Slack. Triggering on Deploy completion (not release:published # / tag push) is deliberate: GITHUB_TOKEN-created events do not start downstream # workflows, and gating on Deploy success means we never announce a version that # is not actually live. # # Two jobs by design (CodeQL actions/untrusted-checkout): the only job that # checks out and runs repo code (`prepare`) is read-only and unprivileged; the # job that holds write + OIDC (`publish`) never checks out repo code — it acts # purely through the GitHub and AWS APIs. on: workflow_run: workflows: ["Deploy"] types: [completed] # Serialize so back-to-back releases announce in order, never overlapping. concurrency: group: release-announce cancel-in-progress: false jobs: prepare: # Deploy only runs on push to main, so head_sha is always a trusted main # commit; assert head_branch == main to make that boundary explicit. if: ${{ github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.head_branch == 'main' }} runs-on: ubuntu-latest permissions: contents: read outputs: release: ${{ steps.rel.outputs.release }} version: ${{ steps.rel.outputs.version }} kind: ${{ steps.rel.outputs.kind }} steps: - uses: actions/checkout@v6 with: ref: ${{ github.event.workflow_run.head_sha }} fetch-depth: 0 fetch-tags: true - uses: actions/setup-python@v5 with: python-version: "3.12" - name: Determine release id: rel env: GH_TOKEN: ${{ github.token }} run: | TOP=$(python scripts/changelog_cli.py top-version CHANGELOG.md) if [ -z "$TOP" ]; then echo "No version entry in CHANGELOG.md — nothing to release." echo "release=false" >> "$GITHUB_OUTPUT" exit 0 fi PREV=$(git tag -l 'v*' --sort=-v:refname | head -1) PREV="${PREV:-v0.0.0}" KIND=$(python scripts/changelog_cli.py bump-kind CHANGELOG.md "$PREV") RELEASE_EXISTS=false gh release view "v$TOP" >/dev/null 2>&1 && RELEASE_EXISTS=true echo "version=$TOP" >> "$GITHUB_OUTPUT" echo "kind=$KIND" >> "$GITHUB_OUTPUT" # Gate the publish job on a clean bump whose Release isn't published yet. if [ "$KIND" != "none" ] && [ "$RELEASE_EXISTS" = "false" ]; then echo "release=true" >> "$GITHUB_OUTPUT" else echo "release=false" >> "$GITHUB_OUTPUT" echo "v$TOP: kind=$KIND release_exists=$RELEASE_EXISTS — no action." fi - name: Build release notes if: ${{ steps.rel.outputs.release == 'true' }} run: | python scripts/changelog_cli.py payload CHANGELOG.md "${{ steps.rel.outputs.version }}" > payload.json python -c "import json; print(json.load(open('payload.json'))['notes'])" > notes.md - name: Upload notes artifact if: ${{ steps.rel.outputs.release == 'true' }} uses: actions/upload-artifact@v4 with: name: release-notes path: | payload.json notes.md retention-days: 1 publish: needs: prepare if: ${{ needs.prepare.outputs.release == 'true' }} runs-on: ubuntu-latest permissions: contents: write # create the tag + GitHub Release id-token: write # OIDC to assume the notifier-invoke role env: VERSION: ${{ needs.prepare.outputs.version }} KIND: ${{ needs.prepare.outputs.kind }} HEAD_SHA: ${{ github.event.workflow_run.head_sha }} steps: - uses: actions/download-artifact@v4 with: name: release-notes # Announce BEFORE publishing the Release: the Release is the durable "done" # marker (prepare skips once it exists), so announcing first keeps this # retryable. Minor/major only, and only once the invoke-role variable has # been bootstrapped (see README). - name: Configure AWS credentials if: ${{ env.KIND != 'patch' && vars.RELEASE_NOTIFY_INVOKE_ROLE_ARN != '' }} uses: aws-actions/configure-aws-credentials@v6 with: role-to-assume: ${{ vars.RELEASE_NOTIFY_INVOKE_ROLE_ARN }} aws-region: us-east-1 - name: Announce in Slack if: ${{ env.KIND != 'patch' && vars.RELEASE_NOTIFY_INVOKE_ROLE_ARN != '' }} run: | aws lambda invoke \ --function-name afterhours-release-notifier \ --cli-binary-format raw-in-base64-out \ --payload file://payload.json \ --output json response.json > invoke-meta.json # aws lambda invoke only emits a FunctionError key when the handler errored. if grep -q '"FunctionError"' invoke-meta.json; then echo "::error::release-notifier returned an error"; cat response.json; exit 1 fi echo "Announced v${VERSION}." - name: Warn if announcement skipped (not bootstrapped) if: ${{ env.KIND != 'patch' && vars.RELEASE_NOTIFY_INVOKE_ROLE_ARN == '' }} run: echo "::warning::RELEASE_NOTIFY_INVOKE_ROLE_ARN is unset — tagging + releasing but not announcing. Set the repo variable from the stack output." # No checkout: gh creates the tag at HEAD_SHA and the Release together. - name: Publish GitHub Release env: GH_TOKEN: ${{ github.token }} run: | gh release create "v${VERSION}" \ --repo "${{ github.repository }}" \ --title "v${VERSION}" \ --notes-file notes.md \ --target "${HEAD_SHA}"