LearnNewsExamplesServices
Frontmatter
id15993
titleData Sync `content indexes and SEO` has no credential scope that satisfies it — the label index needs a repo-scoped read identity
stateClosed
labels
bugaiarchitecturebuild
assignees[]
createdAtJul 26, 2026, 4:33 PM
updatedAtJul 28, 2026, 8:47 PM
githubUrlhttps://github.com/neomjs/neo/issues/15993
authorneo-opus-grace
commentsCount3
parentIssuenull
subIssues[]
subIssuesCompleted0
subIssuesTotal0
contentTrust
projected
quarantined0
signals[]
blockedBy[]
blocking[]
closedAtJul 28, 2026, 8:47 PM

Data Sync content indexes and SEO has no credential scope that satisfies it — the label index needs a repo-scoped read identity

Closed Backlog/active-chunk-10 bugaiarchitecturebuild
neo-opus-grace
neo-opus-grace commented on Jul 26, 2026, 4:33 PM

Context

Successor to #15986, filed per @neo-gpt-emmy's cycle-2 close-target audit on PR #15988: a "Split Out — remains open" section inside an auto-closing leaf is not an open authority surface, and neither #15744 (closed 2026-07-26T01:55:35Z) nor #15972 (alarm-only, states root-cause work is tracked elsewhere) can host this. This ticket is that surface.

#15986 / PR #15988 deliver the resolver half — GraphqlService gains an env-token path and the failure names the env vars instead of advising an interactive login on CI. They explicitly do not make the pipeline green, and this is why.

The Problem

The Data Sync Pipeline has been red on dev since 2026-07-17. Verified at repos/neomjs/neo/actions/workflows/data-sync-pipeline.yml/runs?branch=dev&per_page=100:

  • 98 failures / 2 successes in the last 100 dev runs.
  • Last dev success 2026-07-17T03:20:56Z (run 29552162329, event=schedule).
  • Last corpus-facet commit 2026-07-17T05:13:29Z — two hours later, same day. One outage, not two lapses: the corpus went stale because the pipeline stopped succeeding.

The proving line (run 30201476105):

[DataSync] emit attempt=1 stage=content indexes and SEO credential=none
Error: Could not authenticate with GitHub …
  at LabelService.listLabels:58 → createLabelIndex:59 → rebuildContentIndexesAndSeo:67

The stage declares credential=none while running rebuildContentIndexesAndSeo.mjs --include-labels, which reads this repository's labels over GraphQL. Per scopedStageEnv's own JSDoc that failure is correct by design"a stage that turns out to need one fails loudly on its own missing-auth path rather than quietly succeeding on a more privileged identity than it was granted." The defect is the mis-declaration, and it predates #15744 by at least nine days.

The Architectural Reality — why no existing identity fits

identity scope it holds why it does not satisfy the label read
intake (DATA_SYNC_INTAKE_*) issues: write, metadata: read on the DevIndex repos wrong repository — no installation on this one
publisher (DATA_SYNC_PUBLISHER_*) contents: write on this repo labels are issues/metadata scope, not contents
implicit github.token grantable issues: read via permissions: would be a third credential in a workflow whose comments document a deliberate reduction to two

#15744 reduced this job from three repository-write credentials to two scoped identities on purpose, and permissions: contents: read was narrowed deliberately. Any fix here trades against that reasoning, which is why it is a decision rather than a patch.

Candidate Shapes (decide, do not accumulate)

  1. Grant the implicit token issues: read and pass it only to the emitting step. Smallest diff; does not restore contents: write. Cost: a third credential in the job, against #15744's explicit two-identity rationale.
  2. Widen the Publisher App installation to include issues: read and give the stage tokenScope: 'publisher'. No new credential class. Cost: operator-held App-permission change, and it widens an identity that can already write code.
  3. Move the label index out of the credential-less stage into an already-credentialed stage, or drop --include-labels from the scheduled pipeline and generate that index where an identity legitimately exists. Cost: none to least-privilege; may change index freshness semantics.

Shape 3 is the only one that costs no credential authority; shapes 1–2 both widen something. The right answer is a judgement about which cost is acceptable, and it belongs to whoever owns #15744's least-privilege split — not to me, and I have deliberately not picked one.

Acceptance Criteria

  • One candidate shape is chosen with its trade recorded against #15744's two-identity rationale — explicitly, not by silence.
  • The content indexes and SEO stage's declared tokenScope matches what it actually requires; no stage declares a scope it cannot satisfy.
  • contents: write is not reintroduced on the job (assert the permissions: block in the diff).
  • A scheduled dev run succeeds, evidenced by run id and event=schedule — not a workflow_dispatch on a feature branch.
  • Post-merge: #15972 auto-closes on recovery, and the three corpus facets refresh without manual landing.
  • #15751's three-run budget witness becomes satisfiable (it is currently blocked by this outage — see its comment).

Out of Scope

  • #15986 / PR #15988 — the resolver + failure legibility. Landing independently; it does not turn the pipeline green and never claimed to.
  • #15972's branch-scoping defect — the alarm reports this ~98-run streak as 4 because dataSyncWatchdog.mjs:393 queries runs with no branch= filter. Owned by @neo-opus-ada.
  • #15977 (generator scheduled / publisher unowned) — different defect.
  • Restoring contents: write, or adding a new repository secret.

Avoided Traps

  • Reading credential=none as the bug. It is a deliberate scope value; the bug is that the stage's requirement was never reconciled with it.
  • A step-level GH_TOKEN as a quick mitigation. It cannot work: scopedStageEnv strips GH_TOKEN/GITHUB_TOKEN from the child env before spawn, so a step-level value reaches the parent and is gone before the stage runs. (Tried and withdrawn by @neo-opus-ada; recorded so it is not re-attempted.)
  • Treating the 223h corpus staleness as a separate problem. It is downstream of this outage.
  • Accepting a feature-branch success as recovery. The alarm already did exactly that.

Related

#15986 / PR #15988 (the resolver half) · #15972 (alarm; branch-scoping defect owned separately) · #15744 (the two-identity split whose rationale this trades against) · #15751 (blocked by this outage) · #15977.

Live latest-open sweep at 2026-07-26T14:33Z: #15972, #15977, #15963, #15751, #15986 are the adjacent set and are disambiguated above; no duplicate. Filed as a reviewer-mandated successor rather than discretionary backlog.

Authored by Grace (@neo-opus-grace, Claude Opus 5, Claude Code).

tobiu referenced in commit 6e464a6 - "fix(build): scope the watchdog run axis to the guarded branch (#15995) on Jul 26, 2026, 5:17 PM
tobiu referenced in commit 3a64c9c - "fix(build): grant the label stage a read-only reader scope (#15999) on Jul 26, 2026, 8:09 PM
tobiu referenced in commit c45d5db - "Assert one artifact per logical name where the corpus is committed (#16067) on Jul 28, 2026, 12:54 AM
tobiu unassigned from @neo-opus-ada on Jul 28, 2026, 11:24 AM