diff --git a/docs/action-runtime.md b/docs/action-runtime.md index 83662a20..0f88e304 100644 --- a/docs/action-runtime.md +++ b/docs/action-runtime.md @@ -19,7 +19,7 @@ Order in `src/index.ts`: 1. Resolve GitHub token (`GITHUB_TOKEN` env, else `github-token` input). Fail if empty. 2. Resolve `cwd` via `path.resolve(cwd input || "")`, validate `commitMode` (`git-cli` | `github-api`), construct `Git` with that `cwd` (+ Octokit when `github-api`). 3. Optionally `setupGitUser` (`true` by default). -4. Write `$HOME/.netrc` for `github.com` so git-cli pushes authenticate. +4. **Overwrite** `$HOME/.netrc` with a single `github.com` machine (`login github-actions[bot]`, password = token) so git-cli pushes authenticate — see [`auth-and-publishing.md`](./auth-and-publishing.md) §2. 5. `readChangesetState()` → filtered `changesets` list (see §3). 6. Set outputs early: `published=false`, `publishedPackages=[]`, `hasChangesets=`. @@ -44,11 +44,13 @@ After #486 the action no longer calls `process.chdir`. `cwd` is passed explicitl | Condition | Returned `changesets` | Returned `preState` | |-----------|----------------------|---------------------| -| Not in pre mode | All changesets from `@changesets/read` | `undefined` | +| No pre-state file, or `preState.mode !== "pre"` (e.g. after `changeset pre exit`) | All changesets from `@changesets/read` | **`undefined`** (exit/other modes are not surfaced) | | `preState.mode === "pre"` | Changesets **whose ids are not** in `preState.changesets` | The pre-state object | Changesets already consumed into the current pre release are excluded. That can make `hasChangesets` false while files still exist under `.changeset/`, which is why a merge of a pre Version Packages PR can enter the publish path. +**Pitfall:** Only `mode === "pre"` keeps `preState` for PR title/body suffixes and the pre-mode warning. After exit, `runVersion` treats the branch as normal releases even if a pre-state file still exists on disk. + --- ## 4. Dispatch state machine (`src/index.ts`) @@ -132,12 +134,14 @@ When `preState` is set, the body includes a warning that `branch` is in pre mode ### Changelog parsing constraints (`getChangelogEntry` in `src/utils.ts`) - Finds a markdown heading whose text **exactly equals** the new version string (e.g. `3.0.1`, not `v3.0.1`). -- Slices until the next heading of the **same depth**. -- While walking that slice, headings containing `major` / `minor` / `patch` update `highestLevel` for `sortTheThings` (public packages before private; higher bump first). +- Slices `content` from the node after that heading until the next heading of the **same depth**. +- **`highestLevel` walk is not limited to that slice.** The helper scans headings from the **start of the file** until it hits the end of the version section (or EOF). Any heading whose text matches `/(major|minor|patch)/i` (e.g. `### Major Changes`) updates `highestLevel` for `sortTheThings` (public packages before private; higher bump first). - **Always returns** `{ content, highestLevel }` — never `null` / `undefined`. -- If the version heading is **missing**, `content` is the **entire** changelog AST stringified (no slice), and `highestLevel` reflects `major`/`minor`/`patch` headings anywhere in the file. +- If the version heading is **missing**, `content` is the **entire** changelog AST stringified (no slice), and `highestLevel` reflects those headings anywhere in the file. - Version path: missing `CHANGELOG.md` for a changed package throws on `readFile`. Publish path: see §6. +**Ordering pitfall (newest-first changelogs):** for an older version entry, a *newer* section above it that contains `### Major Changes` (etc.) can inflate `highestLevel` even when the sliced `content` is only a patch. That only affects Version Packages PR package order, not Release bodies. + --- ## 6. Publish path — detection and GitHub releases (`runPublish`) @@ -146,9 +150,11 @@ Runs only from the dispatch publish case (no remaining changesets + `publish` in 1. Execute the `publish` script (`script.split(/\s+/)`), injecting `GITHUB_TOKEN` into the child env. 2. Parse **stdout** for new tags (not git tags on disk): - - **Monorepo / non-root tool:** lines matching `New tag:\s+(@[^/]+\/[^@]+|[^/]+)@([^\s]+)`. - - **Root package:** first line matching `New tag:` → treat the single root package as released; GitHub tag name `v{version}` (then `break` — further `New tag:` lines ignored). + - **Monorepo / non-root tool:** lines matching `New tag:\s+(@[^/]+\/[^@]+|[^/]+)@([^\s]+)`. Name must exist in `getPackages(cwd)` or the action **throws** (`Package "" not found`). + - **Root package:** first line matching `New tag:` → treat the single root package as released; GitHub tag name `v{version}` (then `break` — further `New tag:` lines ignored). Empty package list → throw `No package found.` 3. If `createGithubReleases` is true (default) and packages were detected: + - **Monorepo:** `Promise.all` over released packages — each `pushTag` + `createRelease` runs concurrently. + - **Root:** sequential for the single matched package (loop breaks after the first `New tag:`). - Push each tag via `git.pushTag` (see [`push-changes-internals.md`](./push-changes-internals.md) §5 — github-api tags `github.context.sha`; failures only warn). - `repos.createRelease` with `body` from `getChangelogEntry(...).content`; `prerelease` when the version contains `-`. - Missing changelog file → skip that package’s release silently (`ENOENT`). diff --git a/docs/auth-and-publishing.md b/docs/auth-and-publishing.md index bf47c183..6ad0ca8e 100644 --- a/docs/auth-and-publishing.md +++ b/docs/auth-and-publishing.md @@ -29,13 +29,30 @@ If both are empty, the action fails with: `Please add the GITHUB_TOKEN to the ch ### Where the token is used 1. **Octokit** — `setupOctokit(githubToken)` for PRs, releases, and (with `commitMode: github-api`) commits/tags. -2. **`.netrc`** — written as `password` for `github.com` so git-cli pushes authenticate. +2. **`.netrc`** — always rewritten (see below) so git-cli pushes authenticate to `github.com`. 3. **Child env** — `runPublish` / `runVersion` pass `GITHUB_TOKEN: githubToken` into the publish/version script env so nested tools see the same token even when the workflow did not set `env.GITHUB_TOKEN`. +### `.netrc` rewrite (always) + +Before reading changesets, `src/index.ts` does `fs.writeFile` on `$HOME/.netrc` with: + +``` +machine github.com +login github-actions[bot] +password +``` + +| Constraint | Detail | +|------------|--------| +| **Overwrite, not merge** | `writeFile` replaces the whole file. Other `machine` entries that were in `.netrc` are lost for the rest of the job. | +| **Login name is fixed** | Always `github-actions[bot]`; auth is the password token. Works for `github.token` and PATs; attribution of API commits still follows the token owner when using `commitMode: github-api`. | +| **git-cli only needs this** | `commitMode: github-api` still writes `.netrc`, but pushes go through Octokit, not `git push`. | + ### Constraints - Default `github.token` permissions depend on the workflow `permissions:` block. Opening/updating release PRs typically needs `contents: write` and `pull-requests: write`. - Prefer a PAT / GitHub App token via `github-token` (or `GITHUB_TOKEN` env) when you need to trigger other workflows (`github.token` often cannot). +- If a prior step needs a multi-host `.netrc`, recreate it **after** this action (or avoid sharing that file across steps that must keep other machines). --- diff --git a/docs/push-changes-internals.md b/docs/push-changes-internals.md index 3c7948da..974cf774 100644 --- a/docs/push-changes-internals.md +++ b/docs/push-changes-internals.md @@ -27,11 +27,15 @@ async pushChanges({ branch, message }: { branch: string; message: string }) { 3. **`commitAll` (lines 35–38)** — runs `git add .` followed by `git commit -m ` in `cwd`. This stages all changes (new/modified/deleted) and creates a local commit. -4. **Always pushes** — regardless of whether a new commit was created, `push(branch, { cwd })` (lines 11–13) runs: +4. **Always force-pushes** — regardless of whether a new commit was created, `push(branch, { cwd })` (lines 11–13) runs: ``` git push origin HEAD: --force ``` - This force-pushes the current HEAD to the remote branch. The force-push is safe here because `prepareBranch` (lines 94–101) has already reset the local branch to `github.context.sha`, so the only commit on the branch is the one just created. + `prepareBranch` (lines 94–101) has already checked out the version branch and `git reset --hard` to `github.context.sha`. So: + - **Dirty after version script** → `commitAll` creates one commit on top of that SHA, then force-pushes it (intended path). + - **Clean after version script** → no commit; force-push publishes the reset SHA itself. The remote `changeset-release/` tip becomes identical to the release base commit, which can **empty the PR diff** and let GitHub auto-close the Version Packages PR. + + Treat a no-op version script (no `package.json` / changelog edits) as a real failure mode on git-cli, not a no-op. --- @@ -112,7 +116,7 @@ This is GitHub's official [Commits API](https://docs.github.com/en/graphql/refer | **Signing** | Unsigned (unless runner is configured for GPG signing externally) | **GPG-signed by GitHub** automatically (server-side commits are always signed) | | **`setupUser`** | Configures `user.name`/`user.email` for git commits | **No-op** — returns immediately when `this.octokit` is set (line 59–61) | | **`prepareBranch`** | Checks out (or creates) branch locally, then `git reset --hard` to `github.context.sha` | **No-op** — returns immediately (lines 95–98); branch creation/update is handled server-side by the API | -| **Dirty-check** | Explicit: runs `git status --porcelain`; skips commit if clean | Implicit: `isomorphic-git.walk` diffs working tree vs base; if no changes detected, `fileChanges` will be empty | +| **Dirty-check** | Explicit: `git status --porcelain`; skips commit if clean, **still force-pushes** (see pitfall above) | Implicit: `isomorphic-git.walk` diffs working tree vs base; empty `fileChanges` means no new commit via the API (branch may still be force-aligned to `baseOid` when diverged) | | **Push method** | `git push origin HEAD: --force` | `updateRefMutation` with `force: true` (if branch diverged from base), then `createCommitOnBranch` which advances the ref | | **File scoping** | `git add .` stages everything in `cwd` and below | `isomorphic-git` walk filtered to paths starting with `relative(repoRoot, cwd)` | | **Limitations** | None for standard files | Unchanged committed symlinks/executables are OK; **changing** them still fails (GitHub API limitation) — use `git-cli` | diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index 43202eff..60ae7828 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -22,7 +22,9 @@ Deeper notes: [`action-runtime.md`](./action-runtime.md) · [`auth-and-publishin |---------|--------------|---------------| | Custom step `if: hasChangesets == 'false'` never runs | Empty-changeset skip left `hasChangesets=true` | See §1; empty changesets block both the Version PR and this pattern. | | Action ran publish script but `published` is `false` | Stdout had no `New tag:` lines | Detection parses **stdout only**. Monorepo lines must match `New tag: name@version`; root needs any `New tag:` line. | +| Publish throws `Package "…" not found` | Monorepo `New tag:` name not in `getPackages(cwd)` | Typo, wrong `cwd`, or private/tooling package filtered from the workspace. | | npm 401 / auth errors | Token or OIDC misconfigured | [`auth-and-publishing.md`](./auth-and-publishing.md): `NPM_TOKEN` vs trusted publishing (`id-token: write`), custom registries need your own `.npmrc`. | +| Later step lost other `.netrc` machines | Action `writeFile`s `$HOME/.netrc` every run | [`auth-and-publishing.md`](./auth-and-publishing.md) §2 — recreate multi-host `.netrc` after the action if needed. | --- @@ -62,6 +64,9 @@ Tag names: monorepo `{name}@{version}`; root package `v{version}`. Details: [`pu | `version: bash -c "…"` mis-parses | `script.split(/\s+/)` — no shell | Use a package.json script, or a single command + simple argv. | | PR opens with empty `# Releases` | Version command changed no `package.json` versions | `getChangedPackages` compares versions before/after the version command. | | Duplicate open Version Packages PRs; only one updates | List returns multiple; action updates `data[0]` only | Close extras; keep a single `changeset-release/` PR. | +| PR diff suddenly empty / PR auto-closed (`commitMode: git-cli`) | Version script left a **clean** tree after `prepareBranch` reset | git-cli still **force-pushes** HEAD; without a new commit that is the base SHA. See [`push-changes-internals.md`](./push-changes-internals.md) §1. | +| Package order in `# Releases` looks wrong | `highestLevel` saw `Major`/`Minor`/`Patch` headings **above** the version entry | Newest-first changelogs can inflate sort order for older entries; body `content` can still be correct. [`action-runtime.md`](./action-runtime.md) §5. | +| No `(tag)` suffix / pre warning after `changeset pre exit` | `readChangesetState` only returns `preState` when `mode === "pre"` | Expected; exit mode is treated as normal releases. | See [`action-runtime.md`](./action-runtime.md) §5.