Skip to content

[Bug?]: npm publish aborts on its own Rekor 409 when the tlog POST is retried #7308

Open

Description

@JounQin

Self-service

  • I'd be willing to implement a fix

Describe the bug

yarn npm publish with provenance (npmPublishProvenance: true) can abort after its attestation has been persisted in Rekor, leaving the package unpublished and an orphan entry in the transparency log:

Some packages failed to publish:
[email protected]
└ YN0001: gM: error creating tlog entry - (409) an equivalent entry already exists
  in the transparency log with UUID 108e9186e8c5677a54340083e014f9715fa30a9c88a599e46878bb4c5e68f8f4bdc66337dcd942d7
    at wre.createEntry (…/.yarn/releases/yarn-4.18.1.cjs:430:90654)
    at async Bre.testify (…/.yarn/releases/yarn-4.18.1.cjs:430:94323)
    at async Promise.all (index 0)
    at async mX.create (…/.yarn/releases/yarn-4.18.1.cjs:400:20687)

Reconstructed mechanism:

  1. Rekor.createEntry POSTs to /api/v1/log/entries through fetchWithRetry, which retries 5xx, 408, 429 and network failures.
  2. If Rekor persists the entry but the response is one of those retryable outcomes (or is lost/times out), the retry re-POSTs the identical DSSE entry.
  3. The second POST is answered with 409 an equivalent entry already exists.
  4. TLogClient.createEntry can recover from exactly this — it catches the conflict and fetches the existing entry — but only when fetchOnConflict is true, and the witness is built with it hard-coded to false:
new RekorWitness({
  rekorBaseURL: t.rekorURL,
  entryType: t.legacyCompatibility ? 'intoto' : 'dsse',
  fetchOnConflict: !1,          // <-- conflict is fatal
  retry: t.retry ?? DEFAULT_RETRY,
  timeout: t.timeout ?? DEFAULT_TIMEOUT,
})

So the conflict surfaces as TLOG_CREATE_ENTRY_ERROR, the publish aborts, and the tarball is never uploaded — even though the attestation it needed is already in the log.

Evidence from the public Rekor API (GET https://rekor.sigstore.dev/api/v1/log/entries/<uuid>):

attempt entry UUID integratedTime error raised payloadHash
1 108e9186…42d7 2026-10-16T16:45:29Z 16:45:35 5ccb61f143f430544777f6df560a456e698517c0a8b4d0ff7e6fbff2dec1f936
2 108e9186…66f7 2026-10-16T16:49:53Z 16:50:00 462f820fd1fddb2f5f651c06ebf83b2056cba37492bf40e4b0c8bbd82cef2ace
  • Each error is raised ~6–7 s after the entry it names was created — the shape of a first request that was retried rather than one that simply succeeded.
  • The payload hashes differ between the attempts because the statement embeds runDetails.metadata.invocationId (…/actions/runs/${GITHUB_RUN_ID}/attempts/${GITHUB_RUN_ATTEMPT}) and resolvedDependencies[].digest.gitCommit. Attempt 2 therefore cannot have collided with attempt 1's entry — it collided with its own, i.e. the duplicate is generated inside a single publish, not by a stale log entry.
  • The version never appears in the registry while its entry exists.

Expected instead, any of:

  • treat a conflict as success when the entry is already logged (that is all attest() needs);
  • or honour fetchOnConflict / default it to true for the publish path — the bundled sigstore client already implements the recovery;
  • or at minimum, never abort the registry upload when the log entry was written by this very run.

To reproduce

A registry publish can't be reduced to a package.json, so the reproduction is the two public runs plus the Rekor entries above. The essential setup:

  1. GitHub Actions release workflow with id-token: write (here npm Trusted Publishing), .yarnrc.yml with npmPublishProvenance: true.
  2. Publish a public workspace whose version is not yet in the registry, one yarn npm publish per package (here driven by changeset publish).
  3. Watch for YN0001: … error creating tlog entry - (409) an equivalent entry already exists.

It is a race, so it is intermittent: the same repository published [email protected] and [email protected] with provenance successfully on 2026-09-24 (yarn 4.17.1), then failed twice in a row ~6 minutes apart on 2026-10-16. Presumably the tlog POST has to be retried for the bug to fire.

Failing runs:

Environment

GitHub Actions: ubuntu-latest (GitHub-hosted), 2026-10-16
Node: 24 (lts/*)
Yarn: 4.18.1 — /home/runner/work/prettier/prettier/.yarn/releases/yarn-4.18.1.cjs
      (.yarnrc.yml: nodeLinker: node-modules, npmPublishProvenance: true)
Publish driver: changeset publish (@changesets/[email protected]) -> yarn npm publish
Registry auth: npm Trusted Publishing (OIDC); Fulcio/OIDC signing works, the
      failure is at the Rekor write

Additional context

  • This is a race, not a deterministic failure, and I do not think it is caused by the yarn 4.17.1 → 4.18.1 upgrade: the publish-side code (Rekor.createEntry, fetchWithRetry, RekorWitness construction, the TLogClient conflict branch) reads the same across those bundles. What did change is the bundled sigstore client — 4.18.x also carries a TLogV2Client that 4.17.1 does not — so I can't rule out a changed retry/timeout policy. The symptom needs a retried POST either way.
  • The last successful provenance publish in that repository was on 2026-09-24 with yarn 4.17.1; the next release with something to publish is the one that failed, so there is no counter-example in between.
  • Our workaround, not yet applied: publish the blocked version once with npmPublishProvenance: false, then re-enable provenance.
  • Other ecosystems see the same class of failure with npm's CLI and treat a Rekor 409 as already-published in CI (chore: handle Rekor 409 conflict during npm provenance publish ChainSafe/lodestar#9703, vectorize-io/hindsight ec384e9, Release 3.1.50 partially published WordPress/wordpress-playground#4269).

Happy to supply the full job logs, the raw Rekor entry bodies, or to test a patch if you want one.

Activity

  1. JounQin commented on Oct 17, 2026

    @JounQin
    ContributorAuthor

    Update: the same release went through on a later attempt, with nothing changed.

    The run from the report was re-run; on attempt 4 (2026-10-17, job started
    12:01:38Z, publish step 12:01:58Z → 12:02:08Z, same commit, same tarball,
    same Yarn 4.18.1) the publish completed and the package is now live:

    That is consistent with the mechanism described above: the failure needs the
    tlog POST to be retried. When the first POST is observed as successful, the same
    publish writes exactly one entry and finishes normally. Same repository, same
    version, same Yarn version — the only difference between the failing attempts
    and this one is timing.

    Two notes that survive this:

    • The failing attempts each left their own orphaned entry (the UUIDs quoted in
      their error messages), so a retry-only diagnosis is still worth handling in
      code: a conflict on a retried POST should not abort a publish whose
      attestation is already logged, and should not leave the log with entries for
      versions that were never published.
    • Since it is a race, CI cannot distinguish "release broken" from "release
      unlucky"; treating the conflict as success (or honouring fetchOnConflict)
      would turn the current worst case into a normal publish.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions