Skip to content

feat: read the org update policy from the synced standards copy - #1074

Open
kyle-sexton wants to merge 3 commits into
mainfrom
feat/org-update-policy-phase-2
Open

kyle-sexton wants to merge 3 commits into
mainfrom
feat/org-update-policy-phase-2

Conversation

@kyle-sexton

@kyle-sexton kyle-sexton commented Nov 11, 2026 •

Copy link
Copy Markdown
Member

No related issue: phase 2 of the org update policy (standards#987, owner decisions 2026-11-11)

session: GitHub Support

Summary

ci-workflows now runs the org dependency-update policy from the copy of standards components/dependabot-policy/policy.json (schemaVersion 2) that the standards sync places at .github/standards/dependabot-policy/policy.json. It no longer relies on its own hard-coded defaults.

  • bump-versions takes cooldownDays per change class from that copy when the caller's .github/version-bump/policy.yaml sets none. A policy.yaml that still sets cooldownDays keeps its own values until phase 4 removes them. With neither source present, prepare fails closed and never assumes a cooldown.

  • Dependabot auto-merge, through both the reusable pr-automerge-dependabot.yml and this repository's pr-automerge-dependabot-queue.yml, builds its publisher allowlist from the copy when the caller leaves publisher-allowlist at its default. The list is the copy's tier: auto publishers plus fastTrack.github-actions and fastTrack.own. The reusable reads the copy through the contents API at the pull request's base commit, so the PR cannot change it.

    • Absent copy: today's input stands, so auto-merge is never wider than now.
    • Unreadable or malformed copy: nothing is armed, and an earlier arm is disarmed.
    • A caller list other than the default still wins.
  • New composite action check-dependabot-policy is a port of standards' dependabot-policy.mjs. It adds two rules that standards' dependabot-policy.mjs at 2303d43 lacks:

    • semver-major-days, when set, must be at least the major class.
    • every cooldown.exclude entry must be in the ecosystem's fastTrack list, plus own for github-actions.

    The existing floor still holds default-days at or above the patch class.

    A consumer calls it as a step inside ci-status. It is not enabled anywhere yet; that is phase 4.

  • This repository's own .github/version-bump/policy.yaml has 20 tool entries in 4 groups. It also lists 7 pins the bot leaves to a person, each with the reason. A new daily maintenance-bump-pinned-versions.yml workflow runs it.

ci-workflows does not receive the synced policy.json today. Its target block in standards distribution/sync-manifest.yml (lines 515-548 at 2303d43) does not list the dependabot-policy component, and .github/standards/ here holds only runner-policy-contract. Until a standards sync-manifest target adds it (phase 4, or a follow-up):

  • the bump workflow warns and bumps nothing;
  • both auto-merge paths here keep today's allowlist.

Fix

  • bump.py: load_policy resolves cooldownDays once. Both readers use that value: cooldown_days and the python-org source's uv cooldown. A new --dependabot-policy flag and matching action input default to the synced path.
  • gate.cjs: resolvePublisherAllowlist holds the shared resolution. runEvent reads the copy at pull_request.base.sha, and queue.cjs reads the copy in the checked-out main. Neither the publisher-allowlist default nor the reusable's workflow_call inputs changed:
    • standards' automerge-lockstep.mjs parses that default;
    • the runner-policy contract allows only runner, package-auto-merge and review-only-dependencies.
  • check-dependabot-policy:
    • dependabot-policy.schema.json and the test fixture fixtures/policy.json are byte-identical to standards' files at 2303d43.
    • policy.schema.json is standards' schema with additionalProperties: false removed in three places (top level, fastTrack, autoMerge) and schemaVersion relaxed from const: 2 to minimum: 2, so a newer synced copy with added keys still validates.
    • It has its own package.json and lockfile with the same ajv 8.20.0 and yaml 2.9.1 pins.
    • It installs into RUNNER_TEMP.
    • Standards' tests are ported as pins. Five of them changed because the new rules now apply to them; the action's test file lists each one.
  • policy.yaml:
    • Every tool is capped at its current major (line: major; Python is held at line: minor). A merged bump reaches every @main caller, and the owner's decision sends majors to review, while the engine arms auto-merge on every bump.
    • Groups: shellcheck (the action plus actionlint's pin), lychee (offline plus online), golangci-lint (Linux plus Windows), Python (4 actions).
    • Pins left to a person: actionlint (kjanat fork, tag-commit attestation), markdownlint-cli2 (follows standards), Node (standards node-runtime), zizmor (follows dotfiles), govulncheck (go.sum hash), osv-scanner and slsa-verifier (automaticMerge: false), osslsigncode (Ubuntu archive).
  • pr-require-checks.yml test-contracts now installs the engine's pinned yq and runs the new action's tests.
  • Release notes come from PR titles, so the README entries are the only docs change.

Verification

All run locally on this branch:

  • python3 -B -m unittest discover -s .github/actions/bump-versions -p 'test_*.py': 76 tests, OK.
    • New: synced-versus-policy cooldown resolution, fail-closed cases, and the cpython path.
    • New: an offline check that every pin and field in the repository's own policy.yaml resolves to exactly one line, and that each group pins one release. Breaking one yq path turns it red.
  • node --test .github/scripts/pr-automerge-dependabot.test.cjs: 75 pass. node --test .github/scripts/pr-automerge-dependabot-queue.test.cjs: 41 pass. The new cases cover:
    • the base-commit read;
    • an absent copy keeps the old allowlist;
    • fast-track own arms a patch, while a fast-tracked major is skipped;
    • a review-tier publisher is skipped;
    • a caller's narrowed list wins;
    • a malformed copy or a read error (including a non-ENOENT one, the policy path being a directory) arms nothing and disarms an already-armed pull request;
    • the queue's review-only list equals the checker's package.json dependencies.
  • node --test .github/actions/check-dependabot-policy/check-dependabot-policy.test.mjs: 49 pass. Before the new rules were added, the 41 ported standards tests passed. Flipping one new rule's comparison turns tests red.
  • node --test .github/scripts/node-version-agreement.test.cjs passes; it now also covers the new action's node-version.
  • ruff check and format pass, as do biome, shellcheck and shfmt on run.sh, and zizmor 1.30.0 offline on the new workflow and the action. actionlint 1.7.12 (upstream) reports only the known $/ refs, which the kjanat fork used in CI accepts.

What CI cannot exercise here. This PR's lanes call the reusables at @main, and nothing runs live from a PR:

  • the reusable's contents-API read on a real Dependabot PR;
  • the queue against a real synced copy;
  • a live bump-versions prepare, which makes network calls and runs bare gh. The verify blocks (checksums, assetFile digests, npm signature, PyPI, PSGallery, python.org) are untested until the first run with the synced file. Run workflow_dispatch with dry-run: true first.
  • the check-dependabot-policy action end to end in a consumer.

Related

  • standards#987: schemaVersion 2 (cooldownDays, fastTrack).
  • Should standards' dependabot-policy.mjs become a pointer? Recommended yes, once a consumer runs this action inside ci-status (phase 4): then standards keeps data only, as decided. Until then the two copies can drift. The schemas here derive from standards' too (one byte-identical, one relaxed), and standards' README already says the analyzer moves here.
  • Phase 4 needs a standards sync-manifest target that adds dependabot-policy for ci-workflows.

Follow-ups (no code change here)

  • standards REVIEW.md lists too few auto-merge publishers. Lines 86-89 name only actions/*, github/* and anthropics/*. melodic-software/* needs adding there in lockstep with fastTrack.own (owner decision D125).
  • 0.x tools under line: major. line: major now keeps the minor number below 1.0, so a 0.x tool's minor releases are never prepared, even though majors now go to review anyway. Whether to lift the cap for 0.x tools is a phase-3 decision.
  • Consumers with a schemaVersion 1 synced copy. The gate now narrows by autoMerge.ecosystems; a consumer whose synced copy predates schemaVersion 2 may lack an ecosystem there (uv absent would stop uv auto-merge). Check every consumer's synced copy before phase 4.
  • This repository's github-actions Dependabot entry runs daily, which the new checker reports as schedule-not-standard. It predates this PR (main since feat(workflows): Claude lane feature set — config currency, cadence, retry, kill-switches, copy (PR-B, 2b-2i) #280) and is left for the phase that enables the checker here.
  • bump.py does not read fastTrack. A pinned @anthropic-ai/claude-code would wait the 3-day patch cooldown instead of fast-tracking. No such pin exists today; phase 3 or 4 handles it.

Review fixes (follow-up commit)

  • 0.x minors: below 1.0 a minor move is now class major, and line: major keeps the minor number. This matches the Dependabot gate's breaksBelowOne.

  • Majors: arm_auto_merge no longer arms a bump that holds any major move. The PR still opens for review.

  • Checker rules beyond standards: the checker adds two rules that standards' dependabot-policy.mjs at 2303d43 lacks:

    • semver-major-days, when set, must be at least the major class (cooldown-major-below-major-class).
    • cooldown.exclude must be a subset of the ecosystem's fast track (cooldown-exclude-outside-fast-track).

    default-days stays held at or above the patch class by the existing floor. The policy.schema.json here accepts schemaVersion 2 and above and ignores unknown top-level, fastTrack and autoMerge keys.

  • Gate: a synced policy's autoMerge.ecosystems and its autoMerge.reviewOnly entries for this repository now narrow what is armed, whatever the input.

  • Dependabot: dependabot.yml gains an npm entry for the action's lockfile, on the policy's weekly schedule.

Second review fixes (f9c98a2)

  • Major hold wiring: arm_auto_merge takes the bump's rows and calls has_major itself; the hasMajor dict key is gone. A publish_bump-level test with a major row and with a 0.x minor row (0.24.2 -> 0.25.0) asserts --disable-auto is called and --auto --squash never is. With the has_major call removed, both subtests fail.
  • Disarm fails closed: disarm_auto_merge accepts a failed gh pr merge --disable-auto only when the pull request then reads back with no autoMergeRequest; any other failure raises, so a bump rebuilt to a major never keeps an earlier arm. Both outcomes are tested. The not-armed case is identified by reading the PR's state, not by matching gh's error text, which neither gh's source nor GitHub's docs state.
  • Checker runtime held for review: this repository's queue sets REVIEW_ONLY_DEPENDENCIES to ["ajv","yaml"], the check-dependabot-policy runtime that phase 4 runs in every consumer's ci-status, so neither is auto-merged. The phase-4 home is a standards autoMerge.reviewOnly entry for ci-workflows; the env value can go once the synced copy carries it.

🤖 Generated with Claude Code

https://claude.ai/code/session_01VQb9Q1HHi6JACp77x2ZbE7

kyle-sexton and others added 3 commits November 11, 2026 14:45
bump-versions takes cooldownDays per change class from the standards
dependabot-policy copy at .github/standards/dependabot-policy/policy.json
when the policy file sets none; a policy file that still sets them keeps
its own, and with neither prepare fails closed.

The Dependabot auto-merge gate (reusable and merge-queue paths) takes its
publisher allowlist from that copy (tier auto publishers plus the
github-actions and own fast track) when the caller leaves the input at
its default. The reusable reads it at the base commit; an absent copy
keeps today's allowlist, an unreadable one arms nothing.

Adds check-dependabot-policy, a port of the standards analyzer with the
cooldown-class and fast-track exclude rules, and this repository's own
version-bump policy with a daily maintenance-bump-pinned-versions run.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VQb9Q1HHi6JACp77x2ZbE7
bump-versions rates a minor move below 1.0 as major and keeps the minor
number under line: major, as the Dependabot gate's breaksBelowOne does,
and never arms auto-merge on a bump that holds a major move.

check-dependabot-policy follows standards' floor semantics:
semver-major-days, when set, is at least the major class, and the
cooldown exclude must be a subset of the ecosystem's fast track. Its
policy schema accepts schemaVersion 2 and up and ignores unknown
top-level, fastTrack and autoMerge keys.

The auto-merge gate also applies the synced policy's
autoMerge.ecosystems and this repository's autoMerge.reviewOnly entries,
which only narrow. Dependabot now tracks the checker's lockfile.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VQb9Q1HHi6JACp77x2ZbE7
- arm_auto_merge takes the bump's rows and calls has_major itself, so the
  major hold no longer rides on a dict key a caller has to set.
- disarm_auto_merge raises unless gh's --disable-auto succeeds or the pull
  request reads back with no arm, so a bump rebuilt to a major never keeps
  an earlier arm.
- The queue holds ajv and yaml, the check-dependabot-policy runtime that
  consumers run in ci-status, for review.
- The checker's npm Dependabot entry runs weekly, as the policy requires.
- Tests: a publish_bump-level hold for a major and a 0.x minor row, both
  disarm outcomes, and the queue's disarm under a malformed or unreadable
  synced policy.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VQb9Q1HHi6JACp77x2ZbE7
@kyle-sexton
kyle-sexton marked this pull request as ready for review November 11, 2026 23:55
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Nov 11, 2026 •

Copy link
Copy Markdown
Codex Review Summary: ✅ Completed

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-11-12T00:03:06.905754Z f9c98a2 Draft marked ready
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@github-actions

Copy link
Copy Markdown

claude-review reviewed f9c98a2917d8773c1ad971f4ad12749b9da2e3e1: read 24 of 24 changed file(s); 0 finding(s), 0 as inline comments.

No findings. I read every hunk of all 24 changed files. The lane has no shell, so I ran no tests; the PR description's test counts and lint results are the author's and unverified.

Checked and found sound:

  • Gate and queue (gate.cjs, queue.cjs):
    • resolvePolicyInputs fails closed on an unreadable or malformed copy.
    • Both paths disarm an earlier arm in that case.
    • A 404 for the file means no copy, so the caller's input stands.
    • The reusable's job has contents: write, so the contents-API read at the base commit is permitted.
    • The queue's checkout is a full one, so reading the workspace copy works.
  • Bump engine (bump.py):
    • has_major reads the current and target fields that cmd_publish supplies for prepared rows.
    • disarm_auto_merge accepts a failed disarm only when the PR reads back with no auto-merge request.
    • The 0.x handling in change_class and in_line is consistent.
    • Every key policy.yaml uses is one the engine reads.
  • check-dependabot-policy:
    • The action copies the script, both schemas and the lockfile into RUNNER_TEMP, which import.meta.dirname needs.
    • The two added rules match the README and the tests.
  • CI wiring:
    • install-tools.sh appends yq to GITHUB_PATH, so the test-contracts step gets it.
    • The new workflow's if and inputs.* handling is correct for scheduled runs.

Manifest items: none, because the diff changes no plugin.json or marketplace.json.

Closing issue: none ("No related issue"), so no acceptance criteria to check.

Spend: the daily maintenance-bump-pinned-versions.yml run and the added test steps use only existing Actions minutes. Nothing bills, so there is no hold.

Embedded instructions in the PR context: none.

Observations, not findings (the author lists them as follow-ups):

  • line: major pins 0.x tools such as ruff to patch releases only.
  • The gate matches a policy reviewOnly entry by dependency name alone, ignoring ecosystem and directory. That only narrows what is armed.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

claude-security-review: 1 inline finding(s) on f9c98a2917d8773c1ad971f4ad12749b9da2e3e1; the summary is in this lane's pull request comment.

claude-lane: claude-security-review

REVIEW_ONLY_DEPENDENCIES: "[]"
# The check-dependabot-policy action's runtime: consumers run that
# checker in ci-status, so a bump to it waits for review.
REVIEW_ONLY_DEPENDENCIES: '["ajv","yaml"]'

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The review-only list holds only the checker's two direct dependencies, ajv and yaml. The checker's runtime tree has four more locked packages that the list leaves out: fast-uri, fast-deep-equal, json-schema-traverse and require-from-string (check-dependabot-policy/package-lock.json). fast-uri is a runtime dependency of ajv.

The test the queue holds every runtime dependency of the policy checker for review compares the list only with package.json dependencies, so it cannot see the transitive packages.

Path: a Dependabot update (a security update to a transitive package, where the cooldown does not apply) names one of those packages. The gate only checks reviewOnly.includes(name) at gate.cjs:196, and a patch or minor npm_and_yarn update passes. The queue then arms it with no review, and the new code runs in every consumer's ci-status in phase 4.

This is low likelihood and nothing is enabled yet. You could add the transitive names, or derive the list from the lockfile in the test, so the stated intent that the checker runtime is never auto-merged holds.

severity: SUGGESTION

claude-lane: claude-security-review

@github-actions

Copy link
Copy Markdown

claude-security-review reviewed f9c98a2917d8773c1ad971f4ad12749b9da2e3e1: read 24 of 24 changed file(s); 1 finding(s), 1 as inline comments.

Security review of #1074 at f9c98a2. I read every hunk of all 24 files. I found no CRITICAL issues and one SUGGESTION. This is the first review of this lane for this pull request, so it is not blocking-only.

critical-findings: 0

What I checked

  • Auto-merge gate (gate.cjs).
    • The synced policy is read through the contents API at pull_request.base.sha, so the pull request cannot change its own tier. The read is at gate.cjs readBasePolicy.
    • A 404 keeps the caller's input as before. Any other read error, non-JSON text, or a wrong shape fails closed. The caller disarms an earlier arm.
    • A synced policy only narrows: autoMerge.ecosystems bounds the armed ecosystems, and autoMerge.reviewOnly entries for this repository are never armed. Moving the review-only check ahead of the ecosystem branch also applies it to GitHub Actions names, which is narrower than before.
    • Widening happens only when the caller leaves publisher-allowlist at its exact default. The list then becomes the policy's tier: auto publishers plus fastTrack.github-actions and fastTrack.own. That reaches melodic-software/* patch and minor bumps. It is owner decision D125 and needs a standards sync that does not exist for this repository yet, so no behavior changes today.
  • Queue (queue.cjs). It reads the policy from the checked-out main. A read error or a malformed copy sets policyFailure, which skips the gate and takes the existing disarm path. An undefined GITHUB_WORKSPACE also lands in that failure path, so it fails closed.
  • bump.py.
    • disarm_auto_merge raises unless the pull request reads back with no autoMergeRequest, so a failed disable no longer leaves an old arm live.
    • arm_auto_merge never arms a bump holding a major move. Below 1.0 a minor move counts as major. All of these only narrow.
    • load_policy fails closed when neither a cooldownDays source nor the synced copy exists.
    • BUMP_DEPENDABOT_POLICY is passed through env and then a quoted bash array, so there is no injection.
  • maintenance-bump-pinned-versions.yml.
    • Triggers are schedule and workflow_dispatch only. The job is limited to refs/heads/main, and it checks out ref: main with persist-credentials: false.
    • The job token is read-only. The App token is minted only after prepare and is not available to the verification and download step. The token request is contents, pull-requests and workflows write, scoped to this one repository.
    • The tools input reaches the engine only through env. No expression sits inside a run: script.
  • Checker action.
    • The action copies its files into RUNNER_TEMP and installs with npm ci --ignore-scripts from a lockfile with integrity hashes. Inputs reach the script through env and quoted arguments.
    • Both schema changes (additionalProperties removed, schemaVersion relaxed to minimum: 2) leave the required fields and value checks in place.
    • The checker reads the policy from the consumer's own checkout. The gate reads it from the base commit. The checker is not enabled anywhere yet, so this has no effect today. When phase 4 turns it on, confirm the sync keeps policy.json a managed file the pull request cannot hand-edit.
  • pr-require-checks.yml. The new test-contracts steps run npm ci --ignore-scripts and node --test on pull-request code in the same job that already runs the Python tests from the head. No new secret or permission is involved.
  • Manifests. This repository has no plugin.json or marketplace.json in the diff, so the manifest rules do not apply.

Owner-threat-model note (one line, no hold). The new daily workflow arms auto-merge on third-party tool pins, several marked unverified (shellcheck, shfmt, typos, yq), after the cooldown and capped at the current major. This removes the manual pin/repin ceremony, which the throughput-phase section exempts, so I report no finding for it.

Spend. The daily workflow and its checks use Actions minutes and no paid plan or billing setting, so there is no cost hold.

Instruction-surface lens. The diff deletes no standing instruction. The protected-class register was not needed.

Finding

  • SUGGESTION: the review-only list holds only the direct dependencies ajv and yaml. See the inline comment.

This branch has not been deployed

No deployments
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