forked from heavy-duty/rig
The arming rule (#66) guards ONE heading — does the top section agree with VERSION? — and is silent about the rest of the file. The failure that lives there is an author adding an entry under `## Unreleased` who replaces the shipped heading below it instead of inserting above it. git merges the one-line edit cleanly, `changelog_armed()` stays green (correctly: the top section is still right), and the shipped release loses its section entirely. It surfaces a whole release later, when release.yml refuses to publish a section `changelog_section()` can no longer find by heading. "A heading disappeared" is a property of a DIFF, not of a tree, so this is its own script rather than a clause in the arming check — which is also driven from test/release.sh against constructed non-git VERSION + CHANGELOG pairs that could not express it. The rule needs no tuning: release headings are append-only, so SUPERSET is exact, and the ceremony's stamp passes by construction because `Unreleased` fails the version shape. Ported from heavy-duty/box#122, with box's second half intact: containment cannot catch a DUPLICATED heading, since the duplicate is head-side surplus and `comm -23` is blind to extras there — so uniqueness on HEAD is asserted alongside it. rig's symptom differs from box's and the comments say so: box's extractor re-arms on every `## ` line and ABSORBS what sits between the copies, while rig's `changelog_section()` has `if (found) exit` and TRUNCATES at the second copy, dropping the release's real body. Wired on pull requests only (on a push to main the merge base is HEAD, so the assert is vacuous), with CHANGELOG_MONOTONIC_STRICT=1 and fetch-depth: 0 so a checkout that cannot reach the base ref fails rather than skipping quietly forever. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
81 lines
4.2 KiB
YAML
81 lines
4.2 KiB
YAML
name: ci
|
|
on:
|
|
push:
|
|
branches: [main]
|
|
pull_request:
|
|
jobs:
|
|
check:
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
with:
|
|
# fetch-depth: 0, for the changelog-monotonic step below and only
|
|
# for it. That check is about a DIFF — which release headings the
|
|
# merge base had — so it needs the base branch's history present,
|
|
# and the default depth-1 checkout has none of it. An explicit
|
|
# `git fetch origin <base>` would be narrower, but it has to be
|
|
# right on both event types and on fork PRs, and getting it subtly
|
|
# wrong degrades to a SKIP (a guard that silently stops guarding —
|
|
# the exact failure this repo keeps refusing). Full history on a
|
|
# pure-bash tree costs a second; the STRICT flag below turns any
|
|
# remaining skip red rather than green.
|
|
fetch-depth: 0
|
|
- name: shellcheck
|
|
# -x follows the `source=SCRIPTDIR/...` directives into commands/lib/.
|
|
# globstar so a script in a new subdirectory is linted without anyone
|
|
# remembering to edit this list; bin/* covers the extensionless entrypoints.
|
|
# dotglob because globs skip dot-prefixed names: without it `**/` never
|
|
# descends into `.github/`, so `.github/scripts/*.sh` — release-lib.sh
|
|
# among them — was swept up by nothing (#70). It also makes `**`
|
|
# descend into `.git/`, which holds no tracked `.sh` on a checkout.
|
|
# The file list is printed so under-coverage shows up in the log, and
|
|
# the comm below turns under-coverage into a failure rather than a
|
|
# thing someone has to notice: every tracked `.sh` must be in the set.
|
|
run: |
|
|
shopt -s globstar dotglob
|
|
files=(bin/* **/*.sh)
|
|
printf 'shellcheck: %s\n' "${files[@]}"
|
|
uncovered=$(comm -23 <(git ls-files '*.sh' | sort) <(printf '%s\n' "${files[@]}" | sort))
|
|
if [ -n "$uncovered" ]; then
|
|
printf 'tracked .sh files the glob does not lint:\n%s\n' "$uncovered" >&2
|
|
exit 1
|
|
fi
|
|
shellcheck -x "${files[@]}"
|
|
- name: cli tests
|
|
run: bash test/cli.sh
|
|
# test/labels-reconcile.sh existed here since #87 but ran nowhere: the
|
|
# label state machine gates every PR on this repo and its fixtures were
|
|
# green only when someone remembered to run them by hand. Same step, same
|
|
# place as heavy-duty/box.
|
|
- name: labels state-machine tests
|
|
run: bash test/labels-reconcile.sh
|
|
- name: release-flow tests
|
|
run: bash test/release.sh
|
|
# No SHIPPED release heading was deleted (#98). Its own step rather than
|
|
# a line inside test/release.sh: that suite drives the arming rule
|
|
# against constructed VERSION + CHANGELOG.md trees that are not git
|
|
# repos, and this assert needs a git history — folding it in would make
|
|
# those cases skip or lie. It is also a DIFFERENT invariant: arming is a
|
|
# fact about this tree, monotonicity is a fact about this tree versus
|
|
# its merge base. Pull requests only: on a push to main the merge base
|
|
# IS HEAD, so the assert is vacuous and would only add a green step that
|
|
# proves nothing. STRICT=1 so a checkout that cannot reach the base ref
|
|
# fails here instead of skipping quietly forever.
|
|
- name: no shipped changelog heading was deleted
|
|
if: github.event_name == 'pull_request'
|
|
env:
|
|
CHANGELOG_MONOTONIC_STRICT: '1'
|
|
run: bash .github/scripts/changelog-monotonic.sh "origin/${{ github.base_ref }}"
|
|
|
|
# Kept SEPARATE from `check` on purpose: this job pulls a Postgres image and
|
|
# stands up throwaway containers, and a slow image pull must never delay the
|
|
# fast shellcheck + cli.sh feedback above. ubuntu-latest ships Docker running
|
|
# and passwordless sudo, so test/db-integration.sh EXECUTES here (it only
|
|
# skips where Docker is absent). It is the automated proof that dump/restore
|
|
# actually round-trips, not just that the args parse.
|
|
db-integration:
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
- name: db dump/restore round-trip
|
|
run: bash test/db-integration.sh
|