Repository navigation
feat(ci): add the upstream-apply-changelog release lane - #12508
Merged
Merged
Conversation
apply --ci <release|range> decides every row on evidence with nobody to answer: only a cited, verified correct row confined to one owner plugin and not security-relevant is committed on a local branch per owner plugin, and every other row becomes a handoff with a dedupe key. It asks nothing, pushes nothing, opens no pull request, files nothing and never moves the read marker. It writes ci-plan.json for the release-intake workflow's push and publish jobs. Two eval cases cover the capped-range stop and an adopt row handed off. Refs: #7407 Co-Authored-By: Claude Opus 5.5 <[email protected]>
release-intake.yml runs `/harness-ops:changelog apply --ci <release>` in a model job that holds only the API key and a read-only token, seals the plan and branch bundle to the scan-carrier key, scans them, then pushes each changelog/<plugin>/v<release> branch (mutate-branch) and opens its pull request and files handoffs (publish-artifact) with lanes App tokens minted through the broker, keyed on the release. Adds the lane, its two activities and limits.release-intake-per-day to pr-pipeline.yaml, the lane's rules and grant rows, and a `release` input to request-lane-token. Refs: #7407 Co-Authored-By: Claude Opus 5.5 <[email protected]>
Rename the workflow, scripts, lane, limit, artifacts and handoff marker to upstream-apply-changelog (stage upstream, function apply-changelog), and add the reserved vocabulary entry the standards change adds. The gate now reads the unread releases from changelog-status.sh and skips a release only on its done artifact, so a run after a failed publish opens the missing pull request. lane-rules.json keeps its original formatting. Refs: #7407 Co-Authored-By: Claude Opus 5.5 <[email protected]>
The cap line carries a count suffix, so match its prefix; releases are space-separated, oldest first. Refs: #7407 Co-Authored-By: Claude Opus 5.5 <[email protected]>
The schema's stage enum lists upstream, the scheduled producer that files work into intake, and the lane's name carries that stage word. Refs: #7407 Co-Authored-By: Claude Opus 5.5 <[email protected]>
Add the workflow to the log-api-budget job table, the kill-switch-ok caller list, the read-only cache list and the lane-funding catalog; give live-config a grant test for both activities and request-lane-token release cases (valid, malformed, with another id, check mode). Regenerate the hold floor, which now gates the workflow. Document the release field in the request-lane-token README and the broker body and refusal rows. The workflow's kill-switch-ok is a YAML boolean, publish skips on any push-branches skip-reason, scan and push-branches restore the cache read-only, and a missing space in the scan job's outputs is fixed. Refs: #7407 Co-Authored-By: Claude Opus 5.5 <[email protected]>
The gate's trusted-actor check passed trusted-actors-path and triggering-actor at their action defaults; it now passes only denied-actor-ids, require-pr and require-kind. The step's comment says that actor-only mode refuses a schedule event, so a re-run of a scheduled run stops and the release is dispatched instead. Refs: #7407 Co-Authored-By: Claude Opus 5.5 <[email protected]>
push-branches now recreates each planned branch through the Git Data API (blobs, trees, commits with no author or committer, then the ref) with apply-patch.mjs's createGitHubApi, so GitHub authors and signs every commit as the lanes App, as work-resolve-issue's apply-patch does. It refuses a merge commit, reads blobs as bytes, and refuses a replay whose tree is not the planned head's, so what is pushed is what the scan job scanned. The mutate-branch token's contents write covers every call. A branch that already exists counts only when its tip holds the planned tree. push reports each branch's head as a job output, and publish opens a pull request only for a branch at that head. Refs: #7407 Co-Authored-By: Claude Opus 5.5 <[email protected]>
The model job stages upload-lane-transcript, the seal-lane-output and scan-upload actions it loads, and lib/publish-gate.mjs into RUNNER_TEMP before the root checkout replaces .base. After the leg it copies them back to a fresh .lane-transcript and uploads the sealed transcript of the run step's execution file under the job's id, always and without changing the job's outcome. Nothing loads from ./.base after the root checkout. The skill's memory directory is now its default .work/ (decisions.md Persistence; plugin-artifact-protocol.md), which it resolves on its own because this job removes every project instruction file; .work/ is gitignored, so the plan stays out of the skill's commits. The seal step reads the plan from there. publish restores the Actions cache read-only, since it posts the model's plan text, and the read-only cache test lists it. Refs: #7407 Co-Authored-By: Claude Opus 5.5 <[email protected]>
shellcheck SC2016, which actionlint reports, read the backtick template's
${...} inside the single-quoted node -e script as a shell expansion that
never happens; plain string joins print the same line.
Refs: #7407
Co-Authored-By: Claude Opus 5.5 <[email protected]>
…utable The lane changes docs/conventions/pr-pipeline/, so it carries a minor pr-pipeline fragment describing the upstream-apply-changelog lane, and upstream-apply-changelog.mjs, which starts with #!, is 100755 in the index. Refs: #7407 Co-Authored-By: Claude Opus 5.5 <[email protected]>
…rier key Owner decision (2026-11-02, in the Automation Lanes session): "Second holder (Recommended)". The lane's scan job names the pr-pipeline-scan-carrier environment so its open step gets the key. The callers test pins the holder to that one job and one open step, with contents: read alone, a read-only cache, and the main-only gate before it. The scan also covers the branch commit messages, which push replays as public commits. Co-Authored-By: Claude Opus 5.5 <[email protected]>
The publish job filed every plan handoff as its own issue. It now files one issue per release, titled "Claude Code v<release> changelog: <n> handoffs", opening with a release marker and holding one checklist item per handoff, each body indented under it with its key marker. Bodies over GitHub's issue body limit are cut in proportion and a line says so. An issue carrying the release marker, in any state, is never filed twice; an open one missing keys gets the missing items appended in one update, and a closed one is left alone. The update stays within the existing issues write grant. Co-Authored-By: Claude Opus 5.5 <[email protected]>
When an open release issue lacks keys and appending them would pass GitHub's issue body limit, publish now posts the missing items as one comment on that issue instead of throwing, each with its key marker. Dedupe reads the issue's comments for key markers too, a full page at a time up to a fixed cap, and files nothing when a read fails or the cap is reached. Co-Authored-By: Claude Opus 5.5 <[email protected]>
…orkflow # Conflicts: # .github/workflows/pr-require-checks.yml
A partial or full re-run of upstream-apply-changelog skips the model job, so a retry of the model is a new dispatch. Co-Authored-By: Claude Opus 5.5 <[email protected]>
fileReleaseHandoffs searches with author:app/melodic-automation-lanes, takes an issue only when its user is the lanes App, and reads key markers only from the App's comments, so an outsider's issue or comment carrying the markers cannot suppress a handoff. Co-Authored-By: Claude Opus 5.5 <[email protected]>
Before the credential scan, nothing prints plan or bundle text: validatePlan and checkRawLog name the field and index, the plan parse throws a fixed message, the bundle verifies quietly, and its list-heads output is checked against the validated plan heads without being printed. A plan naming branches with no bundle, or a head equal to the base, fails. Co-Authored-By: Claude Opus 5.5 <[email protected]>
Push no longer replays the model's commits: each branch becomes one lanes App commit on the base whose tree is the planned head's (still checked) and whose message is the plan's scanned pull request title. The path check reads the same net diff, so a merge commit cannot hide a change from it. The scan job scans the net diff and the plan, and no longer logs commit messages, which are no longer published. Co-Authored-By: Claude Opus 5.5 <[email protected]>
The credential scanners read text, so the scan job and push both refuse a branch whose net diff numstat marks any file binary, naming no path. Co-Authored-By: Claude Opus 5.5 <[email protected]>
The scan job rebuilds the bundle from the refs/scan heads it fetched and checked, and uploads that copy with the plan from a fresh directory, so nothing else the model's bundle carried reaches push. Co-Authored-By: Claude Opus 5.5 <[email protected]>
After the plan's checks, the scan job writes every string publish can post (each pull request title and body, the release issue title and body from the same renderer publish uses, and the untruncated handoff items an append or comment posts) decoded to a text file, and scans it before the upload. Co-Authored-By: Claude Opus 5.5 <[email protected]>
…fact The model job takes the sealer, its key and the transcript uploader from a fresh checkout of the base made after the leg, at a cleared path, instead of a RUNNER_TEMP stage the model could rewrite; only the plugins stay staged. The scan job outputs the uploaded artifact's id and the SHA-256 of plan.json and branches.bundle; push and publish download by artifact-ids and check the hashes before the lane token is minted. Co-Authored-By: Claude Opus 5.5 <[email protected]>
The scan job's binary check captured numstat before grep -q, so a SIGPIPE under pipefail can no longer read as no match. Publish verifies the bundle hash as push does, and refuses a bundle the scan job recorded no hash for. Co-Authored-By: Claude Opus 5.5 <[email protected]>
The rebuilt bundle push and publish download is an artifact and still holds each commit's message and blobs, so the scan now reads the per-commit log and refuses a binary file in any commit. The seal step's git calls ignore global and system config and fsmonitor the model could have set during its leg. Co-Authored-By: Claude Opus 5.5 <[email protected]>
…history The scan job rebuilt the bundle from the model's heads, so merge commits, commit headers and blobs a merge added then removed reached the artifact unscanned: git log -p shows no merge diffs and never prints author or committer fields. The scan job now commits each planned head's tree on the base with a fixed identity, date and message (git commit-tree, deterministic) and bundles only those commits. What it scans is the net diff, the plan and the publish text; the per-commit log -p and numstat are gone, and the binary refusal reads the net numstat. The scan job outputs each branch's synthetic commit and tree; push fetches that commit, checks it is one commit on the base holding the recorded tree, and builds its published commit from that tree. The plan's head_sha is only the scan's input. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01VcupojVgxFqBnrah9DwFLE
A ~/.gitconfig the model writes during its leg reached the checkout after it, which copies the global config into its temporary HOME, and every later git call; only the seal step ignored it. GIT_CONFIG_GLOBAL=/dev/null and GIT_CONFIG_NOSYSTEM=1 are now job-level env on the model, scan, push-branches and publish jobs, and the seal step's copies are gone. The leg needs no global config: it commits with the repository-local identity the prepare step sets, and the pinned run-claude-cli and apply-claude-managed-settings actions read none. actions/checkout's safe.directory write under --global is caught and logged, not fatal. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01VcupojVgxFqBnrah9DwFLE
…orkflow Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01VcupojVgxFqBnrah9DwFLE
scan-upload reads diffs.txt as plain text, so a token split across added lines joined with its `+` prefixes and never matched. The scan job now runs the lane-branch scan (scan-reuse.mjs) on every planned head: the diff read as a diff, and each changed file's bytes. The net diff ignores external diff and textconv drivers, and checkouts under GIT_CONFIG_GLOBAL=/dev/null skip the safe.directory write that cannot land there. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01VcupojVgxFqBnrah9DwFLE
…orkflow Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01VcupojVgxFqBnrah9DwFLE
…ase done - A stopped plan keeps and files its handoffs, publishes no branch, and fails publish so the done marker is not uploaded. - An existing branch with another tree and no pull request fails push instead of being skipped. - The start record has one fixed name and the per-day cap lists it by name, so other workflows' artifacts no longer push it off the page. - Create .lane before copying the staged plugins into it. - Restore set-safe-directory: false on the six checkouts whose job sets GIT_CONFIG_GLOBAL=/dev/null: checkout's --global write cannot lock it. - Run the suite through its own Node package in the test-node lane. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01VcupojVgxFqBnrah9DwFLE
Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01VcupojVgxFqBnrah9DwFLE
…ease-intake-workflow
…orkflow Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01VcupojVgxFqBnrah9DwFLE
…filed A stopped plan is the same on every run for its release, so failing publish on purpose only re-ran the model on that release up to the per-day cap. - validatePlan requires a stopped plan's stop_reason to be cap-exceeded or range-not-computed and returns it. - The release issue opens with a line naming the stop_reason, and a stopped plan files it even with no handoffs; the scan text matches. - Publish no longer throws on a stopped plan: once the issue holds every key it succeeds and the done marker uploads. A closed issue missing a key, or a failed write, still fails publish so the release is retried. - ci-mode.md, the workflow comments and the harness-ops fragment say so. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01VcupojVgxFqBnrah9DwFLE
…ease-intake-workflow
…x the branch's CI failures A range-not-computed stop can pass on a later run (the changelog fetch failed, or the page lags the tag), so publish now files nothing for it and outputs done=false; only the done output uploads the done marker. A cap-exceeded stop still files its issue and marks the release done. CI fixes for the new gated workflow: - artifact-retention.json: rows for the started, plan and done artifacts. - model job: API_BUDGET_TOKEN on resolve-config and a job-end log-api-budget step from the trusted checkout. - CARRY_REFUSES (both copies) holds the changelog skill's scripts, which the workflow runs. - Floor pins: 126 classes, 73 person-cleared globs, 54 gate, new digests. - read-lane-broker-check.test.sh: the check-mode refusal names release, with a case for it. Co-Authored-By: Claude Opus 5.5 <[email protected]>
…hold path The lane's model acts on the Claude Code changelog, text written outside the org, and the path check allowed any regular file under plugins/<plugin>/, so injected text could steer it to edit the security review's own skill or the merge risk scorer. The scan job (no write token, after the model) now runs the script's scan subcommand once it fetched each planned head: each net diff must stay in its plugin and change no path in the security review's generated hold-paths list (the hold floor, the trusted-actor list and the security review's own files, written by scripts/sync-hold-floor.py). A hold path rewrites the plan to stopped with stop_reason hold-path and no branch, so publish files the release issue naming it and marks the release done; push refuses a hold path again before minting anything. The lane script's API calls now go through the rate guard, which the rate-guard test requires of every lane script that calls fetch. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01VcupojVgxFqBnrah9DwFLE
…h stop Only the scan job's path check sets stop_reason hold-path; a model plan that claims it would file an issue saying a branch changed a hold path. The scan subcommand now refuses it. The scan test's child process runs git with the isolated config the scan job uses. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01VcupojVgxFqBnrah9DwFLE
Contributor
|
The pr-verify verifier failed head What it wrote (47 lines)This pull request leaves the merge queue, and ci-status holds it out while it carries |
github-merge-queue
Bot
removed this pull request from the merge queue due to failed status checks
Nov 3, 2026
#7407 asks for a start within minutes of a release and a per-day cap sized to the most releases in one day. Schedule: every ten minutes (5-59/10, minutes no other schedule here uses). A tick with nothing new makes at most 10 GITHUB_TOKEN REST calls (one done-marker artifact read per candidate); a tick that finds a release adds one cap read, so at most 11. Checkout uses git transport and setup-pinned-node makes no API call. 6 ticks/h x 11 = 66 calls/h, 6.6% of the 1,000/h GITHUB_TOKEN limit and 9.4% of its 700/h plan (rate-limits.json plan 0.7), leaving the rest to the other lanes. Window: --ci never moves the read marker, so the unread list only grows (16 today past 2.1.296), and changelog-status.sh's replay cap omitted the list beyond 10 releases, which made the scheduled gate throw. The gate now raises the status caps so the list always prints and checks only the ten newest unread releases, newest first, which bounds the per-tick cost; older ones wait for a person to move the marker. Each tick re-reads the list, so every release of a burst is taken on a later tick. Cap: 3, the most anthropics/claude-code releases published in one UTC day or any 24 hours from 2026-05-02 to 2026-11-01 (158 releases, GitHub releases API, read 2026-11-02). The schema default and the workflow's fallback follow. Refs: #7407 Co-Authored-By: Claude Opus 5.5 <[email protected]>
Contributor
Post-merge mutation testingKilled 3, survived 2, timeout 0, no coverage 0, invalid 0, not run 2.
Surviving mutants
The run's summary has the full report. |
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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Refs: #7407
Summary
The #7407 lane (steps 3 onward; what stays open is under Remaining for #7407):
.github/workflows/upstream-apply-changelog.yml, the lane that turns each Claude Code release into changelog-apply pull requests without a person. On schedule, or a dispatch by a listed human, it asks the lanes token broker for an App token keyed on the release, runs/harness-ops:changelog apply --ci(from #12444) in a read-only model job, scans everything the model produced, then pushes each planned branch as one signed lanes App commit, opens its pull request, and files the release's handoffs as one issue.Owner decisions this builds on, verbatim:
Fix
GIT_CONFIG_GLOBAL=/dev/nullandGIT_CONFIG_NOSYSTEM.hold-pathsblock inpr-review-security-hosted.yml, matched withmerge-holds.mjsmatchesHoldPath), asstop_reason: hold-path; push checks again before minting. A plan claiming that stop itself is refused.rateLimitLatch.guard).range-not-computedstop files nothing and leaves the release undone, so the next scheduled run retries it; other stops file and mark the release done.changelog-status.sh.Verification
upstream-apply-changelog.test.mjs,callers.test.mjs,request-lane-token.test.mjs,live-config.test.mjs,log-api-budget.test.mjs;scripts/check-affected.sh --base origin/main.Residuals, deferred to the hardening phase:
diffs.txtas plain text, so a token split across two added lines can pass (LOW); fix by runningscan-reuse.mjs'sscanBranchper head.plan.json(scanned model output) for 90 days; uploading only the release key would keep no model text.Remaining for #7407
Done in this PR, against the #7407 requirements the verifier named:
5-59/10 * * * *). A tick with no new release makes at most ten GITHUB_TOKEN REST calls (one done-marker read per candidate), at most 60 an hour of the token's 1,000; a tick that finds a release adds one call for the cap. Checkout runs over git transport andsetup-pinned-nodemakes no API call (itsaction.ymlheader).limits.upstream-apply-changelog-per-dayis 3, the most anthropics/claude-code releases published in one UTC day or any 24 hours from 2026-05-02 to 2026-11-01 (158 releases, GitHub releases API, read 2026-11-02). Dispatches and retries count against it.workflow_dispatchcan be lost that way, when a later run replaces it; dispatch it again.Left for later, or by design:
--cinever writes the ledger (plugins/harness-ops/skills/changelog/context/ci-mode.md,marker.advanceis always false). The marker moves in the ledger pull request once every row is final. The lane uses theupstream-apply-changelog-done-<release>artifact instead, so this lane does not meet feat(lanes): Claude Code release intake lane #7407's "advances the read marker" acceptance line as written.docs/upstream/claude-code.md(today it is at 2.1.296, with 16 releases unread). The ten-release window also keeps the gate inside the done marker's 90-day retention at the observed release rate.run-claude-cliwith its defaultcli-version: latest, not the analyzed release. The skill's version warning (plugins/harness-ops/skills/changelog/SKILL.md:45-47) fires only when the installed CLI is older than the newest release in the range, andlatestis never older than a release the changelog already lists (on 2026-11-02 the status script read installed 2.1.317 against changelog latest 2.1.316). Pinning the exact release would run backlog releases on an older CLI than the plugins on main expect, fail the install if npm lags the changelog, and conflict with the lanes' no-pin decision (docs/conventions/pr-pipeline/README.md, Claude Code CLI releases, ci(pr-pipeline): re-run the lanes' shell-permission sweep on each Claude Code version bump #8595).diffs.txtscan (split-token, LOW) and the 90-day done marker keepingplan.json.Related
--cimode of changelog apply, step 2)🤖 Generated with Claude Code
https://claude.ai/code/session_01VcupojVgxFqBnrah9DwFLE