diff --git a/README.md b/README.md index 1962a48a30..46c24055f0 100644 --- a/README.md +++ b/README.md @@ -15,10 +15,9 @@ RxJS is a library for composing asynchronous and event-based programs with Observable values. This repository contains the platform-based next generation of RxJS, planned for release as **RxJS 9**. -> RxJS 9 is prerelease work in development. The planned first beta is -> `9.0.0-beta.0`, but it has not been published to npm yet. The `next` tag still -> points to the earlier RxJS 8 prerelease, while RxJS 7 remains the production -> `latest` line and continues to be maintained. +> RxJS 9 is prerelease software. `9.0.0-beta.0` is available under npm's `next` +> tag, while RxJS 7 remains the production `latest` line and continues to be +> maintained. ## Why RxJS 9? What happened to RxJS 8? @@ -44,8 +43,8 @@ product. ## Preview the RxJS 9 API -The following example shows the planned beta API. Until `9.0.0-beta.0` is -published, do not use npm's `next` tag to install RxJS 9. +Install the beta with `npm install rxjs@next`. The following example shows the +RxJS 9 beta API. ```ts import { ColdObservable } from 'rxjs'; @@ -84,7 +83,7 @@ Repository-wide design records live in ## Supported environments -The planned beta supports Node 22.13+ and Node 24 as blocking lanes, with Node +The RxJS 9 beta supports Node 22.13+ and Node 24 as blocking lanes, with Node 26 in an advisory lane. Current Chrome, Firefox, desktop Safari, Mobile Safari, Deno, Bun, and Webpack 5 are blocking. Every supported consumer receives the same ESM implementation, so Deno and Bun support adds no runtime-specific diff --git a/docs/RELEASE_PROCESS.md b/docs/RELEASE_PROCESS.md index e2e9486f03..8482e68cbc 100644 --- a/docs/RELEASE_PROCESS.md +++ b/docs/RELEASE_PROCESS.md @@ -52,6 +52,12 @@ The command performs these steps in order: 8. compares each registry integrity with the local tarball; 9. verifies every package's `next` tag and confirms `rxjs@latest` remains RxJS 7. +For a newly created package, npm may accept the immutable version before its +ordinary package metadata stops returning the pre-publication 404. The command +revalidates online and waits up to ten minutes for that metadata to expose the +published SHA-512 instead of treating normal registry propagation as an +integrity failure. + npm may request OTP/WebAuthn once per package. That repetition is deliberate: the four packages are independent registry publications. Nothing attempts to bypass npm's proof-of-presence requirement. @@ -68,6 +74,12 @@ freshly packed tarball; a mismatch stops the release. Because `rxjs` is last, the main consumer entry remains unpublished until the three supporting packages are present and verified. +If a successful first publication is followed by a metadata 404, do not attempt +an immediate duplicate publish. Wait until `npm view @ +dist.integrity --prefer-online` exposes the expected SHA-512, then rerun the +same command. The post-publication retry makes this manual recovery unnecessary +for later betas, but the rule remains safe for an interrupted older client. + If a package was published correctly but a later package cannot be published, fix only the operational problem and rerun the same command from the same clean commit. If any source or package byte must change, bump to a fresh beta version. diff --git a/docs/rxjs-next/ARCHITECTURE.md b/docs/rxjs-next/ARCHITECTURE.md index e39bcf6dbb..59e2417987 100644 --- a/docs/rxjs-next/ARCHITECTURE.md +++ b/docs/rxjs-next/ARCHITECTURE.md @@ -75,7 +75,10 @@ the packages, prints their SHA-512 integrities, and runs npm publication dry runs. Ben must then type the exact version before npm's own OTP/WebAuthn flow publishes each tarball under `next`. The supporting packages publish first and `rxjs` publishes last. Registry integrity and dist-tags are verified before the -command reports success. +command reports success. npm may accept a brand-new immutable version before +its ordinary package metadata has replaced a cached pre-publication 404, so +post-publication verification revalidates online and retries that read path for +up to ten minutes. CI has no npm publishing credential and no workflow can publish. The design deliberately trusts Ben's local machine and npm account at the publication diff --git a/docs/rxjs-next/DECISIONS.md b/docs/rxjs-next/DECISIONS.md index 8fa787d248..848fd3a7cd 100644 --- a/docs/rxjs-next/DECISIONS.md +++ b/docs/rxjs-next/DECISIONS.md @@ -1482,8 +1482,10 @@ Status meanings: been initialized. - **Recovery and verification:** A rerun skips an already-published package only when npm's registry integrity matches the freshly packed tarball. Any byte - mismatch stops. Success requires all four `next` tags to resolve to the exact - version while `rxjs@latest` remains on RxJS 7. + mismatch stops. After a successful publish, the command revalidates online + and waits for npm's eventually consistent package metadata to expose the + immutable version and SHA-512. Success requires all four `next` tags to + resolve to the exact version while `rxjs@latest` remains on RxJS 7. - **Tradeoff:** The process trusts the maintainer's local machine and npm authentication and does not provide private staging or automatic npm provenance. That explicit, understandable boundary is accepted over a more diff --git a/docs/rxjs-next/PROJECT_PLAN.md b/docs/rxjs-next/PROJECT_PLAN.md index cdf47fc508..29e85bce03 100644 --- a/docs/rxjs-next/PROJECT_PLAN.md +++ b/docs/rxjs-next/PROJECT_PLAN.md @@ -80,9 +80,10 @@ wrappers in favor of direct D-037 `[create]` calls and recorded a further bundle-size reduction. P6.8 completed durable pull-request and `master` CI ownership for every accepted RxJS 9 test and release check. P6.9 implemented truthful status signals and security automation and validated their first live -GitHub results. The user has now prioritized P6.10: one understandable, -interactive single-maintainer beta publication command with npm two-factor -authentication and no CI publishing credential. +GitHub results. P6.10 then delivered one understandable, interactive +single-maintainer publication command with npm two-factor authentication and no +CI publishing credential, and used it to publish and verify the first RxJS 9 +beta. The planned execution queue is complete. RxJS 9 and `9.0.0-beta.0` are selected under D-007. D-053 defines runtime, browser, bundler, channel, and RxJS 7 maintenance policy. Dates and staffing @@ -1568,7 +1569,7 @@ names. | `DONE` | P6.7 | Use direct `[create]` construction and record bundle-size evidence | | `DONE` | P6.8 | Complete RxJS 9 CI coverage and validate the resulting pull-request workflow matrix | | `DONE` | P6.9 | Validate the first live dependency-review and Scorecard runs on GitHub | -| `NEXT` | P6.10 | Publish and verify the first beta with the interactive release command | +| `DONE` | P6.10 | Publish and verify the first beta with the interactive release command | #### P6.9 completion bar @@ -1644,8 +1645,22 @@ review`. This completes P6.9; the later P6.10 ruleset migration must preserve an operator-focused runbook. Existing CI, CodeQL, dependency review, OSV, package gates, and release-readiness coverage remain. - Local verification is recorded in the P6.10 session entry. Live npm - OTP/WebAuthn publication, package-access hardening, registry verification, - and the immutable GitHub Release remain required before `DONE`. + OTP/WebAuthn publication completed for all four packages from verified commit + `11d931645c03cc4b67eb70ee3a1122a53f8e0bc9`. The exact registry SHA-512 values + match the rehearsed tarballs, all four `next` tags resolve to + `9.0.0-beta.0`, and `rxjs@latest` remains `7.8.2`. +- npm accepted each new scoped package before its ordinary package metadata + replaced a cached pre-publication 404. Duplicate publication attempts were + rejected immutably. The command now revalidates online and waits up to ten + minutes for post-publication metadata instead of reporting that propagation + interval as an integrity failure. +- `npm access set mfa=publish` completed through browser authentication for + `@rxjs/observable-polyfill`, `@rxjs/test`, `@rxjs/migrate`, and `rxjs`, + requiring interactive 2FA and disallowing automation-token publication. +- Annotated tag `9.0.0-beta.0` targets the verified commit. The public + prerelease is + `https://github.com/ReactiveX/rxjs/releases/tag/9.0.0-beta.0`. P6.10 is + complete and the completed execution queue has no `NEXT` item. #### P6.1 completion bar @@ -3680,3 +3695,32 @@ conformance implementation depends on a runnable harness. remains the sole `NEXT` item until the command publishes and verifies `9.0.0-beta.0`, package access disallows automation tokens, and the immutable GitHub Release is recorded. + +### 2026-08-04 — P6.10 first RxJS 9 beta publication + +- Published `@rxjs/observable-polyfill`, `@rxjs/test`, `@rxjs/migrate`, and + `rxjs` at `9.0.0-beta.0` under npm `next` from verified commit + `11d931645c03cc4b67eb70ee3a1122a53f8e0bc9`. Master qualification run + `30940527701` and its browser, Safari, Deno, Bun, package, WPT, OSV, CodeQL, + and TypeScript companion workflows passed before publication. +- Verified registry SHA-512 values: + `@rxjs/observable-polyfill` = + `sha512-G2o17zDOxYgCPzM8+//WlDdViovC7NNkGBtlN57YwcVdbt9eMY/UkN1jJWC5j0s+f8Zuiqog1Mf5wFkef8wuAA==`; + `@rxjs/test` = + `sha512-wLBKwkYVHeMLqyJkdz9Yfcex+2/K9lvidOmtrkWWeCZTT+uwNdSgxKlkYbUTFFGjE8PhJsZtCZUmbgBp3tDXQA==`; + `@rxjs/migrate` = + `sha512-a7qOj3mPMOaif0CKVONDvciX6KCe7Pi7TdX/VUgxhLq62le6KikTB+d3n2Ei1pYEU/ngqZj2JIMu/90pZxvgQw==`; + `rxjs` = + `sha512-X7Bpak4JzO/cLamwHfxhtlFtQHpfIjvowcyNErrvHywcdlI+krkbNJc0vQsXKbVOTLrBipCJcIhuawhwsJfWwg==`. +- npm's new-package write path became visible before its ordinary metadata read + path, causing safe partial exits and immutable duplicate rejections during + recovery. Added online revalidation, ETARGET handling, and a bounded + post-publication wait so later betas tolerate the observed propagation lag + without weakening exact-integrity verification. +- Confirmed `rxjs@next` resolves to `9.0.0-beta.0` and `rxjs@latest` remains + `7.8.2`. Applied interactive-only `mfa=publish` access to all four packages. + Created annotated tag `9.0.0-beta.0` and published + `https://github.com/ReactiveX/rxjs/releases/tag/9.0.0-beta.0` as a GitHub + prerelease. +- Marked P6.10 `DONE`. All planned phases are complete, so the execution queue + intentionally has no `NEXT` item. diff --git a/packages/rxjs/docs/SECURITY_ASSURANCE.md b/packages/rxjs/docs/SECURITY_ASSURANCE.md index 3944655fb1..dcacc94792 100644 --- a/packages/rxjs/docs/SECURITY_ASSURANCE.md +++ b/packages/rxjs/docs/SECURITY_ASSURANCE.md @@ -24,6 +24,8 @@ free of vulnerabilities. packages and confirms that `rxjs@latest` remains on RxJS 7. - An interrupted command can skip an already-published package only when the registry integrity equals the freshly packed tarball. +- Post-publication verification revalidates online and tolerates npm metadata + propagation without weakening the exact SHA-512 comparison. ## One-maintainer reality diff --git a/scripts/release/beta.mjs b/scripts/release/beta.mjs index f564256e63..ed6689e249 100644 --- a/scripts/release/beta.mjs +++ b/scripts/release/beta.mjs @@ -7,6 +7,7 @@ import { mkdtemp, readFile, rm } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import path from 'node:path'; import { createInterface } from 'node:readline/promises'; +import { setTimeout as delay } from 'node:timers/promises'; import { fileURLToPath, pathToFileURL } from 'node:url'; const repositoryRoot = fileURLToPath(new URL('../..', import.meta.url)); @@ -90,7 +91,11 @@ export async function main(argv, options = {}) { process.stdout.write(`\nPublishing ${candidate.name}@${version}. npm may request OTP/WebAuthn.\n`); interactiveCommand('npm', publishArguments(candidate.tarballPath, { cache: npmCache }), { cwd: root }); - const publishedIntegrity = registryIntegrity(candidate.name, version, { cache: npmCache, command, root }); + const publishedIntegrity = await waitForRegistryIntegrity(candidate.name, version, { + cache: npmCache, + command, + root, + }); assert.equal( publishedIntegrity, candidate.integrity, @@ -191,10 +196,34 @@ export function publishArguments(tarballPath, { cache, dryRun = false } = {}) { function registryIntegrity(name, version, options) { const result = npmViewResult(`${name}@${version}`, 'dist.integrity', options); if (result.status === 0) return parseNpmView(result.stdout); - if (/E404|404 Not Found|is not in this registry/i.test(`${result.stdout}\n${result.stderr}`)) return null; + if ( + /E404|ETARGET|404 Not Found|is not in this registry|No matching version found|version not found/i.test( + `${result.stdout}\n${result.stderr}` + ) + ) { + return null; + } throw new Error(`Could not determine whether ${name}@${version} already exists.\n${result.stdout}${result.stderr}`); } +export async function waitForRegistryIntegrity( + name, + version, + options, + { attempts = 121, retryDelayMs = 5_000, wait = delay, output = process.stdout } = {} +) { + for (let attempt = 1; attempt <= attempts; attempt++) { + const integrity = registryIntegrity(name, version, options); + if (integrity !== null) return integrity; + if (attempt === attempts) return null; + if (attempt === 1 || attempt % 6 === 0) { + output.write(`Waiting for npm metadata to expose ${name}@${version} (${attempt}/${attempts})...\n`); + } + await wait(retryDelayMs); + } + return null; +} + function npmView(specifier, field, options) { const result = npmViewResult(specifier, field, options); if (result.status !== 0) throw new Error(`npm view ${specifier} ${field} failed.\n${result.stdout}${result.stderr}`); @@ -202,7 +231,7 @@ function npmView(specifier, field, options) { } function npmViewResult(specifier, field, { cache, command, root }) { - return command('npm', ['view', specifier, field, '--json', ...(cache ? ['--cache', cache] : [])], { + return command('npm', ['view', specifier, field, '--json', '--prefer-online', ...(cache ? ['--cache', cache] : [])], { cwd: root, encoding: 'utf8', allowFailure: true, diff --git a/scripts/release/beta.test.mjs b/scripts/release/beta.test.mjs index 2ffa602d88..5dacc11527 100644 --- a/scripts/release/beta.test.mjs +++ b/scripts/release/beta.test.mjs @@ -12,6 +12,7 @@ import { publishArguments, releasePackages, validateBetaVersion, + waitForRegistryIntegrity, } from './beta.mjs'; test('accepts one explicit RxJS 9 beta and an optional dry run', () => { @@ -109,3 +110,43 @@ test('live publication refuses CI, environment tokens, and non-interactive termi assertInteractivePublishingEnvironment({ dryRun: true, env: { CI: 'true', NPM_TOKEN: 'secret' }, stdin: {}, stdout: {} }) ); }); + +test('waits for npm metadata propagation after a successful publication', async () => { + let lookups = 0; + const waits = []; + const output = []; + const command = (_command, args) => { + assert.ok(args.includes('--prefer-online')); + lookups++; + if (lookups < 3) return { status: 1, stdout: '', stderr: 'npm error code E404' }; + return { status: 0, stdout: '"sha512-published"\n', stderr: '' }; + }; + + const integrity = await waitForRegistryIntegrity( + '@rxjs/new-package', + '9.0.0-beta.0', + { cache: '/tmp/cache', command, root: '/repo' }, + { + attempts: 3, + retryDelayMs: 10, + wait: async (milliseconds) => waits.push(milliseconds), + output: { write: (message) => output.push(message) }, + } + ); + + assert.equal(integrity, 'sha512-published'); + assert.equal(lookups, 3); + assert.deepEqual(waits, [10, 10]); + assert.match(output.join(''), /Waiting for npm metadata/); +}); + +test('treats an existing package with an unpropagated version as temporarily missing', async () => { + const command = () => ({ status: 1, stdout: '', stderr: 'npm error code ETARGET\nNo matching version found' }); + const integrity = await waitForRegistryIntegrity( + 'rxjs', + '9.0.0-beta.1', + { cache: '/tmp/cache', command, root: '/repo' }, + { attempts: 1, wait: async () => {}, output: { write: () => {} } } + ); + assert.equal(integrity, null); +});