name: Release # Runs after a successful Deploy. When the top of CHANGELOG.md names a version # that has no tag 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. on: workflow_run: workflows: ["Deploy"] types: [completed] permissions: contents: write # create the tag + GitHub Release id-token: write # OIDC to assume the notifier-invoke role # Serialize so back-to-back releases announce in order (queued Deploys -> queued # Releases), never overlapping. concurrency: group: release-announce cancel-in-progress: false jobs: release: if: ${{ github.event.workflow_run.conclusion == 'success' }} runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 with: # The exact commit Deploy deployed — NOT the branch HEAD, which a later # merge may have already moved past. 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 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") TAG_EXISTS=false git rev-parse "v$TOP" >/dev/null 2>&1 && TAG_EXISTS=true 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" echo "tag_exists=$TAG_EXISTS" >> "$GITHUB_OUTPUT" echo "release_exists=$RELEASE_EXISTS" >> "$GITHUB_OUTPUT" # "release" gates the rest: 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 env: GH_TOKEN: ${{ github.token }} # Each artifact is created independently and idempotently so a re-run after # a mid-job failure can finish the release rather than skip it forever. - name: Create tag if: ${{ steps.rel.outputs.release == 'true' && steps.rel.outputs.tag_exists == 'false' }} run: | V="v${{ steps.rel.outputs.version }}" git tag -a "$V" -m "$V" git push origin "$V" - name: Build payload 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 # Announce BEFORE publishing the Release: the Release is the durable "done" # marker, so announcing first keeps the step retryable. Minor/major only, # and only once the invoke-role variable has been bootstrapped (see README). - name: Configure AWS credentials if: ${{ steps.rel.outputs.release == 'true' && steps.rel.outputs.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: ${{ steps.rel.outputs.release == 'true' && steps.rel.outputs.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${{ steps.rel.outputs.version }}." - name: Warn if announcement skipped (not bootstrapped) if: ${{ steps.rel.outputs.release == 'true' && steps.rel.outputs.kind != 'patch' && vars.RELEASE_NOTIFY_INVOKE_ROLE_ARN == '' }} run: echo "::warning::RELEASE_NOTIFY_INVOKE_ROLE_ARN is unset — tagged + released but did not announce. Set the repo variable from the stack output." - name: Publish GitHub Release if: ${{ steps.rel.outputs.release == 'true' }} run: | gh release create "v${{ steps.rel.outputs.version }}" \ --title "v${{ steps.rel.outputs.version }}" \ --notes-file notes.md \ --target "${{ github.event.workflow_run.head_sha }}" env: GH_TOKEN: ${{ github.token }}