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
1 change: 1 addition & 0 deletions .agents/lessons.md
Original file line number Diff line number Diff line change
Expand Up @@ -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@<v6-tag-commit>) — 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.
Expand Down
6 changes: 6 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
34 changes: 25 additions & 9 deletions scripts/release.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand All @@ -26,12 +26,23 @@ async function isAlreadyPublished(
return res.exitCode === 0;
}

async function ensureReleaseTag(tag: string): Promise<void> {
// `git tag -a` needs an identity; changesets/action `commitMode: github-api` does not set one.
async function ensureGitIdentity(): Promise<void> {
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<boolean> {
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[] {
Expand Down Expand Up @@ -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 {
Expand All @@ -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;
}

Expand All @@ -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++;
}

Expand Down
Loading