afterhours-shift-manager/.github/workflows/release.yaml
Adam Moussa 1ccedff872 Add changelog-driven releases and App Home tab
Version the bot continuously from CHANGELOG.md (the single source of
truth for both the version and the staff-readable notes) and surface
changes to users in two ways:

- A new afterhours-release-notifier Lambda posts a "What's New" message
  to the shift channel on minor/major releases (patches stay silent).
- The bot gains an App Home "About" tab showing what it does, the
  command list, and the current version's notes.

release.yaml runs on Deploy success (not release:published — GITHUB_TOKEN
events don't start downstream workflows), checks out the deployed commit,
and tags + publishes a GitHub Release + invokes the notifier. It assumes a
dedicated, boundary-carrying OIDC role scoped to InvokeFunction on the
notifier; the account's cfn role gates role creation on that boundary.
The manual Version Bump workflow is retired. A CI guard enforces that a
CHANGELOG edit is a clean SemVer bump and that the in-package copy matches.
2026-06-11 19:15:20 -04:00

125 lines
5.4 KiB
YAML

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 }}