diff --git a/.changeset/README.md b/.changeset/README.md index fb5ee37..946b046 100644 --- a/.changeset/README.md +++ b/.changeset/README.md @@ -9,12 +9,14 @@ need to understand the release modes or debug an unexpected failure. ## Quick reference -| I want to... | Section | Command | -| ----------------------------------------------------- | ------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- | -| Ship a changeset through the normal main-merge flow | [Regular flow](#regular-flow-release-everything-at-once) | `npx changeset`; merge PR; merge the Release PR | -| Ship one package immediately | [Single-package flow](#single-package-flow-release-one-package-now) | `cd .changeset && npm run release -- @fujocoded/` | -| Publish a beta/prerelease of all packages | [Prerelease flow](#prerelease-flow-ship-betas-of-everything) | `npx changeset pre enter ` + `npx changeset version`, then dispatch workflow `mode: prerelease` | -| Set up a brand-new package for NPM Trusted Publishing | [New-package bootstrap](#new-package-bootstrap) | `cd .changeset && npm run release:bootstrap -- @fujocoded/` | +| I want to... | Section | Command | +| ------------------------------------------------------ | ------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- | +| Ship a changeset through the normal main-merge flow | [Regular flow](#regular-flow-release-everything-at-once) | `npx changeset`; merge PR; merge the Release PR | +| Ship one package immediately | [Single-package flow](#single-package-flow-release-one-package-now) | `cd .changeset && npm run release -- @fujocoded/` | +| Publish a beta/prerelease of all packages | [Prerelease flow](#prerelease-flow-ship-betas-of-everything) | `npx changeset pre enter ` + `npx changeset version`, then dispatch workflow `mode: prerelease` | +| Set up a brand-new package for NPM Trusted Publishing | [New-package setup](#new-package-setup) | `cd .changeset && npm run release:trust -- @fujocoded/` | +| See which packages lack Trusted Publishing (read-only) | [Every package at once](#every-package-at-once) | `cd .changeset && npm run release:trust -- --all --dry-run` | +| Configure Trusted Publishing everywhere it is missing | [Every package at once](#every-package-at-once) | `cd .changeset && npm run release:trust -- --all` | ## Regular flow @@ -107,11 +109,12 @@ npm run release -- @fujocoded/astro-smooth-actions This command: - Checks whether the package is already on the npm registry -- If it is not, bootstraps it: publishes `0.0.0` locally, deprecates it, and +- If it is not, sets it up: publishes `0.0.0` locally, deprecates it, and runs `npm trust github` to configure Trusted Publishing -- If it is on npm at `0.0.0` but Trusted Publishing isn't configured (e.g. a - previous bootstrap failed at the trust step), deprecates `0.0.0` if needed - and runs just the trust step without re-publishing +- If it is on npm but Trusted Publishing isn't configured, either because a previous + attempt failed at the trust step, or the package predates Trusted Publishing + and was published with a classic token: runs just the trust step without + re-publishing. Never deprecates a real release. - Asks whether to release the package immediately or leave it for the regular main-merge flow @@ -137,7 +140,7 @@ npm run release -- @fujocoded/astro-smooth-actions --dry-run > accumulated prerelease state across the whole branch. Release everything > together (the [prerelease flow](#prerelease-flow)) or exit pre mode first. -### New-package bootstrap +### New-package setup NPM Trusted Publishing cannot be configured until a package already exists on the registry. For a brand-new package we therefore publish a `0.0.0` placeholder @@ -145,29 +148,80 @@ locally, deprecate it, and configure trust. After that, the package can publish through GitHub Actions OIDC like every other package. The top-level `release` command detects an unpublished package and runs the -bootstrap automatically. To run the bootstrap step by itself: +trust setup automatically. To run it by itself: ```bash cd .changeset -npm run release:bootstrap -- @fujocoded/astro-smooth-actions +npm run release:trust -- @fujocoded/astro-smooth-actions ``` > [!WARNING] -> Before bootstrapping, confirm that: +> Before running it, confirm that: > > - You are logged in to npm (check with `npm whoami`) -> - The package is at version `0.0.0` in its `package.json` and has a pending -> changeset +> - If the package has never been published, it is at version `0.0.0` in its +> `package.json` -This step: +`release:trust` asks npm whether Trusted Publishing is already configured, and does +whichever of these applies: -- Builds the package and runs `npm publish --access public` at `0.0.0` -- Deprecates `0.0.0` with a message pointing to `0.0.1` or later -- Runs `npm trust github --repo FujoWebDev/fujocoded-plugins --file release.yaml --allow-publish` +| On npm | What runs | +| ------------------------ | -------------------------------------------------- | +| Not published | build → publish `0.0.0` → deprecate it → trust | +| Published at `0.0.0` | deprecate `0.0.0` if it isn't already → trust | +| Published above `0.0.0` | trust only — nothing published, nothing deprecated | +| Published, trust present | nothing | + +The trust step is: + +```bash +npm trust github --repo FujoWebDev/fujocoded-plugins --file release.yaml --allow-publish +``` + +Only the `0.0.0` placeholder is ever deprecated; a real release never is. Row 2 +is a partly-finished setup — a previous run published `0.0.0` and then died +before configuring trust. Row 3 is a package that predates Trusted Publishing in +this repo and was published with a classic token; it needs trust attached but +must not be touched otherwise. + +The first row is the only one that publishes, and it is the only one that +requires the manifest to be at `0.0.0` — otherwise the command refuses rather +than publishing a placeholder over a real version number. + +None of this requires a pending changeset: configuring trust is independent of +releasing. The `0.0.0` placeholder remains on npm but is deprecated and hidden from default installs. It exists only so Trusted Publishing can be configured. +#### Every package at once + +To see which packages are missing Trusted Publishing, without changing anything: + +```bash +cd .changeset +npm run release:trust -- --all --dry-run +``` + +This is read-only. It prints the trust commands it would run and a per-package +summary, and touches neither npm nor the repo. + +To actually configure the ones that are missing it: + +```bash +cd .changeset +npm run release:trust -- --all +``` + +This **writes to npm**: every package without Trusted Publishing gets it +configured. Packages that already have it are left untouched, and nothing is +published or deprecated for a package that is already on npm at a real version. + +Each package is independent: if one fails — an expired npm session is the usual +cause, since the OTP challenge can reappear part way through — it is recorded +and the run continues, then the command exits non-zero with a per-package +summary. Re-running is safe and only retries what is still missing. + ### Other single-package commands All the steps that make up `release` are available separately. @@ -188,9 +242,9 @@ under the package and runs focused checks. > [!WARNING] > `release:prepare` assumes the package already exists on npm with Trusted -> Publishing configured. It will prepare a release for an unbootstrapped -> package, but the dispatch step will then fail. Use `release:bootstrap` (or the -> top-level `release` command, which bootstraps first) for new packages. +> Publishing configured. It will prepare a release for a package without trust +> configured, but the dispatch step will then fail. Use `release:trust` (or the +> top-level `release` command, which runs it first) for such packages. #### Dispatch the single-package workflow @@ -243,13 +297,13 @@ a branch that has `pre.json` plus exactly one versioned package. ## Command summary -| Command | What it does | -| ------------------- | -------------------------------------------------------------- | -| `release` | Bootstrap if needed, then version + dispatch + sync-back | -| `release:bootstrap` | Publish `0.0.0`, deprecate it, configure Trusted Publishing | -| `release:prepare` | Version one package on a temporary branch | -| `release:dispatch` | Push branch, trigger `release.yaml`, watch, sync back | -| `release:sync-back` | Carry versioned state from a release branch to a target branch | +| Command | What it does | +| ------------------- | ------------------------------------------------------------------------------------------ | +| `release` | Configure trust if needed, then version + dispatch + sync-back | +| `release:trust` | Configure Trusted Publishing, publishing a `0.0.0` placeholder first if the package is new | +| `release:prepare` | Version one package on a temporary branch | +| `release:dispatch` | Push branch, trigger `release.yaml`, watch, sync back | +| `release:sync-back` | Carry versioned state from a release branch to a target branch | ## Common flags @@ -260,7 +314,7 @@ If you don't pass a package name, you get an interactive prompt to pick one. ### `release` -`release` combines bootstrap, prepare, and dispatch, so it accepts flags that +`release` combines trust setup, prepare, and dispatch, so it accepts flags that affect the prepare and dispatch phases. - `--branch ` — use this branch name in `release:prepare`, skipping the @@ -270,8 +324,10 @@ affect the prepare and dispatch phases. - `--allow-dirty` — allow dispatch when the working tree is not clean - `--dry-run` — show planned operations -### `release:bootstrap` +### `release:trust` +- `--all` — check every public package and configure the ones missing Trusted + Publishing; takes no package name - `--dry-run` — show planned operations ### `release:prepare` diff --git a/.changeset/package.json b/.changeset/package.json index 90c2f92..c499b96 100644 --- a/.changeset/package.json +++ b/.changeset/package.json @@ -3,7 +3,7 @@ "type": "module", "scripts": { "release": "node ../.github/scripts/.changeset/release.mjs release", - "release:bootstrap": "node ../.github/scripts/.changeset/release.mjs bootstrap", + "release:trust": "node ../.github/scripts/.changeset/release.mjs trust", "release:prepare": "node ../.github/scripts/.changeset/release.mjs prepare", "release:dispatch": "node ../.github/scripts/.changeset/release.mjs dispatch", "release:sync-back": "node ../.github/scripts/.changeset/release.mjs sync-back" diff --git a/.github/scripts/.changeset/release-dispatch.mjs b/.github/scripts/.changeset/release-dispatch.mjs index 2ba8eb5..9a052a0 100644 --- a/.github/scripts/.changeset/release-dispatch.mjs +++ b/.github/scripts/.changeset/release-dispatch.mjs @@ -4,296 +4,13 @@ import { resolveReleasePackage, } from "./release-packages.mjs"; import { maybeSyncBackAfterDispatch } from "./release-sync-back.mjs"; - -const packageUrl = (packageName) => - `https://www.npmjs.com/package/${packageName}`; +import { getLatestPublishedVersion, packageUrl } from "./release-trust.mjs"; const getWorkflowRunIdFromText = (text) => text.match( /https:\/\/github\.com\/[^/\s]+\/[^/\s]+\/actions\/runs\/(\d+)/, )?.[1] ?? null; -// Returns the latest version currently published on npm for the package, or -// null when the package is not on npm at all. Used only for existence checks -// (bootstrap decides whether to publish the 0.0.0 placeholder; dispatch -// refuses to run before bootstrap). Throws on any other npm failure so callers -// can distinguish a clean 404 from a real registry problem. -const getLatestPublishedVersion = (packageName) => { - const result = spawnSync("npm", ["view", packageName, "version"], { - encoding: "utf8", - stdio: ["ignore", "pipe", "pipe"], - }); - - if (result.status === 0) { - return result.stdout.trim() || null; - } - - const combined = `${result.stderr}\n${result.stdout}`; - if (/E404|404 Not Found/i.test(combined)) { - return null; - } - - throw new Error( - `Could not check npm for ${packageName}:\n${combined.trim()}`, - ); -}; - -// Returns true when a specific published version is deprecated on npm. -// Uses `npm view` (read-only, no auth). A non-deprecated version returns -// an empty string; a deprecated version returns the deprecation message. -const isVersionDeprecated = (packageName, version) => { - const result = spawnSync( - "npm", - ["view", `${packageName}@${version}`, "deprecated"], - { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }, - ); - - return result.status === 0 && result.stdout.trim().length > 0; -}; - -const assertNpmLoggedIn = (repoRoot) => { - const whoami = spawnSync("npm", ["whoami"], { - cwd: repoRoot, - encoding: "utf8", - stdio: ["ignore", "pipe", "pipe"], - }); - - if (whoami.status !== 0) { - throw new Error( - `npm login is required before bootstrapping.\n\n${whoami.stderr.trim()}`, - ); - } - - return whoami.stdout.trim(); -}; - -const trustGithubArgs = (pkg, repo, workflow) => [ - "trust", - "github", - pkg.name, - "--repo", - repo, - "--file", - workflow, - "--allow-publish", -]; - -// Configure npm Trusted Publishing for a package. Runs `npm trust github` -// interactively: stdin/stdout are inherited so the OTP/browser-auth prompts -// work, but stderr is piped so we can inspect the error code. A 409 Conflict -// means trust is already configured for this package — treat it as success. -// Any other failure throws with the manual recovery command. -const configureTrustedPublishing = (pkg, repo, workflow) => { - const trust = spawnSync("npm", trustGithubArgs(pkg, repo, workflow), { - stdio: ["inherit", "inherit", "pipe"], - encoding: "utf8", - }); - - if (trust.status === 0) { - return; - } - - if (/E409|409 Conflict/i.test(trust.stderr ?? "")) { - console.log(`Trusted Publishing for ${pkg.name} is already configured.`); - return; - } - - throw new Error( - `npm trust failed for ${pkg.name}. Run manually, then re-run the release command:\n\nnpm trust github ${pkg.name} --repo ${repo} --file ${workflow} --allow-publish`, - ); -}; - -// Publish a placeholder 0.0.0 for a brand-new package, deprecate it, and -// configure npm Trusted Publishing so future publishes go through GitHub -// Actions OIDC with provenance. This is the one-time bootstrap that makes the -// single-package release workflow able to publish without a classic npm token. -// -// The package must already be at version 0.0.0 in its package.json. We publish -// the real dist (the build output), not an empty package, so the placeholder is -// a usable if throwaway version rather than a broken stub. -export const bootstrapRelease = async ({ - choosePackage, - confirmYes, - logStep, - note, - options, - outro, - packageNameOrDir, - repo, - repoRoot, - run, - workflow, -}) => { - const pkg = await resolveReleasePackage({ - choosePackage, - phase: "prepare", - repoRoot, - requestedPackage: packageNameOrDir, - }); - - if (pkg.version !== "0.0.0") { - throw new Error( - `${pkg.name} is at ${pkg.version}, but bootstrap expects 0.0.0. Bootstrap only runs for packages that have never been published.`, - ); - } - - const published = getLatestPublishedVersion(pkg.name); - if (published) { - // A version greater than 0.0.0 can only have been published through the - // release workflow (GitHub Actions OIDC), which requires Trusted - // Publishing. The 0.0.0 placeholder is published locally, so it doesn't - // count. - if (published !== "0.0.0") { - note( - `${pkg.name} is already on npm at ${published}. Trusted Publishing is configured (a real version was published through the workflow). Bootstrap is not needed.`, - "Skip bootstrap", - ); - return { pkg, skipped: true }; - } - - // Recovery: 0.0.0 is published but Trusted Publishing is not configured - // (e.g. a prior bootstrap died after publish). Don't re-publish — just - // deprecate (if not already) and configure trust. - note( - `${pkg.name} is on npm at 0.0.0 but Trusted Publishing is not configured. The deprecation and trust steps will run.`, - "Bootstrap recovery", - ); - - logStep("Checking npm login."); - note(assertNpmLoggedIn(repoRoot), "npm user"); - - if ( - !(await confirmYes( - options.dryRun - ? `Dry run: configure Trusted Publishing for ${pkg.name}?` - : `Configure npm Trusted Publishing for ${pkg.name} now?\n\nThis completes the bootstrap without re-publishing 0.0.0.`, - )) - ) { - throw new Error("Canceled."); - } - - if (options.dryRun) { - if (!isVersionDeprecated(pkg.name, "0.0.0")) { - run( - "npm", - [ - "deprecate", - `${pkg.name}@0.0.0`, - "Bootstrap placeholder release; use 0.0.1 or later.", - ], - { dryRun: true }, - ); - } - run("npm", trustGithubArgs(pkg, repo, workflow), { dryRun: true }); - } else { - if (!isVersionDeprecated(pkg.name, "0.0.0")) { - logStep(`Deprecating 0.0.0 for ${pkg.name}.`); - const deprecate = spawnSync( - "npm", - [ - "deprecate", - `${pkg.name}@0.0.0`, - "Bootstrap placeholder release; use 0.0.1 or later.", - ], - { stdio: "inherit" }, - ); - if (deprecate.status !== 0) { - note( - `npm deprecate failed. Run manually:\nnpm deprecate ${pkg.name}@0.0.0 "Bootstrap placeholder release; use 0.0.1 or later."`, - "Deprecate failed", - ); - } - } - logStep("Configuring npm Trusted Publishing."); - configureTrustedPublishing(pkg, repo, workflow); - } - - note( - [`${packageUrl(pkg.name)}`, `npm trust list ${pkg.name}`].join("\n"), - "Verify on npm", - ); - outro(`Configured Trusted Publishing for ${pkg.name}.`); - return { pkg, skipped: false }; - } - - logStep("Checking npm login."); - note(assertNpmLoggedIn(repoRoot), "npm user"); - - if ( - !(await confirmYes( - options.dryRun - ? `Dry run: publish 0.0.0 for ${pkg.name}, deprecate it, and configure Trusted Publishing?` - : `Publish 0.0.0 for ${pkg.name} on npm, deprecate it, and configure Trusted Publishing?\n\nThis publishes a permanent but deprecated placeholder version so npm Trusted Publishing can be configured. The real release happens afterwards through the release workflow.`, - )) - ) { - throw new Error("Canceled."); - } - - if (options.dryRun) { - run("npm", ["run", "build"], { cwd: pkg.absoluteDir, dryRun: true }); - run("npm", ["publish", "--access", "public"], { - cwd: pkg.absoluteDir, - dryRun: true, - }); - run( - "npm", - [ - "deprecate", - `${pkg.name}@0.0.0`, - "Bootstrap placeholder release; use 0.0.1 or later.", - ], - { dryRun: true }, - ); - run("npm", trustGithubArgs(pkg, repo, workflow), { dryRun: true }); - } else { - logStep(`Building ${pkg.name}.`); - const build = spawnSync("npm", ["run", "build"], { - cwd: pkg.absoluteDir, - stdio: "inherit", - }); - if (build.status !== 0) { - throw new Error(`Build failed for ${pkg.name}.`); - } - - logStep(`Publishing 0.0.0 for ${pkg.name}.`); - const publish = spawnSync("npm", ["publish", "--access", "public"], { - cwd: pkg.absoluteDir, - stdio: "inherit", - }); - if (publish.status !== 0) { - throw new Error(`Could not publish 0.0.0 for ${pkg.name}.`); - } - - logStep(`Deprecating 0.0.0 for ${pkg.name}.`); - const deprecate = spawnSync( - "npm", - [ - "deprecate", - `${pkg.name}@0.0.0`, - "Bootstrap placeholder release; use 0.0.1 or later.", - ], - { stdio: "inherit" }, - ); - if (deprecate.status !== 0) { - note( - `npm deprecate failed. Run manually:\nnpm deprecate ${pkg.name}@0.0.0 "Bootstrap placeholder release; use 0.0.1 or later."`, - "Deprecate failed", - ); - } - - logStep("Configuring npm Trusted Publishing."); - configureTrustedPublishing(pkg, repo, workflow); - } - - note( - [`${packageUrl(pkg.name)}`, `npm trust list ${pkg.name}`].join("\n"), - "Verify on npm", - ); - - outro(`Bootstrapped ${pkg.name}.`); - return { pkg, skipped: false }; -}; - const findWorkflowRun = ({ branchName, capture, @@ -357,7 +74,7 @@ const waitForWorkflowRun = ({ // Dispatch the release workflow for a single pre-versioned package. // The workflow publishes through GitHub Actions OIDC (Trusted Publishing), so // no npm token or NPM_TOKEN secret is touched. Requires the package to already -// exist on npm with Trusted Publishing configured (run bootstrap first for a +// exist on npm with Trusted Publishing configured (run the trust command first // brand-new package). export const dispatchRelease = async ({ choosePackage, @@ -368,6 +85,7 @@ export const dispatchRelease = async ({ options, outro, packageNameOrDir, + remote, repo, repoRoot, workflow, @@ -406,7 +124,7 @@ export const dispatchRelease = async ({ const published = getLatestPublishedVersion(pkg.name); if (!published) { throw new Error( - `${pkg.name} is not on npm yet. Run the bootstrap command first to publish 0.0.0 and configure Trusted Publishing.`, + `${pkg.name} is not on npm yet. Run the trust command first to publish 0.0.0 and configure Trusted Publishing.`, ); } @@ -421,7 +139,7 @@ export const dispatchRelease = async ({ } logStep(`Pushing ${branchName}.`); - run("git", ["push", "fujo", `${branchName}:${branchName}`], { + run("git", ["push", remote, `${branchName}:${branchName}`], { cwd: repoRoot, dryRun: options.dryRun, }); diff --git a/.github/scripts/.changeset/release-packages.mjs b/.github/scripts/.changeset/release-packages.mjs index f5bdb6a..f89febf 100644 --- a/.github/scripts/.changeset/release-packages.mjs +++ b/.github/scripts/.changeset/release-packages.mjs @@ -26,7 +26,7 @@ const hasChangelogEntry = (changelog, version) => return headingText === version; }); -const getPublicWorkspacePackages = (repoRoot) => +export const getPublicWorkspacePackages = (repoRoot) => getPackagesSync(repoRoot) .packages.filter( ({ packageJson }) => !packageJson.private && packageJson.name, @@ -126,6 +126,10 @@ export const assertVersionedReleasePackage = (pkg, { repoRoot } = {}) => { }; const getReleaseCandidates = (phase, repoRoot) => { + if (phase === "trust") { + return getPublicWorkspacePackages(repoRoot); + } + if (phase === "prepare") { return getReleasePrepareCandidates(repoRoot); } @@ -189,7 +193,9 @@ export const resolveReleasePackage = async ({ const requirement = phase === "prepare" ? "It must be public and referenced by a pending changeset." - : "It must be public and have a CHANGELOG entry matching its current version."; + : phase === "trust" + ? "It must be a public workspace package." + : "It must be public and have a CHANGELOG entry matching its current version."; throw new Error( `${requestedPublicPackage.name} is not a release ${phase} candidate. ${requirement}`, ); diff --git a/.github/scripts/.changeset/release-runner.mjs b/.github/scripts/.changeset/release-runner.mjs index d28d2f3..e364215 100644 --- a/.github/scripts/.changeset/release-runner.mjs +++ b/.github/scripts/.changeset/release-runner.mjs @@ -6,10 +6,11 @@ import { getPendingChangesets, resolveReleasePackage, } from "./release-packages.mjs"; +import { dispatchRelease as runDispatchRelease } from "./release-dispatch.mjs"; import { - bootstrapRelease as runBootstrapRelease, - dispatchRelease as runDispatchRelease, -} from "./release-dispatch.mjs"; + ensureTrustAll as runEnsureTrustAll, + ensureTrust as runEnsureTrust, +} from "./release-trust.mjs"; import { syncBackRelease as runSyncBackRelease } from "./release-sync-back.mjs"; const run = (cmd, args, options = {}) => { @@ -291,13 +292,20 @@ export const dispatchRelease = async (context) => helpers: { assertCleanTree, capture, + findLockfilePackageDirs, getBranchName, run, }, }); -export const bootstrapRelease = async (context) => - runBootstrapRelease({ +export const ensureTrust = async (context) => + runEnsureTrust({ + ...context, + run, + }); + +export const ensureTrustAll = async (context) => + runEnsureTrustAll({ ...context, run, }); diff --git a/.github/scripts/.changeset/release-trust.mjs b/.github/scripts/.changeset/release-trust.mjs new file mode 100644 index 0000000..69fac3c --- /dev/null +++ b/.github/scripts/.changeset/release-trust.mjs @@ -0,0 +1,520 @@ +import { spawnSync } from "node:child_process"; +import { closeSync, mkdtempSync, openSync, readFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { + getPublicWorkspacePackages, + resolveReleasePackage, +} from "./release-packages.mjs"; + +export const packageUrl = (packageName) => + `https://www.npmjs.com/package/${packageName}`; + +// Returns the latest version currently published on npm for the package, or +// null when the package is not on npm at all. Used only for existence checks +// (the trust command decides whether to publish the 0.0.0 placeholder; dispatch +// refuses to run before trust is configured). Throws on any other npm failure +// can distinguish a clean 404 from a real registry problem. +export const getLatestPublishedVersion = (packageName) => { + const result = spawnSync("npm", ["view", packageName, "version"], { + encoding: "utf8", + stdio: ["ignore", "pipe", "pipe"], + }); + + if (result.status === 0) { + return result.stdout.trim() || null; + } + + const combined = `${result.stderr}\n${result.stdout}`; + if (/E404|404 Not Found/i.test(combined)) { + return null; + } + + throw new Error( + `Could not check npm for ${packageName}:\n${combined.trim()}`, + ); +}; + +// Returns true when a specific published version is deprecated on npm. +// Uses `npm view` (read-only, no auth). A non-deprecated version returns +// an empty string; a deprecated version returns the deprecation message. +const isVersionDeprecated = (packageName, version) => { + const result = spawnSync( + "npm", + ["view", `${packageName}@${version}`, "deprecated"], + { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }, + ); + + return result.status === 0 && result.stdout.trim().length > 0; +}; + +// Returns true when npm Trusted Publishing is configured for a package. +// Asks the registry rather than inferring it from the published version: +// packages published before this repo adopted Trusted Publishing sit at a +// real version with no trust configuration at all. +// +// `npm trust list --json` exits 0 either way. A configured package prints a +// JSON configuration; an unconfigured one prints nothing. Any non-zero exit +// (EOTP, network, rate limit) is indeterminate, not "unconfigured", so it +// throws rather than sending the caller down the placeholder-publish path blind. +// +// This check is deliberately non-interactive: capturing stdout makes npm +// refuse to prompt anyway (it errors with EOTP and the auth URL instead), so +// callers must refresh the session first via assertTrustSession. That keeps +// sweeps from stalling on a hidden prompt mid-run. +const isTrustConfigured = (packageName) => { + const outPath = join(mkdtempSync(join(tmpdir(), "npm-trust-")), "out.json"); + const outFd = openSync(outPath, "w"); + + let result; + try { + result = spawnSync("npm", ["trust", "list", packageName, "--json"], { + stdio: ["ignore", outFd, "inherit"], + }); + } finally { + closeSync(outFd); + } + + const stdout = readFileSync(outPath, "utf8"); + + if (result.status !== 0) { + throw new Error( + `Could not read Trusted Publishing config for ${packageName}. If this is an OTP failure, run 'npm trust list ${packageName}' once to refresh your npm session, then retry.`, + ); + } + + return stdout.trim().length > 0; +}; + +// Make sure the npm session can actually run trust commands before starting +// work that depends on them. `npm whoami` passes on a cached token that trust +// commands still reject with an OTP challenge, so this probes with a real +// trust read. Every stdio stream is inherited — npm only runs its OTP/ +// browser-auth flow when it sees a terminal, which is exactly what we want: +// authenticate once, up front, and the refreshed session covers the rest of +// the run. If it still fails, surface the manual command instead of retrying. +const assertTrustSession = (packageName) => { + const result = spawnSync("npm", ["trust", "list", packageName], { + stdio: "inherit", + }); + + if (result.status !== 0) { + throw new Error( + [ + "Your npm session cannot run trust commands right now. Refresh it by running:", + "", + ` npm trust list ${packageName}`, + "", + "then re-run this command.", + ].join("\n"), + ); + } +}; + +const assertNpmLoggedIn = (repoRoot) => { + const whoami = spawnSync("npm", ["whoami"], { + cwd: repoRoot, + encoding: "utf8", + stdio: ["ignore", "pipe", "pipe"], + }); + + if (whoami.status !== 0) { + throw new Error( + `npm login is required to configure Trusted Publishing.\n\n${whoami.stderr.trim()}`, + ); + } + + return whoami.stdout.trim(); +}; + +const trustGithubArgs = (pkg, repo, workflow) => [ + "trust", + "github", + pkg.name, + "--repo", + repo, + "--file", + workflow, + "--allow-publish", + // The script already asks for confirmation before touching the registry; + // without --yes, npm trust adds its own y/n prompt per package on top. + "--yes", +]; + +// Configure npm Trusted Publishing for a package. Runs `npm trust github` +// interactively: stdin/stdout are inherited so the OTP/browser-auth prompts +// work, but stderr is piped so we can inspect the error code. A 409 Conflict +// means trust is already configured for this package — treat it as success. +// Any other failure throws with the manual recovery command. +const configureTrustedPublishing = (pkg, repo, workflow) => { + const trust = spawnSync("npm", trustGithubArgs(pkg, repo, workflow), { + stdio: ["inherit", "inherit", "pipe"], + encoding: "utf8", + }); + + if (trust.status === 0) { + return; + } + + if (/E409|409 Conflict/i.test(trust.stderr ?? "")) { + console.log(`Trusted Publishing for ${pkg.name} is already configured.`); + return; + } + + const npmError = (trust.stderr ?? "").trim(); + throw new Error( + [ + `npm trust failed for ${pkg.name}.`, + ...(npmError ? ["", npmError] : []), + "", + "Run manually, then re-run this command:", + "", + ` npm trust github ${pkg.name} --repo ${repo} --file ${workflow} --allow-publish`, + ].join("\n"), + ); +}; + +const deprecatePlaceholderArgs = (packageName) => [ + "deprecate", + `${packageName}@0.0.0`, + "Bootstrap placeholder release; use 0.0.1 or later.", +]; + +// Read-only classification of what a package needs before the release workflow +// can publish it through GitHub Actions OIDC: +// +// - "none": published with Trusted Publishing configured. Nothing to do. +// - "trust": published without trust — either a real release that predates +// Trusted Publishing in this repo (published with a classic token), or an +// already-deprecated placeholder from a prior run that died after the +// deprecate step. Configure trust only; a real release is never deprecated. +// - "deprecate-trust": a prior run died after publishing the 0.0.0 +// placeholder but before deprecating it. Deprecate, then configure trust. +// - "bootstrap": never published. Publish a placeholder 0.0.0 (the real +// dist, not an empty stub, so the throwaway version is still coherent), +// deprecate it, and configure Trusted Publishing. npm cannot attach trust +// to a package that does not exist, hence the placeholder. Requires the +// manifest at 0.0.0. +// +// Nothing here mutates the registry, so a full planning sweep can run before +// any configuration happens. +const planTrust = (pkg) => { + const published = getLatestPublishedVersion(pkg.name); + + // Only a package that has never been published needs the 0.0.0 placeholder, + // and that path requires the manifest to be at 0.0.0. An already-published + // package skips the publish entirely, so its manifest version is irrelevant. + if (!published && pkg.version !== "0.0.0") { + throw new Error( + `${pkg.name} is at ${pkg.version} and is not on npm, but the placeholder publish expects 0.0.0.`, + ); + } + + if (!published) { + return { pkg, published, placeholderOnly: false, action: "bootstrap" }; + } + + // Ask the registry instead of inferring trust from the version. Packages + // published before this repo adopted Trusted Publishing sit at a real + // version with no trust configuration, and the old version-based check + // reported them as already configured. + if (isTrustConfigured(pkg.name)) { + return { pkg, published, placeholderOnly: false, action: "none" }; + } + + const placeholderOnly = published === "0.0.0"; + + return { + pkg, + published, + placeholderOnly, + action: + placeholderOnly && !isVersionDeprecated(pkg.name, "0.0.0") + ? "deprecate-trust" + : "trust", + }; +}; + +// Perform (or dry-run print) the registry mutations a plan calls for. Assumes +// the caller has already checked the npm login and confirmed with the user. +const executeTrustPlan = ( + { action, pkg }, + { logStep, note, options, repo, run, workflow }, +) => { + if (options?.dryRun) { + if (action === "bootstrap") { + run("npm", ["run", "build"], { cwd: pkg.absoluteDir, dryRun: true }); + run("npm", ["publish", "--access", "public"], { + cwd: pkg.absoluteDir, + dryRun: true, + }); + } + if (action === "bootstrap" || action === "deprecate-trust") { + run("npm", deprecatePlaceholderArgs(pkg.name), { dryRun: true }); + } + run("npm", trustGithubArgs(pkg, repo, workflow), { dryRun: true }); + return; + } + + if (action === "bootstrap") { + logStep(`Building ${pkg.name}.`); + const build = spawnSync("npm", ["run", "build"], { + cwd: pkg.absoluteDir, + stdio: "inherit", + }); + if (build.status !== 0) { + throw new Error(`Build failed for ${pkg.name}.`); + } + + logStep(`Publishing 0.0.0 for ${pkg.name}.`); + const publish = spawnSync("npm", ["publish", "--access", "public"], { + cwd: pkg.absoluteDir, + stdio: "inherit", + }); + if (publish.status !== 0) { + throw new Error(`Could not publish 0.0.0 for ${pkg.name}.`); + } + } + + if (action === "bootstrap" || action === "deprecate-trust") { + logStep(`Deprecating 0.0.0 for ${pkg.name}.`); + const deprecate = spawnSync("npm", deprecatePlaceholderArgs(pkg.name), { + stdio: "inherit", + }); + if (deprecate.status !== 0) { + note( + `npm deprecate failed. Run manually:\nnpm deprecate ${pkg.name}@0.0.0 "Bootstrap placeholder release; use 0.0.1 or later."`, + "Deprecate failed", + ); + } + } + + logStep(`Configuring npm Trusted Publishing for ${pkg.name}.`); + configureTrustedPublishing(pkg, repo, workflow); +}; + +// Make a package publishable through GitHub Actions OIDC with provenance, so +// the release workflow never needs a classic npm token. See planTrust for the +// per-state breakdown of what that takes. +export const ensureTrust = async ({ + choosePackage, + confirmYes, + logStep, + note, + options, + outro, + packageNameOrDir, + repo, + repoRoot, + run, + workflow, +}) => { + const pkg = await resolveReleasePackage({ + choosePackage, + phase: "trust", + repoRoot, + requestedPackage: packageNameOrDir, + }); + + logStep("Checking the npm OTP session."); + assertTrustSession(pkg.name); + + const plan = planTrust(pkg); + const { action, placeholderOnly, published } = plan; + + if (action === "none") { + note( + `${pkg.name} is already on npm at ${published} with Trusted Publishing configured. Nothing to do.`, + "Already configured", + ); + return { pkg, skipped: true }; + } + + if (published) { + note( + placeholderOnly + ? `${pkg.name} is on npm at 0.0.0 but Trusted Publishing is not configured. The deprecation and trust steps will run.` + : `${pkg.name} is on npm at ${published} but Trusted Publishing is not configured. The trust step will run; ${published} is a real release and will not be deprecated.`, + "Trusted Publishing", + ); + } + + logStep("Checking npm login."); + note(assertNpmLoggedIn(repoRoot), "npm user"); + + const confirmMessage = options.dryRun + ? published + ? `Dry run: configure Trusted Publishing for ${pkg.name}?` + : `Dry run: publish 0.0.0 for ${pkg.name}, deprecate it, and configure Trusted Publishing?` + : published + ? placeholderOnly + ? `Configure npm Trusted Publishing for ${pkg.name} now?\n\nThis configures trust without re-publishing 0.0.0.` + : `Configure npm Trusted Publishing for ${pkg.name} now?\n\n${pkg.name} is already on npm at ${published}. Nothing will be published or deprecated; this only configures Trusted Publishing so future releases can publish through the workflow.` + : `Publish 0.0.0 for ${pkg.name} on npm, deprecate it, and configure Trusted Publishing?\n\nThis publishes a permanent but deprecated placeholder version so npm Trusted Publishing can be configured. The real release happens afterwards through the release workflow.`; + + if (!(await confirmYes(confirmMessage))) { + throw new Error("Canceled."); + } + + executeTrustPlan(plan, { logStep, note, options, repo, run, workflow }); + + note( + [`${packageUrl(pkg.name)}`, `npm trust list ${pkg.name}`].join("\n"), + "Verify on npm", + ); + + outro(`Configured Trusted Publishing for ${pkg.name}.`); + return { pkg, skipped: false }; +}; + +// Run the trust check across every public workspace package, in two phases: +// first a read-only sweep that classifies every package, then a single batch +// at the end that configures whatever is missing behind one npm login check. +// Batching keeps the interactive npm commands (and their OTP prompts) together +// instead of interleaving them package by package through the sweep. +// +// Each package is isolated in both phases: a failure (an expired npm session +// is the likely one, since the OTP challenge can reappear part way through a +// long run) is recorded and the run continues, so one bad package does not +// hide the state of the others. The summary at the end is the point of the +// command; re-running it is safe and only touches whatever is still missing. +export const ensureTrustAll = async (context) => { + const { + confirmYes, + logStep, + note, + options, + outro, + repo, + repoRoot, + run, + workflow, + } = context; + const packages = getPublicWorkspacePackages(repoRoot); + + if (packages.length === 0) { + outro("No public workspace packages found."); + return { results: [] }; + } + + logStep("Checking npm login."); + note(assertNpmLoggedIn(repoRoot), "npm user"); + + logStep("Checking the npm OTP session."); + assertTrustSession(packages[0].name); + + const firstErrorLine = (error) => + (error instanceof Error ? error.message : String(error)) + .split("\n")[0] + .trim(); + + const resultsByName = new Map(); + const pending = []; + + for (const pkg of packages) { + logStep(`Checking ${pkg.name}.`); + + try { + const plan = planTrust(pkg); + if (plan.action === "none") { + resultsByName.set(pkg.name, { ok: true, detail: "already configured" }); + } else { + pending.push(plan); + } + } catch (error) { + resultsByName.set(pkg.name, { ok: false, detail: firstErrorLine(error) }); + } + } + + // Report the read-only sweep before asking anything: what is already done, + // what could not even be checked, and what the batch would touch. + const alreadyConfigured = packages.filter( + ({ name }) => resultsByName.get(name)?.ok, + ); + const checkFailed = packages.filter( + ({ name }) => resultsByName.has(name) && !resultsByName.get(name).ok, + ); + + if (alreadyConfigured.length > 0) { + note( + alreadyConfigured.map(({ name }) => `✓ ${name}`).join("\n"), + "Already configured", + ); + } + + if (checkFailed.length > 0) { + note( + checkFailed + .map(({ name }) => `✗ ${name} — ${resultsByName.get(name).detail}`) + .join("\n"), + "Could not check", + ); + } + + if (pending.length > 0) { + note( + pending + .map( + ({ action, pkg }) => + `${pkg.name}${action === "bootstrap" ? " (publishes a deprecated 0.0.0 placeholder first)" : ""}`, + ) + .join("\n"), + options?.dryRun ? "Would configure" : "Will configure", + ); + + // The sweep is read-only, so this is the last stop before anything touches + // the registry: everything the batch will do is on screen, ask once. + if ( + !(await confirmYes( + options?.dryRun + ? `Dry run: print the trust commands for these ${pending.length} packages?` + : `Configure Trusted Publishing for these ${pending.length} packages now?`, + )) + ) { + throw new Error("Canceled."); + } + + for (const plan of pending) { + try { + executeTrustPlan(plan, { logStep, note, options, repo, run, workflow }); + resultsByName.set(plan.pkg.name, { + ok: true, + detail: options?.dryRun ? "would be configured" : "configured now", + }); + } catch (error) { + // The end-of-run summary only keeps the first line, which would drop + // the recovery command — print the full error here instead. + note( + error instanceof Error ? error.message : String(error), + `✗ ${plan.pkg.name}`, + ); + resultsByName.set(plan.pkg.name, { + ok: false, + detail: firstErrorLine(error), + }); + } + } + } + + const results = packages.map(({ name }) => ({ + name, + ...resultsByName.get(name), + })); + + note( + results + .map(({ name, ok, detail }) => `${ok ? "✓" : "✗"} ${name} — ${detail}`) + .join("\n"), + "Trusted Publishing", + ); + + const failed = results.filter(({ ok }) => !ok); + if (failed.length > 0) { + throw new Error( + `${failed.length} of ${results.length} packages could not be configured. Re-run to retry just those; packages already configured are skipped.`, + ); + } + + outro(`Trusted Publishing verified for all ${results.length} packages.`); + return { results }; +}; diff --git a/.github/scripts/.changeset/release.mjs b/.github/scripts/.changeset/release.mjs index 207147f..aa39f0b 100644 --- a/.github/scripts/.changeset/release.mjs +++ b/.github/scripts/.changeset/release.mjs @@ -12,7 +12,8 @@ import { } from "@clack/prompts"; import { Command } from "commander"; import { - bootstrapRelease, + ensureTrustAll, + ensureTrust, dispatchRelease, findRepoRoot, prepareRelease, @@ -21,6 +22,7 @@ import { const repo = "FujoWebDev/fujocoded-plugins"; const workflow = "release.yaml"; +const defaultRemote = "origin"; const scriptDir = dirname(fileURLToPath(import.meta.url)); const getErrorMessage = (error) => @@ -102,26 +104,42 @@ const runCommand = const prepare = runCommand(prepareRelease); -const dispatch = runCommand(dispatchRelease, { - repo, - workflow, -}); +const dispatch = runCommand( + async (context) => + dispatchRelease({ + ...context, + remote: context.options.remote ?? defaultRemote, + }), + { + repo, + workflow, + }, +); const syncBack = runCommand(syncBackRelease); -const bootstrap = runCommand(bootstrapRelease, { - repo, - workflow, -}); +const trust = runCommand( + async (context) => { + if (!context.options.all) { + return await ensureTrust(context); + } + + return await ensureTrustAll(context); + }, + { + repo, + workflow, + }, +); // The release command checks whether the package needs a first-publish -// bootstrap (publish 0.0.0, deprecate it, configure npm Trusted Publishing), +// trust setup (publish 0.0.0 if new, deprecate it, configure Trusted Publishing), // runs that if needed, then asks whether to release the package immediately // through the single-package workflow or leave it for the normal main-merge // flow. const release = async (packageNameOrDir, options) => { try { - const { skipped, pkg } = await bootstrapRelease({ + const { skipped, pkg } = await ensureTrust({ ...commandContext, repo, workflow, @@ -131,7 +149,7 @@ const release = async (packageNameOrDir, options) => { const prompt = skipped ? `${pkg.name} is already on npm. Release just this package now through the single-package workflow?` - : `Bootstrap complete for ${pkg.name}. Version it and dispatch the single-package release now?`; + : `Trusted Publishing configured for ${pkg.name}. Version it and dispatch the single-package release now?`; const proceed = await confirmYes(prompt); if (!proceed) { @@ -155,6 +173,7 @@ const release = async (packageNameOrDir, options) => { await dispatchRelease({ ...commandContext, + remote: options.remote ?? defaultRemote, repo, workflow, options, @@ -170,13 +189,13 @@ const program = new Command(); program .name("release") .description( - "Bootstrap, version, and release one workspace package at a time.", + "Configure Trusted Publishing, version, and release one workspace package at a time.", ); program .command("release") .description( - "Bootstrap a new package if needed, then release it through the single-package workflow.", + "Configure Trusted Publishing if needed, then release the package through the single-package workflow.", ) .argument("[package-name-or-dir]", "workspace package name or directory") .option("--branch ", "branch name to create") @@ -189,17 +208,34 @@ program "--allow-dirty", "allow running dispatch with an uncommitted working tree", ) + .option( + "--remote ", + "git remote to push the release branch to", + defaultRemote, + ) .option("--dry-run", "show planned operations without applying changes") .action(release); program - .command("bootstrap") + .command("trust") + .alias("bootstrap") .description( - "Publish 0.0.0, deprecate it, and configure npm Trusted Publishing for one package.", + "Configure npm Trusted Publishing for a package, publishing a 0.0.0 placeholder first if it has never been published.", ) .argument("[package-name-or-dir]", "workspace package name or directory") .option("--dry-run", "show planned operations without applying changes") - .action(bootstrap); + .option( + "--all", + "check every public package and configure the ones missing Trusted Publishing", + ) + .hook("preAction", (thisCommand, actionCommand) => { + if (actionCommand.opts().all && actionCommand.args.length > 0) { + actionCommand.error( + "error: option '--all' cannot be used with a package name", + ); + } + }) + .action(trust); program .command("prepare") @@ -224,6 +260,11 @@ program "branch to sync release state back to after publish", ) .option("--commit", "prompt for a local sync-back commit after publishing") + .option( + "--remote ", + "git remote to push the release branch to", + defaultRemote, + ) .option("--dry-run", "show planned operations without applying changes") .action(dispatch); diff --git a/package-lock.json b/package-lock.json index bb6922c..b15e94c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,10 @@ "devDependencies": { "@changesets/changelog-github": "^0.7.0", "@changesets/cli": "^2.29.8", + "@changesets/parse": "^0.4.3", + "@clack/prompts": "^1.7.0", + "@manypkg/get-packages": "^1.1.3", + "commander": "^15.0.0", "oxfmt": "^0.46.0", "oxlint": "^1.61.0", "oxlint-tsgolint": "^0.22.1", @@ -458,7 +462,7 @@ }, "astro-atproto-loader": { "name": "@fujocoded/astro-atproto-loader", - "version": "0.2.1", + "version": "0.2.2", "license": "MIT", "dependencies": { "@atproto/api": "^0.17.3", @@ -478,7 +482,7 @@ }, "astro-authproto": { "name": "@fujocoded/authproto", - "version": "0.3.1", + "version": "0.4.0", "license": "MIT", "dependencies": { "@atproto/api": "^0.17.3", @@ -868,7 +872,7 @@ }, "astro-dev-only": { "name": "@fujocoded/astro-dev-only", - "version": "0.0.4", + "version": "0.0.5", "license": "MIT", "devDependencies": { "astro-types-v4": "npm:astro@^4.0.0", @@ -2610,9 +2614,9 @@ } }, "node_modules/@changesets/parse": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/@changesets/parse/-/parse-0.4.2.tgz", - "integrity": "sha512-Uo5MC5mfg4OM0jU3up66fmSn6/NE9INK+8/Vn/7sMVcdWg46zfbvvUSjD9EMonVqPi9fbrJH9SXHn48Tr1f2yA==", + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@changesets/parse/-/parse-0.4.3.tgz", + "integrity": "sha512-ZDmNc53+dXdWEv7fqIUSgRQOLYoUom5Z40gmLgmATmYR9NbL6FJJHwakcCpzaeCy+1D0m0n7mT4jj2B/MQPl7A==", "dev": true, "license": "MIT", "dependencies": { @@ -2735,6 +2739,36 @@ "integrity": "sha512-YslZMgtJUyuMbZ+aKvfF3x1f5liK4mWNxghFRv7jqRR9C3R3fAOGTTKvxXDa2Y1s9zSbcpuO0cAxDYsc9SrXoQ==", "license": "Apache-2.0" }, + "node_modules/@clack/core": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/@clack/core/-/core-1.4.3.tgz", + "integrity": "sha512-/kr3UWNtdJfxZtPgDqUOmG2pvwlmcLGheex5yiZKdwbzZJxhV+HMNR9QNmyY5cGwTNV6LrR7Jtp+KjhUAP1qBQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-wrap-ansi": "^0.2.0", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 20.12.0" + } + }, + "node_modules/@clack/prompts": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@clack/prompts/-/prompts-1.7.0.tgz", + "integrity": "sha512-y7/yvZ2TPAnR9+jnc00klvNNLkJiXFFrQA/hlLCcxA9a2A4zQIOimyFQ9XfwYKiGD1fb5GY8vbKIIgO8d5Tb2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@clack/core": "1.4.3", + "fast-string-width": "^3.0.2", + "fast-wrap-ansi": "^0.2.0", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 20.12.0" + } + }, "node_modules/@ctrl/tinycolor": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/@ctrl/tinycolor/-/tinycolor-4.2.0.tgz", @@ -11122,12 +11156,13 @@ } }, "node_modules/commander": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", - "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "version": "15.0.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-15.0.0.tgz", + "integrity": "sha512-z67u4ZhzCL/Tydu1lJARtEZYWbWaN7oYLHbsuzocr6y4N6WZAagG3RQ4FW61V1/0+jImpj293XfrcYnd1qxtPg==", + "dev": true, "license": "MIT", "engines": { - "node": ">= 10" + "node": ">=22.12.0" } }, "node_modules/common-ancestor-path": { @@ -11573,6 +11608,15 @@ "node": ">=12" } }, + "node_modules/d3-dsv/node_modules/commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, "node_modules/d3-dsv/node_modules/iconv-lite": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", @@ -20151,7 +20195,7 @@ }, "remark-alt-text-files": { "name": "@fujocoded/remark-alt-text-files", - "version": "0.0.2", + "version": "0.1.0", "license": "MIT", "dependencies": { "unist-util-visit": "^5.0.0" @@ -20165,7 +20209,7 @@ }, "remark-capitalize-titles": { "name": "@fujocoded/remark-capitalize-titles", - "version": "0.1.0", + "version": "0.2.0", "license": "MIT", "dependencies": { "mdast-util-from-markdown": "^2.0.2", @@ -20184,7 +20228,7 @@ }, "remark-excalidraw": { "name": "@fujocoded/remark-excalidraw", - "version": "0.0.2", + "version": "0.1.0", "license": "MIT", "dependencies": { "@excalidraw/excalidraw": "^0.18.0", @@ -20559,7 +20603,7 @@ }, "zod-transform-socials": { "name": "@fujocoded/zod-transform-socials", - "version": "0.1.0", + "version": "0.1.1", "license": "MIT", "dependencies": { "social-links": "^1.14.0" diff --git a/package.json b/package.json index a365dd3..fdc1f1c 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,10 @@ "devDependencies": { "@changesets/changelog-github": "^0.7.0", "@changesets/cli": "^2.29.8", + "@changesets/parse": "^0.4.3", + "@clack/prompts": "^1.7.0", + "@manypkg/get-packages": "^1.1.3", + "commander": "^15.0.0", "oxfmt": "^0.46.0", "oxlint": "^1.61.0", "oxlint-tsgolint": "^0.22.1",