Skip to content

fix(release): state the propagation budget as a duration, not an attempt count (META-158) - #45

Open
qmarcelle wants to merge 2 commits into
mainfrom
fix/meta-158-registry-propagation-budget
Open

fix(release): state the propagation budget as a duration, not an attempt count (META-158)#45
qmarcelle wants to merge 2 commits into
mainfrom
fix/meta-158-registry-propagation-budget

Conversation

@qmarcelle

@qmarcelle qmarcelle commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

META-158 with a reproduction and numbers. The 0.5.0 release is the case it describes.

What happened

00:05:20  Publishing "@workspacejson/rules" at "0.5.0"
00:05:20  Publishing "@workspacejson/spec"  at "0.5.0"
00:05:31  success  packages published successfully
00:06:48  release:verify-published gives up — ETARGET, no matching version

Both packages were on the registry. The same script, unchanged, against the same registry, passed minutes later. The release was real; the receipt said otherwise.

The cause is a comment and a constant disagreeing

The comment above the constants says propagation lags "seconds to low minutes." The configuration was:

const REGISTRY_PROPAGATION_RETRIES = 6;
const REGISTRY_PROPAGATION_BASE_DELAY_MS = 5000;   // delay = BASE * attempt

Which is 5 + 10 + 15 + 20 + 25 = 75 seconds. Nobody multiplies that out while reading, so the claim and the behavior drifted apart unnoticed until a release landed in the gap.

The fix

The budget is now a duration, not an attempt count:

const REGISTRY_PROPAGATION_BUDGET_MS = 10 * 60 * 1000;
const REGISTRY_PROPAGATION_BASE_DELAY_MS = 5000;
const REGISTRY_PROPAGATION_MAX_DELAY_MS = 30000;   // capped so the tail stays responsive

A duration sits next to the sentence describing it and cannot silently contradict it. An attempt count has to be mentally expanded before it can be compared to the claim, which is how this drifted.

Progress lines now carry elapsed time against the budget, so a future failure arrives with its own evidence:

@workspacejson/spec@0.5.0 not yet visible on the registry (attempt 3, 16s of 600s elapsed) — retrying in 15000ms

Exhaustion now says which event this is

The old message exited on an ETARGET and left the reader to infer a failed publish from a failed lookup. Those are different events needing opposite responses:

version present on the registry -> the package shipped, only the receipt is
                                   missing; re-run this script
version absent                  -> the publish did not land; re-cut

The script prints exactly that, with the npm view command to settle it.

Verified

  • Happy path: passes against the live 0.5.0 packages — both install from the registry into throwaway directories and their entry points execute.
  • Retry path: exercised against 99.99.99, a version that will never exist, and killed at 25s. Backoff, elapsed accounting and budget reporting all behave (output above is from that run).
  • check:docs and check:architecture pass.

Scripts are not in either package's files list and do not ship, so this carries no changeset and does not move the version.

Not addressed here

The budget is generous rather than tuned — 10 minutes was chosen because a published version with no receipt is the one state this pipeline cannot recover from, so waiting too long is the cheaper error. The actual propagation time for the 0.5.0 publish is unknown: it was over 75 seconds and under "several minutes later when I re-ran it by hand." Tightening it needs a measurement this release did not produce.

Summary by Sourcery

Improve release verification reliability and preserve the associated causal-review experiment evidence.

Bug Fixes:

  • Make published-package verification wait against a ten-minute duration budget so registry propagation delays do not incorrectly fail real releases.
  • Clarify propagation failures by distinguishing delayed registry visibility from an absent publication and provide a direct registry-check command.

Enhancements:

  • Add elapsed-time progress reporting and capped retry delays to make verification status and exhaustion actionable.

Documentation:

  • Add a recorded causal-review experiment receipt and supporting evidence artifacts, protocols, results, and redaction documentation.

…mpt count (META-158)

The 0.5.0 release published both packages successfully at 00:05:31 and then
failed its own receipt at 00:06:48. Re-running the identical script against the
identical registry minutes later passed. The release was real; the receipt said
otherwise, which is precisely the false-red META-158 describes.

The cause is a comment and a constant disagreeing. The comment says propagation
lags "seconds to low minutes". The configuration was six attempts with a linear
five-second step — seventy-five seconds. Nobody multiplies that out while
reading, so the claim and the behavior drifted apart unnoticed until a release
landed in the gap.

The budget is now a duration: ten minutes, with capped backoff so the tail of a
long wait stays responsive. A duration sits next to the sentence that describes
it and cannot silently contradict it.

On exhaustion the script no longer just exits. It says which of the two possible
events this is, because they need opposite responses:

  version present on the registry -> the package shipped, only the receipt is
                                     missing; re-run this script
  version absent                  -> the publish did not land; re-cut

A failed lookup is not proof of a failed publish, and the old message let a
reader infer that it was.

Scripts are not in the packages' "files" lists and do not ship, so this carries
no changeset and does not move the version.
Copilot AI lite review requested due to automatic review settings August 18, 2026 00:13

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@sourcery-ai

sourcery-ai Bot commented Aug 18, 2026

Copy link
Copy Markdown

Reviewer's Guide

Switches registry propagation handling in the release verification script from a fixed retry count to a time-budget-driven loop, enhances logging to expose elapsed time and guidance when propagation appears exhausted, and caps backoff delays to keep the tail of long waits responsive.

File-Level Changes

Change Details Files
Replace fixed retry-count based propagation handling with a duration-based retry budget and dynamic backoff.
  • Remove REGISTRY_PROPAGATION_RETRIES and introduce REGISTRY_PROPAGATION_BUDGET_MS to represent the total allowed waiting time.
  • Add REGISTRY_PROPAGATION_MAX_DELAY_MS and compute each retry delay as a capped multiple of REGISTRY_PROPAGATION_BASE_DELAY_MS.
  • Change the retry loop to track start time, compute elapsed time, and continue until either success or budget exhaustion.
scripts/verify-published.mjs
Improve observability and user guidance around registry propagation lag and exhaustion.
  • Introduce a helper to render millisecond values as human-readable seconds for log messages.
  • Log when a package becomes visible after one or more retries, including elapsed time and attempt count.
  • On propagation-budget exhaustion, print a detailed explanation distinguishing publish failure from delayed visibility and instruct the user to check npm view and act accordingly.
  • Update retry log lines to report attempt number, elapsed time against the budget, and the next delay duration.
scripts/verify-published.mjs

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

qmarcelle has reached the 50-credit limit for trial accounts. To continue receiving code reviews, upgrade your plan.

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 1 issue, and left some high level feedback:

  • Consider using a monotonic clock (e.g., process.hrtime.bigint() or performance.now()) instead of Date.now() for elapsed time calculations so the propagation budget isn’t affected by system clock adjustments.
  • The retry/backoff constants and logic are now more complex; consider extracting them into a small helper (e.g., withRegistryPropagationBudget(...)) so the core verification flow reads more linearly and the policy is easier to reuse and adjust.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- Consider using a monotonic clock (e.g., `process.hrtime.bigint()` or `performance.now()`) instead of `Date.now()` for elapsed time calculations so the propagation budget isn’t affected by system clock adjustments.
- The retry/backoff constants and logic are now more complex; consider extracting them into a small helper (e.g., `withRegistryPropagationBudget(...)`) so the core verification flow reads more linearly and the policy is easier to reuse and adjust.

## Individual Comments

### Comment 1
<location path="scripts/verify-published.mjs" line_range="136-141" />
<code_context>
-    const delayMs = REGISTRY_PROPAGATION_BASE_DELAY_MS * attempt;
-    console.log(`${pkg.name}@${version} not yet visible on the registry (attempt ${attempt}/${REGISTRY_PROPAGATION_RETRIES}) — retrying in ${delayMs}ms`);
+
+    const delayMs = Math.min(REGISTRY_PROPAGATION_BASE_DELAY_MS * attempt, REGISTRY_PROPAGATION_MAX_DELAY_MS);
+    console.log(
+      `${pkg.name}@${version} not yet visible on the registry ` +
+        `(attempt ${attempt}, ${seconds(elapsedMs)} of ${seconds(REGISTRY_PROPAGATION_BUDGET_MS)} elapsed) — retrying in ${delayMs}ms`,
+    );
     await new Promise((resolve) => setTimeout(resolve, delayMs));
   }
 }
</code_context>
<issue_to_address>
**suggestion (bug_risk):** Retry delay selection can overshoot the intended propagation budget by up to the max delay.

Since `exhausted` is checked before the sleep and `delayMs` is capped independently, the loop can run past `REGISTRY_PROPAGATION_BUDGET_MS` by up to `REGISTRY_PROPAGATION_MAX_DELAY_MS`. If the budget is meant to be a hard limit, clamp `delayMs` to `REGISTRY_PROPAGATION_BUDGET_MS - elapsedMs`, and skip the retry when that computed delay is non‑positive so the final sleep cannot exceed the declared budget.

```suggestion
    const remainingMs = REGISTRY_PROPAGATION_BUDGET_MS - elapsedMs;
    const delayMs = Math.min(
      REGISTRY_PROPAGATION_BASE_DELAY_MS * attempt,
      REGISTRY_PROPAGATION_MAX_DELAY_MS,
      remainingMs,
    );

    if (delayMs <= 0) {
      console.log(
        `${pkg.name}@${version} not yet visible on the registry ` +
          `(attempt ${attempt}, ${seconds(elapsedMs)} of ${seconds(REGISTRY_PROPAGATION_BUDGET_MS)} elapsed) — propagation budget exhausted, giving up.`,
      );
      break;
    }

    console.log(
      `${pkg.name}@${version} not yet visible on the registry ` +
        `(attempt ${attempt}, ${seconds(elapsedMs)} of ${seconds(REGISTRY_PROPAGATION_BUDGET_MS)} elapsed) — retrying in ${delayMs}ms`,
    );
    await new Promise((resolve) => setTimeout(resolve, delayMs));
```
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines +136 to 141
const delayMs = Math.min(REGISTRY_PROPAGATION_BASE_DELAY_MS * attempt, REGISTRY_PROPAGATION_MAX_DELAY_MS);
console.log(
`${pkg.name}@${version} not yet visible on the registry ` +
`(attempt ${attempt}, ${seconds(elapsedMs)} of ${seconds(REGISTRY_PROPAGATION_BUDGET_MS)} elapsed) — retrying in ${delayMs}ms`,
);
await new Promise((resolve) => setTimeout(resolve, delayMs));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (bug_risk): Retry delay selection can overshoot the intended propagation budget by up to the max delay.

Since exhausted is checked before the sleep and delayMs is capped independently, the loop can run past REGISTRY_PROPAGATION_BUDGET_MS by up to REGISTRY_PROPAGATION_MAX_DELAY_MS. If the budget is meant to be a hard limit, clamp delayMs to REGISTRY_PROPAGATION_BUDGET_MS - elapsedMs, and skip the retry when that computed delay is non‑positive so the final sleep cannot exceed the declared budget.

Suggested change
const delayMs = Math.min(REGISTRY_PROPAGATION_BASE_DELAY_MS * attempt, REGISTRY_PROPAGATION_MAX_DELAY_MS);
console.log(
`${pkg.name}@${version} not yet visible on the registry ` +
`(attempt ${attempt}, ${seconds(elapsedMs)} of ${seconds(REGISTRY_PROPAGATION_BUDGET_MS)} elapsed) — retrying in ${delayMs}ms`,
);
await new Promise((resolve) => setTimeout(resolve, delayMs));
const remainingMs = REGISTRY_PROPAGATION_BUDGET_MS - elapsedMs;
const delayMs = Math.min(
REGISTRY_PROPAGATION_BASE_DELAY_MS * attempt,
REGISTRY_PROPAGATION_MAX_DELAY_MS,
remainingMs,
);
if (delayMs <= 0) {
console.log(
`${pkg.name}@${version} not yet visible on the registry ` +
`(attempt ${attempt}, ${seconds(elapsedMs)} of ${seconds(REGISTRY_PROPAGATION_BUDGET_MS)} elapsed) — propagation budget exhausted, giving up.`,
);
break;
}
console.log(
`${pkg.name}@${version} not yet visible on the registry ` +
`(attempt ${attempt}, ${seconds(elapsedMs)} of ${seconds(REGISTRY_PROPAGATION_BUDGET_MS)} elapsed) — retrying in ${delayMs}ms`,
);
await new Promise((resolve) => setTimeout(resolve, delayMs));

Evidence packet for the M2A causal-review proof experiment:
- 3 scenarios (billfold, integrations, syncpack) x 3 arms x 3 runs = 27 accepted runs
- 4 degraded-evidence conditions x 3 runs = 12 accepted runs
- 27 preflight/rejected runs (evidence injection bug, labeled, not counted)
- Protocol scripts, registered diffs, and evidence payloads
- MANIFEST.json with SHA-256 for every file
- REDACTIONS.md documenting all redactions (local paths only, no credentials)
- RECEIPT.md with corrected bounded result

Result: 1/3 scenarios PASS, 2/3 FAIL, 4/4 degraded controls PASS.
Disposition: NARROW (META-372).
@greptile-apps

greptile-apps Bot commented Aug 21, 2026

Copy link
Copy Markdown

Too many files changed for review (283 files, 100 file limit).

Bypass the limit by tagging @greptile-apps to review.

@sonarqubecloud

Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

Failed conditions
52.3% Duplication on New Code (required ≤ 3%)

See analysis details on SonarQube Cloud

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.

2 participants