@ebalzan/ci 1.0.1 → 2.0.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 ADDED
@@ -0,0 +1,281 @@
1
+ # @ebalzan/ci
2
+
3
+ One set of CI/CD rules for all your GitHub repos:
4
+
5
+ - **Composite GitHub Actions**: they open work and release PRs with AI descriptions, check every PR, and cut semver releases from Conventional Commits.
6
+ - **A CLI** (`ebalzan-ci`): it onboards a repo and keeps its Actions variables, secrets, settings and branch rulesets in sync.
7
+ - **A commitlint `commit-msg` hook** and a **Renovate preset** for the shared tooling.
8
+
9
+ Repos call the actions pinned to a major tag (`ebalzan/ci/actions/<name>@v2`), so fixes reach every repo with each release, with nothing to copy around.
10
+
11
+ ## The git flow
12
+
13
+ ```mermaid
14
+ flowchart LR
15
+ F["feat/… fix/… branch"] -- "work PR<br/>squash merge" --> D[dev]
16
+ D -- "release PR<br/>ship label = fast-forward" --> M[main]
17
+ M -- "tag vX.Y.Z<br/>GitHub Release" --> R((deploy))
18
+ ```
19
+
20
+ There are three kinds of branch, and history stays **linear**: no merge commits anywhere.
21
+
22
+ | Branch | Lives | Gets changes by | Rewritten? |
23
+ | ---------------------- | ----------- | ------------------------------------------------ | ------------------------------------------ |
24
+ | `<type>/<description>` | hours, days | your commits | yes, by you (rebase, `--force-with-lease`) |
25
+ | `dev` | forever | squash-merged work PRs | never |
26
+ | `main` | forever | the Release workflow fast-forwarding it to `dev` | never |
27
+
28
+ The two hops merge differently:
29
+
30
+ - **work branch → `dev`: squash merge.** All of a branch's commits become one commit on `dev`, titled after the PR.
31
+ - **`dev` → `main`: fast-forward.** `main` moves forward to `dev`'s commits as they are. It gets no new commits and no merge commit.
32
+
33
+ What happens to the commits:
34
+
35
+ ```text
36
+ 1. Work on a branch: as many commits as you like.
37
+
38
+ feat/search-filters a ── b ── c
39
+ /
40
+ dev, main ── X ────┘
41
+
42
+ 2. Squash-merge the work PR: a, b and c become ONE new commit S on dev.
43
+ The branch is deleted; main has not moved.
44
+
45
+ dev ── X ── S S = feat: add search filters (#12)
46
+ main ── X
47
+
48
+ 3. More work PRs land on dev the same way, one commit each.
49
+
50
+ dev ── X ── S ── T T = fix: empty results (#13)
51
+ main ── X
52
+
53
+ 4. Add the `ship` label to the release PR: the Release workflow adds the
54
+ release commit R, then moves main AND dev forward to it. Nothing is merged
55
+ or rewritten: main gets dev's exact commits, with the same SHAs.
56
+
57
+ dev, main ── X ── S ── T ── R R = chore(release): 1.4.0, tagged v1.4.0
58
+ ```
59
+
60
+ After step 4, `dev` and `main` point at the same commit, so the next work branch starts from the release.
61
+
62
+ ### 1. Work on a branch
63
+
64
+ Branch off `dev`. The branch name is `<type>/<description>`, where `<type>` is one of the [work types](#work-types):
65
+
66
+ ```sh
67
+ git switch dev && git pull
68
+ git switch -c feat/search-filters
69
+ git commit -m "feat: add search filters" # Conventional Commits, checked by the hook
70
+ git push -u origin feat/search-filters
71
+ ```
72
+
73
+ **Auto PR** opens a PR into `dev` on that push. It sets:
74
+
75
+ - the title: the first commit whose type is the branch's type;
76
+ - the type label, with you as the assignee;
77
+ - the milestone and the project board, when the repo configures them;
78
+ - an AI-written description.
79
+
80
+ It opens the PR as a draft and marks it ready for review once all of that is done.
81
+
82
+ While you work, keep the branch on top of `dev` by **rebasing, never merging**. The branch is yours, so rewriting it is fine:
83
+
84
+ ```sh
85
+ git fetch && git rebase origin/dev
86
+ git push --force-with-lease
87
+ ```
88
+
89
+ Optionally, tidy the commits before review: commit fixes with `git commit --fixup <sha>`, then fold them in with `git rebase -i --autosquash origin/dev`.
90
+
91
+ ### 2. Merge the work PR into dev (squash)
92
+
93
+ **PR Checks** (`pr-check / Work PR`) must pass:
94
+
95
+ - the PR title and every commit follow Conventional Commits;
96
+ - the base is `dev`;
97
+ - the branch is `<type>/…`, and the title's type matches the branch's type;
98
+ - it has the type label and an assignee, and the author is the owner, a member or a collaborator;
99
+ - it has the milestone and is on the board, when those are set;
100
+ - it has a description.
101
+
102
+ It warns about large PRs, breaking changes and a branch that is behind `dev`.
103
+
104
+ Then **squash-merge** it. The PR title becomes the one commit on `dev`, and that commit is what drives the next version.
105
+
106
+ ### 3. The release PR (dev → main)
107
+
108
+ Every push to `dev` makes **Auto PR** open or update **the** release PR, `dev` → `main`:
109
+
110
+ - **Title:** the release it will cut, e.g. `feat: release v1.4.0`. git-cliff computes it from the commits since the last tag.
111
+ - **Description:** an AI summary above the changelog.
112
+ - **Label:** `release`.
113
+
114
+ Its check (`pr-check / Release PR`) holds it to the computed title. It also requires the label, an assignee, the milestone and board when they're set, and a description. Finally, `dev` must contain `main` and must not contain a merge commit.
115
+
116
+ ### 4. Ship it with the `ship` label, never the merge button
117
+
118
+ When the release PR's check passes, **add the `ship` label**. The **Release** workflow then does this:
119
+
120
+ 1. **Verify.** `dev` is still the PR's head, `main` fast-forwards to it, there's no merge commit, and the PR check passed. If the check is still running, it waits.
121
+ 2. **Version and tag.** It recomputes the title and requires it to equal the PR's. In a Node repo it commits `chore(release): X.Y.Z` to bump `package.json`. Then it pushes **`main` and `dev` to that same commit**, along with `vX.Y.Z` and the moving major tag `vX`, in **one atomic push**.
122
+ 3. **Publish.** It creates the GitHub Release `vX.Y.Z`, using the PR's description as the notes.
123
+ 4. **Deploy.** It calls the repo's own `.github/workflows/deploy.yml` with the new tag.
124
+
125
+ GitHub then marks the release PR as merged. `main` got exactly `dev`'s commits, with the same SHAs. `dev` and `main` end on the same commit, so the next release starts clean and no open work branch is disturbed.
126
+
127
+ If something moved in the meantime, nothing is pushed: remove the label and add it again. If someone merges the release PR with a GitHub button anyway, the job fails and prints the command that puts `main` back. Squash and rebase both rewrite `dev`'s commits, which would make `main` and `dev` diverge.
128
+
129
+ > There's no hotfix path straight to `main`. A fix goes `fix/…` → `dev` → release PR like any other change. `main` only ever moves through a release.
130
+
131
+ ## Work types
132
+
133
+ [`config/types.json`](config/types.json) is the only place a type is defined. Each type works as a branch prefix, a Conventional Commit and PR-title type, and a PR label.
134
+
135
+ | Type | Release |
136
+ | ---------------------------------------------------------------------------------- | ------- |
137
+ | `feat` | minor |
138
+ | `fix`, `refact`, `perf`, `revert`, `build`, `ci`, `docs`, `test`, `style`, `chore` | patch |
139
+
140
+ A `!` after the type (`feat!: …`), or a `BREAKING CHANGE:` footer, makes any type a **major** release, from 0.x too. A repo with no tag yet starts at `v0.1.0`. Note that the type is `refact`, not `refactor`.
141
+
142
+ ## Branch protection
143
+
144
+ `ebalzan-ci sync` applies these rulesets ([`config/rulesets/`](config/rulesets)) and repository settings ([`config/repo-settings.json`](config/repo-settings.json)):
145
+
146
+ | Ruleset | Default | Effect |
147
+ | ---------------------------------- | -------- | ------------------------------------------------------------------------------------------ |
148
+ | `ci: protect main` | active | no deletion, no force-push, linear history |
149
+ | `ci: protect dev` | active | no deletion, no force-push, linear history |
150
+ | `ci: require pull request` | disabled | `dev` changes only via a squash-merged work PR that passed `pr-check / Work PR` |
151
+ | `ci: require release pull request` | disabled | nobody but GitHub Actions can update `main`, so the release PR's merge buttons are blocked |
152
+
153
+ **Settings:** squash merge only, with the PR title as the commit title. Merge commits and rebase merges are off. Head branches are deleted on merge.
154
+
155
+ The two disabled rulesets need the GitHub Actions app to be able to bypass them, because the release pushes with `GITHUB_TOKEN`. Enable one per repo with `.github/rulesets/<file>.json` containing `{ "enforcement": "active" }`, run one release, and confirm it went through.
156
+
157
+ ## Getting started
158
+
159
+ Requirements:
160
+
161
+ - Node >= 22.12;
162
+ - [`gh`](https://cli.github.com), authenticated with admin rights on the repo.
163
+
164
+ ### 1. Shared secrets (once)
165
+
166
+ ```sh
167
+ cp config/secrets.env.example config/secrets.env # in a checkout of this repo, or ~/.config/ebalzan-ci/
168
+ ```
169
+
170
+ | Secret | Used for |
171
+ | ------------------- | ---------------------------------------------------------------------------------------------------------------------------- |
172
+ | `ANTHROPIC_API_KEY` | the AI descriptions of work and release PRs. Without it, PRs keep the commit list or the changelog. |
173
+ | `GH_PROJECT_TOKEN` | a **classic** PAT (`repo` + `project`), so the PRs Auto PR opens trigger PR Checks and can reach a user-owned Projects board |
174
+
175
+ ### 2. Onboard a repo
176
+
177
+ In the repo's checkout:
178
+
179
+ ```sh
180
+ npx @ebalzan/ci onboard
181
+ ```
182
+
183
+ `onboard` does the following:
184
+
185
+ 1. Creates the branch `ci/onboard-ebalzan-ci` from `dev`.
186
+ 2. Writes the workflows (`auto-pr.yml`, `pr-checks.yml`, `release.yml`, and a starter `deploy.yml`), plus `.github/actions.env` and `renovate.json`.
187
+ 3. For a Node repo, adds the husky `commit-msg` hook, the `@ebalzan/ci` and `husky` devDependencies, and the shared Prettier and ESLint config.
188
+ 4. Gitignores the secrets files and commits everything.
189
+ 5. Creates `dev` if it's missing, runs `sync`, and pushes.
190
+
191
+ Auto PR then opens the onboarding PR into `dev`. Squash-merge it. Run `npx @ebalzan/ci onboard --help` for the options (`--ref`, `--runs-on`, `--force FILE`, `--no-npm`, `--no-push`, `--dry-run`, …).
192
+
193
+ Then:
194
+
195
+ - add the repo's own build, test and lint jobs to `.github/workflows/pr-checks.yml` (it has a commented example);
196
+ - put what ships a release into `.github/workflows/deploy.yml`. It's the repo's own file, and `onboard` never overwrites it.
197
+
198
+ ### 3. Keep it in sync
199
+
200
+ ```sh
201
+ npx @ebalzan/ci sync --dry-run # see the plan
202
+ npx @ebalzan/ci sync
203
+ ```
204
+
205
+ Each setting comes from two layers, and the repo's own value wins:
206
+
207
+ | What | Shared (every repo) | The repo's own |
208
+ | ------------------- | ----------------------------- | ---------------------------------- |
209
+ | variables | `config/actions.env` | `.github/actions.env` |
210
+ | secrets | `<secrets>/secrets.env` | `.github/secrets.env` (gitignored) |
211
+ | environment secrets | `<secrets>/secrets.<env>.env` | `.github/secrets.<env>.env` |
212
+ | repository settings | `config/repo-settings.json` | `.github/repo-settings.json` |
213
+ | branch rulesets | `config/rulesets/<file>.json` | `.github/rulesets/<file>.json` |
214
+
215
+ How the layers combine:
216
+
217
+ - **JSON files** deep-merge.
218
+ - **Shared variables** are read by the actions at run time, never copied onto repos, so changing one only needs a release of this package.
219
+ - **Removals:** `--prune` deletes what no file defines.
220
+
221
+ ## Configuration
222
+
223
+ Repo variables (`.github/actions.env`) or the shared `config/actions.env`:
224
+
225
+ | Variable | Default | Effect |
226
+ | ---------------------------------------------- | -------------- | ------------------------------------------------------------- |
227
+ | `AUTO_PROJECT_NUMBER` | none | the Projects (v2) board every PR must be on |
228
+ | `AUTO_PROJECT_OWNER` | the repo owner | that board's owner |
229
+ | `AUTO_MILESTONE_TITLE` | none | the milestone every PR must have |
230
+ | `AUTO_MAX_CHANGED_LINES` | `500` | work PRs over this get a size warning |
231
+ | `AUTO_DESCRIBE_MODEL` / `AUTO_DESCRIBE_EFFORT` | shared | the Claude model, and its effort, that writes PR descriptions |
232
+
233
+ ## What's inside
234
+
235
+ | Path | What |
236
+ | ----------------------------------- | --------------------------------------------------------------------------------- |
237
+ | `actions/auto-pr` | opens or updates the work PR of a branch, or the release PR of `dev` |
238
+ | `actions/pr-checks` | commitlint on the title and commits, then Danger rules for the PR's kind |
239
+ | `actions/release` | verifies, fast-forwards, tags and publishes the release on the `ship` label |
240
+ | `bin/`, `lib/` | the `ebalzan-ci` CLI: `onboard`, `sync`, `commit-msg` |
241
+ | `templates/` | the files `onboard` writes into a repo |
242
+ | `config/` | types, shared variables, repo settings, rulesets, the git-cliff config |
243
+ | `renovate/tooling.json` | Renovate preset for the shared tooling (`github>ebalzan/ci//renovate/tooling#v2`) |
244
+ | `commitlint.config.mjs`, `scripts/` | the commit rules, the release title and the changelog |
245
+
246
+ ## Upgrading from v1
247
+
248
+ In v1, the release PR was merged with a merge commit. In v2 it ships with the `ship` label (a fast-forward). Repos pinned to `@v1` keep working until they move. Per repo, **between two releases**:
249
+
250
+ 1. If `git rev-list --merges origin/main..origin/dev` prints anything (an unreleased `Merge branch 'main' into dev`), rebase it away once, before syncing:
251
+ `git switch dev && git rebase origin/main && git push --force-with-lease`.
252
+ 2. Refresh the workflows. `release.yml` is required; `deploy.yml` is kept:
253
+ ```sh
254
+ npx @ebalzan/ci@2 onboard --ref v2 \
255
+ --force .github/workflows/release.yml --force .github/workflows/pr-checks.yml \
256
+ --force .github/workflows/auto-pr.yml --force renovate.json
257
+ ```
258
+ 3. Apply the new settings and rulesets with `npx @ebalzan/ci@2 sync`. Drop any `allow_merge_commit: true` override in `.github/repo-settings.json`.
259
+ 4. Squash-merge the onboarding PR into `dev`, then release by adding the `ship` label to the release PR.
260
+
261
+ Do steps 2–4 together. The old `release.yml` expects a merge commit, and step 3 turns merge commits off.
262
+
263
+ ## Developing this repo
264
+
265
+ ```sh
266
+ npm ci
267
+ npm run check # bash -n / node --check on every script
268
+ npm run lint # ESLint, zero warnings
269
+ npm run format:check # Prettier
270
+ npm test # node:test, on throwaway git repos with a stubbed gh
271
+ ```
272
+
273
+ The same flow applies here: work branches squash into `dev`. This repo has no workflows of its own, so release it by hand, with a fast-forward rather than a merge commit:
274
+
275
+ ```sh
276
+ git switch main && git merge --ff-only dev && git push
277
+ ```
278
+
279
+ ## License
280
+
281
+ [MIT](LICENSE)
@@ -1,12 +1,10 @@
1
1
  {
2
- "$comment": "Repository settings every managed repo gets (PATCH /repos/{owner}/{repo}). A repo overrides any key in its own .github/repo-settings.json. Keys starting with $ are documentation and are never sent. Work PRs are squash-merged into dev (the PR title becomes the commit); the dev → main release PR is merged with a merge commit (GitHub's default title, which commitlint and git-cliff skip). Rebase merges are off: they rewrite commits.",
2
+ "$comment": "Repository settings every managed repo gets (PATCH /repos/{owner}/{repo}). A repo overrides any key in its own .github/repo-settings.json. Keys starting with $ are documentation and are never sent. History is linear, with no merge commits: work PRs are squash-merged into dev (the PR title becomes the commit); the dev → main release PR is never merged with a button — its `ship` label makes the Release workflow fast-forward main to dev. Merge commits and rebase merges are off: rebase rewrites dev's commits, so main and dev would diverge.",
3
3
  "allow_squash_merge": true,
4
- "allow_merge_commit": true,
4
+ "allow_merge_commit": false,
5
5
  "allow_rebase_merge": false,
6
6
  "squash_merge_commit_title": "PR_TITLE",
7
7
  "squash_merge_commit_message": "COMMIT_MESSAGES",
8
- "merge_commit_title": "MERGE_MESSAGE",
9
- "merge_commit_message": "PR_TITLE",
10
8
  "delete_branch_on_merge": true,
11
9
  "allow_update_branch": true
12
10
  }
@@ -1,10 +1,14 @@
1
1
  {
2
- "$comment": "dev is the integration branch: it cannot be deleted or force-pushed. The Release workflow only fast-forwards it (or merges main into it), so nothing in the pipeline needs a bypass.",
2
+ "$comment": "dev is the integration branch: it cannot be deleted or force-pushed, and its history stays linear (main fast-forwards to it). Work PRs squash into it; the Release workflow only fast-forwards it to the release commit, so nothing in the pipeline needs a bypass.",
3
3
  "name": "ci: protect dev",
4
4
  "target": "branch",
5
5
  "enforcement": "active",
6
6
  "conditions": {
7
7
  "ref_name": { "include": ["refs/heads/dev"], "exclude": [] }
8
8
  },
9
- "rules": [{ "type": "deletion" }, { "type": "non_fast_forward" }]
9
+ "rules": [
10
+ { "type": "deletion" },
11
+ { "type": "non_fast_forward" },
12
+ { "type": "required_linear_history" }
13
+ ]
10
14
  }
@@ -1,10 +1,14 @@
1
1
  {
2
- "$comment": "main is the release branch: it cannot be deleted or force-pushed. The Release workflow only fast-forwards it with the `chore(release): X.Y.Z` commit, so nothing in the pipeline needs a bypass.",
2
+ "$comment": "main is the release branch: it cannot be deleted or force-pushed, and its history stays linear. The Release workflow only fast-forwards it to dev (plus the `chore(release): X.Y.Z` commit), so nothing in the pipeline needs a bypass.",
3
3
  "name": "ci: protect main",
4
4
  "target": "branch",
5
5
  "enforcement": "active",
6
6
  "conditions": {
7
7
  "ref_name": { "include": ["refs/heads/main"], "exclude": [] }
8
8
  },
9
- "rules": [{ "type": "deletion" }, { "type": "non_fast_forward" }]
9
+ "rules": [
10
+ { "type": "deletion" },
11
+ { "type": "non_fast_forward" },
12
+ { "type": "required_linear_history" }
13
+ ]
10
14
  }
@@ -1,5 +1,5 @@
1
1
  {
2
- "$comment": "Changes reach dev only through a squash-merged work PR whose 'pr-check / Work PR' check passed — its title becomes the one commit on dev. main is require-release-pr.json's. DISABLED by default: the Release workflow brings dev up to main with GITHUB_TOKEN (a fast-forward, or a merge of main when work landed meanwhile), so the GitHub Actions app (Integration 15368) must be able to bypass it. Enable it per repo with .github/rulesets/require-pull-request.json containing {\"enforcement\": \"active\"}, run one release, and confirm it went through.",
2
+ "$comment": "Changes reach dev only through a squash-merged work PR whose 'pr-check / Work PR' check passed — its title becomes the one commit on dev. main is require-release-pr.json's. DISABLED by default: the Release workflow fast-forwards dev to the release commit with GITHUB_TOKEN, so the GitHub Actions app (Integration 15368) must be able to bypass it. Enable it per repo with .github/rulesets/require-pull-request.json containing {\"enforcement\": \"active\"}, run one release, and confirm it went through.",
3
3
  "name": "ci: require pull request",
4
4
  "target": "branch",
5
5
  "enforcement": "disabled",
@@ -1,33 +1,11 @@
1
1
  {
2
- "$comment": "Changes reach main only through the dev → main release PR, merged with a merge commit after 'pr-check / Release PR' passed: dev's commits reach main unchanged, so dev catches up without a force-push. The Release workflow fails on a squash or rebase merge; this ruleset hides those buttons. DISABLED by default: the Release workflow pushes the `chore(release): X.Y.Z` commit to main with GITHUB_TOKEN, so the GitHub Actions app (Integration 15368) must be able to bypass it. Enable it per repo with .github/rulesets/require-release-pr.json containing {\"enforcement\": \"active\"}, run one release, and confirm it went through.",
2
+ "$comment": "main moves only through the Release workflow: the release PR's `ship` label makes it fast-forward main to dev after 'pr-check / Release PR' passed (the workflow checks it). This ruleset restricts updates to its bypass list — GitHub Actions only — so nobody can merge into or push to main, and the release PR's merge buttons stay blocked (squash and rebase would rewrite dev's commits). In an emergency, an admin disables it. DISABLED by default: the Release workflow pushes main with GITHUB_TOKEN, so the GitHub Actions app (Integration 15368) must be able to bypass it. Enable it per repo with .github/rulesets/require-release-pr.json containing {\"enforcement\": \"active\"}, run one release, and confirm it went through.",
3
3
  "name": "ci: require release pull request",
4
4
  "target": "branch",
5
5
  "enforcement": "disabled",
6
6
  "conditions": {
7
7
  "ref_name": { "include": ["refs/heads/main"], "exclude": [] }
8
8
  },
9
- "bypass_actors": [
10
- { "actor_id": 5, "actor_type": "RepositoryRole", "bypass_mode": "always" },
11
- { "actor_id": 15368, "actor_type": "Integration", "bypass_mode": "always" }
12
- ],
13
- "rules": [
14
- {
15
- "type": "pull_request",
16
- "parameters": {
17
- "required_approving_review_count": 0,
18
- "dismiss_stale_reviews_on_push": false,
19
- "require_code_owner_review": false,
20
- "require_last_push_approval": false,
21
- "required_review_thread_resolution": false,
22
- "allowed_merge_methods": ["merge"]
23
- }
24
- },
25
- {
26
- "type": "required_status_checks",
27
- "parameters": {
28
- "strict_required_status_checks_policy": false,
29
- "required_status_checks": [{ "context": "pr-check / Release PR" }]
30
- }
31
- }
32
- ]
9
+ "bypass_actors": [{ "actor_id": 15368, "actor_type": "Integration", "bypass_mode": "always" }],
10
+ "rules": [{ "type": "update", "parameters": { "update_allows_fetch_and_merge": false } }]
33
11
  }
package/lib/onboard.mjs CHANGED
@@ -508,6 +508,8 @@ async function run(opts) {
508
508
  info(' commented example).')
509
509
  if (tsconfigNote) info(` · ${tsconfigNote}`)
510
510
  info(' 3. Squash-merge the PR: the dev → main release PR opens.')
511
+ info(' 4. Once its check passes, add the `ship` label to release: main fast-forwards')
512
+ info(' to dev. Never merge the release PR with a button.')
511
513
  return 0
512
514
  }
513
515
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ebalzan/ci",
3
- "version": "1.0.1",
3
+ "version": "2.0.0",
4
4
  "description": "One set of CI/CD rules for all your GitHub repos: reusable workflows (auto work and release PRs with AI descriptions, PR checks, semver releases), a CLI that syncs Actions config, repo settings and branch rulesets, and a commitlint commit-msg hook.",
5
5
  "keywords": [
6
6
  "github-actions",
@@ -11,7 +11,8 @@
11
11
  #
12
12
  # The `production` environment: required reviewers and environment secrets
13
13
  # work. Do not limit it to the main branch: a release's run has the ref
14
- # refs/pull/<n>/merge (it starts from the merged PR), so it would be refused.
14
+ # refs/pull/<n>/merge (it starts from the release PR's event), so it would be
15
+ # refused.
15
16
  # Generated by `ebalzan-ci onboard`, which never overwrites it (not even with
16
17
  # --force): changes here are yours to keep.
17
18
  # =============================================================================
@@ -1,36 +1,42 @@
1
1
  # =============================================================================
2
2
  # Release — runs __CI_REPO__'s release action.
3
3
  #
4
- # Runs when a PR into main is closed; the `release` job runs only when it is
5
- # the dev → main release PR and it was merged (with a merge commit). Merging a
6
- # work PR into dev never runs it. The release: the version from the
7
- # Conventional Commits since the last tag, package.json bumped when there is
8
- # one, the vX.Y.Z tag, the GitHub Release (the PR's description as notes), and
9
- # dev brought up to main.
4
+ # The dev → main release PR is never merged with a GitHub button. Once its
5
+ # check passes, add the `ship` label: the `release` job fast-forwards main to
6
+ # dev — the same commits and SHAs, no merge commit — and GitHub marks the PR
7
+ # merged. The release: the version from the Conventional Commits since the
8
+ # last tag, package.json bumped when there is one, the vX.Y.Z tag, the GitHub
9
+ # Release (the PR's description as notes), and main and dev on the release
10
+ # commit. Merging a work PR into dev never runs it.
10
11
  #
11
- # Then `deploy` hands the new tag to deploy.yml — this repo's own file, where
12
- # what ships the release lives. Generated by `ebalzan-ci onboard`; the release
13
- # logic lives in the action (it follows __CI_REF__). Keep deploying out of this
14
- # file.
12
+ # When the release PR is merged with a button anyway (squash or rebase rewrite
13
+ # dev's commits), the same job fails on the `closed` event with how to put main
14
+ # back. Then `deploy` hands the new tag to deploy.yml — this repo's own file,
15
+ # where what ships the release lives. Generated by `ebalzan-ci onboard`; the
16
+ # release logic lives in the action (it follows __CI_REF__). Keep deploying out
17
+ # of this file.
15
18
  # =============================================================================
16
19
  name: Release
17
20
 
18
21
  on:
19
22
  pull_request:
20
- types: [closed]
23
+ types: [labeled, closed]
21
24
  branches: ['main']
22
25
 
23
26
  permissions:
24
27
  contents: write
25
28
  pull-requests: read
29
+ checks: read
26
30
 
27
31
  jobs:
28
32
  release:
29
33
  name: Version, tag and publish
30
34
  if: >-
31
- ${{ github.event.pull_request.merged
32
- && github.event.pull_request.head.ref == 'dev'
33
- && github.event.pull_request.head.repo.full_name == github.repository }}
35
+ ${{ github.event.pull_request.head.ref == 'dev'
36
+ && github.event.pull_request.head.repo.full_name == github.repository
37
+ && ((github.event.action == 'labeled' && github.event.label.name == 'ship'
38
+ && github.event.pull_request.state == 'open')
39
+ || (github.event.action == 'closed' && github.event.pull_request.merged)) }}
34
40
  runs-on: __RUNS_ON__
35
41
  # Queued, never cancelled: a run cut short could tag without a Release.
36
42
  concurrency:
@@ -43,11 +49,11 @@ jobs:
43
49
  tag: ${{ steps.release.outputs.tag }}
44
50
  sha: ${{ steps.release.outputs.sha }}
45
51
  steps:
46
- # The merge commit, with full history and the tags: git-cliff computes the
52
+ # The PR's head, with full history and the tags: git-cliff computes the
47
53
  # version from them. Credentials stay: the job pushes main and dev.
48
54
  - uses: actions/checkout@v4
49
55
  with:
50
- ref: ${{ github.event.pull_request.merge_commit_sha }}
56
+ ref: ${{ github.event.pull_request.head.sha }}
51
57
  fetch-depth: 0
52
58
 
53
59
  - uses: actions/setup-node@v4
@@ -56,8 +62,6 @@ jobs:
56
62
 
57
63
  - id: release
58
64
  uses: __CI_REPO__/actions/release@__CI_REF__
59
- with:
60
- dev-token: ${{ secrets.GH_PROJECT_TOKEN }}
61
65
 
62
66
  deploy:
63
67
  needs: release