Skip to content
Draft
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
20 changes: 13 additions & 7 deletions docs/action-runtime.md
Original file line number Diff line number Diff line change
Expand Up @@ -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=<bool>`.

Expand All @@ -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`)
Expand Down Expand Up @@ -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`)
Expand All @@ -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 "<name>" 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`).
Expand Down
19 changes: 18 additions & 1 deletion docs/auth-and-publishing.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <githubToken>
```

| 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).

---

Expand Down
10 changes: 7 additions & 3 deletions docs/push-changes-internals.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <message>` 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:<branch> --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/<branch>` 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.

---

Expand Down Expand Up @@ -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:<branch> --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` |
Expand Down
5 changes: 5 additions & 0 deletions docs/troubleshooting.md
Original file line number Diff line number Diff line change
Expand Up @@ -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. |

---

Expand Down Expand Up @@ -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/<branch>` 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.

Expand Down
Loading