@ebalzan/ci 1.0.0 → 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 +281 -0
- package/config/repo-settings.json +2 -4
- package/config/rulesets/protect-dev.json +6 -2
- package/config/rulesets/protect-main.json +6 -2
- package/config/rulesets/require-pull-request.json +1 -1
- package/config/rulesets/require-release-pr.json +3 -25
- package/lib/onboard.mjs +2 -0
- package/package.json +1 -1
- package/templates/.github/workflows/deploy.yml +2 -1
- package/templates/.github/workflows/release.yml +22 -18
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.
|
|
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":
|
|
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.
|
|
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": [
|
|
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
|
|
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": [
|
|
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
|
|
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": "
|
|
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
|
-
|
|
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": "
|
|
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
|
|
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
|
-
#
|
|
5
|
-
#
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
# one, the vX.Y.Z tag, the GitHub
|
|
9
|
-
#
|
|
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
|
-
#
|
|
12
|
-
#
|
|
13
|
-
#
|
|
14
|
-
#
|
|
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.
|
|
32
|
-
&& github.event.pull_request.head.
|
|
33
|
-
&& github.event.
|
|
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
|
|
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.
|
|
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
|