Skip to content

Let catalog-freshness open a reviewable, off-by-default evidence PR - #95

Merged
seathatflowsinourveins merged 13 commits into
mainfrom
claude/catalog-refresh-pr-20260923
Sep 23, 2026
Merged

seathatflowsinourveins merged 13 commits into
mainfrom
claude/catalog-refresh-pr-20260923

Conversation

@seathatflowsinourveins

Copy link
Copy Markdown
Owner

Draft, awaiting Opus and Codex review. Adds an off-by-default propose job to catalog-freshness.yml that opens an evidence-only PR, meaning no selection or pin changes; pin bumps require a qualified receipt. Its required checks are dispatched on the bot branch, because GITHUB_TOKEN events don't trigger pull_request runs. Decision record: docs/decisions/2026-09-23-bot-pr-dispatch.md. Local checks: validators pass, 2503 unit tests OK, zizmor, actionlint and gitleaks are clean. The live round trip will be tested after merge, once the Actions PR-creation setting is enabled.

🤖 Generated with Claude Code

seathatflowsinourveins and others added 13 commits September 22, 2026 23:14
The freshness job now diffs against the newest published
catalogs/sota-convergence/manifest-*.json (sorted by name) instead of a
hard-coded dated filename, exposes a `drift` job output, and reports the
test suite's skipped-test count in the job summary.

A new `propose` job (needs: freshness, off by default; runs only on a
manual open_pr:true dispatch or a scheduled run with the repository
variable CATALOG_FRESHNESS_PROPOSE=true) turns a detected drift into a
force-created automation/catalog-freshness branch and PR: it copies the
run's drift.md/manifest artifact into evidence/artifacts/, writes a
scripts/validate.py-shaped upstream_provenance receipt under
evidence/receipts/, registers both via scripts/host_receipts.py's
register_file (matched by path/id, never list position), conditionally
rebuilds/rehashes docs/ecosystem/index.html only while it stays a
tracked file, then commits and pushes with the job's own GITHUB_TOKEN
via GIT_CONFIG_COUNT/KEY/VALUE (never persisted to disk) and dispatches
validate.yml/token-report.yml on the branch, since a GITHUB_TOKEN push
does not trigger pull_request-event runs. It never touches
catalogs/sota-convergence/*, catalogs/landscape/*.json,
manifests/stack.json, or layer-verdicts* -- those stay owned by the
separate SOTA-convergence lane review.

The receipt/registration logic is factored into scripts/freshness_propose.py
(new) so it is unit-testable independent of the workflow YAML;
tests/test_catalog_freshness_propose.py covers drift-table parsing,
component-id selection with its stack-component fallback, an end-to-end
fixture that runs scripts.validate.validate() against apply()'s output,
list-order independence, and text-level checks of the committed workflow.
docs/decisions/2026-09-23-bot-pr-dispatch.md records the evidence
(GitHub's GITHUB_TOKEN and Actions-settings documentation), the
alternatives (GitHub App token, PAT, stay report-only) and the overturn
condition. docs/github-automation.md gets a matching section covering the
one-time "Allow GitHub Actions to create and approve pull requests"
setting, the CATALOG_FRESHNESS_PROPOSE variable, and why auto-merge stays
off.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Independent Opus review (H1/H2/M1/L1-L6/T1-T3) and Codex cross-family
review (P1) on the prior commit's catalog-freshness propose job and
scripts/freshness_propose.py:

- H1: rebuild_explorer() no longer lets build_ecosystem.py's own stdout
  leak into main()'s single-JSON-document stdout contract (capture_output
  on both the --write and --check subprocess calls); verified with a real
  subprocess integration test against a throwaway git-tracked repo copy.
- H2: propose's checkout now uses fetch-depth: 0, matching validate.yml,
  so host_receipts.py validate can resolve every existing receipt's
  pinned catalog_revision commit instead of failing on a shallow clone.
- M1: a rebuilt row with upstream.latest=None (a bounded --max-repos run
  or an API error, not an observed change) is excluded from the drift
  count and reported separately as "unfetched"; propose additionally
  refuses to run unless this run's own fetch was unbounded and recorded
  zero upstream errors.
- P1 (Codex, refuting the prior design): a workflow_dispatch run's checks
  do not satisfy a required status check on a pull request at all
  (GitHub's troubleshooting docs), so dispatching validate.yml/
  token-report.yml after the push was a green-looking, not-actually-
  required substitute. Removed. propose now relies on the PR's own
  pull_request-triggered runs, which GitHub puts into an approval-required
  state for a GITHUB_TOKEN-created PR (GITHUB_TOKEN docs); the job prints
  the PR URL and instructs a write-access reviewer to approve them. The
  REST approve-a-run endpoint is documented only for fork PRs, so this is
  left as a manual UI step rather than an unverified automatic call.
  docs/decisions/2026-09-23-bot-pr-dispatch.md records the corrected claim,
  the quotes, and the overturn condition.
- Race guards (Codex P2): a single `${{ github.workflow }}` concurrency
  group (no event_name, cancel-in-progress: false) serializes every run
  regardless of trigger; the evidence-branch push uses
  --force-with-lease against the remote tip `git ls-remote` just
  observed, not a plain --force.
- L1: drift.md/PR-body table cells are rendered through a new md_cell()
  helper (backtick-wrapped, `|`-escaped) so an upstream release tag
  fetched from an external API can't break the Markdown table.
- L2: no fallback to an unrelated fixed component set when no drifted id
  matches a stack component; raises instead, so the job fails loudly
  rather than opening a PR with a misleading component_ids.
- L4: the basic-auth header is masked (::add-mask::) before use.
- L6: doc fixes (explorer is rewritten, not just rehashed; `gh variable
  set` instead of a PATCH that fails on a new variable).
- Codex low: refuse to write through an existing symlink at any
  destination this module creates (receipt, drift/manifest copies).
- T3: the diff logic moved into scripts/freshness_propose.py's
  build_drift_report()/compute_drift()/render_drift_markdown(), imported
  by catalog-freshness.yml's diff step instead of duplicated there, so
  the drift-table header and the drift-vs-unfetched rule can't drift out
  of sync between the workflow and the module.

tests/test_catalog_freshness_propose.py rewritten: 52 tests covering all
of the above, including a full normalized `if:` expression assertion (T2)
and a real subprocess suite against a throwaway git-tracked repository
copy for the stdout contract and explorer rehash paths (T1).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…h-pr-20260923

# Conflicts:
#	docs/ecosystem/index.html
#	manifests/evidence.json
Merging origin/main (796f759, #96: docs/ecosystem/index.html is no
longer committed or tracked) left RebuildExplorerSubprocessTests'
setUpClass asserting the file WAS tracked in its scratch copy, which no
longer holds once the copied .gitignore excludes it. Split the class:
RebuildExplorerSubprocessTests now builds its scratch copy the plain way
(matching main's real, untracked-by-default state) and asserts
rehashed_explorer is False; a new TrackedExplorerSubprocessTests
force-adds a locally built index.html to keep exercising
rebuild_explorer()'s --write/register/--check loop for forward
compatibility, explicitly labeled as exercising a path that is currently
dead code on main.

This edit was made after the prior merge commit but not staged into it;
committing it separately here rather than amending, since the merge
commit already has other reviewers' context.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
docs/github-automation.md's propose-job walkthrough now says plainly
that step 4 (rebuild/rehash docs/ecosystem/index.html) is inert today,
since #96 already made the file .gitignore'd and uncommitted; it is kept
for the H1 regression it guards against if a future change ever tracks
the file again, and TrackedExplorerSubprocessTests keeps exercising it.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Independent Opus pass-with-findings review (N1/N2/N2b/N3/N4) and a
Codex cross-family re-review (reproducing N1/N2, plus P2-3):

- N1/N2 (compute_drift bug): the prior code skipped a row entirely
  whenever the *rebuilt* row's upstream.latest was None, before ever
  comparing pin. This hid real pin changes on any no-release repository
  (7 already exist in the published manifest: tavily-cli, skills-ref,
  poppler, ...) and, separately, treated a releases-endpoint failure
  papered over by a tags-endpoint fallback (partial_errors) as ordinary
  clean data. compute_drift() now returns three buckets -- drifted,
  unfetched, no_release -- comparing pin unconditionally and excluding a
  row as unfetched only when it lacks fetch evidence (upstream.pushed_at
  is None) or its raw github-freshness.json record shows a fetch problem
  (error or partial_errors, matched by URL or normalized GitHub slug).
  Regression tests reproduce both Opus's and Codex's exact scenarios.
- N2 (job-level gate): freshness now also emits a partial_errors output;
  propose's if: requires it to be '0' alongside upstream_errors.
- N2b: a missing/malformed github-freshness.json now fails closed
  (raises) instead of silently reading as zero errors.
- N3: corrected the approval instructions (the "Awaiting approval"
  button near the PR's merge box opens the merge status panel, which
  holds "Approve workflows to run" -- not the Actions tab) and the
  GITHUB_TOKEN quote (current wording: "...will not create a new
  workflow run, with the following exceptions: ..."), and noted the
  30-day auto-deletion of unapproved runs.
- N4: render_drift_markdown() now receives only rebuilt_path.name, never
  the absolute $RUNNER_TEMP path that used to land in committed evidence.
- P2-3 (Codex): the workflow-level concurrency group's default
  queue:single let a plain scheduled run silently replace a pending
  manual open_pr:true request. queue:max is the documented fix but is
  rejected by this repository's pinned actionlint 1.7.12 (checked
  directly). Moved concurrency to a job-scoped group on `propose` only,
  keyed on manual vs. scheduled, so the two categories can never replace
  each other's pending slot; --force-with-lease remains the actual
  data-safety guard for the resulting rare concurrent-execution case.
- Also documented that force-creating automation/catalog-freshness from
  main on every run discards any commit a human pushed to it directly.

docs/decisions/2026-09-23-bot-pr-dispatch.md records the corrected
quotes/wording as corrections, not silent edits, plus the queue:max
rejection and the chosen alternative with its overturn condition.
tests/test_catalog_freshness_propose.py: 76 tests, including exact N1
(skills-ref 0.1.0->0.1.1) and N2 (503+tag-fallback) reproductions at
both the compute_drift() and build_drift_report() levels, N2b fail-closed
coverage, N4's relative-path assertion, and the job-scoped/keyed
concurrency text checks.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…h-pr-20260923

# Conflicts:
#	manifests/evidence.json
… claim (Opus verification)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ch head (Codex verification)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
52d136a)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… prose (Codex verification of 250adae)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@seathatflowsinourveins
seathatflowsinourveins marked this pull request as ready for review September 23, 2026 05:21
@seathatflowsinourveins
seathatflowsinourveins merged commit 12a5351 into main Sep 23, 2026
13 checks passed
@seathatflowsinourveins
seathatflowsinourveins deleted the claude/catalog-refresh-pr-20260923 branch September 23, 2026 05:21
seathatflowsinourveins added a commit that referenced this pull request Sep 23, 2026
…immutable releases, target ruleset (#108)

* Close the catalog's GitHub automation: OSV/zizmor-online scans, gates, immutable releases, target ruleset

Add security-scan.yml: an osv-scanner job (OSV-Scanner 2.6.0, checksum-verified)
over every tracked lockfile in .github/osv-scanner-lockfiles.json with
--no-resolve, failing on unignored vulnerabilities and uploading SARIF off PRs,
plus a zizmor-online SARIF job. A unittest fails when a tracked lockfile is
missing from the inventory or an ignore lacks a <=90-day expiry.

Gate dependency review at high (warn-only removed) and grype at --fail-on high
with a reviewed, empty .grype.yaml; upload Scorecard SARIF with a job-scoped
security-events write; add a 7-day Dependabot cooldown. publish-catalog.yml
gains a tag-only release job (contents: write only) that re-checks the attested
digests and creates the immutable release with both files attached at creation.

.github/main-ruleset.json becomes the target (dependency-review and osv-scanner
required, strict checks, signatures, CodeQL code_scanning, squash only); the
tag rulesets match live 23829417 and 23859358. Record the evidence, the
CodeQL-vs-zizmor comparison on 168a3a8 and the superseded decisions in
docs/decisions/2026-09-22-github-automation-closure.md; update SECURITY.md,
automation.json, the docs, the regenerated verdicts/explorer and evidence hashes.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* Drop required_signatures from the target ruleset; gate .grype.yaml changes; fix stale gate docs

A measured agent-lab run (2026-09-23) blocked PRs with unsigned branch
commits under required_signatures even though GitHub signs the squash merge,
so the target main ruleset leaves the rule out as keep-but-compare until every
writer signs. supply-chain.yml now runs its grype gate when .grype.yaml
changes, with a test. Scorecard, dependency-review and grype docs no longer
call the new gates report-only, and the CodeQL default-setup row cites the
re-read settings GET.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* Resolve the catalog-automation review findings after rebasing on main

- Rebase onto origin/main 8faca90 (#96, #99-#104, #106): take main's removal of
  docs/ecosystem/index.html, merge main's explorer build/attest/upload steps
  with the release job's digest outputs, and re-register manifest hashes
  with host_receipts.register_file.
- OSV inventory: add a reasoned "excluded" list for the #101 grype
  positive-control fixture; the coverage test accepts only listed,
  fixture-scoped exclusions with an evidence path and still fails on any
  unlisted tracked lockfile. osv-scanner 2.6.0 exits 0 on the 37 listed files.
- Target main ruleset: strict_required_status_checks_policy false (auto-merge
  without a merge queue would stall every open PR when main moves);
  required_signatures stays out; regression tests assert both.
- automation.json: gating lanes move to security_gate_lanes with boolean
  required_check/target_required_check; fresh CodeQL default-setup GET cited.
- foundation.json: restore ci-supply-chain lane gap text, replace stale
  automation.json line citations with JSON-path anchors at 92bb279;
  regenerate verdicts and the handbook section with build_verdicts.
- Handbook automation summary and closure record updated (mlx branch dropped
  after #99, fixture alerts 7-15 dismissed and #105 closed, security updates
  kept on, strict decision, gap-ledger mapping).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* Align merge-queue and osv-scanner catalog text with the strict-off target; record #104 CodeQL triage

- automation.json merge-queue non-adoption no longer cites strict checks;
  it points at the strict-off decision (closure record section 10) with its
  overturn condition.
- osv-scanner selection says "required in the target ruleset once applied"
  and names the 37 listed lockfiles, 2 covered manifests and 1 fixture
  exclusion.
- Closure record section 2: the 10 first-analysis CodeQL alerts were
  resolved in #104, not left for owners.
- Hashes re-registered in manifests/evidence.json.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* Keep Dependabot security PRs off the grype positive-control fixture

ignore: urllib3 on a pip entry scoped to the fixture directory (ignore applies to
security updates; exclude-paths does not). Clarify that osv-scanner becomes a
required check only after the target ruleset is applied.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* Refresh evidence hashes after rebasing onto #95

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* Fix the OSV SARIF path under bash -e; keep the zizmor write token away from the analyzer

shell: bash implies -e, so a findings exit ended the OSV step before the SARIF run
(reproduced; found by an independent-implementation comparison and the Codex
cross-family review). zizmor now runs read-only and a tool-free upload job holds
security-events: write. Tests cover both, plus OSV PackageOverrides and grype
review-by dates.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: seathatflowsinourveins <234074349+seathatflowsinourveins@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant