diff --git a/.agents/lessons.md b/.agents/lessons.md index 0080bf0..a2bbde9 100644 --- a/.agents/lessons.md +++ b/.agents/lessons.md @@ -19,6 +19,7 @@ alwaysApply: true - npm trusted publishing (OIDC) needs npm ≥ 11.5.1 + Node ≥ 22.14; oven-sh/setup-bun leaves npm 10.x in PATH, so a release job running `changeset publish`/`npm publish` must also run actions/setup-node (Node 24 → npm 11) or OIDC isn't detected → ENEEDAUTH. - Release git auth: GitHub App on the `release` environment (`RELEASE_APP_CLIENT_ID` + `RELEASE_APP_PRIVATE_KEY`, Workflows write) — `permissions.workflows` on `GITHUB_TOKEN` is invalid in workflow YAML (startup_failure) and cannot tag when the tip touched `.github/workflows/*`. +- Release `git tag -a` needs `user.name`/`user.email` — `changesets/action` `commitMode: github-api` does not set them; publish can succeed then die on tag (retry must still tag on skip). - Don't pin a GitHub action to a moving major tag's commit SHA (e.g. setup-node@) — the tag moves and orphans/GCs the commit → "unable to find version"; pin to an immutable release-tag commit or use the moving @vN tag. - Format `apps/docs` non-content files at `printWidth: 80` — oxfmt uses **nearest-config-wins per file**, so the nested `apps/docs/.oxfmtrc.json` (ignore-only → default printWidth 100) shadows root `.oxfmtrc.json` (80) for every `apps/docs/*` file regardless of CLI scope; fix by adding `"printWidth": 80` to `apps/docs/.oxfmtrc.json` (or `-c .oxfmtrc.json --disable-nested-config`). `content/**` stays excluded by the nested ignore so writes won't collapse `:::note` callouts. - The layers repo has tracked `.oxlintrc.json` + root `.oxfmtrc.json` (since the initial commit) with real plugins/rules/ignorePatterns (`.oxlintrc.json` ignores `**/*.svelte` and `**/*.vue`); merge additions into them — never overwrite/replace. Verify tracked lint/format configs via `git ls-files '*.oxlintrc*' '*oxfmt*'` before touching (3 tracked today: `.oxlintrc.json`, `.oxfmtrc.json`, `apps/docs/.oxfmtrc.json`); the "no oxlint config" explore report was wrong. diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 3ba9426..989598c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -58,6 +58,12 @@ jobs: node-version: "24" registry-url: "https://registry.npmjs.org" + # `git tag -a` needs identity; `commitMode: github-api` does not set one. + - name: Configure git identity for release tags + run: | + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + # Opens/updates the Version PR when `.changeset/*.md` exist; otherwise publishes # unpublished versions. Failures: step log (App credentials, OIDC, registry). - name: Create Release Pull Request or Publish to npm diff --git a/scripts/release.ts b/scripts/release.ts index 4d8babe..23b7b69 100644 --- a/scripts/release.ts +++ b/scripts/release.ts @@ -4,7 +4,7 @@ import { join } from "node:path"; // Pack with Bun (resolves workspace:*) then npm-publish the tarball — Bun can't do npm OIDC/provenance. // Build first and assert `exports` dist paths exist (CI checkout has no dist/). -// After publish: annotated `name@version` tag + `New tag:` line for changesets/action (push + GitHub Release). +// Tag after publish and on skip (partial retry): annotated `name@version` + `New tag:` for changesets/action. // Skip versions already on the registry so partial releases can retry. import { $ } from "bun"; @@ -26,12 +26,23 @@ async function isAlreadyPublished( return res.exitCode === 0; } -async function ensureReleaseTag(tag: string): Promise { +// `git tag -a` needs an identity; changesets/action `commitMode: github-api` does not set one. +async function ensureGitIdentity(): Promise { + const name = await $`git config user.name`.quiet().nothrow(); + if (name.exitCode === 0 && name.text().trim()) return; + await $`git config user.name ${"github-actions[bot]"}`; + await $`git config user.email ${"41898282+github-actions[bot]@users.noreply.github.com"}`; +} + +/** @returns whether a new local tag was created */ +async function ensureReleaseTag(tag: string): Promise { const exists = await $`git rev-parse -q --verify ${`refs/tags/${tag}`}` .quiet() .nothrow(); - if (exists.exitCode === 0) return; + if (exists.exitCode === 0) return false; + await ensureGitIdentity(); await $`git tag -a ${tag} -m ${tag}`; + return true; } function distExportPaths(exportsField: unknown): string[] { @@ -64,10 +75,14 @@ function assertDistReady(dir: string, pkg: PackageJson): void { console.log("release: building packages…"); await $`bun run build`; +const packageDirs = readdirSync(PACKAGES_DIR, { withFileTypes: true }) + .filter((entry) => entry.isDirectory()) + .map((entry) => entry.name) + .sort(); + let published = 0; -for (const entry of readdirSync(PACKAGES_DIR, { withFileTypes: true })) { - if (!entry.isDirectory()) continue; - const dir = join(PACKAGES_DIR, entry.name); +for (const name of packageDirs) { + const dir = join(PACKAGES_DIR, name); let pkg: PackageJson; try { @@ -77,8 +92,11 @@ for (const entry of readdirSync(PACKAGES_DIR, { withFileTypes: true })) { } if (pkg.private || !pkg.name || !pkg.version) continue; + const tag = `${pkg.name}@${pkg.version}`; + if (await isAlreadyPublished(pkg.name, pkg.version)) { console.log(`Skipping ${pkg.name}@${pkg.version} (already on registry)`); + if (await ensureReleaseTag(tag)) console.log(`New tag: ${tag}`); continue; } @@ -95,9 +113,7 @@ for (const entry of readdirSync(PACKAGES_DIR, { withFileTypes: true })) { } await $`npm publish ${tarball} --provenance --access public`.cwd(dir); - const tag = `${pkg.name}@${pkg.version}`; - await ensureReleaseTag(tag); - console.log(`New tag: ${tag}`); + if (await ensureReleaseTag(tag)) console.log(`New tag: ${tag}`); published++; }