@avi2dg/checks 0.52.1 → 0.53.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/docs/design.md CHANGED
@@ -186,24 +186,34 @@ So a checkout without tags, a fork, and a branch that merged `main` in all write
186
186
  A section keeps the date it was written, because the squash merge that lands the release commit may fall on another day.
187
187
  Entries come from commit subjects, which are the squash-merged pull request titles that commitlint holds to the conventional format.
188
188
  A commit body holds the branch's own messages, and nothing lints it, so no entry comes from a body.
189
- The changelog arrives in the release pull request, and no workflow pushes to a branch a person works on.
189
+ The changelog arrives in the release commit, and no workflow pushes to a branch a person works on.
190
190
 
191
- ## A release is cut every day
191
+ ## A release is cut from every push
192
192
 
193
193
  A release waits for no quiet moment, since a busy repository always has work under way.
194
- The daily release opens the release pull request from `main` whenever it holds a feature or a fix since the last tag, and the pull request merges through the repository's usual merge path once its checks pass.
194
+ A package published to npm releases from every push to `main` that holds a feature or a fix since the last tag.
195
+ A version only reaches its users once it is published.
196
+ A daily release pull request would leave each merged change unpublished until a person merged the release too.
197
+ `checks-release-push` builds the release commit on top of `main` and runs the required checks on it from the `release/<branch>` branch it owns.
198
+ Only then does it move `main` to it without a force, so `main` only ever gains a release commit whose checks passed.
199
+ It tags the commit it landed and nothing else.
200
+ A repository that is no package keeps the daily release pull request.
195
201
  `checks-release-pr` writes only to the `release/<branch>` branch it owns, and rebuilds that branch on `main` rather than merging `main` into it, so the changelog it carries is the one the build writes.
196
202
  `checks-release-tag` writes only the `v*` tag of a release commit that already landed.
197
203
  The `github-release` job's `contents: write` creates or updates the GitHub release from the tag's `CHANGELOG.md` section.
198
204
 
199
- Both jobs act with the workflow token.
205
+ Every release bin acts with the workflow token.
200
206
  GitHub starts no run for a push that token makes, and holds the runs of a pull request it opens until a maintainer approves them.
201
207
  A GitHub App or a personal token would start them, but either is a credential each repository stores and someone rotates.
202
- So `checks-release-pr` dispatches the required checks on the release head, and `checks-release-tag` dispatches the release workflow on the tag, since a dispatch is the one run the token can start.
208
+ A dispatch is the one run the token can start.
209
+ So `checks-release-pr` and `checks-release-push` dispatch the required checks on the release head.
210
+ `checks-release-tag` and `checks-release-push` dispatch the release workflow on the tag.
203
211
  GitHub keeps a dispatched run's checks off the pull request, and branch protection does not count them.
204
212
  So each dispatched job reports its result as a commit status named for the job, which a required check of that name counts.
205
213
  Where a status and a check share a name, branch protection requires both, so the status never passes a pull request whose own check failed.
206
- The cost is a `workflow_dispatch` trigger and a status step on each workflow a release needs, a title lint that reads its title from the open pull request, and the repository setting that lets the token open a pull request.
214
+ The cost is a `workflow_dispatch` trigger and a status step on each workflow a release needs.
215
+ The title lint reads its title from the open pull request, or lints the head subject when none is open.
216
+ The daily release also needs the repository setting that lets the token open a pull request.
207
217
 
208
218
  ## The docs gate judges what a change touches
209
219
 
@@ -300,6 +310,18 @@ No finding can be accepted.
300
310
  A secret has no false positive worth keeping in git, because a placeholder carries the same shape without the value.
301
311
  So the gate ignores a repository's `.gitleaks.toml`, `.gitleaksignore` and `gitleaks:allow` comments, which would each let one repository pass what another fails.
302
312
 
313
+ A person's name, an employer's domain, host names and IP ranges are no secret to gitleaks, yet a public repository must not carry them.
314
+ A rule that names them would publish them, so the owner keeps the terms in a private pack outside every repository.
315
+ The pack is a file rather than an inline variable, so one file serves the shell and the runner, and no workflow names a term.
316
+ A pack that is set but cannot serve fails the gate, because a scan that quietly dropped it would pass what it exists to catch.
317
+ An empty `CHECKS_SECRETS_PACK` counts as set, since a CI secret that is missing expands to an empty value.
318
+ No report prints a matched term text or the pack path, because the report lands in a public CI log.
319
+ A finding location prints as it stands, such as a file name or a link.
320
+ So a term inside a file name or a repository name shows there.
321
+ That location is already public in the scanned commit or the scanned pull request.
322
+
323
+ A pull request's title, body and comments reach the forge without a commit, so [checks-pr-text](gates/checks-pr-text.md) scans them with the same rules and the same pack.
324
+
303
325
  ## The frontend checks are opt-in
304
326
 
305
327
  A browser check needs a built site and a browser, and a backend repository has neither.
@@ -23,7 +23,7 @@ It refuses a step or job with `if: false` or `continue-on-error: true`.
23
23
  It also refuses a workflow that does not trigger on both `opened` and `synchronize` pull requests to the target branch.
24
24
  A path filter cannot cover every pull request and therefore cannot satisfy the check.
25
25
  It refuses a reference to a workflow or job the repository does not hold.
26
- Each literal `checks-release-pr` and `checks-release-tag` argument names a workflow file, so every one must match a file under `.github/workflows/`.
26
+ Each literal `checks-release-pr`, `checks-release-push` and `checks-release-tag` argument names a workflow file, so every one must match a file under `.github/workflows/`.
27
27
  A call counts directly, through `bunx`, or through `bun` or `bun run` with the bin's path or its source file.
28
28
  An argument built from a variable or an expression is known only when the job runs, so the check passes over it.
29
29
  An argument built by command substitution is known only when the job runs, so the check passes over it.
@@ -15,7 +15,7 @@ A commit that GitHub committed allows any author name with an allowed email.
15
15
  GitHub authors a web merge under the account's display name.
16
16
  The same author on a commit with any other committer must match the allowed name too.
17
17
  `github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>` is allowed as author only of a commit whose subject is `chore: release <version>`, with or without the pull request number a squash merge adds.
18
- GitHub attributes the release commit [checks-release-pr](checks-release-pr.md) makes to that bot, since it commits through the workflow token.
18
+ GitHub attributes the release commit [checks-release-pr](checks-release-pr.md) or [checks-release-push](checks-release-push.md) makes to that bot, since each commits through the workflow token.
19
19
  Such a commit may carry a `Co-authored-by` trailer naming that bot, since GitHub adds one to some squash merges of a pull request the bot opened, and every other trailer is refused.
20
20
 
21
21
  ## What it reads
@@ -8,12 +8,13 @@ audience: consumers
8
8
 
9
9
  ## What it checks
10
10
 
11
- It lints the pull request title and nothing else.
11
+ It lints the pull request title, or the head subject when no open pull request heads the branch.
12
12
  The title comes from `PR_TITLE` when the event carries one.
13
13
  A dispatched run carries no pull request, so the bin reads the title of the one open pull request its branch heads.
14
- It fails when there is none.
14
+ When none heads the branch, it lints the head subject as it stands and without ` (#0000)`.
15
+ It fails when more than one heads the branch.
15
16
  A run with no title to lint passes and says so.
16
- The bin lints the title with ` (#0000)` attached, since GitHub appends the pull request number to the squashed subject.
17
+ The bin lints a pull request title with ` (#0000)` attached, since GitHub appends the pull request number to the squashed subject.
17
18
  It moves the git comment character off `#`, so a title starting with `#` is linted like any other.
18
19
 
19
20
  ## What it reads
@@ -31,7 +32,7 @@ The bin takes no arguments.
31
32
  | Code | Result |
32
33
  | --- | --- |
33
34
  | 0 | The title lints clean, or there is no title to lint. |
34
- | 1 | The title breaks the rules, or no one open pull request heads the branch. |
35
+ | 1 | The title or the head subject breaks the rules, or more than one open pull request heads the branch. |
35
36
  | 2 | The kit config or binary cannot be resolved, or a dispatched run misses its environment. |
36
37
 
37
38
  ## Sample output
@@ -13,9 +13,9 @@ It holds them to the three separator rules of [The prose rules](checks-docs.md#t
13
13
 
14
14
  | Refused | For example | Passes |
15
15
  | --- | --- | --- |
16
- | an em dash | `hexn.io — платформа` | `hexn.io, платформа` |
16
+ | an em dash | `example.com — платформа` | `example.com, платформа` |
17
17
  | an en dash | `pages 1–5` | `pages 1 to 5` |
18
- | a hyphen used as a dash | `hexn.io - платформа` or `a -- b` | `well-known`, `kebab-case`, `1-5` and `--flag` |
18
+ | a hyphen used as a dash | `example.com - платформа` or `a -- b` | `well-known`, `kebab-case`, `1-5` and `--flag` |
19
19
 
20
20
  A hyphen used as a dash is a run of one to three hyphens with white space on both sides and text on both sides.
21
21
  So a hyphen inside a word or a number, a flag and a list marker all pass.
@@ -117,7 +117,7 @@ jobs:
117
117
  path: reports/mutation/mutation.json
118
118
  retention-days: 1
119
119
  tests:
120
- runs-on: ${{ vars.CI_RUNS_ON || 'ubuntu-latest' }}
120
+ runs-on: ubuntu-latest
121
121
  steps:
122
122
  - uses: actions/checkout@v5
123
123
  - uses: oven-sh/setup-bun@v2
@@ -135,7 +135,7 @@ jobs:
135
135
  report:
136
136
  needs: [mutation, tests]
137
137
  if: ${{ !cancelled() && needs.tests.result == 'success' }}
138
- runs-on: ${{ vars.CI_RUNS_ON || 'ubuntu-latest' }}
138
+ runs-on: ubuntu-latest
139
139
  permissions:
140
140
  contents: read
141
141
  actions: read
@@ -176,9 +176,9 @@ It passes `--mutation` only when the mutation job succeeded, and the failed muta
176
176
  The concurrency group queues a run started by hand behind the scheduled one, so two runs never file the same finding twice.
177
177
  A dispatch on a branch runs both jobs and lists its findings in the report job's log, and `if-no-files-found: ignore` lets that run upload no record.
178
178
  `issues: write` on the report job is the one permission beyond reading the repository and its runs.
179
- A public repository keeps `runs-on: ubuntu-latest` on the mutation job.
179
+ A public repository keeps `runs-on: ubuntu-latest` on every job.
180
180
  A private repository sets `runs-on` on the mutation job to the labels declared under `checks.mutationRunner` in `package.json`, and [checks-ci-wiring](checks-ci-wiring.md#runners) refuses a mutation job that names `CI_RUNS_ON`.
181
- The report job reads `CI_RUNS_ON`, because it runs no mutant.
181
+ A private repository sets `runs-on: ${{ vars.CI_RUNS_ON || 'ubuntu-latest' }}` on the tests and report jobs.
182
182
  A successful run uploads `night-run`, so the next night compares with it, and a failed run uploads nothing a later night reads.
183
183
  The artifact keeps 90 days, so a gap of nights still leaves a previous night to compare with.
184
184
 
@@ -0,0 +1,90 @@
1
+ ---
2
+ kind: reference
3
+ audience: consumers
4
+ ---
5
+ # checks-pr-text
6
+
7
+ `checks-pr-text` scans the title, the body and every comment of a public pull request for a secret or a term of the private pack.
8
+
9
+ ## What it checks
10
+
11
+ It scans the title, the body, each conversation comment, each review and each review comment, with the rules [checks-secrets](checks-secrets.md) runs on a commit.
12
+ On a fork's pull request a review or a review comment starts no scan, because GitHub gives those runs no secret.
13
+ That text is scanned only when another trigger runs next.
14
+ It adds the private pack when `CHECKS_SECRETS_PACK` names one, as [The private pack](checks-secrets.md#the-private-pack) describes, and its last line says whether the pack ran.
15
+ With `CHECKS_SECRETS_PACK` set but the pack unable to serve, it exits 2 and scans nothing.
16
+ It reads the text as it stands now, so a text edited clean passes on the next run.
17
+ A finding names the text, its line and its rule, and never the matched text.
18
+ A finding location prints as it stands, such as a link.
19
+ So a term inside a repository name shows there.
20
+ That location is already public in the scanned pull request.
21
+
22
+ Deleting a finding from the current text leaves it in GitHub's edit history of that text, which the text's edit menu deletes revision by revision.
23
+ The timeline keeps a renamed title's old text, and only GitHub Support removes it.
24
+
25
+ ## What it reads
26
+
27
+ It calls `gh api` with `GH_TOKEN` for the pull request and its three kinds of comment, in the repository of the checkout's remote.
28
+ It reads the path of the private pack from `CHECKS_SECRETS_PACK`, and the kit's rules from `src/delivery/gitleaks.toml` in the installed package.
29
+ It writes each text to a file in a temporary directory, and deletes the directory when the scan ends.
30
+
31
+ ## Arguments
32
+
33
+ | Argument | Meaning |
34
+ | --- | --- |
35
+ | `<pull-request-number>` | the number of the pull request to scan |
36
+
37
+ ## Exit codes
38
+
39
+ | Code | Result |
40
+ | --- | --- |
41
+ | 0 | No text carries a secret or a term of the pack. |
42
+ | 1 | A text carries a secret or a term of the pack. |
43
+ | 2 | The argument is not one pull request number, `gh api` fails, no verified scanner is at hand, gitleaks fails, or `CHECKS_SECRETS_PACK` is set and the pack cannot serve. |
44
+
45
+ ## Sample output
46
+
47
+ ```
48
+ pr-text: pull request 42 carries 2 secret(s) or private term(s); delete each from the text it names, and delete the revision GitHub keeps in that text's edit history:
49
+ the body:3 private-domain: A domain the private pack lists
50
+ https://github.com/owner/repo/pull/42#issuecomment-1:1 private-name: A name form the private pack lists
51
+ pr-text: the kit's rules and the private pack CHECKS_SECRETS_PACK names ran
52
+ ```
53
+
54
+ ## When it runs
55
+
56
+ A public repository runs it from a workflow of its own on every change to a pull request's text:
57
+
58
+ ```yaml
59
+ on:
60
+ pull_request_target:
61
+ types: [opened, edited, synchronize, reopened]
62
+ issue_comment:
63
+ types: [created, edited, deleted]
64
+ pull_request_review:
65
+ types: [submitted, edited]
66
+ pull_request_review_comment:
67
+ types: [created, edited, deleted]
68
+ ```
69
+
70
+ The job writes the pack from a secret, as [The private pack](checks-secrets.md#the-private-pack) shows, then runs `bunx checks-pr-text` with the pull request's number.
71
+ It skips the scan when the secret is unset, as [When no pack is configured](#when-no-pack-is-configured) says.
72
+ `pull_request_target` runs the base branch's workflow with the repository's secrets, even for a fork's pull request.
73
+ The job reads the text through the API and checks out none of the pull request's code, so a fork runs nothing there.
74
+ A comment's run reports on no pull request, so the job posts its result as a `pr-text` status on the head commit.
75
+ The kit's own `.github/workflows/pr-text.yml` is a complete example.
76
+
77
+ ## When no pack is configured
78
+
79
+ The kit's workflow skips the scan when `CHECKS_SECRETS_PACK` is unset or empty, instead of failing.
80
+ It writes a notice and a job summary line that say so, and posts a `pr-text` status of success whose description reads `scan skipped: no private pack is configured (CHECKS_SECRETS_PACK is unset)`.
81
+ A skip is a coverage gap, not a pass: no private term was checked on that pull request.
82
+ Only the repository secret decides a skip, so no input from a pull request's author can cause one.
83
+ To scan with the pack, set the repository secret `CHECKS_SECRETS_PACK` to the pack's JSON, as [The private pack](checks-secrets.md#the-private-pack) describes.
84
+ The next eligible run then scans the text with it.
85
+ On a fork's pull request a review or a review comment still starts no run, so that text waits for another trigger.
86
+
87
+ ## Related topics
88
+
89
+ - [checks-secrets](checks-secrets.md)
90
+ - [checks-commit-title](checks-commit-title.md)
@@ -47,7 +47,7 @@ Released 2026-09-27.
47
47
 
48
48
  Each consumer owns `.github/workflows/release.yml` and calls the kit bins from it.
49
49
  The version lives in `package.json`, and the tag names the same version with a leading `v`.
50
- The build writes the changelog, so the release pull request carries the notes before the tag exists.
50
+ The build writes the changelog, so the release commit carries the notes before the tag exists.
51
51
  A repository that publishes a package to npm releases with this workflow:
52
52
 
53
53
  ```yaml
@@ -80,7 +80,7 @@ jobs:
80
80
  with:
81
81
  node-version: 24
82
82
  registry-url: https://registry.npmjs.org
83
- - run: npm publish
83
+ - run: ./node_modules/.bin/checks-release-publish
84
84
  github-release:
85
85
  needs: publish
86
86
  runs-on: ubuntu-latest
@@ -140,8 +140,9 @@ jobs:
140
140
  ```
141
141
 
142
142
  A private repository sets `runs-on: ${{ vars.CI_RUNS_ON || 'ubuntu-latest' }}` on each job in either workflow, as [checks-ci-wiring](checks-ci-wiring.md#runners) requires.
143
- A release is a pull request that holds only the version bump and the built changelog.
144
- [checks-release-pr](checks-release-pr.md) opens it on the daily release's schedule, or on every push to `main` in a package repository.
143
+ A release is one commit that holds only the version bump and the built changelog.
144
+ A package repository lands it on every push to `main` through [checks-release-push](checks-release-push.md), which tags it and dispatches this workflow on the tag.
145
+ Another repository takes it as the pull request [checks-release-pr](checks-release-pr.md) opens on the daily release's schedule.
145
146
  When it lands, [checks-release-tag](checks-release-tag.md) tags the merge commit and dispatches this workflow on the tag.
146
147
  A tag the workflow token pushes starts no `push` run, so the workflow triggers on `workflow_dispatch` as well, and refuses a dispatch on a ref that is not a tag.
147
148
  A tag a person pushes starts the same workflow through its `push` trigger.
@@ -156,3 +157,4 @@ A repository with no versioned releases does not need it.
156
157
 
157
158
  - [checks-changelog](checks-changelog.md)
158
159
  - [checks-release-report](checks-release-report.md)
160
+ - [checks-release-publish](checks-release-publish.md)
@@ -75,15 +75,13 @@ release-pr: https://github.com/acme/widget/pull/12 releases 0.4.0 from 3f2a9c81d
75
75
  ## When it runs
76
76
 
77
77
  The daily release workflow below runs it once a day on the default branch.
78
- A package repository runs it on every push to `main` other than a release commit.
78
+ A package published to npm releases each push to `main` through [checks-release-push](checks-release-push.md) instead, with no pull request.
79
79
  Run the workflow by hand with `gh workflow run daily-release` to refresh the release pull request sooner.
80
80
 
81
81
  ## Running it in CI
82
82
 
83
- A repository takes the daily release as `.github/workflows/daily-release.yml`.
83
+ A repository takes the daily release as the `daily-release` workflow below.
84
84
  A consuming repository pins daily-release between 03:00 and 04:00 UTC at a minute chosen so repositories do not start together.
85
- A package repository released on push drops the `schedule` trigger, so every push to `main` but a release commit opens or refreshes the release pull request.
86
- Its `pull-request` job's `if` reads `"github.event_name != 'push' || !startsWith(github.event.head_commit.message, 'chore: release ')"`.
87
85
  The `pull-request` job checks out its branch's tip when it starts, so a run that starts late never rebuilds the release on an older commit.
88
86
  The `pull-request` job runs on the schedule, and the `tag` job runs when a release commit lands on `main`, as [checks-release-tag](checks-release-tag.md) says:
89
87
 
@@ -186,5 +184,6 @@ gh api --method PUT repos/<owner>/<repo>/actions/permissions/workflow -F can_app
186
184
 
187
185
  - [checks-release-report](checks-release-report.md)
188
186
  - [checks-release-tag](checks-release-tag.md)
187
+ - [checks-release-push](checks-release-push.md)
189
188
  - [checks-changelog](checks-changelog.md)
190
189
  - [checks-release-notes](checks-release-notes.md)
@@ -0,0 +1,63 @@
1
+ ---
2
+ kind: reference
3
+ audience: consumers
4
+ ---
5
+ # checks-release-publish
6
+
7
+ `checks-release-publish` publishes the version `package.json` holds to npm, unless npm already holds it or a newer version.
8
+
9
+ ## What it checks
10
+
11
+ It reads the `latest` dist-tag of the package from npm.
12
+ It runs `npm publish` when npm holds no version of the package yet, or when the version is above `latest`.
13
+ It publishes nothing and ends cleanly when `latest` is the version, so a rerun of a release that published passes.
14
+ It publishes nothing and ends cleanly when `latest` is above the version.
15
+ The release workflow refuses a tag off `main`, so the newer version already carries every change of the older one.
16
+ When `npm publish` fails, it reads `latest` again.
17
+ A newer version that published while it ran ends it cleanly as well, and any other failure fails it.
18
+
19
+ Two uploads that overlap can still leave `latest` on the older version, since the order check runs before either upload completes.
20
+ `checks-release-push` holds its concurrency group until the release workflow it dispatched finishes for that reason, so releases it cuts publish one at a time, while a release workflow dispatched or rerun by hand relies on this bin alone.
21
+ That hold ends early in two cases.
22
+ One is when the 45 minute wait times out.
23
+ The other is when polling fails past the retry bound or the dispatch of the release workflow fails after GitHub accepted it.
24
+ In those cases an unfinished publisher can overlap the next release, and if latest lands on the older version the fix is npm dist-tag add <package>@<newer version> latest.
25
+
26
+ ## What it reads
27
+
28
+ It reads `name` and `version` from `package.json` in the working directory.
29
+ It reads `latest` with `npm view <name> dist-tags --json`, and takes npm's `E404` answer for a package npm does not hold yet.
30
+ `npm publish` takes its registry and credentials from the workflow, as trusted publishing sets them.
31
+
32
+ ## Arguments
33
+
34
+ ```sh
35
+ checks-release-publish
36
+ ```
37
+
38
+ It takes no arguments.
39
+
40
+ ## Exit codes
41
+
42
+ | Code | When |
43
+ | --- | --- |
44
+ | 0 | the version published, npm already holds it, or npm holds a newer version |
45
+ | 2 | the arguments do not parse, `package.json` or npm's answer does not parse, or npm refuses the publish for a reason other than a newer version |
46
+
47
+ ## Sample output
48
+
49
+ A release that runs after a newer one published prints one line:
50
+
51
+ ```
52
+ release-publish: npm already holds @acme/widget@0.4.1, which carries every change in 0.4.0, so 0.4.0 stays off npm
53
+ ```
54
+
55
+ ## When it runs
56
+
57
+ The release workflow of a package published to npm runs it in place of `npm publish`, as [checks-release-notes](checks-release-notes.md#release-workflow) shows.
58
+ A repository that never publishes to npm does not need it.
59
+
60
+ ## Related topics
61
+
62
+ - [checks-release-notes](checks-release-notes.md)
63
+ - [checks-release-push](checks-release-push.md)
@@ -0,0 +1,153 @@
1
+ ---
2
+ kind: reference
3
+ audience: consumers
4
+ ---
5
+ # checks-release-push
6
+
7
+ `checks-release-push` lands the next version of a package on its branch once the checks pass.
8
+ It then tags the release and dispatches the release workflow on the tag.
9
+
10
+ ## What it checks
11
+
12
+ It lists the unreleased changes the way [checks-release-report](checks-release-report.md) does, and does nothing when there are none.
13
+ It bumps the version `package.json` holds by the rule [checks-release-pr](checks-release-pr.md#what-it-checks) states, and refuses what that bin refuses.
14
+ It refuses when `v<version>` of the next version already tags a commit, since a version is released once.
15
+ It writes the next version into `package.json` and runs `bun run build`, which writes `CHANGELOG.md`.
16
+ It commits both as `chore: release <version>` on top of `HEAD` through the GitHub API, as `checks-release-pr` does.
17
+ It points the branch `release/<branch>` at that commit and dispatches every check workflow its arguments name there.
18
+ It waits up to 45 minutes in all for those workflows to finish on the release commit.
19
+ It reads the newest run of each, so a rerun stands in for the run it repeats.
20
+ A failed read of the runs list does not end the wait.
21
+ The wait retries the read at the same interval.
22
+ A successful read resets the count of failed reads in a row.
23
+ Past four failed reads in a row the run fails with the last read error.
24
+ When any of them concludes short of success, it refuses and leaves `<branch>` and the tags alone.
25
+ When all of them pass, it moves `<branch>` to the release commit without a force.
26
+ GitHub refuses that update when `<branch>` moved while the checks ran.
27
+ The run then ends cleanly, since the run of the push that moved it releases the new head.
28
+ When the update fails but `<branch>` already holds the release commit, the update landed and the run goes on.
29
+ It then tags the release commit `v<version>`, pushes the tag and dispatches the release workflow on it, and waits up to 45 minutes for that workflow's run on the release commit to finish.
30
+ When that run concludes short of success, or does not finish in time, the bin fails, though `<branch>` already holds the release and the tag exists.
31
+ When `HEAD` is itself a release commit, it tags it as [checks-release-tag](checks-release-tag.md) does, and waits for the release workflow it dispatches the same way.
32
+ So a rerun after a failed tag push finishes the release.
33
+ It leaves the working tree as it found it.
34
+
35
+ No pull request is opened, so a package release needs neither a merge by hand nor the setting that lets the token open pull requests.
36
+
37
+ ## What it reads
38
+
39
+ It reads the `v*` tags, the commit subjects since the last one and `package.json` from the checkout.
40
+ It refuses a shallow checkout, a detached `HEAD` and a working tree with changes to tracked files.
41
+ It reads the tags and the branch from `origin` with `git ls-remote`, and pushes the tag with the credentials the checkout holds.
42
+ It calls the GitHub API through `gh api`, which takes the repository from the checkout's remote and the token from `GH_TOKEN`.
43
+ The token needs `contents: write` and `actions: write`.
44
+ The branch's protection must let the workflow token push a commit whose required checks passed, so it requires status checks and no pull request review.
45
+
46
+ ## Arguments
47
+
48
+ ```sh
49
+ checks-release-push <release-workflow> <workflow>...
50
+ ```
51
+
52
+ The first argument names the release workflow file under `.github/workflows/`, such as `release.yml`, which triggers on `workflow_dispatch` and keeps its own guards, as [checks-release-notes](checks-release-notes.md) shows.
53
+ Each later argument names a workflow file whose jobs report the checks the branch requires, such as `ci.yml`.
54
+ Each named workflow triggers on `workflow_dispatch`.
55
+ Name no check workflow that runs something else on dispatch, such as a mutation baseline.
56
+
57
+ ## Exit codes
58
+
59
+ | Code | When |
60
+ | --- | --- |
61
+ | 0 | nothing is unreleased, the branch moved while the checks ran, the release tag already tags `HEAD`, or the release landed and its release workflow finished |
62
+ | 2 | the arguments do not parse, a refusal above applies, a check fails or does not finish in time, the build fails, the push or a GitHub API call fails, or the dispatched release workflow fails or does not finish in time |
63
+
64
+ ## Sample output
65
+
66
+ A run that releases prints the build's own output, then three lines:
67
+
68
+ ```
69
+ release-push: dispatched ci.yml on release/main to check 0.4.0 at 3f2a9c81d0b4
70
+ release-push: released 0.4.0 as 3f2a9c81d0b4 on main, tagged it v0.4.0, and dispatched release.yml on it
71
+ release-push: release.yml finished on v0.4.0
72
+ ```
73
+
74
+ A run whose release workflow fails refuses with the run that failed, though the branch already holds the release and the tag exists:
75
+
76
+ ```
77
+ checks-release-push: the release workflow failed on 3f2a9c81d0b4: release.yml concluded failure at https://github.com/acme/widget/actions/runs/7
78
+ ```
79
+
80
+ A run whose checks fail refuses with the run that failed:
81
+
82
+ ```
83
+ checks-release-push: the checks failed on 3f2a9c81d0b4, so the release stays off the branch: ci.yml concluded failure at https://github.com/acme/widget/actions/runs/7
84
+ ```
85
+
86
+ ## When it runs
87
+
88
+ A package published to npm runs it on every push to `main`, so each merged feature or fix ships as a versioned release.
89
+ Run the workflow by hand with `gh workflow run release-push` to cut a release now.
90
+
91
+ ## Running it in CI
92
+
93
+ A package repository takes it as `.github/workflows/release-push.yml`:
94
+
95
+ ```yaml
96
+ name: release-push
97
+ on:
98
+ workflow_dispatch:
99
+ push:
100
+ branches: [main]
101
+ permissions:
102
+ contents: read
103
+ jobs:
104
+ release:
105
+ if: github.ref == 'refs/heads/main'
106
+ runs-on: ubuntu-latest
107
+ timeout-minutes: 120
108
+ concurrency:
109
+ group: release-push
110
+ cancel-in-progress: false
111
+ permissions:
112
+ contents: write
113
+ actions: write
114
+ steps:
115
+ - uses: actions/checkout@v5
116
+ with:
117
+ ref: ${{ github.ref }}
118
+ fetch-depth: 0
119
+ - uses: oven-sh/setup-bun@v2
120
+ with:
121
+ bun-version-file: .bun-version
122
+ - run: bun install --frozen-lockfile
123
+ - name: release what main holds once its checks pass
124
+ env:
125
+ GH_TOKEN: ${{ github.token }}
126
+ run: ./node_modules/.bin/checks-release-push release.yml ci.yml
127
+ ```
128
+
129
+ The push the workflow token makes starts no run, so landing the release commit never starts the workflow again.
130
+ The concurrency group holds from reading the branch until the release workflow it dispatched finishes, so releases cut by release-push publish one at a time, and a queued run the group cancels had cut nothing.
131
+ That hold ends early in two cases.
132
+ One is when the 45 minute wait times out.
133
+ The other is when polling fails past the retry bound or the dispatch of the release workflow fails after GitHub accepted it.
134
+ In those cases an unfinished publisher can overlap the next release, and if latest lands on the older version the fix is npm dist-tag add <package>@<newer version> latest.
135
+ [checks-release-publish](checks-release-publish.md) stays the guard for a release workflow dispatched or rerun by hand.
136
+ A run that waits behind it checks out the branch's tip when it starts, so it releases on top of what landed.
137
+ The job waits on the checks it dispatched and on the release workflow, so a hosted runner bills the minutes those workflows take.
138
+ A private repository sets `runs-on: ${{ vars.CI_RUNS_ON || 'ubuntu-latest' }}`, as [checks-ci-wiring](checks-ci-wiring.md#runners) requires of every job but a mutation job.
139
+
140
+ The other workflows accept the dispatch the way [checks-release-pr](checks-release-pr.md#running-it-in-ci) lists:
141
+
142
+ - `.github/workflows/ci.yml` triggers on `workflow_dispatch` and grants `statuses: write` and `pull-requests: read`.
143
+ - Each required job ends with the step that reports its result as a commit status.
144
+ - The title lint lints the subject of the head commit when no open pull request heads the dispatched branch, as [Commit messages](../configs/commit-messages.md) says.
145
+ - `.github/workflows/release.yml` triggers on `workflow_dispatch` and refuses a ref that is not a tag.
146
+
147
+ ## Related topics
148
+
149
+ - [checks-release-report](checks-release-report.md)
150
+ - [checks-release-pr](checks-release-pr.md)
151
+ - [checks-release-tag](checks-release-tag.md)
152
+ - [checks-release-notes](checks-release-notes.md)
153
+ - [checks-release-publish](checks-release-publish.md)
@@ -56,7 +56,7 @@ release-report: no unreleased changes since v0.1.0
56
56
  ## When it runs
57
57
 
58
58
  A person runs it to see what the next release holds.
59
- [checks-release-pr](checks-release-pr.md) reads the same changes before it opens the release pull request.
59
+ [checks-release-pr](checks-release-pr.md) and [checks-release-push](checks-release-push.md) read the same changes before they cut a release.
60
60
  A repository with no versioned releases does not need it.
61
61
 
62
62
  ## Related topics
@@ -64,3 +64,4 @@ A repository with no versioned releases does not need it.
64
64
  - [checks-changelog](checks-changelog.md)
65
65
  - [checks-release-notes](checks-release-notes.md)
66
66
  - [checks-release-pr](checks-release-pr.md)
67
+ - [checks-release-push](checks-release-push.md)
@@ -56,8 +56,10 @@ release-tag: tagged 3f2a9c81d0b4 as v0.4.0, and dispatched release.yml on it
56
56
  ## When it runs
57
57
 
58
58
  The `tag` job of the daily release workflow runs it on each push to `main` whose head commit reads as a release, as [checks-release-pr](checks-release-pr.md#running-it-in-ci) shows.
59
+ A package repository needs no `tag` job, since [checks-release-push](checks-release-push.md) tags the release it lands.
59
60
 
60
61
  ## Related topics
61
62
 
62
63
  - [checks-release-pr](checks-release-pr.md)
64
+ - [checks-release-push](checks-release-push.md)
63
65
  - [checks-release-notes](checks-release-notes.md)