Skip to content

fix(standards-sync-stuck-automerge-alert): name the tracking-issue repository instead of assuming the caller - #346

Merged
kyle-sexton merged 5 commits into
mainfrom
fix/watchdog-tracking-issue-repo
Aug 4, 2026
Merged

kyle-sexton merged 5 commits into
mainfrom
fix/watchdog-tracking-issue-repo

Conversation

@kyle-sexton

@kyle-sexton kyle-sexton commented Aug 4, 2026 •

Copy link
Copy Markdown
Contributor

Summary

The watchdog's tracking-issue token is minted with owner and repositories
both unset, which actions/create-github-app-token resolves to the calling
repository. The caller is melodic-software/standards — the sync SOURCE, and
deliberately not a manifest target — so the sync App is not installed on it and
the mint 404s:

Inputs 'owner' and 'repositories' are not set. Creating token for this repository (melodic-software/standards).
##[error]Not Found - url: 'https://api.github.com/repos/melodic-software/standards/installation' status: 404

254 of 254 scheduled runs have failed since 2026-07-22. The stuck-PR alarm has
never fired
, including while a real instance of the condition was in flight
(melodic-software/dotfiles#328 sat BLOCKED for ~90 minutes with the watchdog
running hourly throughout).

A required tracking-issue-repository input now names the destination: a bare
repository name under the caller's own owner that the App is installed on.

Why required, not defaulted

A default reproduces the defect's shape. The App is what authors the issue, so
the destination must be a repository the App is installed on — and the caller
need not be one. Defaulting to the caller makes the reachable case the exception
and turns an unreachable destination back into a 404 discovered only when the
alert first has something to report, which is precisely when it must not fail.
Every consumer of this repository pins by 40-char SHA, so the contract change
strands no existing caller; a re-pinned caller that omits the input is rejected
for the missing input it can see and fix, rather than 404-ing at the mint. (The
docs state required means "the input must be supplied" but do not specify
whether that is detected at workflow validation or at invocation, and this was
not tested live — either way the failure names the input.)

Why a bare name, not owner/repo

The mint leaves owner unset, which resolves a repository entry's owner to the
caller's own owner — the same owner that owns the App installation the caller's
secrets belong to. A destination under a different owner needs a different
installation and therefore different secrets: a different caller, not a
different input value. A first-step guard rejects anything that is not a bare
name, because owner/repo mints successfully and then fails later and less
legibly — listForRepo 404s on a name containing a slash, and the issue write
concatenates a second owner onto it.

Every issue-touching surface moves

surface before after
token mint owner/repositories unset, so the caller repositories: <input>
issue lookup context.repo caller's owner + <input>
create-issue-from-file repository default ${{ github.repository }} <owner>/<input>
recovery close (list, comment, update) context.repo caller's owner + <input>

The input reaches both script: bodies through env:, never interpolated into
script text.

The mint stays unconditional — deliberately

Its three consumers partition the outcome space rather than sharing one
condition: the lookup and the open/update run when a count is non-zero, and the
recovery close runs when both are zero. Guarding the mint on the alerting
condition would strand the close branch on exactly the runs it exists for, so a
clean-recovery run would try to close the issue with no token. A test now pins
that partition, so the guard cannot be added later by someone reading the
unconditional mint as an oversight.

Scope: this PR does not make the watchdog work on its own

The caller pins by SHA, so landing this changes nothing until
melodic-software/standards re-pins and passes the input.
That follow-up is
in a different repository and is not attempted here. It must:

  1. re-pin the caller to a SHA of this branch, and
  2. pass tracking-issue-repository: <a manifest target>, and
  3. fix the caller's own header prose, which still says the issue is filed "in
    this repo — the calling repo".

The destination must be one of the 8 manifest targets — the installation's
selected set must EQUAL the derived target set for the sync's attest step, so
installing the App on standards is closed on design grounds. That is a real
consequence to accept knowingly: the standards-fleet alert will live in a sync
target rather than in standards. Two axes to weigh when picking one:

  • Private targets (medley, dotfiles, github-iac, provisioning) keep
    the alert body — which embeds PR links for private repositories — unpublished.
    Strictly better on disclosure than today, where it would land in the public
    standards.
  • Public targets (ci-workflows, .github, ci-runner,
    claude-code-plugins)
    republish those links, which is no worse than the
    current design but no better.

Recommendation: a private target, and medley if the operator's watch list is
already there.

Test plan

  • node --test .github/scripts/*.test.cjs — 504 pass, 0 fail (was 501).
  • actionlint — clean. zizmor --persona=regular — no findings.
  • biome ci --error-on-warnings at the repo's own config — clean.
  • editorconfig-checker, typos, markdownlint-cli2 — clean on every changed
    file.
  • Mutation-checked, every new assertion: reverting the lookup/close scripts
    to context.repo.repo, dropping repositories: from the mint, dropping
    repository: from the issue write, adding a default:, dropping
    required: true, guarding the mint, and widening the guard regex each fail at
    least one new test. The caller and destination fixtures are deliberately
    different values, so a regression to context.repo.repo cannot stay green.
  • The guard's run: block is executed under bash by the tests (the
    pattern claude-security-review-fail-closed.test.cjs already uses), not
    pattern-matched, and the test refuses a block containing a ${{ }}
    expression, so what runs locally is what runs in CI.

Verified against sources rather than assumed

  • owner unset + repositories set resolves the owner to
    GITHUB_REPOSITORY_OWNER and looks up
    GET /repos/{owner}/{repo}/installation against the named repository —
    read from lib/main.js at the pinned SHA bcd2ba4, not from the input
    descriptions. The observed failure log matches that source's both-unset
    branch verbatim.
  • peter-evans/create-issue-from-file defaults repository to
    ${{ github.repository }} — read from action.yml at the pinned SHA
    fca9117.
  • The melodic-standards-sync App holds issues: write and
    repository_selection: selected — confirmed live via
    gh api orgs/melodic-software/installations.

An independent fresh-context verifier reviewed the diff with the reasoning
withheld and confirmed the four judgement calls above. Its one substantive
finding — that the bare-name shape was documented but unenforced — is the
second commit.

Related

No linked issue: this repository holds no tracking issue for the defect, and
this change does not on its own make the watchdog work (see Scope above), so
it would be wrong to auto-close one. It relates to
melodic-software/standards#273 and melodic-software/standards#274, which
track the defect and stay open until the caller-side follow-up lands there.

🤖 Generated with Claude Code

https://claude.ai/code/session_01TFFtZLysg75gm7hviQM5wU

kyle-sexton and others added 2 commits August 3, 2026 20:36
…pository instead of assuming the caller

The tracking-issue token was minted with `owner` and `repositories` both
unset, which scopes it to the calling repository. The caller is
melodic-software/standards — the sync SOURCE, deliberately not a manifest
target — so the sync App is not installed on it and the mint 404s. Every
scheduled run has failed since 2026-07-22, so the watchdog has never
reported a stuck PR.

A required `tracking-issue-repository` input now names the destination:
a bare repository name under the caller's own owner that the App is
installed on. Required rather than defaulted, because a default would
keep the reachable case the exception and turn an unreachable
destination back into a 404 discovered only when the alert first has
something to report.

All four issue-touching surfaces move onto it — the mint's
`repositories`, the lookup's and close's `listForRepo`/`createComment`/
`update`, and create-issue-from-file's `repository`, which otherwise
defaults to the caller. The input reaches the two `script:` bodies
through `env:`, never interpolated.

The mint stays unconditional. Its three consumers partition the outcome
space: the lookup and open/update run when a count is non-zero, and the
recovery close runs when BOTH are zero, so guarding the mint on the
alerting condition would strand the close branch on exactly the runs it
exists for. A test pins that partition.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…racking-issue repository

An `owner/repo` value mints a token successfully — create-github-app-token
parses an owner-qualified entry and checks the owner half — so the run
gets past the credential and dies later and less legibly: `listForRepo`
404s on a name containing a slash, and the issue write concatenates a
second owner onto it.

A first-step guard rejects anything that is not a bare repository name,
before the standards checkout and the scan. Its `run:` block is executed
by the tests, not pattern-matched.

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

cursor Bot commented Aug 4, 2026

Copy link
Copy Markdown

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

@claude

claude Bot commented Aug 4, 2026 •

Copy link
Copy Markdown

Claude finished @kyle-sexton's task in 3m 19s —— View job


Review of PR #346

  • Gather context (diff, CLAUDE.md, README)
  • Review workflow YAML changes
  • Review test file changes
  • Review README changes
  • Post inline findings
  • Post summary

Scope: .github/workflows/standards-sync-stuck-automerge-alert.yml, .github/scripts/standards-sync-stuck-automerge-alert.test.cjs, README.md (diffed against origin/main).

Summary

This is a well-scoped, well-documented fix. I read the full diff and the surrounding unchanged code for context; findings below are my own read, not a restatement of the PR's claimed test plan.

Correctness:

  • The new tracking-issue-repository required input, the first-step bare-name guard (^[A-Za-z0-9._-]+$), and every downstream surface (mint repositories:, lookup/close owner+repo, create-issue-from-file's repository:) are all internally consistent — I traced each one and didn't find a spot still addressing context.repo.repo or the caller.
  • The guard step correctly runs before checkout, so a malformed value fails in seconds rather than after the manifest scan.
  • The "mint stays unconditional" partition (lookup+open/update on non-zero, close on both-zero) is correctly implemented — no if: on the mint step, and the three consumer conditions are exact complements as documented.
  • Guard regex correctly rejects owner/repo, URLs, empty strings, and trailing whitespace, and accepts bare names including ones starting with . (e.g. .github).
  • CLAUDE.md's "pin everything by SHA" rule: no new uses: was introduced, and the existing create-github-app-token pin is untouched, so nothing new needed a SHA here.

One minor test-coverage gap (posted inline on standards-sync-stuck-automerge-alert.test.cjs lines 1070–1076): the mutation-tested assertions cover repositories: being set on the mint step, but nothing asserts owner: stays absent — which is what makes the destination resolve under the caller's own owner rather than a hardcoded one. Not a functional bug today (this workflow already hardcodes melodic-software elsewhere, e.g. line 191), but it's the one design invariant from the header/step comments that isn't locked down by a test the way everything else here is.

Verification notes:

  • I was not able to execute node --test or actionlint/zizmor in this sandbox (commands required approval that wasn't available, and the linters aren't installed here) — the Test plan's pass counts and lint results in the PR description are author-claimed and I did not independently reproduce them. Everything above is from reading the diff and cross-referencing the unchanged file, not from running the suite.
  • No caller in this repository invokes this reusable workflow, so there was nothing local to check for breakage; the PR's own "Scope" section correctly identifies the follow-up as living in melodic-software/standards.

Nothing else stood out — the README update accurately reflects the new contract, and the architectural reasoning in the header comments matches what the code actually does.
· branch fix/watchdog-tracking-issue-repo

@claude

claude Bot commented Aug 4, 2026 •

Copy link
Copy Markdown

Claude finished @kyle-sexton's task in 2m 13s —— View job


Security review

  • Gather context (diff, changed files)
  • Review token-mint / auth-boundary changes in the watchdog workflow
  • Review script-injection / github context handling in run:/script: blocks
  • Review the bare-name guard logic for the tracking-issue repository input
  • Review test coverage assertions for security-relevant behavior
  • Post findings

No security issues found. This PR only touches .github/workflows/standards-sync-stuck-automerge-alert.yml, its test file, and README prose — I reviewed the workflow end-to-end against the trust-boundary questions this lane covers (authorization scope, injection surfaces, credential handling), deferring pin/permission/trigger-pattern checks to zizmor as instructed.

What I specifically checked and why it holds up:

  • Token scoping narrows, doesn't widen. The second App token now mints with repositories: ${{ inputs.tracking-issue-repository }} and owner left unset (standards-sync-stuck-automerge-alert.yml#L496-L503) — this resolves to exactly one repository under the calling repo's own owner, matching the App installation the caller's secrets belong to. There's no path for the token to reach a repository under a different owner (that would require a different installation/secrets — i.e. a different caller), and the bare-name guard's character class ([A-Za-z0-9._-]) excludes ,, so a single input value can't be smuggled into create-github-app-token's comma/newline-separated repositories list to grant scope beyond one repo.
  • The guard closes the owner/repo trap correctly, and fails closed. L130-L138 runs first, before checkout and before either token mint, so a malformed value is rejected before any credential material is touched. The regex correctly matches GitHub's actual repo-name character set and rejects slashes, whitespace, and empty values — verified against the mutation-tested cases in the test file (owner/repo, a URL, empty, trailing space).
  • No script-injection surface. Every place inputs.tracking-issue-repository reaches an actions/github-script step, it's threaded through env: and read via process.env (L525, L593), never interpolated as ${{ }} into the script: body text itself — the pattern that would make this template injection by construction. The guard's own run: block reads the same way through env: (L131-L132), not inline into the script.
  • with: interpolation is inert. The one place the input is spliced directly via ${{ }} — repository: ${{ github.repository_owner }}/${{ inputs.tracking-issue-repository }} on the create-issue-from-file step (L581) — is a with: action-input value, not a run: shell/script body, so it's passed to the action as a plain string rather than interpreted/executed. No injection surface there.
  • Owner derivation is consistent everywhere. context.repo.owner (in the two github-script steps) and github.repository_owner (in the create-issue-from-file with: block) both resolve to the calling repository's owner during a workflow_call run, matching the token mint's implicit-owner resolution — so lookup, write, and close all address the same owner/repo the token is actually scoped to. No mismatch that could cause a write against a repo the token can't cover (which would just fail) or, more importantly, a broader one.
  • Permissions only shrink. Top-level and job permissions: stay contents: read; the ambient GITHUB_TOKEN no longer needs issues: write at all now that the second App token owns every issue write (per the README diff) — a reduction, not a widening.

No pull_request_target/workflow_run-with-secrets patterns, no permission-widening, and no unpinned/loosened action references in this diff (left to zizmor's static lane per the review scope).

…om the test comments

comment-hygiene flags an issue-tracker reference in a code comment, and the
two here carry no meaning a future reader loses: the comments state the
credential mismatch the fixtures exist to catch, which stands without a
number to look up.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Comment thread .github/scripts/standards-sync-stuck-automerge-alert.test.cjs
@claude

claude Bot commented Aug 4, 2026 •

Copy link
Copy Markdown

Claude finished @kyle-sexton's task in 1m 31s —— View job


Security review

  • Gather context (diff, changed files, prior commits)
  • Review token-mint / auth-boundary changes in the watchdog workflow
  • Review script-injection / github context handling in run:/script: blocks
  • Review the bare-name guard logic for the tracking-issue repository input
  • Post findings

No security issues found.

The only commit added since the last review on this PR (c9c1c15) touches test-comment prose only (dropping issue-tracker references) — no workflow or logic change, so this re-confirms the prior finding on 249bfa7 rather than superseding it.

What I checked, scoped to .github/workflows/standards-sync-stuck-automerge-alert.yml (the only security-relevant file in the diff):

  • Token scoping narrows, doesn't widen. The tracking-issue App token now mints with repositories: ${{ inputs.tracking-issue-repository }} and owner left unset (workflow#L496-L503), which resolves to exactly one repository under the calling repo's own owner — the same owner the caller's App installation/secrets belong to. There's no path to a different owner's repository (that would need a different installation, i.e. a different caller), so the change cannot be used to widen the token's reach.
  • The guard runs first and fails closed. workflow#L130-L138 is the very first step — before checkout and before either token mint — so a malformed tracking-issue-repository is rejected before any credential material is touched. The character class ^[A-Za-z0-9._-]+$ matches GitHub's actual repo-name charset and excludes /, whitespace, and ,, which closes both the owner/repo confusion the header describes and any attempt to smuggle multiple entries into create-github-app-token's comma-separated repositories list.
  • No script-injection surface. Every place inputs.tracking-issue-repository reaches an actions/github-script step is threaded through env: and read via process.env (workflow#L525, workflow#L593), never interpolated as ${{ }} into the script: body text — the pattern that would make this template injection by construction. The guard's own run: block (workflow#L131-L132) follows the same discipline.
  • The one direct ${{ }} splice is inert. repository: ${{ github.repository_owner }}/${{ inputs.tracking-issue-repository }} on the create-issue-from-file step (workflow#L581) is a with: action-input value evaluated by the Actions runner, not a shell/script body — no execution context for it to inject into.
  • Owner derivation is consistent everywhere. context.repo.owner in both github-script steps and github.repository_owner in the create-issue-from-file with: block all resolve to the calling repository's owner during workflow_call, matching the token mint's implicit-owner resolution — lookup, write, and recovery-close all address the same owner/repo the token is scoped to, so there's no path to a write silently targeting a repo the token can't cover.
  • Permissions only shrink. Top-level and job permissions: stay contents: read; the ambient GITHUB_TOKEN no longer carries issues: write at all now that the dedicated App token owns every issue write (confirmed against the README diff).

No pull_request_target/workflow_run-with-secrets patterns, no permission-widening, and no unpinned/loosened action references in this diff (left to zizmor's static lane per review scope).

@github-actions

github-actions Bot commented Aug 4, 2026 •

Copy link
Copy Markdown

Claude has reviewed this PR 2 times. The lane skips further automatic reviews after 5; deleting this comment resets the count.

…cking-issue repository names

Both match the repository-name character class the guard already enforces,
and GitHub forbids both as repository names, so either would reach the REST
client as a path segment rather than a name. Rejecting them at the guard
keeps its claim exact rather than approximate — the same reason the guard
rejects `owner/repo` instead of letting the mint accept it and something
downstream fail less legibly.

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

claude Bot commented Aug 4, 2026 •

Copy link
Copy Markdown

Claude finished @kyle-sexton's task in 50s —— View job


Security review

  • Gather context (diff since last review, new commit f80cd5a)
  • Review the new ./.. guard change
  • Confirm no other security-relevant changes since last review
  • Post findings

No security issues found.

Since the last security review on this PR (c9c1c15), only one commit landed — f80cd5a, which tightens the malformed-repository guard. It's the only security-relevant change (the accompanying test-file diff is coverage-only, out of scope for this lane).

What changed: workflow#L130-L139 extends the first-step guard to explicitly reject tracking-issue-repository values of . and .., in addition to the existing character-class check. Both strings match ^[A-Za-z0-9._-]+$ (the class permits .), so previously a single . or .. would have passed the guard and flowed into the token mint's repositories: input and the REST calls built from it as a literal path segment — a plausible path-normalization/traversal vector against the API path (e.g. .../repos/{owner}/.. collapsing to an unintended endpoint), rather than a "repository not found" failure. This closes that gap.

This is a strict tightening, not a new surface:

  • It runs as the very first step, before checkout and before either token mint — same fail-closed placement as the existing guard, confirmed unchanged.
  • The two new comparisons are plain Bash string equality (==) against literal constants, read from env: via $TRACKING_ISSUE_REPOSITORY — no interpolation of the input into the run: script body, so no new injection surface.
  • No other file in the diff (mint, lookup, close, create-issue-from-file) changed between the last review and this commit, so the trust-boundary analysis from the prior two reviews on this PR still holds in full.

No pull_request_target/workflow_run-with-secrets patterns, no permission changes, and no unpinned/loosened action references in this diff (left to zizmor's static lane per review scope).

@kyle-sexton

Copy link
Copy Markdown
Contributor Author

Closing unmerged: authorization for this fix shape was withdrawn. The fix shape for this defect is a human-reserved decision on melodic-software/standards#274 (status: needs-decision, needs-human), and it is returning there rather than being picked here.

No change is being proposed. The branch fix/watchdog-tracking-issue-repo is left in place so the analysis is recoverable if the human decision lands on this shape; delete it freely otherwise.

@kyle-sexton kyle-sexton closed this Aug 4, 2026
@kyle-sexton kyle-sexton reopened this Aug 4, 2026
@claude

claude Bot commented Aug 4, 2026 •

Copy link
Copy Markdown

Claude finished @kyle-sexton's task in 1m 59s —— View job


Review of PR #346 (re-triggered on reopen)

  • Gather context (diff since last review, PR close/reopen history)
  • Re-review current diff against CLAUDE.md and prior findings
  • Check whether the previously-flagged test-coverage gap was addressed
  • Post summary

Summary

No code has changed since the prior review round on this PR — HEAD is still f80cd5a, the same commit the last two reviews covered, and git status shows a clean, up-to-date checkout. This update re-confirms that review rather than superseding it; I'm noting it here because the PR was reopened after the author's close comment ("authorization for this fix shape was withdrawn... returning to melodic-software/standards#274"), and no new commits were pushed in between.

What I re-checked directly (not restating the PR's own claims):

  • git diff origin/main...HEAD --stat matches the PR description's stated diff (293/-46 across the same 3 files).
  • Read the full workflow diff and the full test diff. The two commits added after the first correctness pass (c9c1c15, comment cleanup only, and f80cd5a, the ./.. guard) are both sound: the guard sits before checkout as the job's first step, the regex correctly excludes /, whitespace, and the two GitHub-reserved names that would otherwise reach the REST client as path segments, and the corresponding test (standards-sync-stuck-automerge-alert.test.cjs:1057-1072) executes the run: block under bash rather than pattern-matching it — same discipline as the rest of the suite.
  • README diff stays consistent with the workflow contract change (required input, no issues: grant on the caller, App-authored write).
  • I was not able to run node --test, actionlint, or zizmor in this sandbox — same limitation as the earlier correctness review on this PR (commands require approval unavailable here). The PR's Test plan pass counts remain author-claimed and unverified by me.

Still outstanding — the one finding from the prior review round has not been addressed: the mutation-tested assertions at standards-sync-stuck-automerge-alert.test.cjs#L1074-L1087 check that the mint step sets repositories: to the input, but nothing asserts owner: stays absent — the invariant the header (workflow lines 45-50) and the step comment (workflow lines 496-503) both hinge the caller-owner resolution on. Not a functional bug today, but it's the one documented design decision in this diff that isn't locked down by a test the way everything else here is (already flagged inline on the prior round; not re-posting a duplicate).

No new correctness or CLAUDE.md-alignment issues found beyond that one.
· branch fix/watchdog-tracking-issue-repo

@claude

claude Bot commented Aug 4, 2026 •

Copy link
Copy Markdown

Claude finished @kyle-sexton's task in 39s —— View job


Security review

  • Confirm what changed since the PR was reopened
  • Re-verify trust-boundary findings still hold
  • Post findings

No security issues found.

This run was triggered by the PR being reopened, not by a new commit — HEAD is still f80cd5a58f1f2b745fcf03a0d7fc5150dccfca0b, identical to the SHA already covered by the prior security review posted on this PR at 2026-08-04T00:58:54Z. git diff origin/main...HEAD shows the same three files (standards-sync-stuck-automerge-alert.yml, its test file, README.md) with the same shape as that review covered — no new commits to analyze.

That review's conclusions stand:

  • Token scoping only narrows (repositories: ${{ inputs.tracking-issue-repository }} with owner left unset resolves to exactly one repo under the caller's own owner — no path to a different owner/installation).
  • The malformed-repository guard (including the ./.. rejection added in the final commit) runs before checkout and before either token mint, fails closed, and its character class blocks /, whitespace, and ,.
  • No script-injection surface: every github-script step reads inputs.tracking-issue-repository through env:/process.env, never interpolated as ${{ }} into a script: body; the one direct ${{ }} splice (create-issue-from-file's repository: with: input) is an action input, not a shell/script context.
  • Owner derivation (context.repo.owner, github.repository_owner) is consistent across lookup, write, and recovery-close, so no write can silently drift to an unintended repo.
  • permissions: only shrink (ambient GITHUB_TOKEN no longer needs issues: write).

No pull_request_target/workflow_run-with-secrets patterns, no permission-widening, and no unpinned/loosened action references (left to zizmor's static lane per review scope).

Separately, for context: the PR author's reopen/close history on this thread indicates the fix shape here was later withdrawn pending a human decision on melodic-software/standards#274 — that's a design/authorization call, not a security finding, and doesn't change the analysis above.
· branch fix/watchdog-tracking-issue-repo

…nt's absent owner

The absence of `owner` on that step is what makes a bare repository name
resolve under the calling repository's owner, and it was the one property
of the step nothing asserted. The sibling every-target mint pins
`owner: melodic-software` deliberately, so both halves are pinned: the
tracking mint must have no `owner` key and the every-target mint must
keep its own, which fails a harmonization of the two in either direction
rather than letting one silently undo the other.

Mutation-checked both ways: adding `owner:` to the tracking mint, and
removing it from the every-target mint, each turn the test red.

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

claude Bot commented Aug 4, 2026 •

Copy link
Copy Markdown

Claude finished @kyle-sexton's task in 38s —— View job


Security review (re-triggered on new commit)

  • Gather context (diff since last review, new commit 48fc97f)
  • Review the new commit's changes
  • Confirm no other security-relevant changes since last review
  • Post findings

No security issues found.

The only commit added since the last security review on this PR (f80cd5a) is 48fc97f, and it is test-only — a 17-line addition to .github/scripts/standards-sync-stuck-automerge-alert.test.cjs that pins two assertions (the tracking-issue mint's owner: stays absent; the sibling every-target mint's owner: melodic-software stays explicit). No workflow file, script logic, or permission changed. This directly closes the test-coverage gap flagged in the earlier correctness review round — out of scope for this security lane to comment on further.

git diff f80cd5a..48fc97f --stat confirms the single-file, test-only shape of this commit; no other file changed.

Since no workflow logic changed, the trust-boundary analysis from the prior security reviews on this PR (c9c1c15, f80cd5a) still holds in full:

  • Token scoping only narrows (repositories: ${{ inputs.tracking-issue-repository }} with owner left unset resolves to exactly one repo under the caller's own owner — no path to a different owner/installation).
  • The malformed-repository guard (including the ./.. rejection) runs before checkout and before either token mint, fails closed, and its character class blocks /, whitespace, and ,.
  • No script-injection surface: every github-script step reads inputs.tracking-issue-repository through env:/process.env, never interpolated as ${{ }} into a script: body; the one direct ${{ }} splice (create-issue-from-file's repository: with: input) is an action input, not a shell/script context.
  • Owner derivation (context.repo.owner, github.repository_owner) is consistent across lookup, write, and recovery-close.
  • permissions: only shrink (ambient GITHUB_TOKEN no longer needs issues: write).

No pull_request_target/workflow_run-with-secrets patterns, no permission-widening, and no unpinned/loosened action references (left to zizmor's static lane per review scope).
· branch fix/watchdog-tracking-issue-repo

@kyle-sexton

Copy link
Copy Markdown
Contributor Author

Closing the loop on the owner-unset coverage gap. Verified the assertion at 48fc97f:

assert.doesNotMatch(
  mintStep,
  /^\s*owner:/mu,
  "`owner` must stay unset so a bare repository name resolves under the caller's own owner",
);

This is stronger than the gap required. It pins both mints — the tracking-issue mint must have no owner, and the every-target mint two hundred lines above must keep owner: melodic-software. As the comment there puts it, harmonizing them in either direction now fails rather than silently undoing one. That was the exact hazard: the two steps look inconsistent, the inconsistency is deliberate, and nothing previously encoded that.

Also confirmed independently on this head: the mint at line 500 carries no if:, and the consumer partition holds — 521 and 579 on stuck != '0' || unarmed != '0', 593 on the complement. That guard was proposed twice during this work and would have starved the close branch on clean runs; because standards is public the unauthenticated call would have succeeded and the run would have gone green, turning a visibly-broken watchdog into an invisibly-broken one. It is now pinned by a test.

38 checks green.

One thing to carry forward, not a change to this PR: merging it does not stop the hourly failures. standards pins this reusable by SHA, so the defect closes only when a follow-up re-pins the caller to this PR's merge commit, passes tracking-issue-repository: medley, and corrects the caller's header line claiming the issue is filed "in this repo — the calling repo". That PR is prepared and holding for the merge SHA.

@kyle-sexton
kyle-sexton merged commit ed6d410 into main Aug 4, 2026
1 check passed
@kyle-sexton
kyle-sexton deleted the fix/watchdog-tracking-issue-repo branch August 4, 2026 02:07
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