rylease 3.0.0 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -4,15 +4,9 @@ A release pipeline that doesn't fight you.
4
4
 
5
5
  Everything is a **step**: a small, named, strictly typed function that does one auditable thing and says why. Steps run in a fixed order of phases. Each one receives the environment, your own context, and the outputs of every step before it.
6
6
 
7
- ```
8
- read-version → read-changes → determine-kind → determine-version → determine-notes → write → stage → release
9
- ```
10
-
11
- rylease handles `read-version` and `determine-version` itself. It finds the previous release with the release's **history**, e.g. the nearest tag on HEAD, and works out the next version with its **scheme**, e.g. semver, on a **channel** if there is one, e.g. `2.0.0-next.1`. Both come from the preset, unless the release sets its own; there are no other defaults. No step parses or bumps a version; every step takes it as plain text. `write` steps change files, `stage` steps commit them (or propose them in a pull request), and `release` steps ship the result.
12
-
13
7
  Nothing assumes semver, npm or GitHub, and each of them is one line to add. Every step declares its options once, and they work the same in a config file as in TypeScript. Steps also declare what they **need** and **provide**, e.g. `labels/determine-kind` needs changes with labels. A release that can't work fails before anything runs, and the error says which step would fix it.
14
8
 
15
- ## GitHub Actions
9
+ ## Quick start
16
10
 
17
11
  ```yaml
18
12
  permissions:
@@ -24,22 +18,7 @@ steps:
24
18
  - uses: ryangarber/rylease@v3
25
19
  ```
26
20
 
27
- That runs the bundled `conventional` preset, which versions by semver and finds the previous release by its tag. To configure it, or add more steps, use `steps`: a list shaped like a workflow's. Each entry `uses` a step or preset, gives it options `with`, and may run only `if` an expression holds.
28
-
29
- ```yaml
30
- - uses: ryangarber/rylease@v3
31
- with:
32
- steps: |
33
- - uses: conventional
34
- with:
35
- github/create-release: { draft: true }
36
- conventional/determine-kind:
37
- types: { docs: none, chore: none, "chore(deps)": fix }
38
- - uses: ./.github/release/announce.ts
39
- if: "!dryRun && kind != 'fix'"
40
- with:
41
- channel: "#releases"
42
- ```
21
+ That runs the bundled [`conventional`](#presets) preset: conventional commits since the last tag become a semver version, a git tag and a GitHub release.
43
22
 
44
23
  ```
45
24
  rylease release · main · /home/runner/work/project/project
@@ -48,864 +27,87 @@ rylease release · main · /home/runner/work/project/project
48
27
  ✔ read-changes conventional/read-commits 12ms 3 changes in 2 commits since 0d92bfa
49
28
  ✔ determine-kind conventional/determine-kind <1ms 'feat' in title (commit 3a7a7e3)
50
29
  ✔ determine-version semver <1ms 1.1.0: 'feature' from 1.0.0, bumps minor
51
- ✔ determine-notes conventional/determine-notes <1ms feature release; features: 1, bug fixes: 1, other: 1
30
+ ✔ determine-notes conventional/determine-notes <1ms feature release; features: 1, bug fixes: 1, other: 1
52
31
  ✔ release git/create-tag 92ms tagged v1.1.0 at 3a7a7e3 and pushed to origin
53
32
  ✔ release github/create-release 410ms created release v1.1.0: https://github.com/me/project/releases/tag/v1.1.0
54
- ✔ release announce 130ms announced 1.1.0 in #releases
55
33
 
56
34
  1.0.0 → 1.1.0 (feature) · released
57
35
  ```
58
36
 
59
- - **`uses`** names a bundled step or preset, a path to a file (`./release/announce.ts`, its default export), or an installed package (`some-package`, or `some-package#namedExport`). `.ts` files run as they are; they can `import { defineStep, z } from "rylease"` without installing it.
60
- - **`with`** holds the options. A preset takes options for each of its steps by name, and `false` leaves a step out, for instance to replace it with your own, or to list it again with an `if`. Listing a step that a preset already has is an error, so nothing runs twice by accident.
61
- - **`if`** runs the step only when an expression holds; see [Conditions](#conditions). On a preset, it applies to each of its steps.
62
- - **Options** are checked before anything runs. A typo fails the job with its exact location, e.g. `steps[0].with.git/create-tag: Unrecognized key: "pushh"`.
63
-
64
- The same keys can live in a config file instead: `rylease.yml`, `.github/rylease.yml`, or `rylease.config.ts` exporting a release made with `defineRelease`. The action, the [CLI](#cli-gitlab-and-other-ci) and other CI all read it, and the action's inputs override its keys.
65
-
66
- ```yaml
67
- # rylease.yml
68
- steps:
69
- - uses: conventional
70
- channels:
71
- next: { branches: [next] }
72
- ```
73
-
74
- | Input | Default | |
75
- |---|---|---|
76
- | `steps` | `- uses: conventional` | The steps, as above. |
77
- | `scheme` | the preset's | How versions look and are bumped: `semver`, `calver`, or with options, `{ uses: calver, with: { format: YY.MINOR.MICRO } }`. Needed when no preset provides one. See [Schemes](#schemes). |
78
- | `tag` | `v{version}` | Each release's tag. With several components, `{name}@{version}`. |
79
- | `history` | the preset's | Where the previous release is found: `git/tags`, or a list tried in turn, e.g. `[git/tags, { uses: fs/version-file, with: { format: npm } }]`. Needed when no preset provides one. |
80
- | `components` | the whole project | What's versioned, for monorepos: `npm/workspaces`, or a list. See [Monorepos](#monorepos). |
81
- | `channels` | none | Prerelease and build channels, e.g. `{ next: { branches: [next] } }`. See [Channels](#channels). |
82
- | `channel` | | The channel to release on, e.g. `nightly`; by default the first whose branches match. |
83
- | `config` | the first that exists | A config file with any of the keys above. |
84
- | `dry-run` | `false` | Runs every step without side effects; each reports what it would do. |
85
- | `version` | | A version that's already been worked out, e.g. by an earlier job: only the `release` steps run, for it. |
86
- | `notes` | | The release notes, along with `version`. |
87
- | `component` | | The component `version` is for, when there are several. |
88
- | `working-directory` | `.` | The project to release. |
89
- | `summary` | `true` | Writes the release, a row per step and the notes to the job's summary page. |
90
- | `token` | `${{ github.token }}` | Used when `GITHUB_TOKEN` isn't set. |
91
-
92
- Outputs: `status` (`released`, `pending` when a [release pull request](#release-pull-requests) is proposed, `unchanged` or `failed`), `released` (`true`/`false`), `previous-version`, `version`, `kind` (`breaking`, `feature`, `fix` or `none`), `notes`, `channel` and `prerelease`. There's also `components`, each component's values as JSON, and `released-components`, a JSON list of the names of those released, e.g. for a matrix.
93
-
94
- ### Conditions
95
-
96
- `if` is evaluated just before the step would run, so it can depend on what earlier steps did:
97
-
98
- ```yaml
99
- - uses: npm/publish
100
- if: "!dryRun && !prerelease"
101
- - uses: ./release/announce.ts
102
- if: kind == 'breaking' || contains(notes, 'security')
103
- - uses: ./release/link.ts
104
- if: steps['github/create-release'].output.url != null
105
- ```
106
-
107
- | Name | |
108
- |---|---|
109
- | `dryRun`, `branch`, `cwd` | The run's environment. |
110
- | `channel`, `prerelease` | The channel released on, or `null`, and whether it's a prerelease channel. |
111
- | `previousVersion`, `previousRef`, `previousTag`, `changes`, `kind`, `version`, `tag`, `notes`, `dependencies` | The release so far; a value is `null` until its phase has run. |
112
- | `component` | The component the step runs for: `component.name` and `component.path`. |
113
- | `components` | For steps that run once for all components: each one's values. |
114
- | `steps['<name>']` | Each step that ran before: its `phase`, `status` (`ok` or `skipped`), `reason` and `output`. Names with a `/` need brackets, e.g. `steps['git/commit'].output`; others can also be read as `steps.announce`. |
115
-
116
- Expressions are written like GitHub's: `'strings'` (`''` for a quote), numbers, `true`, `false` and `null`; `.name` and `['name']`; `!`, `==`, `!=`, `<`, `<=`, `>`, `>=`, `&&`, `||` and parentheses; and `contains(text or list, value)`, `startsWith(text, prefix)` and `endsWith(text, suffix)`. Comparisons are exact, and case-sensitive. An `if` starting with `!` needs quotes in YAML.
117
-
118
- GitHub fills in `${{ ... }}` in `steps` before rylease reads it, so `if: ${{ github.event_name == 'workflow_dispatch' }}` works too and becomes `if: true` or `if: false`. If conditions skip every step of a phase, the next phase fails, since it has nothing to go on.
119
-
120
- In TypeScript, a condition is a function of what the step receives: `when(steps.npmPublish(), ({ env }) => !env.dryRun)`.
121
-
122
- ### Node projects
123
-
124
- `conventional` works from git tags alone, in any project. To also keep the version in `package.json` and a `CHANGELOG.md`, add the steps that write them, and `git/commit` to commit what they change. Steps run by phase, so the order you list them in doesn't matter:
125
-
126
- ```yaml
127
- - uses: ryangarber/rylease@v3
128
- with:
129
- history: "[git/tags, { uses: fs/version-file, with: { format: npm } }]"
130
- steps: |
131
- - uses: conventional
132
- - uses: fs/write-version
133
- with: { format: npm }
134
- - uses: fs/write-changes
135
- - uses: git/commit
136
- ```
137
-
138
- `history` finds the previous release from its tag. A project that's never been tagged falls back to the version in `package.json`, released at the commit that set it. If there's neither, every commit counts and the release is the scheme's first (`0.1.0` for semver, set with `initial`).
139
-
140
- `git/commit` and `git/create-tag` each have a `push` option, off by default. `conventional` sets it on `git/create-tag` alone, which pushes the branch along with the tags in one atomic push, so the release commit and its tag land together or not at all.
141
-
142
- ### Publishing to npm
143
-
144
- Add `npm/publish` too. It publishes with `pnpm` or `npm`, whichever the project uses, so lifecycle scripts like `prepublishOnly` run as usual. Install dependencies first, and set up pnpm if the project uses it.
145
-
146
- ```yaml
147
- permissions:
148
- contents: write
149
- id-token: write # for trusted publishing, or provenance
150
- steps:
151
- - uses: actions/checkout@v7
152
- with:
153
- fetch-depth: 0
154
- - uses: pnpm/setup@v3 # installs dependencies too
155
- - uses: ryangarber/rylease@v3
156
- with:
157
- steps: |
158
- - uses: conventional
159
- - uses: fs/write-version
160
- with: { format: npm }
161
- - uses: git/commit
162
- - uses: npm/publish
163
- env:
164
- NPM_TOKEN: ${{ secrets.NPM_TOKEN }} # not needed with trusted publishing
165
- ```
166
-
167
- It publishes the version that `fs/write-version` wrote, after the release is committed, tagged and pushed, and refuses to publish any other. On a channel, it publishes under the channel's dist-tag, e.g. `next`. npm only takes semantic versions, so both steps need a scheme that makes them; with `calver` and a zero-padded format like `YYYY.0M.MICRO`, the release fails before anything runs.
37
+ Outside GitHub Actions, `npx rylease` does the same; see [CLI, GitLab and other CI](docs/cli.md).
168
38
 
169
- ### Other ecosystems
39
+ ## Phases
170
40
 
171
- `fs/write-version` writes the version wherever a project keeps it, keeping the rest of the file as it was, and `fs/version-file` reads it back as a history:
172
-
173
- | `format` | File | |
174
- |---|---|---|
175
- | `plain` | `VERSION` | The whole file. |
176
- | `npm` | `package.json` | `version`, and dependencies on other released components. |
177
- | `cargo` | `Cargo.toml` | `version` in `[package]` or `[workspace.package]`, and `version`s of dependencies on other released crates. |
178
- | `python` | `pyproject.toml` | `version` in `[project]` or `[tool.poetry]`. |
179
- | `helm` | `Chart.yaml` | The chart's `version`. |
180
- | `gradle` | `gradle.properties` | `version=`. |
181
- | `maven` | `pom.xml` | The project's own `<version>`, not its parent's or its dependencies'. |
182
- | `pattern` | any | Wherever a template matches, e.g. `pattern: '__version__ = "{version}"'`, or `appVersion: "{version}"` in `Chart.yaml`. |
183
-
184
- `exec/command` runs any command, with `{placeholders}` filled in, in the `phase` it's given: in `write`, e.g. to update a lockfile, and in `release` to ship it. In a dry run it only says what it'd run, unless it's given a `dryRun` command to run instead.
185
-
186
- ```yaml
187
- - uses: conventional
188
- - uses: fs/write-version
189
- with: { format: cargo }
190
- - uses: exec/command
191
- with: { phase: write, command: cargo update --workspace, files: [Cargo.lock] }
192
- - uses: git/commit
193
- - uses: exec/command
194
- with:
195
- phase: release
196
- command: cargo publish
197
- dryRun: cargo publish --dry-run
198
- env: { CARGO_REGISTRY_TOKEN: "${{ secrets.CARGO_TOKEN }}" }
199
- - uses: exec/command
200
- with: { phase: release, command: "docker buildx build --push -t ghcr.io/me/app:{version} ." }
201
41
  ```
202
-
203
- ### Uploading builds to the release
204
-
205
- Jobs that build after the release, say one per platform, can each upload their files to it. Pass the version (and notes) the release job worked out, and only the `release` steps run. Here the `release` job passes on rylease's `released`, `version` and `notes` outputs, and whether it was a dry run, as its own outputs:
206
-
207
- ```yaml
208
- build:
209
- needs: release
210
- if: needs.release.outputs.released == 'true'
211
- strategy:
212
- matrix:
213
- os: [macos-latest, ubuntu-latest, windows-latest]
214
- runs-on: ${{ matrix.os }}
215
- permissions:
216
- contents: write
217
- steps:
218
- - uses: actions/checkout@v7
219
- with:
220
- ref: v${{ needs.release.outputs.version }}
221
- - run: npm ci && npm run build
222
- - uses: ryangarber/rylease@v3
223
- with:
224
- version: ${{ needs.release.outputs.version }}
225
- notes: ${{ needs.release.outputs.notes }}
226
- dry-run: ${{ needs.release.outputs.dry-run }}
227
- scheme: semver # no preset here to provide them
228
- history: git/tags
229
- steps: |
230
- - uses: github/upload-release-assets
231
- with:
232
- files: ["dist/app-*"]
233
- dryRunArtifact: true
42
+ read-version → read-changes → determine-kind → determine-version → determine-notes → write → stage → release
234
43
  ```
235
44
 
236
- `github/upload-release-assets` uploads to the release that `github/create-release` created, or creates it if no job has yet. When several jobs race to, the losers wait for the winner's release and use it. Failed requests and uploads are retried, and an upload that failed halfway is cleared first. With `dryRunArtifact`, a dry run keeps the files as a workflow artifact instead, so a dry run of the whole pipeline leaves every build to download. `github/upload-artifact` does the same for any files, in a dry run or not.
237
-
238
- ### Other sources and schemes
239
-
240
- Each phase has more than one way to fill it. Leave a preset's step out with `false`, and list another in its place.
241
-
242
- | Phase | `conventional` uses | Or |
45
+ | Phase | | Filled by |
243
46
  |---|---|---|
244
- | `read-changes` | `conventional/read-commits` | `github/read-pull-requests`, `keep-a-changelog/read-unreleased`, `change-files/read` |
245
- | `determine-kind` | `conventional/determine-kind` | `labels/determine-kind`, `components/dependents` |
246
- | `determine-notes` | `conventional/determine-notes` | `labels/determine-notes` |
247
- | `write` | | `fs/write-version`, `fs/write-changes`, `keep-a-changelog/write-release`, `change-files/clean`, `exec/command` |
248
- | `stage` | | `git/commit`, `github/open-release-pr` |
249
- | `release` | `git/create-tag`, `github/create-release` | `gitlab/create-release`, `npm/publish`, `exec/command`, `github/upload-release-assets`, … |
250
-
251
- Steps of neighboring phases work together as long as one provides what the other needs. The `labels/` steps need labels, which pull requests, Keep a Changelog sections and change files provide. The `conventional/` ones read conventional titles, from any source. Commits have no labels, so `labels/determine-kind` after `conventional/read-commits` fails before anything runs:
252
-
253
- ```
254
- labels/determine-kind needs changes.labels, which conventional/read-commits doesn't provide; change-files/read, github/read-pull-requests or keep-a-changelog/read-unreleased do
255
- ```
256
-
257
- Pull requests, by label, as GitHub's own release notes group them:
258
-
259
- ```yaml
260
- - uses: conventional
261
- with:
262
- conventional/read-commits: false
263
- conventional/determine-kind: false
264
- conventional/determine-notes: false
265
- - uses: github/read-pull-requests
266
- - uses: labels/determine-kind # breaking → breaking, enhancement → feature, else fix
267
- - uses: labels/determine-notes
268
- ```
269
-
270
- If your pull request titles are conventional, keep `conventional/determine-kind` and `conventional/determine-notes`; they read titles whatever the source.
271
-
272
- A hand-written [Keep a Changelog](https://keepachangelog.com) has its own preset, `keep-a-changelog`: it releases the `## [Unreleased]` section, labels each entry by its `### Added`, `### Fixed` … heading, and commits the changelog before tagging. Its sections don't imply a scheme, so set one, e.g. `scheme: semver`. Don't use it with `fs/write-changes`, since both write `CHANGELOG.md`.
273
-
274
- On GitLab, replace `github/create-release` with `gitlab/create-release`. In GitLab CI, it takes the project, API and job token from the job. Branch detection, commit links and the identity that `git/commit` and `git/create-tag` use also come from GitLab's variables.
275
-
276
- ### Change files
277
-
278
- Commit messages don't always say what a release should. With change files, whoever makes a change says what it is and how much it changes each component, in a file under `.changes/`:
279
-
280
- ```md
281
- ---
282
- "@x/core": feature
283
- "@x/cli": fix
284
- ---
285
- Add dark mode.
286
-
287
- It follows the system's setting.
288
- ```
289
-
290
- `rylease add --component @x/core:feature --component @x/cli "Add dark mode"` writes one; in a project that's one component, `kind: feature` stands for it. The `change-files` preset releases what the files say. It notes each change under its kind, deletes the files it released in the release commit, and tags and releases as `conventional` does. Kinds don't imply a scheme, so set one. A file naming a component that doesn't exist, or a kind that isn't one, fails the release before anything runs.
291
-
292
- ```yaml
293
- # rylease.yml
294
- scheme: semver
295
- steps:
296
- - uses: change-files
297
- ```
298
-
299
- ### Schemes
300
-
301
- A release's kind says how much changed, whatever the scheme: `breaking`, `feature`, `fix`, or `none` for nothing to release. The scheme decides what each kind bumps. It's also the one place that recognizes and orders versions, so finding the previous release, bumping and tagging always agree.
302
-
303
- `semver` bumps `MAJOR.minor.patch`; `conventional` and `conventional-pr` use it, since conventional commits are defined by it. Before 1.0.0 it shifts bumps down a level (`0.MAJOR.minorOrPatch`) unless `shiftPreMajor` is off.
304
-
305
- Calendar versions, from conventional commits:
47
+ | `read-version` | Finds the previous release: its version, commit and tag. | The release's **history**, e.g. `git/tags`. |
48
+ | `read-changes` | Collects what changed since then. | Steps, e.g. `conventional/read-commits`. |
49
+ | `determine-kind` | Decides how much changed: `breaking`, `feature`, `fix`, or `none` for nothing to release. | Steps, e.g. `conventional/determine-kind`. |
50
+ | `determine-version` | Bumps the previous version by the kind, on a **channel** if there is one, e.g. `2.0.0-next.1`. | The release's **scheme**, e.g. `semver`. |
51
+ | `determine-notes` | Writes the release notes. | Steps, e.g. `conventional/determine-notes`. |
52
+ | `write` | Changes files, e.g. the version in `package.json` or a changelog. | Steps, e.g. `fs/write-version`. |
53
+ | `stage` | Commits what `write` changed, or proposes it in a pull request. | Steps, e.g. `git/commit`. |
54
+ | `release` | Ships the result: tags, releases, packages. | Steps, e.g. `git/create-tag`, `npm/publish`. |
306
55
 
307
- ```yaml
308
- - uses: ryangarber/rylease@v3
309
- with:
310
- scheme: "{ uses: calver, with: { format: YYYY.MM.MICRO } }" # 2026.10.0, 2026.10.1, … 2026.11.0
311
- steps: |
312
- - uses: conventional
313
- with:
314
- git/create-tag: { move: ["v{YYYY}.{MM}"] } # v2026.10, the month's latest
315
- ```
316
-
317
- `calver` takes the date from the clock, in UTC unless you set `timezone`. Its counters number the releases of a period, and the kind picks which: in `YY.MINOR.MICRO`, a fix bumps `MICRO` and a feature `MINOR`. In a new period they all start again from 0. A format without a counter, like `YYYY.0M.0D`, can release once per period. Its tokens are placeholders in templates, e.g. `{YYYY}`; every scheme also fills `{major}`, `{minor}` and `{patch}` with a version's first three numbers.
318
-
319
- Switching schemes is safe. The previous release is still found, so changes start from it, but a version from another scheme isn't bumped: the next release is the new scheme's first.
320
-
321
- A scheme of your own is an object with `matches`, `compare`, `bump` and optionally `fields`, `traits` and `formats` (for [channels](#channels)). Define it with `defineScheme`, like a step, to use it from a workflow too, as `scheme: ./.github/release/four-part.ts`:
322
-
323
- ```ts
324
- // .github/release/four-part.ts: MAJOR.minor.patch.build, the build always counting up
325
- import { defineScheme } from "rylease";
326
-
327
- const numbers = (version: string) => version.split(".").map(Number);
328
-
329
- export default defineScheme({
330
- name: "four-part",
331
- create: () => ({
332
- name: "four-part",
333
- matches: (version) => /^\d+\.\d+\.\d+\.\d+$/.test(version),
334
- compare: (a, b) => {
335
- const [x, y] = [numbers(a), numbers(b)];
336
- return x.reduce((order, n, i) => order || n - (y[i] ?? 0), 0);
337
- },
338
- bump: ({ previous = "0.0.0.0", kind }) => {
339
- const [major = 0, minor = 0, patch = 0, build = 0] = numbers(previous);
340
- const next = {
341
- breaking: [major + 1, 0, 0],
342
- feature: [major, minor + 1, 0],
343
- fix: [major, minor, patch + 1],
344
- }[kind];
345
- return { version: [...next, build + 1].join("."), reason: `bumps ${kind}` };
346
- },
347
- }),
348
- });
349
- ```
56
+ rylease handles `read-version` and `determine-version` itself, with the history and scheme. Both come from the preset, unless the release sets its own; there are no other defaults. No step parses or bumps a version; every step takes it as plain text.
350
57
 
351
- `determine-kind` steps can also set the version outright, e.g. from a `Release-As:` footer, by returning `version` with the kind; the scheme still checks it.
58
+ Steps run by phase, so the order you list them in doesn't matter; steps of the same phase run in the order given, each receiving what the one before it produced.
352
59
 
353
- ### Channels
60
+ ## Configuring
354
61
 
355
- A channel releases versions that lead up to the next stable one, or number builds of the last, whatever the scheme: `2.0.0-next.1`, `2026.10.0-beta.2`, `1.4.0+nightly.7`. Each channel wraps the scheme's version in a format with a counter:
356
-
357
- ```yaml
358
- channels:
359
- next: { branches: [next] } # 2.0.0-next.1, 2.0.0-next.2, …
360
- beta: { branches: ["beta/*"], format: "{version}-beta.{n}" }
361
- nightly: { type: build } # 1.4.0+nightly.1; with channel: nightly
362
- ```
363
-
364
- A run releases on the channel it's given (the `channel` input or `--channel`), or else on the first whose `branches` match, or else as a stable release.
365
-
366
- - A **prerelease** (`type: prerelease`, the default) leads up to the version its changes would bump the last stable release to. Its counter continues across runs, and across branches, so two never share a version. A bigger change moves it on to a bigger version, and starts the counter again.
367
- - A **build** (`type: build`) numbers builds of the last stable release, e.g. nightlies.
368
- - A **stable** release ignores channel releases, so merging `next` into `main` releases `2.0.0` with every change since `1.4.0`.
369
-
370
- The default formats are `{version}-{channel}.{n}` and `{version}+{channel}.{n}`; a scheme can have its own, e.g. `{version}rc{n}` for Python's. On a channel, `github/create-release` marks prereleases, and `npm/publish` publishes under the channel's dist-tag. `git/create-tag` only moves `move` tags that name `{channel}`, e.g. a floating `next` tag; the others, like `v{major}`, wait for the stable release. `fs/write-changes`, `keep-a-changelog/write-release` and `change-files/clean` leave the changelog and change files for the stable release, unless they're given `channels: true`.
371
-
372
- ### Release pull requests
373
-
374
- Instead of releasing on every push, rylease can propose the release in a pull request, kept up to date as changes land, and release it when it's merged:
375
-
376
- ```yaml
377
- - uses: ryangarber/rylease@v3
378
- with:
379
- steps: |
380
- - uses: conventional-pr
381
- - uses: fs/write-version
382
- with: { format: npm }
383
- ```
384
-
385
- On a push, `github/open-release-pr` commits what the `write` steps changed to `rylease/main`, and opens a pull request for it, or updates the open one. Its body holds each release's notes. The run is `pending`, and nothing is tagged. When the pull request is merged, the next run finds it through `conventional-pr`'s history, `github/release-pr`: only the `release` steps run, for the versions and notes it proposed. The job needs `pull-requests: write` as well as `contents: write`.
386
-
387
- ### Monorepos
388
-
389
- A release has one or more **components**: what's versioned and released together, under one version and one tag. Without `components`, the whole project is one. In a monorepo, each package is usually one, and `npm/workspaces` finds them in an npm, Yarn or pnpm workspace:
62
+ `steps` is a list shaped like a workflow's: each entry `uses` a step or preset, gives it options `with`, and may run only `if` an expression holds. A preset takes options for each of its steps by name, and `false` leaves one out.
390
63
 
391
64
  ```yaml
392
65
  - uses: ryangarber/rylease@v3
393
66
  with:
394
- components: npm/workspaces
395
67
  steps: |
396
68
  - uses: conventional
397
- - uses: components/dependents
69
+ with:
70
+ github/create-release: { draft: true }
71
+ conventional/determine-kind:
72
+ types: { docs: none, chore: none, "chore(deps)": fix }
398
73
  - uses: fs/write-version
399
74
  with: { format: npm }
400
- - uses: fs/write-changes
401
75
  - uses: git/commit
402
76
  - uses: npm/publish
77
+ if: "!dryRun && !prerelease"
403
78
  ```
404
79
 
405
- ```
406
- ✔ @x/core read-version git/tags 6ms @x/core@1.0.0 at 0d92bfa, nearest on HEAD
407
- ✔ @x/cli read-version git/tags 5ms @x/cli@1.0.0 at 0d92bfa, nearest on HEAD
408
- ✔ @x/core read-changes conventional/read-commits 9ms 1 change in 1 commit since 0d92bfa
409
- ✔ @x/cli read-changes conventional/read-commits 8ms 0 changes in 0 commits since 0d92bfa
410
- …
411
- ✔ * determine-kind components/dependents <1ms 1 dependent released: @x/cli
412
- ✔ @x/core determine-version semver <1ms 1.1.0: 'feature' from 1.0.0, bumps minor
413
- ✔ @x/cli determine-version semver <1ms 1.0.1: 'fix' from 1.0.0, bumps patch
414
- …
415
- ✔ * write fs/write-version 3ms wrote 1.1.0 to packages/core/package.json and 1.0.1 to packages/cli/package.json (and @x/core@^1.1.0)
416
- ✔ @x/core write fs/write-changes 1ms created packages/core/CHANGELOG.md
417
- ✔ @x/cli write fs/write-changes 1ms created packages/cli/CHANGELOG.md
418
- ✔ * stage git/commit 40ms committed 5e1f00d
419
- ✔ * release git/create-tag 90ms tagged @x/core@1.1.0 and @x/cli@1.0.1 at 5e1f00d and pushed to origin
420
- ✔ @x/core release npm/publish 1.20s published @x/core@1.1.0 to https://registry.npmjs.org/ as latest
421
- ✔ @x/cli release npm/publish 1.10s published @x/cli@1.0.1 to https://registry.npmjs.org/ as latest
422
- ```
423
-
424
- The pipeline is the same as for one package. What changes is what each step runs for:
425
-
426
- - Most steps run **once per component**, dependencies first. `conventional/read-commits` only counts the commits that touch the component's directory, leaving out the directories of components inside it. Paths in options, like `fs/write-changes`'s `CHANGELOG.md`, are relative to the component's directory.
427
- - Some steps run **once for all components** together: `git/commit` makes one commit, `git/create-tag` pushes every tag in one atomic push, and `fs/write-version` points dependencies on released packages at their new versions.
428
- - `components/dependents` releases what depends on a released component, as at least a fix (`kind`), for releases of the kinds in `on`. Each release's notes end with a **Dependencies** section listing the components it depends on that were released with it, e.g. `- @x/core 1.0.0 → 1.1.0`, so a release made only for its dependencies says why.
429
- - A component with nothing to release drops out after `determine-kind`. The run is `released` if any component was.
430
-
431
- Each component is tagged `{name}@{version}`; set `tag` to change that, with `{name}` in it. Packages released under one shared version are a single component, with `fs/write-version`'s `path` set to each `package.json`, e.g. `packages/*/package.json`. Components can also be listed by hand:
432
-
433
- ```yaml
434
- components: |
435
- - { name: core, path: packages/core }
436
- - { name: cli, path: packages/cli, dependsOn: [core] }
437
- - { name: app, path: apps/web, scheme: calver, tag: "app-{version}" }
438
- ```
439
-
440
- Each component can have its own `scheme`, `tag` and `history`. The `components` output lists every component's version and status, and `released-components` names those released, for a matrix of later jobs. Pass a job's `component` along with `version` to run its `release` steps for that component alone.
80
+ The same keys can live in `rylease.yml`, `.github/rylease.yml` or `rylease.config.ts`. Options are checked before anything runs, and a typo fails with its exact location. The action's inputs and outputs, config files and conditions are in [Configuration](docs/configuration.md).
441
81
 
442
- ## CLI, GitLab and other CI
82
+ ## Presets
443
83
 
444
- `npx rylease` runs the release in the config file, or `conventional` without one, anywhere:
445
-
446
- ```sh
447
- npx rylease --dry-run # what would be released, and why
448
- npx rylease --channel next # release on a channel
449
- npx rylease --release 1.2.0 --notes "…" # run only the release steps, e.g. in a later job
450
- npx rylease add --component core:feature "Add dark mode" # a change file
451
- ```
452
-
453
- `--json` prints the report as JSON, `--summary <file>` appends the Markdown summary, and `--outputs <file>` writes the action's outputs as `RYLEASE_STATUS=released` lines, e.g. for a GitLab dotenv report:
454
-
455
- ```yaml
456
- release:
457
- image: node:24
458
- script:
459
- - npx rylease --outputs release.env
460
- artifacts:
461
- reports:
462
- dotenv: release.env
463
- ```
464
-
465
- ## TypeScript
466
-
467
- The same release, as a script:
468
-
469
- ```ts
470
- // release.ts
471
- import { defineRelease, presets, schemes, sources } from "rylease";
472
-
473
- await defineRelease({
474
- scheme: schemes.semver({ initial: "1.0.0" }),
475
- history: [sources.gitTags(), sources.fsVersionFile({ format: "npm" })],
476
- channels: { next: { branches: ["next"] } },
477
- steps: [
478
- ...presets.conventional({
479
- "github/create-release": { draft: true },
480
- "conventional/determine-kind": { types: { docs: "none", chore: "none" } },
481
- }),
482
- announce({ channel: "#releases" }),
483
- ],
484
- })
485
- .run({ dryRun: process.argv.includes("--dry-run") })
486
- .catch(() => {
487
- process.exitCode = 1; // the failure has already been printed
488
- });
489
- ```
490
-
491
- Export the release as the default export of `rylease.config.ts` instead, and the action and the CLI run it. `markdownSummary(report)` renders a report as the Markdown the action writes to the job summary.
492
-
493
- ## Steps
494
-
495
- | Type | Receives (plus `component`, `previousVersion?`, `previousRef?`, `previousTag?`, `env`, `context`, `history`) | Returns |
496
- |---|---|---|
497
- | `read-changes` | `changes?` | `{ changes: { title?, description?, reason, link?, labels? }[], reason }` |
498
- | `determine-kind` | … `changes`, `kind?` | `{ kind: "breaking" \| "feature" \| "fix" \| "none", version?, reason }` |
499
- | `determine-notes` | … `kind` (never `"none"`), `version`, `tag`, `placeholders`, `dependencies`, `notes?` | `{ notes, reason }` |
500
- | `write`, `stage` | … `notes`, `releases` (results of earlier `write`, `stage` and `release` steps) | `{ output, files?, defer?, reason }` |
501
- | `release` | … `notes`, `releases` | `{ output, files?, reason }` |
502
-
503
- - **Order.** Steps are sorted by phase. Steps of the same phase keep the order you gave them.
504
- - **Chaining.** When several steps share a phase, each later one receives the current value as an optional input (e.g. `notes?`). Whatever it returns replaces that value. `write`, `stage` and `release` outputs accumulate instead.
505
- - **One source of truth.** A step takes what it needs from earlier phases and never works it out again. `previousRef` is the commit of the previous release, and changes are collected from there. `tag` is the release's tag, and `placeholders` fill templates. `files` lists what a `write` step changed, and `git/commit` commits exactly those.
506
- - **Scope.** A step runs once per component (`scope: "each"`, the default) or once for all of them (`scope: "all"`). Then it receives `components`, each with what it would receive on its own, and returns `results` by component name, or from `write` on, one result for all.
507
- - **Facts.** A step declares the facts it `needs` and `provides`, e.g. `changes.labels`: every change has labels. Before anything runs, each need must be met by an earlier step, or for `version.semver`, by the scheme. Provided facts are checked on the step's output. Facts about changes last only until another step replaces the changes. `needs` can also be a function of the step's options, e.g. by format.
508
- - **Deferring.** A `write` or `stage` step that returns `defer` leaves the release to later: the `release` steps are skipped with that reason, and the run is `pending`.
509
- - **Nothing to release.** A component whose final kind is `"none"` drops out; if none are left, the remaining steps are skipped.
510
- - **Dry run.** Steps get `env.dryRun`. Steps with side effects must do nothing and explain what they *would* have done. The one exception is `dryRunArtifact`, which keeps files as an artifact of the workflow run, which only that run sees.
511
- - **Audit.** `run()` resolves to a report with a record (phase, name, component, status, duration, reason, output) for every step, and for the previous and next versions. On failure it rejects with a `ReleaseError` whose `.report` holds the same records.
512
-
513
- ### Custom steps
514
-
515
- `defineStep` declares a step and its options, as a [zod](https://zod.dev) shape. The result is a factory: call it with options to get a step. Options are typed from the shape, and validated on every call, whether they come from TypeScript or from a workflow file. Use `.default()` for optional options, and `.describe()` to document them.
516
-
517
- ```ts
518
- // .github/release/announce.ts
519
- import { defineStep, z } from "rylease";
520
-
521
- export default defineStep({
522
- phase: "release",
523
- name: "announce",
524
- description: "Posts the release to Slack.",
525
- options: {
526
- channel: z.string().describe("Where to post."),
527
- mention: z.boolean().default(false),
528
- },
529
- run: async ({ version, env, options }) => {
530
- if (env.dryRun) return { output: null, reason: `would announce in ${options.channel}` };
531
- await postToSlack(options.channel, `Released ${version}`);
532
- return { output: null, reason: `announced ${version} in ${options.channel}` };
533
- },
534
- });
535
- ```
536
-
537
- `needs` types the step's input accordingly, and `provides` its output:
538
-
539
- ```ts
540
- export const byJira = defineStep({
541
- phase: "determine-kind",
542
- name: "jira/determine-kind",
543
- needs: ["changes.labels"],
544
- run: ({ changes }) => ({
545
- // `labels` is `string[]` here, never undefined.
546
- kind: changes.some((change) => change.labels.includes("epic")) ? "feature" : "fix",
547
- reason: "by Jira labels",
548
- }),
549
- });
550
- ```
551
-
552
- Add facts of your own by declaration merging: `declare module "rylease" { interface Facts { "changes.jira": true } }`.
553
-
554
- Steps that run git can use `git(env.cwd, ...args)`, which resolves with its trimmed output and fails with git's own error, or `tryGit`, which resolves with `undefined` instead of failing.
555
-
556
- `definePreset({ name, steps })` bundles factories, like `presets.conventional`, and takes options per step by name. It can also give the `scheme` and `history` of releases made with it, as factories, e.g. `history: sources.gitTags`; a release's own wins, and two presets that both give one are an error. `defineHistory` and `defineComponents` define where previous releases and components come from, the same way.
557
-
558
- Steps given inline to `defineRelease` are plain `{ phase, name, run }` objects, typed from their `phase` and `scope`.
559
-
560
- ### Context
561
-
562
- `context` holds whatever state you want to share between inline steps, and its type is yours. It is the only place for arbitrary state, and the bundled steps never touch it.
563
-
564
- ```ts
565
- defineRelease<{ slackTs?: string }>({
566
- context: {},
567
- steps: [
568
- ...presets.conventional(),
569
- {
570
- phase: "release",
571
- name: "announce",
572
- run: async ({ version, context, env }) => {
573
- if (env.dryRun) return { output: null, reason: "would announce" };
574
- context.slackTs = await postToSlack(`Released ${version}`);
575
- return { output: context.slackTs, reason: "posted to #releases" };
576
- },
577
- },
578
- ],
579
- });
580
- ```
581
-
582
- ## Migrating from v2
583
-
584
- | v2 | v3 |
585
- |---|---|
586
- | `git/read-tag` | `history: git/tags`, which the presets give: the previous release is the nearest tag on HEAD, so a `1.x` branch finds its own. Set its template with the release's `tag`. |
587
- | `npm/read-version`, `fs/read-version` | `history: [git/tags, { uses: fs/version-file, with: { format: npm } }]` (or `format: plain` for a `VERSION` file). |
588
- | `npm/write-version` | `fs/write-version` with `format: npm`. |
589
- | `version/bump-semver` | `scheme: semver`, which `conventional` and `conventional-pr` give, with the same `shiftPreMajor`. |
590
- | `version/calver` | `scheme: { uses: calver, with: { format: … } }`. |
591
- | Kinds `major`, `minor`, `patch` | `breaking`, `feature`, `fix`, in options (`types: { docs: none, "chore(deps)": fix }`), conditions and outputs. |
592
- | `tag` options of `git/create-tag`, `github/create-release`, `gitlab/create-release`, `github/upload-release-assets`, `keep-a-changelog/write-release` | The release's `tag`, set once. Templates get `{tag}`; `git/commit`'s message is `chore(release): {tags}`. |
593
- | Listing steps that write files before a preset | Not needed: `fs/write-*` steps are `write` steps and `git/commit` a `stage` step, so they run in order wherever they're listed. Custom steps that change files should be `write` steps. |
594
- | `working-directory` per package | `components: npm/workspaces`, in one run. |
595
- | Steps of type `read-version` or `bump-version` | A history (`defineHistory`) or scheme (`defineScheme`). |
596
-
597
- ## Bundled steps
598
-
599
- Presets, with the scheme and history they give:
84
+ A preset is a list of steps used as one, with the scheme and history it needs. Every preset pushes the release's tag, along with the branch, to `origin` in one atomic push.
600
85
 
601
86
  | Preset | Steps | `scheme` | `history` |
602
87
  |---|---|---|---|
603
- | `conventional` | The first five below, in this order. | `semver` | `git/tags` |
604
- | `conventional-pr` | `conventional`'s, with `fs/write-changes` and `github/open-release-pr`. | `semver` | `github/release-pr` |
605
- | `keep-a-changelog` | `keep-a-changelog/read-unreleased`, `labels/determine-kind`, `labels/determine-notes`, `keep-a-changelog/write-release`, `git/commit`, `git/create-tag`, `github/create-release`. | | `git/tags` |
606
- | `change-files` | `change-files/read`, `labels/determine-kind`, `labels/determine-notes`, `change-files/clean`, `fs/write-changes`, `git/commit`, `git/create-tag`, `github/create-release`. | | `git/tags` |
607
-
608
- Each step is also exported on its own (`steps.gitCreateTag`, …).
609
-
610
- <!-- options:start -->
611
- #### `conventional/read-commits` · `read-changes`
612
-
613
- Collects each conventional section (`feat:`, `fix(x)!:` …) of every commit touching the component since the previous release, or in all history without one. Provides `changes.title`.
614
-
615
- No options.
616
-
617
- #### `conventional/determine-kind` · `determine-kind`
618
-
619
- `!` or `BREAKING` → breaking; otherwise by conventional type (`feat` → feature, anything else → fix). The highest kind wins.
620
-
621
- | Option | Type | Default | |
622
- |---|---|---|---|
623
- | `types` | `{ [key]: breaking \| feature \| fix \| none }` | `{}` | Kinds by `type` or `type(scope)`, on top of `feat: feature`, e.g. `{ docs: none, chore(deps): fix }`. |
624
- | `fallback` | `breaking \| feature \| fix \| none` | `fix` | The kind of any other change, untyped ones included. |
625
-
626
- #### `conventional/determine-notes` · `determine-notes`
627
-
628
- `### Breaking Changes`, `### Features`, `### Bug Fixes`, `### Performance Improvements` and `### Reverts` sections, then `### Other` for everything else.
629
-
630
- | Option | Type | Default | |
631
- |---|---|---|---|
632
- | `sections` | `{ title: string, types: string[] }[]` | `[{ title: Features, types: [feat] }, { title: Bug Fixes, types: [fix] }, { title: Performance Improvements, types: [perf] }, { title: Reverts, types: [revert] }]` | Sections in order; a change goes in the first that lists its type. |
633
- | `breaking` | `string \| false` | `Breaking Changes` | The first section, for breaking changes of any type; `false` marks them **(breaking)** in their own section instead. |
634
- | `other` | `string \| false` | `Other` | The section for remaining changes; `false` leaves them out. |
635
- | `links` | `boolean` | `true` | Link each change to its source, e.g. its commit. |
636
- | `dependencies` | `string \| false` | `Dependencies` | The last section, for the components it depends on that were released along with it; `false` leaves it out. |
637
-
638
- #### `git/create-tag` · `release`
639
-
640
- Tags HEAD with each release's tag and pushes them, along with the branch, in one atomic push. Runs once for all components.
641
-
642
- | Option | Type | Default | |
643
- |---|---|---|---|
644
- | `message` | `string` | `chore(release): {tag}` | The tag message; `{version}`, `{tag}` and `{name}` (the component's) are filled in, and the scheme's own, e.g. `{major}`, `{minor}` and `{patch}`. |
645
- | `move` | `string[]` | `[]` | Tags to create or move to each release, e.g. `v{major}`; force-pushed. Those with `{channel}` only move on a channel, e.g. to `next`, and the others only on stable releases. |
646
- | `push` | `string \| false` | `origin` in `conventional`, `false` on its own | The remote to push the tags and the current branch to; `false` skips pushing. |
647
-
648
- #### `github/create-release` · `release`
649
-
650
- Creates a GitHub release of the tag with the notes, unless it already has one; the tag is created at HEAD if it doesn't exist. Needs a token with `contents: write`.
651
-
652
- | Option | Type | Default | |
653
- |---|---|---|---|
654
- | `title` | `string` | `{tag}` | `{tag}`, `{version}`, `{name}` and the scheme's placeholders are filled in. |
655
- | `draft` | `boolean` | `false` | |
656
- | `prerelease` | `boolean \| auto` | `auto` | `auto` marks releases on a prerelease channel. |
657
- | `repo` | `string` | | `owner/repo`; defaults to `GITHUB_REPOSITORY` or the `origin` remote. |
658
- | `token` | `string` | | Defaults to `GITHUB_TOKEN` or `GH_TOKEN`. |
659
-
660
- ### More steps
661
-
662
- These aren't in `conventional`; list them with it, or in its place: see [Other sources and schemes](#other-sources-and-schemes). Steps run by phase, so they can be listed in any order; steps of the same phase run in the order given.
663
-
664
- #### `change-files/clean` · `write`
665
-
666
- Deletes the change files of the components being released, for `git/commit` to commit; releases on a channel leave them for the stable release. Runs once for all components.
667
-
668
- | Option | Type | Default | |
669
- |---|---|---|---|
670
- | `directory` | `string` | `.changes` | Where change files are kept, relative to the project. |
671
- | `channels` | `boolean` | `false` | Also run for releases on a channel, e.g. prereleases; by default only stable releases do. |
672
-
673
- #### `change-files/read` · `read-changes`
674
-
675
- Collects the change files in `.changes/`, each naming the components it changes and how much, e.g. `core: feature`; each change is labeled with its kind. Runs once for all components. Provides `changes.title`, `changes.labels`.
676
-
677
- | Option | Type | Default | |
678
- |---|---|---|---|
679
- | `directory` | `string` | `.changes` | Where change files are kept, relative to the project. |
680
-
681
- #### `components/dependents` · `determine-kind`
682
-
683
- Releases each component that depends on a released one, as at least a `fix`; list it after the steps that determine kinds. Runs once for all components.
684
-
685
- | Option | Type | Default | |
686
- |---|---|---|---|
687
- | `kind` | `breaking \| feature \| fix` | `fix` | The least kind of a dependent's release. |
688
- | `on` | `breaking \| feature \| fix[]` | `[breaking, feature, fix]` | The kinds of a dependency's release that release its dependents. |
689
-
690
- #### `exec/command` · `write | stage | release`
691
-
692
- Runs a command for each component in its `phase`: `write` e.g. to update a lockfile, listing what it changes as `files`, or `release` e.g. `cargo publish` or `docker push app:{version}`.
693
-
694
- | Option | Type | Default | |
695
- |---|---|---|---|
696
- | `phase` | `write \| stage \| release` | required | The phase it runs in. |
697
- | `command` | `string \| string[]` | required | `{placeholders}` are filled in. Text runs in `sh -c`; a list runs as it is, without a shell. |
698
- | `dryRun` | `string \| string[]` | | Runs instead in a dry run, e.g. `cargo publish --dry-run`; without it, a dry run only says what it would run. |
699
- | `cwd` | `string` | `.` | Where it runs, relative to the component's directory. |
700
- | `env` | `{ [key]: string }` | `{}` | More environment variables, with `{placeholders}` filled in. |
701
- | `files` | `string[]` | `[]` | What it changes, relative to the component's directory, for `git/commit`. |
702
-
703
- #### `fs/write-changes` · `write`
704
-
705
- Writes a heading and the notes to a file.
706
-
707
- | Option | Type | Default | |
708
- |---|---|---|---|
709
- | `path` | `string` | `CHANGELOG.md` | Relative to the component's directory. |
710
- | `heading` | `string` | `## {tag} ({date})` | `{version}`, `{tag}` and `{name}` (the component's) are filled in, and the scheme's own, e.g. `{major}`, `{minor}` and `{patch}`, and `{date}` (YYYY-MM-DD). |
711
- | `mode` | `prepend \| append \| replace` | `prepend` | |
712
- | `channels` | `boolean` | `false` | Also run for releases on a channel, e.g. prereleases; by default only stable releases do. |
713
-
714
- #### `fs/write-version` · `write`
715
-
716
- Writes each component's new version to its version file: `VERSION`, `package.json`, `Cargo.toml`, `pyproject.toml`, `Chart.yaml`, `gradle.properties`, `pom.xml`, or any file by pattern. With `npm` and `cargo`, it also updates dependencies on the other components released with it, and needs `version.semver`. Runs once for all components.
717
-
718
- | Option | Type | Default | |
719
- |---|---|---|---|
720
- | `format` | `plain \| npm \| cargo \| python \| helm \| gradle \| maven \| pattern` | `plain` | `plain` (the whole file), `npm`, `cargo`, `python`, `helm`, `gradle`, `maven`, or `pattern`. |
721
- | `path` | `string \| string[]` | | Relative to each component's directory; globs, or a list, write several, e.g. `packages/*/package.json` for packages sharing a version. Defaults to the format's file. |
722
- | `pattern` | `string` | | With the `pattern` format, where the version is, e.g. `__version__ = "{version}"`. |
723
-
724
- #### `git/commit` · `stage`
725
-
726
- Commits the files earlier release steps changed, for every component, in one commit. Runs once for all components.
727
-
728
- | Option | Type | Default | |
729
- |---|---|---|---|
730
- | `files` | `string[]` | `[]` | More files to commit, relative to the working directory. |
731
- | `message` | `string` | `chore(release): {tags}` | The commit message; `{tags}` is filled in with every release's tag, and with one component, its placeholders too, e.g. `{version}`. |
732
- | `push` | `string \| false` | `false` | The remote to push the branch to; `false` leaves it to a later step, e.g. `git/create-tag`, so the commit and tag land together. |
733
-
734
- #### `github/open-release-pr` · `stage`
735
-
736
- Commits the prepared release to a branch and opens a pull request for it, or updates the open one; the release waits until it merges. Use with the `github/release-pr` history. List it after the `write` steps, in place of `git/commit`. Runs once for all components.
737
-
738
- | Option | Type | Default | |
739
- |---|---|---|---|
740
- | `branch` | `string` | `rylease/{branch}` | The release branch, force-pushed; `{branch}` is the branch being released. |
741
- | `title` | `string` | `chore(release): {tags}` | The commit message and title; `{tags}` lists every release's tag. |
742
- | `labels` | `string[]` | `[rylease: pending]` | Labels for the pull request; `github/release-pr` looks for the first. |
743
- | `remote` | `string` | `origin` | Where to push the branch. |
744
- | `repo` | `string` | | `owner/repo`; defaults to `GITHUB_REPOSITORY` or the `origin` remote. |
745
- | `token` | `string` | | Defaults to `GITHUB_TOKEN` or `GH_TOKEN`; needs `pull-requests: write`. |
746
-
747
- #### `github/read-pull-requests` · `read-changes`
748
-
749
- Collects the pull requests merged into the branch by the commits touching the component since the previous release, with their labels. Needs a token that can read pull requests. Provides `changes.title`, `changes.labels`, `changes.link`.
750
-
751
- | Option | Type | Default | |
752
- |---|---|---|---|
753
- | `repo` | `string` | | `owner/repo`; defaults to `GITHUB_REPOSITORY` or the `origin` remote. |
754
- | `token` | `string` | | Defaults to `GITHUB_TOKEN` or `GH_TOKEN`. |
755
-
756
- #### `github/upload-artifact` · `release`
757
-
758
- Uploads files as a workflow artifact. Only works in the GitHub Action.
759
-
760
- | Option | Type | Default | |
761
- |---|---|---|---|
762
- | `name` | `string` | required | The artifact's name, unique in the run; `{version}`, `{tag}` and the other placeholders are filled in. |
763
- | `files` | `string[]` | required | Paths, globs or directories, relative to the component's directory unless absolute; each must match. |
764
- | `dryRunArtifact` | `boolean` | `false` | Upload in dry runs too. |
765
- | `retentionDays` | `integer` | | Defaults to the repository's setting. |
766
-
767
- #### `github/upload-release-assets` · `release`
768
-
769
- Uploads files to the GitHub release, creating it (with the notes) if no other job has yet.
770
-
771
- | Option | Type | Default | |
772
- |---|---|---|---|
773
- | `files` | `string[]` | required | Paths or globs, relative to the component's directory unless absolute; each must match. |
774
- | `clobber` | `boolean` | `true` | Replace assets of the same name. |
775
- | `dryRunArtifact` | `boolean` | `false` | In dry runs, keep the files as a workflow artifact instead; only in the GitHub Action. |
776
- | `artifactName` | `string` | | Unique in the run; `{version}`, `{tag}` and the other placeholders are filled in. Defaults to the asset's name, or for several, a name made from theirs. |
777
- | `title` | `string` | `{tag}` | `{tag}`, `{version}`, `{name}` and the scheme's placeholders are filled in. |
778
- | `draft` | `boolean` | `false` | |
779
- | `prerelease` | `boolean \| auto` | `auto` | `auto` marks releases on a prerelease channel. |
780
- | `repo` | `string` | | `owner/repo`; defaults to `GITHUB_REPOSITORY` or the `origin` remote. |
781
- | `token` | `string` | | Defaults to `GITHUB_TOKEN` or `GH_TOKEN`. |
782
-
783
- #### `gitlab/create-release` · `release`
784
-
785
- Creates a GitLab release of the tag with the notes, unless it already has one; the tag is created at HEAD if it doesn't exist.
786
-
787
- | Option | Type | Default | |
788
- |---|---|---|---|
789
- | `title` | `string` | `{tag}` | `{tag}`, `{version}`, `{name}` and the scheme's placeholders are filled in. |
790
- | `project` | `string` | | The project's ID or path, e.g. `group/project`; defaults to `CI_PROJECT_ID`. |
791
- | `token` | `string` | | Defaults to `GITLAB_TOKEN`, then the job's `CI_JOB_TOKEN`. |
792
-
793
- #### `keep-a-changelog/read-unreleased` · `read-changes`
794
-
795
- Collects each entry under `## [Unreleased]` in a Keep a Changelog file, labeled with its `### Added`, `### Fixed` … heading. Provides `changes.title`, `changes.labels`.
796
-
797
- | Option | Type | Default | |
798
- |---|---|---|---|
799
- | `path` | `string` | `CHANGELOG.md` | Relative to the component's directory. |
800
-
801
- #### `keep-a-changelog/write-release` · `write`
802
-
803
- Moves the entries under `## [Unreleased]` to a heading for the version, and updates the compare links, if there are any.
804
-
805
- | Option | Type | Default | |
806
- |---|---|---|---|
807
- | `path` | `string` | `CHANGELOG.md` | Relative to the component's directory. |
808
- | `heading` | `string` | `## [{version}] - {date}` | `{version}`, `{tag}` and `{name}` (the component's) are filled in, and the scheme's own, e.g. `{major}`, `{minor}` and `{patch}`, and `{date}` (YYYY-MM-DD). |
809
- | `channels` | `boolean` | `false` | Also run for releases on a channel, e.g. prereleases; by default only stable releases do. |
810
-
811
- #### `labels/determine-notes` · `determine-notes`
812
-
813
- A `### Title` section per group of labels, e.g. `### Bug Fixes` for `bug`, then `### Other` for everything else. Needs `changes.labels`.
814
-
815
- | Option | Type | Default | |
816
- |---|---|---|---|
817
- | `sections` | `{ title: string, labels: string[] }[]` | `[{ title: Breaking Changes, labels: [breaking, semver:major] }, { title: Features, labels: [feature, enhancement] }, { title: Bug Fixes, labels: [bug, fix] }]` | Sections in order; a change goes in the first with one of its labels, ignoring case. |
818
- | `other` | `string \| false` | `Other` | The section for remaining changes; `false` leaves them out. |
819
- | `exclude` | `string[]` | `[skip-changelog]` | Changes with any of these labels are left out. |
820
- | `descriptions` | `boolean` | `false` | Write each change's description, indented, under it. |
821
- | `links` | `boolean` | `true` | Link each change to its source, e.g. its pull request. |
822
- | `dependencies` | `string \| false` | `Dependencies` | The last section, for the components it depends on that were released along with it; `false` leaves it out. |
823
-
824
- #### `labels/determine-kind` · `determine-kind`
825
-
826
- By each change's labels, e.g. a pull request's (`breaking` → breaking, `enhancement` → feature, anything else → fix). The highest kind wins. Needs `changes.labels`.
827
-
828
- | Option | Type | Default | |
829
- |---|---|---|---|
830
- | `kinds` | `{ [key]: breaking \| feature \| fix \| none }` | `{ breaking: breaking, semver:major: breaking, feature: feature, enhancement: feature, semver:minor: feature }` | Kinds by label, ignoring case. |
831
- | `fallback` | `breaking \| feature \| fix \| none` | `fix` | The kind of a change without a label in `kinds`. |
832
-
833
- #### `npm/publish` · `release`
834
-
835
- Publishes the component's package to the npm registry with `npm` or `pnpm publish`; components are published dependencies first. Needs `version.semver`.
836
-
837
- | Option | Type | Default | |
838
- |---|---|---|---|
839
- | `path` | `string` | `.` | The package's directory, relative to the component's. |
840
- | `client` | `npm \| pnpm` | | Defaults to the `packageManager` in package.json, then `pnpm` if there's a `pnpm-lock.yaml`, then `npm`. |
841
- | `tag` | `string` | | The dist-tag; defaults to the channel's name on a channel, then `publishConfig.tag`, or `latest`. |
842
- | `access` | `public \| restricted` | | Defaults to `publishConfig.access`. |
843
- | `provenance` | `boolean` | `false` | Publishes with a provenance statement; needs `id-token: write`. |
844
- | `registry` | `string` | | Defaults to `publishConfig.registry`, or the npm registry. |
845
- | `token` | `string` | | Defaults to `NPM_TOKEN` or `NODE_AUTH_TOKEN`; without one, the client's own auth is used, e.g. trusted publishing. |
846
-
847
- ### Bundled schemes
848
-
849
- Set one as the release's `scheme`; see [Schemes](#schemes).
850
-
851
- #### `calver` · `scheme`
852
-
853
- Dates the version, as on calver.org, e.g. `YYYY.MM.MICRO` → `2026.10.0`; a counter numbers releases in the same period, bumped by the kind.
854
-
855
- | Option | Type | Default | |
856
- |---|---|---|---|
857
- | `format` | `string` | `YYYY.MM.MICRO` | Tokens joined by `.`: `YYYY`, `YY`, `0Y`, `MM`, `0M`, `WW`, `0W`, `DD`, `0D` (ISO 8601 weeks, with the ISO year), and the counters `MAJOR`, `MINOR`, `MICRO`. Zero-padded tokens (`0M` …) aren't valid semver, which npm needs. |
858
- | `timezone` | `string` | `UTC` | Where the release date is taken, e.g. `America/New_York`. |
859
-
860
- #### `semver` · `scheme`
861
-
862
- `MAJOR.minor.patch` by the kind: breaking, feature or fix; `0.MAJOR.minorOrPatch` before 1.0.0.
863
-
864
- | Option | Type | Default | |
865
- |---|---|---|---|
866
- | `shiftPreMajor` | `boolean` | `true` | Before 1.0.0, bump breaking changes as minor and features as patch. |
867
- | `initial` | `string` | `0.1.0` | The first version, when there's no previous one. |
868
-
869
- ### Bundled sources
870
-
871
- Where previous releases are found, set as `history`, and what's released, set as `components`.
872
-
873
- #### `fs/version-file` · `history`
874
-
875
- The version in the project's version file, in any format `fs/write-version` writes, released at the commit that set it.
876
-
877
- | Option | Type | Default | |
878
- |---|---|---|---|
879
- | `format` | `plain \| npm \| cargo \| python \| helm \| gradle \| maven \| pattern` | `plain` | As for `fs/write-version`. |
880
- | `path` | `string` | | Relative to the component's directory; defaults to the format's file. |
881
- | `pattern` | `string` | | With the `pattern` format, where the version is. |
882
-
883
- #### `git/tags` · `history`
884
-
885
- The nearest release tag on HEAD: the highest version among the tags no other release tag comes after. On a channel, its own releases count too.
886
-
887
- No options.
888
-
889
- #### `github/release-pr` · `history`
890
-
891
- A release proposed by a release pull request from `github/open-release-pr`, once it's merged; otherwise the nearest release tag on HEAD, as `git/tags`.
892
-
893
- | Option | Type | Default | |
894
- |---|---|---|---|
895
- | `label` | `string` | `rylease: pending` | The label of release pull requests not yet released. |
896
- | `repo` | `string` | | `owner/repo`; defaults to `GITHUB_REPOSITORY` or the `origin` remote. |
897
- | `token` | `string` | | Defaults to `GITHUB_TOKEN` or `GH_TOKEN`. |
898
-
899
- ### Bundled components
900
-
901
- Where components are found, set as `components`.
902
-
903
- #### `npm/workspaces` · `components`
904
-
905
- Each package of the npm, Yarn or pnpm workspace, named after it and depending on the workspace packages it depends on.
906
-
907
- | Option | Type | Default | |
908
- |---|---|---|---|
909
- | `private` | `boolean` | `false` | Include packages marked `private`. |
910
- | `exclude` | `string[]` | `[]` | Packages to leave out, by name. |
911
- <!-- options:end -->
88
+ | `conventional` | `conventional/read-commits`, `conventional/determine-kind`, `conventional/determine-notes`, `git/create-tag`, `github/create-release` | `semver` | `git/tags` |
89
+ | `conventional-pr` | `conventional`'s, with `fs/write-changes` and `github/open-release-pr` | `semver` | `github/release-pr` |
90
+ | `keep-a-changelog` | `keep-a-changelog/read-unreleased`, `labels/determine-kind`, `labels/determine-notes`, `keep-a-changelog/write-release`, `git/commit`, `git/create-tag`, `github/create-release` | set one | `git/tags` |
91
+ | `change-files` | `change-files/read`, `labels/determine-kind`, `labels/determine-notes`, `change-files/clean`, `fs/write-changes`, `git/commit`, `git/create-tag`, `github/create-release` | set one | `git/tags` |
92
+
93
+ - **`conventional`** reads every [conventional commit](https://www.conventionalcommits.org) since the previous tag. `!` or `BREAKING` is breaking, `feat` a feature, and anything else a fix. The notes have `Breaking Changes`, `Features`, `Bug Fixes`, `Performance Improvements`, `Reverts` and `Other` sections. It tags HEAD and creates a GitHub release, and writes no files: it works from git tags alone, in any project.
94
+ - **`conventional-pr`** does the same, but instead of releasing on every push it writes `CHANGELOG.md` and proposes the release in a pull request, kept up to date as changes land; merging it tags and releases. See [Release pull requests](docs/release-pull-requests.md).
95
+ - **`keep-a-changelog`** releases the `## [Unreleased]` section of a hand-written [Keep a Changelog](https://keepachangelog.com). `Removed` entries are breaking, `Added`, `Changed` and `Deprecated` ones features, and the rest fixes. It moves the section under the new version, commits it, then tags and releases.
96
+ - **`change-files`** releases what the files in `.changes/` say, each naming the components it changes and how much. It notes each change under its kind, deletes the files in the release commit, writes `CHANGELOG.md`, then tags and releases. See [Other sources](docs/sources.md#change-files).
97
+
98
+ Sections don't imply a version scheme, so with `keep-a-changelog` and `change-files`, set `scheme`, e.g. `scheme: semver`. To keep the version in a project's own files or publish a package, add the steps for it alongside the preset; see [Ecosystems](docs/ecosystems.md).
99
+
100
+ ## Docs
101
+
102
+ - [Configuration](docs/configuration.md): action inputs and outputs, config files, conditions.
103
+ - [Ecosystems](docs/ecosystems.md): version files and publishing for npm, Dart and Flutter, Cargo, Python, Ruby, .NET and more.
104
+ - [Other sources](docs/sources.md): pull requests, labels, Keep a Changelog, change files, GitLab.
105
+ - [Schemes](docs/schemes.md): semver, calendar versions, your own.
106
+ - [Channels](docs/channels.md): prereleases and builds.
107
+ - [Monorepos](docs/monorepos.md): components, released together or apart.
108
+ - [Release pull requests](docs/release-pull-requests.md)
109
+ - [Uploading builds](docs/uploading-builds.md): to the release, from later jobs.
110
+ - [CLI, GitLab and other CI](docs/cli.md), and testing workflows locally.
111
+ - [TypeScript](docs/typescript.md): releases as scripts, custom steps.
112
+ - [Migrating from v2](docs/migrating-from-v2.md)
113
+ - [Reference](docs/reference/README.md): every bundled step, scheme and source, with its options.