-
Notifications
You must be signed in to change notification settings - Fork 1
ci(backlog): index-integrity workflow — Phase 1c per BACKLOG ADR #492
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,164 @@ | ||||||
| name: backlog-index-integrity | ||||||
|
|
||||||
| # Enforces that `docs/BACKLOG.md` (the generated index) stays in | ||||||
| # sync with the per-row files under `docs/backlog/P[0-3]/B-<NNNN>-*.md`. | ||||||
| # When a row file is added/modified/removed, the index must be | ||||||
| # regenerated via `tools/backlog/generate-index.sh` in the same | ||||||
| # commit (or PR) so a fresh reader sees the same row set in both | ||||||
| # places. | ||||||
| # | ||||||
| # Phase 1c per the BACKLOG-per-row-file ADR | ||||||
| # (docs/DECISIONS/2026-04-22-backlog-per-row-file-restructure.md | ||||||
| # §"Existing substrate Phase 1a prior work" → Phase 1c OWED). | ||||||
| # This workflow IS the lint-index gate; it wraps | ||||||
| # `generate-index.sh --check` rather than introducing a separate | ||||||
| # `tools/backlog/lint-index.sh` wrapper, since there is no | ||||||
| # pre-commit-hook framework currently wired up in this repo — | ||||||
| # the CI surface is the equivalent enforcement point. | ||||||
| # | ||||||
| # Note: until Phase 2 bulk-migration runs, `docs/BACKLOG.md` is | ||||||
| # still the monolithic authoritative file. Phase 2 will overwrite | ||||||
| # it with the generated index. This workflow becomes load-bearing | ||||||
| # at that point. Until then it primarily guards the per-row | ||||||
| # files themselves are well-formed (B-0001 example, B-0002 | ||||||
| # Otto-287 Noether) and skips the equivalence check. | ||||||
| # | ||||||
| # Safe-pattern compliance (mirrors memory-index-integrity.yml): | ||||||
| # - SHA-pinned action versions (actions/checkout@de0fac2...) | ||||||
| # - Explicit `permissions:` minimum | ||||||
| # - Only first-party trusted context (github.sha) — no | ||||||
| # user-authored text is referenced. | ||||||
| # - Concurrency group + cancel-in-progress: false. | ||||||
| # - runs-on: ubuntu-24.04 pinned. | ||||||
|
|
||||||
| on: | ||||||
| pull_request: | ||||||
| paths: | ||||||
| - "docs/backlog/**" | ||||||
| - "docs/BACKLOG.md" | ||||||
| - "tools/backlog/**" | ||||||
| push: | ||||||
| branches: [main] | ||||||
| paths: | ||||||
| - "docs/backlog/**" | ||||||
| - "docs/BACKLOG.md" | ||||||
| - "tools/backlog/**" | ||||||
| workflow_dispatch: {} | ||||||
|
|
||||||
| permissions: | ||||||
| contents: read | ||||||
|
|
||||||
| concurrency: | ||||||
| group: backlog-index-integrity-${{ github.ref }} | ||||||
| cancel-in-progress: false | ||||||
|
|
||||||
| jobs: | ||||||
| check: | ||||||
| name: check docs/BACKLOG.md generated-index drift | ||||||
| runs-on: ubuntu-24.04 | ||||||
| steps: | ||||||
| - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | ||||||
| with: | ||||||
| fetch-depth: 1 | ||||||
|
|
||||||
| - name: verify per-row + index parity | ||||||
| shell: bash | ||||||
| run: | | ||||||
| set -euo pipefail | ||||||
|
|
||||||
| # Existence preconditions — fail fast (chatgpt-codex P1 + | ||||||
| # copilot P1 review on PR #492). A missing docs/BACKLOG.md | ||||||
| # or missing generator must NOT silently fall into | ||||||
| # pre-Phase-2 mode and exit 0; that would let an accidental | ||||||
| # delete of the authoritative backlog ship green. | ||||||
| if [ ! -f docs/BACKLOG.md ]; then | ||||||
| echo "ERROR: docs/BACKLOG.md is missing." >&2 | ||||||
| echo "This file is the authoritative backlog (pre-Phase-2)" >&2 | ||||||
| echo "or the generated index (Phase 2+). Either way it" >&2 | ||||||
| echo "must exist. Restore it or revert the deletion." >&2 | ||||||
| exit 1 | ||||||
| fi | ||||||
| if [ ! -x tools/backlog/generate-index.sh ]; then | ||||||
| echo "ERROR: tools/backlog/generate-index.sh missing or" >&2 | ||||||
| echo "non-executable. Phase 1a tooling is required." >&2 | ||||||
| exit 1 | ||||||
| fi | ||||||
|
|
||||||
| # Pre-Phase-2 sentinel: if docs/BACKLOG.md still looks | ||||||
| # monolithic (lacks the "AUTO-GENERATED" header line | ||||||
| # emitted by generate-index.sh), the drift check would | ||||||
| # fire false-positives on every per-row edit since the | ||||||
| # legacy file isn't yet the generated output. In that | ||||||
| # case, only verify the per-row files themselves are | ||||||
| # well-formed (parseable frontmatter), and skip the | ||||||
| # index-equivalence check. | ||||||
| if ! head -5 docs/BACKLOG.md | grep -q "AUTO-GENERATED by tools/backlog/generate-index.sh"; then | ||||||
|
||||||
| if ! head -5 docs/BACKLOG.md | grep -q "AUTO-GENERATED by tools/backlog/generate-index.sh"; then | |
| if ! head -5 docs/BACKLOG.md | grep -Fq -- "AUTO-GENERATED by tools/backlog/generate-index.sh"; then |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fail instead of downgrading when index marker disappears
The phase switch if ! head -5 ... grep "AUTO-GENERATED ..." treats a missing marker as pre-Phase-2 and exits success after the parseability path, so once Phase 2+ is active a PR can remove or move that header in docs/BACKLOG.md and bypass generate-index.sh --check entirely. In that scenario manual edits to the generated index can merge without drift detection, which defeats the workflow’s stated load-bearing role for Phase 2+.
Useful? React with 👍 / 👎.
Copilot
AI
Apr 25, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
extract_frontmatter_field does not actually guarantee the match is confined to frontmatter if the closing --- delimiter is missing: state stays 1 to EOF, so id:/status:/title: in the body can be mistakenly accepted. If the goal is to reject malformed frontmatter, consider explicitly requiring a closing delimiter (and failing non-zero if it’s absent) before attempting to extract fields.
Copilot
AI
Apr 25, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The “non-empty values” validation can be bypassed by values that are only quotes or whitespace (e.g., title: "" or title: ): the awk extraction returns the raw remainder of the line and the -z check treats it as non-empty. To enforce the stated invariant, trim surrounding quotes and trailing whitespace (ideally matching tools/backlog/generate-index.sh’s extract_field behavior) before the emptiness check.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fail when per-row backlog tree cannot be scanned
The row loop reads from process substitution done < <(find docs/backlog ...), but under bash set -e a failing find here does not terminate the script; if docs/backlog is deleted/renamed, the loop runs zero iterations and the job still exits 0 after --stdout. That means this integrity check can report green even when the per-row source tree is missing, so add an explicit existence check (or otherwise enforce find success) before trusting row_count=0.
Useful? React with 👍 / 👎.
Uh oh!
There was an error while loading. Please reload this page.