Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .fas/TASKS.md
Original file line number Diff line number Diff line change
Expand Up @@ -2631,6 +2631,17 @@ No active tasks.
- Policy sensitivity: standard
- Blast radius: cross-cutting

### Task: Harden beta release dist-tag verification against registry propagation lag

- Title: Harden beta release dist-tag verification against registry propagation lag
- Mode: single-agent
- Status: review
- Owner: reviewer
- Brief: .fas/tasks/harden-beta-release-dist-tag-verification-against-registry-p.md
- Verification lane: fast
- Policy sensitivity: standard
- Blast radius: cross-cutting

## Template

### Task: <short task title>
Expand Down
34 changes: 31 additions & 3 deletions .fas/queue/tasks.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,16 +46,17 @@
"task-1783461996097",
"task-1783610965770",
"task-1783719740973",
"task-1784383053533"
"task-1784383053533",
"task-1784408640433"
],
"blocks": [
"task-1781197578529"
],
"branchBase": null,
"prDependencies": [],
"createdAt": "2026-06-12T19:30:13.064Z",
"updatedAt": "2026-07-18T13:57:33.533Z",
"deferredReason": "dependency_reopened: task-1781919547313, task-1783610965770, task-1783719740973",
"updatedAt": "2026-07-18T21:04:00.433Z",
"deferredReason": "dependency_reopened: task-1781919547313, task-1783610965770, task-1783719740973, task-1784408640433",
"invalidationReason": null
},
{
Expand Down Expand Up @@ -472,6 +473,29 @@
"updatedAt": "2026-07-17T14:31:40.854Z",
"deferredReason": "dependency_reopened: task-1784298685324",
"invalidationReason": null
},
{
"id": "task-1784408640433",
"task": "Harden beta release dist-tag verification against registry propagation lag",
"priority": "high",
"source": "manual",
"details": "The beta.9 publish and dist-tag writes succeeded, but the wrapper immediately read stale npm dist-tags and exited 1 with misleading OTP repair instructions. Make post-write verification force fresh registry reads, tolerate bounded propagation lag, and distinguish a persistent verification mismatch from an actual dist-tag write failure so successful releases do not report false failure.\nSee .fas/tasks/harden-beta-release-dist-tag-verification-against-registry-p.md",
"taskClassification": "self-improvement",
"selfImprovementCategory": "runtime-reliability",
"signalFingerprint": null,
"status": "processing",
"owner": "runtime",
"mode": "single-agent",
"dependsOn": [
"task-1784383053533"
],
"blocks": [
"task-1781292613064"
],
"branchBase": null,
"prDependencies": [],
"createdAt": "2026-07-18T21:04:00.433Z",
"updatedAt": "2026-07-18T21:04:11.682Z"
}
],
"completedTasks": [
Expand Down Expand Up @@ -4151,6 +4175,10 @@
{
"taskId": "task-1781197578529",
"epicRole": "stable-cut"
},
{
"taskId": "task-1784408640433",
"epicRole": "beta-release-hardening"
}
]
},
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# Harden beta release dist-tag verification against registry propagation lag

## Source
Created with `fas create-task` on 2026-07-18.

## Problem
The beta.9 publish and dist-tag writes succeeded, but the wrapper immediately read stale npm dist-tags and exited 1 with misleading OTP repair instructions. Make post-write verification force fresh registry reads, tolerate bounded propagation lag, and distinguish a persistent verification mismatch from an actual dist-tag write failure so successful releases do not report false failure.

## Acceptance criteria
- Post-write dist-tag reads explicitly prefer the online npm registry and retry a bounded number of transient stale snapshots.
- Verification succeeds without repair guidance when the expected tags converge within the retry budget.
- Persistent stale verification tells the operator to recheck live tags before applying repair commands, while an actual dist-tag write failure retains fresh-OTP repair guidance.
- Deterministic regression tests cover transient stale-then-converged reads and persistent mismatch behavior.
- TDD: a failing test that captures the new or changed behavior is written before the implementation and lands in the same change.
- TDD: every production code change in the change set is covered by an added or updated test.
- DDD: respect domain boundaries — keep the functional core deterministic and side-effect-free (no reads, writes, network, or clock), confine coordination to the imperative shell, and have adapters return facts instead of throwing.
- The work is tracked in `.fas/TASKS.md`.
- The task has a clear implementation and verification plan before execution starts.
- The task is queued in `.fas/queue/tasks.json` for the runtime.

## Proposed solution
- Use the supplied problem context, acceptance criteria, and affected-file hints to draft the concrete implementation approach during planning.

## Alternatives considered
- None recorded at task creation. Add rejected approaches during planning if scope tradeoffs appear.

## Affected files
- scripts/release-beta.mjs
- scripts/release-beta.test.mjs

## Scope Amendments
- None.

## Implementation plan
- Convert the supplied context into a scoped implementation plan before editing.
- Refresh affected-file scope before implementation if the generated hints are incomplete.

## Verification plan
- Run `fas validate-task` for the inner-loop verification gate.
- Run `.fas/scripts/verify.sh --full` at the final release-quality gate when tracked files change.

## Risks
- Validate generated scope, acceptance criteria, and verification evidence before closeout to avoid workflow drift.

## Dependencies
- None known at task creation.

## Open questions
- None captured at task creation.

## Artifact links
- Planning: `.fas/state/planning.json`
- Task packet: `.fas/state/task-packet.json`
- Commit plan: `.fas/state/commit-plan.json`
- Verification: `.fas/state/verification/latest.json`
- Review: `.fas/state/boundary-review-findings.md`
- Workflow: `.fas/state/workflows/`
126 changes: 110 additions & 16 deletions scripts/release-beta.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,12 @@ import { execSync } from "node:child_process";
import { existsSync, readdirSync, readFileSync, rmSync } from "node:fs";
import { env, exit, stdin, stdout } from "node:process";
import { createInterface } from "node:readline/promises";
import { setTimeout as waitFor } from "node:timers/promises";
import { pathToFileURL } from "node:url";

const DIST_TAG_VERIFY_MAX_ATTEMPTS = 8;
const DIST_TAG_VERIFY_RETRY_DELAY_MS = 2_000;

const RELEASE_PACKAGE_MANIFESTS = [
{
manifest: "packages/ignite-element/package.json",
Expand Down Expand Up @@ -126,6 +130,56 @@ export const assertBetaDistTags = ({
}
};

export const verifyBetaDistTagsWithRetry = async ({
expectedVersion,
mainLatestBefore,
readTags,
maxAttempts = DIST_TAG_VERIFY_MAX_ATTEMPTS,
retryDelayMs = DIST_TAG_VERIFY_RETRY_DELAY_MS,
wait = waitFor,
onRetry = () => {},
}) => {
const attemptLimit =
Number.isInteger(maxAttempts) && maxAttempts > 0 ? maxAttempts : 1;
let lastError = new Error(
"[release:beta] Dist-tag verification did not run.",
);

for (let attempt = 1; attempt <= attemptLimit; attempt += 1) {
try {
const tagsByPackage = await readTags();
assertBetaDistTags({
expectedVersion,
mainLatestBefore,
tagsByPackage,
});
return {
status: "verified",
attempts: attempt,
tagsByPackage,
};
} catch (error) {
lastError =
error instanceof Error ? error : new Error(String(error ?? "unknown"));
if (attempt < attemptLimit) {
onRetry({
attempt,
attemptLimit,
error: lastError,
retryDelayMs,
});
await wait(retryDelayMs);
}
}
}

return {
status: "failed",
attempts: attemptLimit,
error: lastError,
};
};

const args = process.argv.slice(2);
const hasFlag = (flag) => args.includes(flag);
const getOption = (prefix) =>
Expand Down Expand Up @@ -248,8 +302,11 @@ const isPreMode = () => {
}
};

export const createDistTagReadCommand = (packageName) =>
`npm view ${packageName} dist-tags --json --prefer-online`;

const readPublishedDistTags = (packageName, commandEnv = env) => {
const output = execSync(`npm view ${packageName} dist-tags --json`, {
const output = execSync(createDistTagReadCommand(packageName), {
encoding: "utf8",
env: commandEnv,
stdio: ["ignore", "pipe", "inherit"],
Expand All @@ -265,12 +322,34 @@ const readAllPublishedDistTags = (commandEnv = env) =>
]),
);

const printDistTagRecovery = (distTagCommands) => {
console.error(
"[release:beta] Repair the beta tags with a fresh npm OTP, then rerun the release script to verify:",
export const createDistTagRecoveryInstructions = ({
distTagCommands,
kind,
}) => {
const repairCommands = distTagCommands.map(
(command) => ` ${command} --otp=<OTP>`,
);
for (const command of distTagCommands) {
console.error(` ${command} --otp=<OTP>`);
if (kind === "write") {
return [
"[release:beta] A beta dist-tag update failed. Repair the beta tags with a fresh npm OTP, then rerun the release script to verify:",
...repairCommands,
];
}

return [
"[release:beta] Dist-tag verification remained stale or mismatched after bounded online retries.",
"[release:beta] Recheck the live registry before changing any tags:",
...RELEASE_PACKAGE_MANIFESTS.map(
({ name }) => ` ${createDistTagReadCommand(name)}`,
),
"[release:beta] If the live tags are still wrong, repair them with a fresh npm OTP, then rerun the release script:",
...repairCommands,
];
};

const printDistTagRecovery = (options) => {
for (const instruction of createDistTagRecoveryInstructions(options)) {
console.error(instruction);
}
};

Expand Down Expand Up @@ -448,20 +527,35 @@ const main = async () => {
for (const command of plan.distTagCommands) {
run(command, { env: publishEnv });
}
const tagsByPackage = readAllPublishedDistTags(publishEnv);
assertBetaDistTags({
expectedVersion: plan.expectedVersion,
mainLatestBefore,
tagsByPackage,
});
console.log(
`[release:beta] Verified all beta tags at ${plan.expectedVersion}; ignite-element latest remains ${mainLatestBefore}.`,
);
} catch (error) {
printDistTagRecovery(plan.distTagCommands);
printDistTagRecovery({
distTagCommands: plan.distTagCommands,
kind: "write",
});
throw error;
}

const verification = await verifyBetaDistTagsWithRetry({
expectedVersion: plan.expectedVersion,
mainLatestBefore,
readTags: () => readAllPublishedDistTags(publishEnv),
onRetry: ({ attempt, attemptLimit, retryDelayMs }) => {
console.warn(
`[release:beta] Dist-tags have not converged after attempt ${attempt}/${attemptLimit}; retrying in ${retryDelayMs}ms.`,
);
},
});
if (verification.status === "failed") {
printDistTagRecovery({
distTagCommands: plan.distTagCommands,
kind: "verification",
});
throw verification.error;
}
console.log(
`[release:beta] Verified all beta tags at ${plan.expectedVersion} after ${verification.attempts} attempt(s); ignite-element latest remains ${mainLatestBefore}.`,
);

console.log(
"\n✅ Beta release complete. Review git status, commit the changes, and push tags to share the release.",
);
Expand Down
Loading
Loading