f7df920681f233ae0fe064ee659550bdf41ff708
9 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
dced41f536 |
chore(#3211): accept a non-closing issue reference for docs/test-only PRs (#3289)
* test(#3211): failing-first coverage for the issue-link follow-up exemption Adds the regression suite before the policy module exists, so the RED state is recorded against a real verdict rather than asserted. Covers the reported gap (a fork test-only follow-up PR cannot satisfy the gate without an inert closing keyword) and the file-list truncation vector that any diff-shape exemption must fail closed on. Refs #3211 * chore(#3211): accept a non-closing issue reference for docs/test-only PRs The `Issue link required` gate modelled exactly one PR->issue relationship — "this PR closes that issue" — and its sole exemption additionally required same-repo identity (#1389), so a fork PR had no exemption path of any kind. A test-only or docs-only follow-up therefore had to ship a knowingly-inert `Closes #<already-closed-issue>` to get a green check. The verdict now lives in scripts/require-issue-link-policy.cjs as a pure, unit-tested function returning a typed reason. It additionally accepts a non-closing reference (`Refs #N`, `Follow-up to #N`, ...) but only when every changed file is under tests/, under docs/, or is a root-level *.md — the same doc-only shape pre-pr-gate.sh:111 recognizes, minus CHANGELOG.md, which changeset/lint.cjs classes as user-facing. Source-touching PRs still require a closing keyword and a PR with no reference at all still hard-fails, so gate strength is unchanged. Both constraints the issue names as hard requirements are preserved: the backmerge exemption keeps its same-repo conjunct, and the failing step's `if:` stays step-level so the required check reports SUCCESS rather than a branch-protection-blocking `skipped`. Also closes a forgery vector found while building this. `gh pr view --json files` returns at most 100 paths and does not paginate, while the payload's `changed_files` reports the true total (verified live: PR #3202 returns 100 of 118). A >100-file PR could therefore present a falsely tests-only list. The new shared helper scripts/lib/pr-changed-files.cjs fails closed when the list cannot be confirmed complete, and the pre-existing tooling-paths carve-out in scripts/pr-template-policy.cjs — which relaxed template enforcement on the same untrustworthy list — now uses it too. Closes #3211 * fix(#3211): treat the authoritative file count as authority at every list size Both orthogonal review passes independently found the same blocker. `fileListIsComplete` only compared the list length against the PR's true `changed_files` count once the list reached the 100-entry page cap, so any mechanism that shortened the list BELOW the cap went undetected: evaluateIssueLink({prBody:"Refs #1", sameRepo:false, changedFiles:["CONTRIBUTING.md"], changedFilesTotal:3}) -> {ok:true, reason:"ok_followup_reference"} The concrete exploit was a $GITHUB_OUTPUT heredoc collision. Both this workflow and pr-template-format.yml wrote the file list with a fixed terminator (`GSD_EOF` / the even weaker `EOF`), and every path in that value is attacker-controlled on a fork PR. A file named after the delimiter closes the value early and drops every path after it, so a fork PR touching src/ could present a list of only its exempt-looking files and take the follow-up exemption. That is exactly the #1389 property this change is required to preserve. Fixed in two independent layers: 1. The total is now the authority at every size, not only at/above the cap. One rule catches truncation, delimiter collision, and a path containing a newline, without having to enumerate the mechanisms. 2. Both workflows now use an unguessable random delimiter, per GitHub's documented guidance for untrusted multiline output. Also from review: pr-template-format.yml never passed CHANGED_FILES_TOTAL, so the parameter threaded through evaluatePrTemplate was always undefined in production and would have permanently blocked the tooling carve-out for any 100+-file PR; its env is now wired. Root-doc exclusion is case-insensitive. Dropped a no-op `tr '\n' '\n'`. Refs #3211 * chore(#3211): regenerate install-tree fixtures for the new shared helper scripts/lib/** ships in the install tree, so adding scripts/lib/pr-changed-files.cjs drifts all 19 golden fixtures by exactly one path each. Caught by tests/golden-install-tree.test.cjs (25 failures on the remote runner), which is the drift detector doing its job — not a defect. Placement is deliberate: every existing occupant of scripts/lib/ is a CI/dev helper that already ships (alias-drift-families, allowlist-ratchet, cli-exit, drift-scan), so a shared helper used by two policy scripts belongs there. The two policy modules themselves live at the top level of scripts/ and do not ship. Regenerated with `npm run gen:install-tree`; the delta is one added path per fixture and nothing else. Refs #3211 * fix(#3211): keep the shared CI helper out of the shipped install tree The remote runner reported 6 failures on the previous head. Two causes. `scripts/lib/**` is enumerated in `bin/install.js` (GSD_SCRIPTS_LIB_FILES) and ships to users, and the install suite asserts that enumeration is complete. Putting the new shared helper there broke four install tests and drifted all 19 golden install-tree fixtures. The right answer is not to add it to the manifest — it is CI-only tooling used by two scripts that do not ship, so it has no business in a user's config directory. Moved to `scripts/pr-changed-files.cjs`; top-level `scripts/` ships only what the installer names explicitly, so nothing is enumerated and nothing ships. The fixture regeneration from the previous commit is reverted: the install-tree fixtures are byte-identical to `next` again, and `bin/` is untouched. That also keeps the diff free of any user-facing path, so no changeset fragment is required. The other failure was a stale test, not a regression. The workflow carve-out suite asserted the backmerge exemption by grepping require-issue-link.yml for `startsWith(github.head_ref, ...)` and `steps.check.outputs.found`. This change moved the whole verdict — carve-out included — into the policy module and renamed the step, so those assertions measured a location the logic no longer occupies. Rewritten to lock the property at its new home, and made stronger in the process: the step-level placement is now verified by PARSING the YAML and asserting the job carries no `if:` of its own (a job-level `if:` would make the required check report `skipped` and block branch protection), and the #1389 anti-forgery conjunct is asserted BEHAVIORALLY against evaluateIssueLink for both sameRepo branches rather than by matching text. The bootstrap fallback grep is locked too, so the introducing-PR path cannot be silently dropped. Refs #3211 * fix(#3211): correct the contributor guidance and pin it against the rule The sticky comment the gate posts still described the qualifying diff shape as "nothing outside tests/ and docs/". The predicate had since been widened to also accept root-level *.md, so the guidance was narrower than the rule it describes — and narrower in the worst direction: a contributor whose PR is CONTRIBUTING.md plus a test, which is exactly the shape #3211 was filed about, would have been told they do not qualify while the gate was in fact passing them. The two failure explanations now name all three accepted shapes and the CHANGELOG.md exclusion. This is a shared-rule-across-parallel-surfaces drift: the guidance restates a rule whose definition lives in EXEMPT_PATH_PREFIXES / isRootLevelDoc / EXCLUDED_ROOT_DOCS. It was caught by eye, which is not a control. Added the parity assertion CLAUDE.md prescribes for exactly this: the test parses the workflow, pulls the github-script body out of the failing step, and asserts it names every entry of EXEMPT_PATH_PREFIXES and every entry of EXCLUDED_ROOT_DOCS — derived from the module's exports, never from a second hardcoded copy — plus the root-level shape and an actionable `Refs #` example. The test is non-vacuous by construction and by demonstration: it guards against zero-length iteration and an empty script body, and removing any single expected token from the real text makes it fail (verified per token, plus the empty-string case which reports all five missing). Refs #3211 --------- Co-authored-by: sim <sim@local> |
||
|
|
1794acb255 |
chore(#2331): trigger PR-policy workflows on pull_request_target so fork PRs get the verdict (#2333)
* chore(#2331): trigger PR-policy workflows on pull_request_target Three PR-policy workflows (pr-title-validator, pr-target-validator, require-issue-link) triggered on plain `pull_request`, so a fork PR's GITHUB_TOKEN was downgraded to read-only regardless of the declared `permissions:`. Each one comments on the PR and THEN emits its verdict, so the createComment 403 killed the github-script step before core.setFailed ran: the contributor saw an API stack trace instead of the instructions the comment exists to deliver. Confirmed on PR #2084 (job 86573823878), whose title has been non-compliant since 2026-07-08 while the explanatory comment 403'd on every run. Switches all three to pull_request_target (base-repo context, write-capable token), matching the three siblings that already do this correctly (pr-template-format, close-draft-prs, auto-close-unsolicited-prs). Safe: the only checkouts are BASE-branch with persist-credentials: false, and every PR-controlled input is read as data — no head code executes. Also wraps each comment in try/catch so a comment failure can never again suppress the verdict. Extends tests/workflow-maintainer-skip.test.cjs with the trigger lock already applied to close-draft-prs.yml (:32-42) for this same defect class. Closes #2331 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(#2331): strip backticks before echoing untrusted text into bot comments Found by the orthogonal security review of this change. pr-title-validator and pr-target-validator echo attacker-controlled text (the PR title; the fork's branch name) into an inline-code span in a comment posted by github-actions[bot]. A single backtick closes the span early and the remainder renders as live Markdown — GFM autolinks a bare URL — so a fork author could make our own bot post an arbitrary clickable link into a PR thread, borrowing the bot's credibility for phishing. This interpolation is unchanged from next, but it was NOT previously reachable from forks: the createComment call 403'd and the comment was never posted. The trigger switch in the parent commit is what makes it reachable by untrusted authors for the first time, using the write token it grants — so it is in scope here and fixed here rather than deferred. A PR title has no charset restriction, so that vector is fully exploitable. The branch-name vector is weaker (check-ref-format forbids space, ':', '[' and '*', so no bare URL, link or emphasis is expressible) but is the same class and is stripped identically rather than left to the charset to police. Stripping the backtick is complete: it is the only character that can break out of an inline-code span. Only the rendered body needs this — core.warning/setFailed go to the job log, where @actions/core already escapes workflow commands. Also strengthens the try/catch test to assert core.setFailed sits AFTER the catch block rather than merely existing, so moving the verdict inside the try (the exact inversion #2331 fixes) fails the test. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * test(#2331): assert verdict ordering on code, not on comment prose The first cut of the verdict-ordering guard failed against correct code. It used indexOf('core.setFailed') on raw source, and these workflows name core.setFailed in their own comments while explaining the bug — at lines 29/118/147, 16 and 6, all BEFORE the catch block. So the assertion compared a comment to the call and reported the inversion it was written to catch. gsd-test caught it: 4 unique failures across linux-node22/24. The code was right; the test was measuring the wrong text. Fixes: - readWorkflowCode() strips whole-line YAML/JS comments so positional assertions see only executable text. - The ordering check is extracted to verdictSurvivesCommentFailure() and exercised against BOTH a good and an inverted sample, so the guard is proven non-vacuous rather than merely passing. - A test pins the trap itself: raw source really does mention core.setFailed before the catch, while the stripped view puts the real call after it. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(#2331): drop the unnecessary permission widening; the trigger was the whole bug Both orthogonal review passes flagged the permissions block, from opposite directions — one said issues:write was dead surface on require-issue-link, the other said it was the load-bearing scope the two validators lacked. Neither is right, and the repo's own history settles it: - pr-title-validator declares pull-requests:write ONLY, and its sticky comment has posted 26 times. - pr-target-validator declares pull-requests:write ONLY — posted 8 times. - require-issue-link declares issues:write ONLY — posted on same-repo PRs #106, #164, #232, #259. So GitHub accepts EITHER scope for issues.createComment when the target is a PR, and all three files already declared a sufficient one. The 403 was purely the fork token downgrade. My added scopes fixed nothing and widened privilege on precisely the workflows now running as pull_request_target — the context where surplus scope matters most. Reverted: permissions are byte-identical to next, and the diff is now trigger + try/catch + sanitizer only. The permission test previously used an (issues|pull-requests) alternation, so it passed on the pre-fix tree and would not have caught removal of the scope that matters. It now asserts each file's SPECIFIC scope and, more usefully, asserts the absence of the other — locking the least-privilege property against a future 'add it to be safe' regression. It is a forward lock, not a #2331 fails-first test; the trigger assertion is the fails-first one. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
8f2ebbe9bf |
feat(#1928): remove sunset Gemini CLI runtime, redirect to Antigravity (#1996)
* feat(#1928): remove sunset gemini cli runtime, redirect to antigravity Google sunset Gemini CLI on 2026-06-18; Antigravity CLI is its official successor (already a first-class GSD runtime). Remove the gemini runtime from the enum (16->15), aliases, labels, config-home fragment, install path, converters (convertClaudeToGemini{Markdown,Toml,Agent}, convertSlashCommandsToGeminiMentions), capability descriptor, gemini-extension.json, RULESET.GEMINI.*, and the interactive menu (renumbered, no gap). --gemini now prints an explicit deprecation notice citing the 2026-06-18 sunset and redirects to --antigravity (no silent alias, per the issue's Hyrum's-Law rejection). Antigravity is preserved throughout: its GEMINI.md contextFileName, .gemini/antigravity config home, the shared convertGeminiToolName/claudeToGeminiTools tool vocabulary, and the 'gemini' hookEvents dialect it declares. GEMINI.md retargeted as Antigravity's context file. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore(#1928): backfill changeset PR number (#1996) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore(#1928): drop Gemini CLI from issue templates (review nit) Removes the sunset Gemini CLI runtime from the two GitHub issue-template runtime lists that the removal PR missed, per @davesienkowski's review nit: - feature_request.yml: 'Applicable runtimes' checkbox (a user could otherwise request a feature for a runtime GSD no longer supports) - bug_report.yml: 'Runtime' dropdown + the stale ~/.gemini/settings.json retrieval-help line Leaves the post-removal templates fully consistent with the Antigravity redirect. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
2391632973 |
feat(#1855): add Claude plugin marketplace manifest
Add .claude-plugin/marketplace.json so Claude-plugin-compatible runtimes
(ZCODE et al.) discover gsd-core from a custom marketplace source. The
canonical version lives at plugins[0].version and tracks package.json via
the release version-sync.
Refactor scripts/sync-manifest-versions.cjs so VERSIONED_MANIFESTS entries
are {path, versionKey} dot-path descriptors (default 'version'); register
marketplace.json with versionKey 'plugins.0.version'. getByPath/setByPath
reject __proto__/constructor/prototype (prototype-pollution guard).
plugin.json / gemini-extension.json behavior is unchanged.
- tests/issue-1855-marketplace-manifest.test.cjs: schema + version-sync guard
- tests/issue-844-manifest-version-sync.test.cjs: updated for descriptor shape
- VERSIONING.md + auto-backmerge VERSION_STAMP_MANIFESTS: include marketplace.json
- docs/how-to/install-on-your-runtime.md: marketplace discovery how-to
|
||
|
|
0c9f86d495 |
fix(#1404): version-only manifest bumps no longer park the back-merge (#1405)
auto-backmerge's needs_review safety net parked the PR whenever a code file existed only on main. The version manifests (package.json, package-lock.json, .claude-plugin/plugin.json, gemini-extension.json) diverge every release by design (next runs a -dev version), so the net misfired on every release — and that manual-review park is what let the back-merge sit and go stale (e.g. #1379, which then conflicted with a later #1777 purity-gate edit to fragments it had deleted). Exclude the generated package-lock.json outright (it carries a version per package entry, so a dep bump is indistinguishable from a release stamp; it only mirrors package.json, still checked). For package.json / plugin.json / gemini-extension.json, ignore a drop whose main-vs-base diff touches only the top-level "version" field. A substantive (non-version) straight-to-main change still parks, preserving the safety net's real purpose. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
ef2c0a2b7b |
fix(#1389): exempt auto-backmerge PRs from the Require Issue Link gate (#1391)
The required "Issue link required" check failed on automated back-merge PRs (chore/backmerge-main-to-next-<sha>), which legitimately map to no issue — a `Closes #N` would pollute the released CHANGELOG. When such a PR parks for manual review (needs_review), the maintainer could only merge via --admin. Carve them out at the failing step's `if:` (step-level, so the required check still reports SUCCESS rather than a branch-protection-blocking "skipped"), keyed on the workflow-authored branch name AND same-repo identity so a fork PR cannot forge the exemption. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
60e2e5e5f3 |
feat(#761): add scheduled base-context sweep to close SHA-branch-evading draft PRs (#765)
close-draft-prs.yml (on pull_request_target after #760) cannot close fork draft PRs whose head branch name looks like a Git SHA — GitHub never dispatches pull_request_target for such branches, and a pull_request run from a fork gets a read-only token. So a draft PR on a SHA-named fork branch evades the auto-close. Add close-draft-prs-sweep.yml: a schedule (every 6h) + workflow_dispatch sweep running in base-repo context with pull-requests: write that paginates open PRs, filters to non-OWNER/MEMBER/COLLABORATOR drafts, and closes + comments them with the identical policy/message as the event-driven workflow. Re-fetches each candidate before mutating (TOCTOU guard), closes before commenting so enforcement is never gated on the explanatory comment, and core.setFailed on partial failures. The per-PR workflow remains the fast path; this is the safety net for the documented residual bypass. Extends tests/workflow-maintainer-skip.test.cjs with structural guards locking the triggers, write permission, maintainer carve-out, pagination, and message. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
f44605093e |
fix(#758): trigger draft-PR auto-close on pull_request_target (#760)
Bare `pull_request` hands fork PRs a read-only GITHUB_TOKEN, so the close/comment API calls 403 and a first-time/external contributor's draft PR survives — bypassing the auto-close for exactly the population the job targets. Switch to `pull_request_target`, which runs in the base-repo context with a write-capable token even for fork PRs. Safe because the job never checks out or executes PR-supplied code; it only reads event metadata and calls the GitHub API. The minimal `permissions: pull-requests: write` block still constrains the token. Add a regression guard in tests/workflow-maintainer-skip.test.cjs asserting the workflow triggers on pull_request_target and not bare pull_request. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
1c835e208d | ci(#534): skip maintainer PR policy gates |