feat: make automated practice review accountable for what it did not do #291
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Verify Changesets | |
| # Every user-facing PR must carry a .changeset/*.md file with an | |
| # operator-facing description (cf. vercel/ai's verify-changesets model). | |
| # Opt out explicitly with an empty changeset: `pnpm changeset --empty`. | |
| # See docs/contributor/release-management.mdx. | |
| on: | |
| pull_request: | |
| types: [opened, synchronize, reopened, ready_for_review] | |
| branches: [main] | |
| permissions: | |
| contents: read | |
| jobs: | |
| verify-changesets: | |
| name: "Verify changesets" | |
| runs-on: ubuntu-latest | |
| # Bots (renovate, dependabot) can't author changesets; maintainers add one | |
| # when a dependency bump is user-facing. | |
| if: ${{ !github.event.pull_request.draft && github.event.pull_request.user.type != 'Bot' }} | |
| permissions: | |
| contents: read | |
| pull-requests: write | |
| steps: | |
| - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 | |
| with: | |
| fetch-depth: 0 | |
| - name: Check for a changeset | |
| id: check | |
| env: | |
| BASE_SHA: ${{ github.event.pull_request.base.sha }} | |
| run: | | |
| set -euo pipefail | |
| # Everything under these dirs ships in an image (webapp/ is COPY'd | |
| # wholesale, so public/, index.html and the nginx conf ship too), minus | |
| # tests and in-tree docs — forcing `--empty` on those would train people | |
| # to reflex-skip the gate. Keep in sync with the wording in AGENTS.md | |
| # §10, .changeset/README.md, CONTRIBUTING.md and release-management.mdx. | |
| SHIPPED_PATHS=( | |
| "server" "webapp" "docker" | |
| ":!**/*.md" ":!server/src/test" ":!webapp/e2e" | |
| ":!**/*.test.ts" ":!**/*.test.tsx" ":!**/*.stories.tsx" | |
| ) | |
| # `...` (merge-base vs head) = this PR's own changes, fork-safe. | |
| # `--diff-filter=A` on .changeset/*.md answers "did THIS PR add a | |
| # changeset" — which `changeset status` (does one exist anywhere) can't. | |
| shipped_changed=$(git diff --name-only "$BASE_SHA"...HEAD -- "${SHIPPED_PATHS[@]}") | |
| changesets_added=$(git diff --diff-filter=A --name-only "$BASE_SHA"...HEAD -- '.changeset/*.md' | grep -v 'README\.md' || true) | |
| if git diff --name-only "$BASE_SHA"...HEAD -- server/src/main/resources/db/changelog/ | grep -q .; then | |
| echo "::notice::This PR contains Liquibase changesets — the release notes will flag the migration automatically. If the operator must act, say so in your changeset and update MIGRATION.md." | |
| fi | |
| # Pre-1.0 guard: a `major` changeset on 0.x would cut 1.0.0. The 1.0 | |
| # release is a deliberate milestone decision (#1378), not a PR-level | |
| # one. This guard disarms itself once the version reaches 1.x. | |
| if [ "$(jq -r '.version | split(".")[0]' package.json)" = "0" ] && [ -n "$changesets_added" ]; then | |
| for f in $changesets_added; do | |
| if grep -qE '^["'"'"']?hephaestus["'"'"']?:[[:space:]]*major' "$f"; then | |
| echo "::error file=$f::Pre-1.0: a major changeset would cut 1.0.0. Use minor for breaking changes; 1.0.0 ships deliberately with the 1.0 milestone (#1378)." | |
| exit 1 | |
| fi | |
| done | |
| fi | |
| if [ -n "$shipped_changed" ] && [ -z "$changesets_added" ]; then | |
| echo "missing=true" >> "$GITHUB_OUTPUT" | |
| # Echo the actual triggering files rather than restating the pathspec — | |
| # a hardcoded list here would drift from SHIPPED_PATHS above. | |
| { | |
| echo 'shipped<<EOF' | |
| echo "$shipped_changed" | |
| echo EOF | |
| } >> "$GITHUB_OUTPUT" | |
| echo "::error::This PR changes shipped code but carries no changeset:" | |
| echo "$shipped_changed" | |
| exit 1 | |
| fi | |
| echo "missing=false" >> "$GITHUB_OUTPUT" | |
| echo "Changeset check passed." | |
| - name: Comment on PR | |
| uses: marocchino/sticky-pull-request-comment@773744901bac0e8cbb5a0dc842800d45e9b2b405 # v2 | |
| if: failure() && steps.check.outputs.missing == 'true' | |
| continue-on-error: true | |
| with: | |
| header: verify-changesets | |
| message: | | |
| ## 📦 Changeset required | |
| This PR changes code that ships to operators, but carries no changeset: | |
| ``` | |
| ${{ steps.check.outputs.shipped }} | |
| ``` | |
| Changesets become the release notes operators read — write yours for them: | |
| ```bash | |
| pnpm changeset # user-facing change: pick the bump, describe it | |
| pnpm changeset --empty # no user-facing effect — then write why in the file body | |
| ``` | |
| Write the summary in the operator/user's voice — it becomes the changelog verbatim. | |
| > 📖 See the [release management guide](https://ls1intum.github.io/Hephaestus/contributor/release-management). | |
| - name: Remove comment on success | |
| uses: marocchino/sticky-pull-request-comment@773744901bac0e8cbb5a0dc842800d45e9b2b405 # v2 | |
| if: success() | |
| continue-on-error: true | |
| with: | |
| header: verify-changesets | |
| delete: true |