@jakobm/peacekeeper 0.14.1 → 0.16.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 +40 -15
- package/package.json +2 -2
- package/scripts/check-project-requirements.txt +35 -4
- package/scripts/check-project.js +21 -1
- package/scripts/release-core.js +4 -3
- package/scripts/release-protection.js +37 -11
- package/scripts/run-tests.js +9 -0
- package/scripts/verify-release-protection.js +1 -1
package/README.md
CHANGED
|
@@ -90,7 +90,7 @@ Complete example:
|
|
|
90
90
|
}
|
|
91
91
|
```
|
|
92
92
|
|
|
93
|
-
`git.remote` and `git.defaultBranch` identify the branch used for checkpointing and cleanup. `git.branchPrefix` names new task branches. `tests.commands` are complete shell commands, run in order from the caller's Git root; install any executables they use. The test runner stops on the first failure and records a receipt only after every command succeeds. `receiptMaxAgeMinutes` controls how long a receipt can be reused. Set `npmTestUsesRunner` to `true` only when the caller's `npm test` script invokes `peacekeeper run-tests`; this makes the rerun hint `npm test -- rerun`. Otherwise the hint uses the explicit package path. `loggingDirectory` stores hook invocation logs relative to the primary Git worktree's root.
|
|
93
|
+
`git.remote` and `git.defaultBranch` identify the branch used for checkpointing and cleanup. `git.branchPrefix` names new task branches. `run-tests` always runs the local `check-project` validation first, even when reusing a test receipt. Explicit `check-project` invocations in `tests.commands` are rejected as redundant. The configured commands are complete shell commands, run in order from the caller's Git root; install any executables they use. The test runner stops on the first failure and records a receipt only after every command succeeds. `receiptMaxAgeMinutes` controls how long a receipt can be reused. Set `npmTestUsesRunner` to `true` only when the caller's `npm test` script invokes `peacekeeper run-tests`; this makes the rerun hint `npm test -- rerun`. Otherwise the hint uses the explicit package path. `loggingDirectory` stores hook invocation logs relative to the primary Git worktree's root.
|
|
94
94
|
|
|
95
95
|
`loggingEnabled` has the bundled default `true`; set it to `false` to disable invocation logs and the background cleanup worker's log file. Existing logs are kept. The setting is read from the primary checkout's configuration, including for linked worktrees. SessionStart and Stop logs include the process and event working directories, event type, primary-checkout classification, and hook response or error. Only selected event fields are logged, never the full incoming payload. These logs require the Peacekeeper script to start; a missing Node.js executable or package must be diagnosed in Codex's own logs.
|
|
96
96
|
|
|
@@ -244,6 +244,9 @@ does not grant trust automatically.
|
|
|
244
244
|
## Check project conventions locally
|
|
245
245
|
|
|
246
246
|
Run `peacekeeper check-project` to check the local project configuration.
|
|
247
|
+
All projects must declare nonblank `author` and `repository` values in
|
|
248
|
+
`package.json`. npm object forms are supported with nonblank `author.name`
|
|
249
|
+
and `repository.url` strings.
|
|
247
250
|
Run `peacekeeper check-project --explain` for the complete expected
|
|
248
251
|
architecture, requirements, configuration examples, and verification limits.
|
|
249
252
|
The command prints the bundled [formal requirements](scripts/check-project-requirements.txt),
|
|
@@ -284,8 +287,39 @@ from the organization are included. Merge queues are not currently an alternativ
|
|
|
284
287
|
to this strict policy. Repositories must also enable **Automatically delete head
|
|
285
288
|
branches** (`delete_branch_on_merge=true`).
|
|
286
289
|
|
|
290
|
+
The default branch must require pull requests (zero required reviewer approvals
|
|
291
|
+
is allowed), and only merge commits may be used. Either disable squash and rebase
|
|
292
|
+
in repository settings, or restrict the default branch's pull-request ruleset to
|
|
293
|
+
`allowed_merge_methods: ["merge"]`. Other branches are outside this policy.
|
|
294
|
+
Required linear history and any applicable PR rule excluding merge commits fail
|
|
295
|
+
verification. PR and strict-check rules must enforce the policy without bypass.
|
|
296
|
+
Enable `allow_auto_merge` in repository settings: this permits opting in per PR;
|
|
297
|
+
Peacekeeper never enables auto-merge on individual PRs.
|
|
298
|
+
|
|
299
|
+
`check-project` makes no network requests. It runs implicitly before
|
|
300
|
+
`tests.commands`, including when reusing a receipt, and checks that the configured
|
|
301
|
+
PR workflow covers the default branch and runs `verify-release-protection` with `GH_TOKEN` after `npm ci` and
|
|
302
|
+
before tests in a job listed in `git.releaseRequiredChecks`. The job and step must
|
|
303
|
+
not skip verification or ignore failures. The online verifier then checks that
|
|
304
|
+
all those configured check names are mandatory on GitHub, including its own job.
|
|
305
|
+
For example, in a required `test` job:
|
|
306
|
+
|
|
307
|
+
```yaml
|
|
308
|
+
- run: npm ci
|
|
309
|
+
- run: peacekeeper verify-release-protection
|
|
310
|
+
env:
|
|
311
|
+
GH_TOKEN: ${{ secrets.RELEASE_POLICY_TOKEN }}
|
|
312
|
+
- run: npm test
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
This creates a local-to-CI chain: missing workflows are detected on the next local
|
|
316
|
+
`npm test`; incorrect GitHub settings are detected on the next PR run. It is not
|
|
317
|
+
continuous monitoring. Removing both CI and the local invocation prevents this
|
|
318
|
+
chain from running. No central scheduled audit is required by this setup.
|
|
319
|
+
|
|
287
320
|
The PR test job runs `peacekeeper verify-release-protection` before its tests.
|
|
288
|
-
The command
|
|
321
|
+
The command reads the actual default branch from GitHub, regardless of the PR
|
|
322
|
+
base. Other release commands require their configured branch to match it.
|
|
289
323
|
`peacekeeper release` checks the default branch before preparing a release
|
|
290
324
|
and again immediately before each merge attempt. Before publishing,
|
|
291
325
|
`peacekeeper verify-release` checks the policy, matching package/tag/lockfile
|
|
@@ -347,19 +381,10 @@ not retain historical proof of the tested file tree if administrators change
|
|
|
347
381
|
settings between CI and merge. Keep the `package.yml` filename unchanged: npm
|
|
348
382
|
trusted publishing is configured for that workflow file.
|
|
349
383
|
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
- If multiple methods are allowed, `git.releaseMergeMethod` selects
|
|
355
|
-
`"merge"` (merge commit), `"rebase"`, or `"squash"`. A configured method that
|
|
356
|
-
GitHub disallows causes the release to stop before creating a branch or PR.
|
|
357
|
-
- The bundled preference is `"merge"`.
|
|
358
|
-
|
|
359
|
-
For example, set `"releaseMergeMethod": "rebase"` inside the existing `git` object.
|
|
360
|
-
A caller without a configuration file uses the bundled defaults.
|
|
361
|
-
The chosen method is saved with release state and reused on retries; unfinished
|
|
362
|
-
releases created by older versions retain their original squash behavior.
|
|
384
|
+
Releases use merge commits on the default branch, including retries of legacy
|
|
385
|
+
state without a merge method. Saved unfinished releases that explicitly select
|
|
386
|
+
squash or rebase must be restarted. `git.releaseMergeMethod` remains accepted for
|
|
387
|
+
configuration compatibility, but no longer selects the default-branch merge method.
|
|
363
388
|
|
|
364
389
|
From a clean repository root, run `npm run release -- patch`, `minor`, or `major`.
|
|
365
390
|
The command fetches the default branch and uses a temporary detached worktree to
|
package/package.json
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jakobm/peacekeeper",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.16.0",
|
|
4
4
|
"description": "Shared repository test, Git worktree, and Codex hook commands",
|
|
5
5
|
"license": "MIT",
|
|
6
|
+
"author": "Jakob Mattsson",
|
|
6
7
|
"repository": {
|
|
7
8
|
"type": "git",
|
|
8
9
|
"url": "git+https://github.com/jakobmattsson/peacekeeper.git"
|
|
@@ -19,7 +20,6 @@
|
|
|
19
20
|
"scripts/"
|
|
20
21
|
],
|
|
21
22
|
"scripts": {
|
|
22
|
-
"pretest": "node bin/peacekeeper.js check-project",
|
|
23
23
|
"test": "node bin/peacekeeper.js run-tests",
|
|
24
24
|
"release": "node bin/peacekeeper.js release",
|
|
25
25
|
"cleanup:branches": "node bin/peacekeeper.js cleanup-merged-branches",
|
|
@@ -39,6 +39,13 @@ Project configuration
|
|
|
39
39
|
}
|
|
40
40
|
projectChecks and both its fields are mandatory in an existing configuration.
|
|
41
41
|
|
|
42
|
+
Package metadata: all projects, including private packages
|
|
43
|
+
package.json must declare author and repository.
|
|
44
|
+
author must be a nonblank string or an object with a nonblank string name.
|
|
45
|
+
repository must be a nonblank string (including npm repository shorthand)
|
|
46
|
+
or an object with a nonblank string url. Whitespace-only values fail.
|
|
47
|
+
These checks validate presence, not repository reachability or author identity.
|
|
48
|
+
|
|
42
49
|
Toolchain: package.json and all Node-based CI jobs
|
|
43
50
|
Pin exact versions in package.json with:
|
|
44
51
|
volta pin node@<exact-version> npm@<exact-version>
|
|
@@ -325,7 +332,31 @@ What a passing check does and does not establish
|
|
|
325
332
|
online verification; verify-release handles the release checks it supports.
|
|
326
333
|
|
|
327
334
|
Using this check in the test chain
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
335
|
+
run-tests always runs check-project as an implicit first step before
|
|
336
|
+
tests.commands, including when a valid receipt skips the configured commands.
|
|
337
|
+
Explicit check-project invocations in tests.commands are rejected as redundant
|
|
338
|
+
because the runner always includes this step automatically. Supported direct
|
|
339
|
+
peacekeeper, npm exec, npx and node CLI forms are checked, including commands
|
|
340
|
+
separated by shell control operators. Failure stops the run and invalidates any
|
|
341
|
+
existing receipt. This check remains entirely local.
|
|
342
|
+
|
|
343
|
+
Default-branch GitHub policy chain
|
|
344
|
+
The configured PR workflow must run verify-release-protection with step GH_TOKEN
|
|
345
|
+
after npm ci and before tests in at least one job in git.releaseRequiredChecks.
|
|
346
|
+
Verification must be unconditional and must not allow failure. The existing
|
|
347
|
+
required-job validation applies to that job and its dependencies.
|
|
348
|
+
This applies to private projects too; configure nonempty releaseRequiredChecks.
|
|
349
|
+
|
|
350
|
+
Online verification reads the actual GitHub default branch and requires PRs,
|
|
351
|
+
strict mandatory configured checks without bypass, and merge commits only.
|
|
352
|
+
Zero reviewer approvals is allowed. Enable repository allow_auto_merge; this
|
|
353
|
+
only offers per-PR opt-in and does not activate auto-merge for any PR.
|
|
354
|
+
Restrict merge methods either repository-wide (merge enabled, squash and rebase
|
|
355
|
+
disabled) or with a default-branch PR ruleset allowing only merge. Other branches
|
|
356
|
+
are not checked. Linear-history rules or PR rules excluding merge are errors.
|
|
357
|
+
Ruleset PR requirements, like status checks, require active rules without bypass.
|
|
358
|
+
Existing automatic head-branch deletion requirements still apply.
|
|
359
|
+
|
|
360
|
+
Missing CI is detected on the next local npm test. GitHub settings are checked
|
|
361
|
+
on the next PR run, before release merges and before publication. This is not
|
|
362
|
+
continuous monitoring and cannot run if both local checks and CI are removed.
|
package/scripts/check-project.js
CHANGED
|
@@ -40,6 +40,21 @@ function checkProject(cwd = process.cwd()) {
|
|
|
40
40
|
try { action(); } catch (error) { errors.push(`[${category}] ${error.message}`); }
|
|
41
41
|
finally { category = previous; }
|
|
42
42
|
};
|
|
43
|
+
check('tests', () => {
|
|
44
|
+
const projectCheck = /(?:^|&&|\|\||;|\n)\s*(?:peacekeeper|npm\s+exec\s+--no\s+--\s+peacekeeper|npx\s+--no-install\s+peacekeeper|node\s+(?:\.\/)?(?:node_modules\/@jakobm\/peacekeeper\/)?bin\/peacekeeper\.js)\s+check-project(?=\s|$|;|&|\|)/u;
|
|
45
|
+
for (const [index, command] of config.tests.commands.entries()) {
|
|
46
|
+
requireThat(!projectCheck.test(command),
|
|
47
|
+
`tests.commands[${index}]: remove check-project; it is redundant because Peacekeeper always includes it automatically before the configured test commands, even when reusing a test receipt.`);
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
check('package', () => {
|
|
51
|
+
const nonblank = value => typeof value === 'string' && value.trim().length > 0;
|
|
52
|
+
requireThat(nonblank(manifest.author) || nonblank(manifest.author?.name),
|
|
53
|
+
'package.json: author must be a nonblank string or an object with a nonblank name.');
|
|
54
|
+
requireThat(nonblank(manifest.repository) || nonblank(manifest.repository?.url),
|
|
55
|
+
'package.json: repository must be a nonblank string or an object with a nonblank url.');
|
|
56
|
+
});
|
|
57
|
+
|
|
43
58
|
const workflows = new Map();
|
|
44
59
|
check('CI', () => {
|
|
45
60
|
const directory = path.join(root, '.github/workflows');
|
|
@@ -153,6 +168,7 @@ function checkProject(cwd = process.cwd()) {
|
|
|
153
168
|
&& !['paths', 'paths-ignore', 'branches-ignore', 'types'].some(key => Object.hasOwn(filters, key))
|
|
154
169
|
&& (!filters.branches || (Array.isArray(filters.branches) && filters.branches.includes(config.git.defaultBranch)))),
|
|
155
170
|
`${filename}: PR filters must include the default branch and must not skip required checks by path or event type.`);
|
|
171
|
+
let policyJobs = 0;
|
|
156
172
|
const checked = new Set();
|
|
157
173
|
function requiredJob(id, job, testsRequired) {
|
|
158
174
|
if (!requireThat(job && typeof job === 'object', `${filename}/${id}: required job is missing; checks for this job are blocked.`)) return;
|
|
@@ -166,6 +182,9 @@ function checkProject(cwd = process.cwd()) {
|
|
|
166
182
|
const steps = job.steps || [];
|
|
167
183
|
const installed = steps.findIndex(step => directCommand(step, installCommand) && unconditional(step.if) && !step['continue-on-error']);
|
|
168
184
|
const tested = steps.findIndex(step => directCommand(step, testCommand) && unconditional(step.if) && !step['continue-on-error']);
|
|
185
|
+
const policy = steps.findIndex(step => directCommand(step, /^(?:peacekeeper|npm exec --no -- peacekeeper|node (?:\.\/)?(?:node_modules\/@jakobm\/peacekeeper\/)?bin\/peacekeeper\.js) verify-release-protection$/u)
|
|
186
|
+
&& unconditional(step.if) && !step['continue-on-error'] && step.env?.GH_TOKEN);
|
|
187
|
+
if (policy > installed && installed >= 0 && policy < tested && config.git.releaseRequiredChecks.includes(job.name || id)) policyJobs += 1;
|
|
169
188
|
requireThat(installed >= 0 && tested > installed, `${filename}/${id}: run unconditional npm ci before npm test, npm run test:<name>, or npm pack --dry-run (without continue-on-error).`);
|
|
170
189
|
}
|
|
171
190
|
for (const name of required) {
|
|
@@ -175,6 +194,7 @@ function checkProject(cwd = process.cwd()) {
|
|
|
175
194
|
checked.clear();
|
|
176
195
|
requiredJob(...matching[0], true);
|
|
177
196
|
}
|
|
197
|
+
requireThat(policyJobs > 0, `${filename}: a job in git.releaseRequiredChecks must run unconditional verify-release-protection with GH_TOKEN after npm ci and before tests.`);
|
|
178
198
|
});
|
|
179
199
|
|
|
180
200
|
if (published) check('publication', () => {
|
|
@@ -227,7 +247,7 @@ function main() {
|
|
|
227
247
|
if (errors.length) {
|
|
228
248
|
for (const error of errors) console.error(`FAIL ${error}`);
|
|
229
249
|
process.exitCode = 1;
|
|
230
|
-
} else console.log(`Project checks passed: tools, hooks, CI${published ? ', publication' : ' (private package; publication skipped)'}.`);
|
|
250
|
+
} else console.log(`Project checks passed: package, tools, hooks, CI${published ? ', publication' : ' (private package; publication skipped)'}.`);
|
|
231
251
|
}
|
|
232
252
|
if (require.main === module) {
|
|
233
253
|
try { main(); } catch (error) { console.error(`FAIL ${error.message}`); process.exitCode = 1; }
|
package/scripts/release-core.js
CHANGED
|
@@ -223,12 +223,12 @@ function prepare(exec, cwd, bump, log) {
|
|
|
223
223
|
const remote = checked(exec, 'git', ['remote', 'get-url', 'origin'], cwd);
|
|
224
224
|
const { host, slug } = originRepository(remote);
|
|
225
225
|
const repository = github(exec, cwd, host, `repos/${slug}`, {});
|
|
226
|
-
|
|
226
|
+
projectConfig(cwd);
|
|
227
227
|
const allowed = ['merge', 'rebase', 'squash'].filter((method) =>
|
|
228
228
|
repository[{ merge: 'allow_merge_commit', rebase: 'allow_rebase_merge', squash: 'allow_squash_merge' }[method]] === true);
|
|
229
229
|
if (!allowed.length)
|
|
230
230
|
throw new ReleaseError('No supported PR merge method is enabled.');
|
|
231
|
-
const mergeMethod =
|
|
231
|
+
const mergeMethod = 'merge';
|
|
232
232
|
if (!allowed.includes(mergeMethod))
|
|
233
233
|
throw new ReleaseError(`Configured release merge method ${mergeMethod} is not enabled on GitHub.`);
|
|
234
234
|
const defaultBranch = repository.default_branch;
|
|
@@ -374,10 +374,11 @@ async function mergeRelease(exec, cwd, details, now, sleep, log) {
|
|
|
374
374
|
throw new ReleaseError(`Release PR #${details.pr} has conflicts; inspect it before continuing.`);
|
|
375
375
|
verifyProtection({ api: githubApi(exec, cwd, details.host), slug: details.slug,
|
|
376
376
|
branch: details.defaultBranch, checks: requiredChecks(cwd) });
|
|
377
|
+
if (details.mergeMethod && details.mergeMethod !== 'merge') throw new ReleaseError('Release must use merge commits on the default branch; restart this release.');
|
|
377
378
|
// GitHub enforces required checks, reviews, and branch freshness on this request.
|
|
378
379
|
const result = exec('gh', ['api', '--hostname', details.host, '--method', 'PUT',
|
|
379
380
|
`repos/${details.slug}/pulls/${details.pr}/merge`,
|
|
380
|
-
'-f', `merge_method=${details.mergeMethod || '
|
|
381
|
+
'-f', `merge_method=${details.mergeMethod || 'merge'}`, '-f', `sha=${details.sha}`], { cwd });
|
|
381
382
|
if (result.status === 0) {
|
|
382
383
|
const merged = JSON.parse(result.stdout);
|
|
383
384
|
if (merged.merged)
|
|
@@ -11,9 +11,12 @@ function requiredChecks(cwd) {
|
|
|
11
11
|
// other API failures, including permission failures, must propagate.
|
|
12
12
|
function verifyProtection({ api, slug, branch, checks }) {
|
|
13
13
|
const base = `repos/${slug}`;
|
|
14
|
-
|
|
14
|
+
const repository = api(base);
|
|
15
|
+
if (repository?.delete_branch_on_merge !== true) {
|
|
15
16
|
throw new Error(`Repository ${slug} must enable delete_branch_on_merge. Run gh api --method PATCH ${base} -F delete_branch_on_merge=true, then rerun.`);
|
|
16
17
|
}
|
|
18
|
+
if (repository.allow_auto_merge !== true) throw new Error('Repository must enable allow_auto_merge; this only makes auto-merge available per PR.');
|
|
19
|
+
if (repository.default_branch !== branch) throw new Error(`Verify the actual default branch (${repository.default_branch}), not ${branch}.`);
|
|
17
20
|
let protection;
|
|
18
21
|
try {
|
|
19
22
|
protection = api(`${base}/branches/${encodeURIComponent(branch)}/protection`);
|
|
@@ -27,6 +30,33 @@ function verifyProtection({ api, slug, branch, checks }) {
|
|
|
27
30
|
rules.push(...batch);
|
|
28
31
|
if (batch.length < 100) break;
|
|
29
32
|
}
|
|
33
|
+
if (protection?.required_linear_history?.enabled || rules.some(rule => rule.type === 'required_linear_history')) {
|
|
34
|
+
throw new Error('Default branch must allow merge commits; disable required linear history.');
|
|
35
|
+
}
|
|
36
|
+
const pullRules = rules.filter(rule => rule.type === 'pull_request');
|
|
37
|
+
let requiresPullRequest = Boolean(protection?.required_pull_request_reviews && protection.enforce_admins?.enabled === true
|
|
38
|
+
&& !(protection.required_pull_request_reviews.bypass_pull_request_allowances?.users?.length)
|
|
39
|
+
&& !(protection.required_pull_request_reviews.bypass_pull_request_allowances?.teams?.length)
|
|
40
|
+
&& !(protection.required_pull_request_reviews.bypass_pull_request_allowances?.apps?.length));
|
|
41
|
+
let branchMergeOnly = false;
|
|
42
|
+
const verifiedRulesets = new Set();
|
|
43
|
+
function verifyRuleset(rule) {
|
|
44
|
+
if (!Number.isInteger(rule.ruleset_id)) throw new Error('GitHub did not identify the policy ruleset.');
|
|
45
|
+
if (verifiedRulesets.has(rule.ruleset_id)) return;
|
|
46
|
+
const ruleset = api(`${base}/rulesets/${rule.ruleset_id}`);
|
|
47
|
+
if (ruleset.enforcement !== 'active' || !Array.isArray(ruleset.bypass_actors)) {
|
|
48
|
+
throw new Error('Cannot verify active ruleset bypass settings; grant write access to the ruleset so GitHub exposes bypass actors, then rerun.');
|
|
49
|
+
}
|
|
50
|
+
if (ruleset.bypass_actors.length) throw new Error('Default branch rulesets must not allow bypass actors.');
|
|
51
|
+
verifiedRulesets.add(rule.ruleset_id);
|
|
52
|
+
}
|
|
53
|
+
for (const rule of pullRules) {
|
|
54
|
+
verifyRuleset(rule);
|
|
55
|
+
requiresPullRequest = true;
|
|
56
|
+
const methods = rule.parameters?.allowed_merge_methods;
|
|
57
|
+
if (methods && !methods.includes('merge')) throw new Error('Default branch PR rules must allow merge commits.');
|
|
58
|
+
if (Array.isArray(methods) && methods.length === 1 && methods[0] === 'merge') branchMergeOnly = true;
|
|
59
|
+
}
|
|
30
60
|
const strictChecks = new Set();
|
|
31
61
|
const classic = protection?.required_status_checks;
|
|
32
62
|
if (classic?.strict === true && protection.enforce_admins?.enabled === true) {
|
|
@@ -35,24 +65,20 @@ function verifyProtection({ api, slug, branch, checks }) {
|
|
|
35
65
|
}
|
|
36
66
|
// Ruleset bypass actors are not included in the effective-rules response.
|
|
37
67
|
// Read every contributing ruleset and require that it has no bypass actors.
|
|
38
|
-
const checkedRulesets = new Set();
|
|
39
68
|
for (const rule of rules.filter(rule => rule.type === 'required_status_checks')) {
|
|
40
69
|
if (rule.parameters?.strict_required_status_checks_policy !== true) continue;
|
|
41
|
-
|
|
42
|
-
if (!checkedRulesets.has(rule.ruleset_id)) {
|
|
43
|
-
const ruleset = api(`${base}/rulesets/${rule.ruleset_id}`);
|
|
44
|
-
if (ruleset.enforcement !== 'active' || !Array.isArray(ruleset.bypass_actors)) {
|
|
45
|
-
throw new Error('Cannot verify active ruleset bypass settings; grant write access to the ruleset so GitHub exposes bypass actors, then rerun.');
|
|
46
|
-
}
|
|
47
|
-
if (ruleset.bypass_actors.length) throw new Error('Strict release rulesets must not allow bypass actors.');
|
|
48
|
-
checkedRulesets.add(rule.ruleset_id);
|
|
49
|
-
}
|
|
70
|
+
verifyRuleset(rule);
|
|
50
71
|
for (const check of rule.parameters.required_status_checks || []) strictChecks.add(check.context);
|
|
51
72
|
}
|
|
52
73
|
const missing = checks.filter(check => !strictChecks.has(check));
|
|
53
74
|
if (!checks.length || missing.length) {
|
|
54
75
|
throw new Error(`Branch ${branch} must require strict status checks without bypass for: ${missing.join(', ') || '(no checks configured)'}. Enable strict checks and enforce them for administrators, or use an active ruleset without bypass actors; then rerun.`);
|
|
55
76
|
}
|
|
77
|
+
if (!requiresPullRequest) throw new Error('Default branch must require pull requests without bypass.');
|
|
78
|
+
if (repository.allow_merge_commit !== true || (!branchMergeOnly
|
|
79
|
+
&& (repository.allow_squash_merge !== false || repository.allow_rebase_merge !== false))) {
|
|
80
|
+
throw new Error('Default branch must allow only merge commits: restrict its pull request ruleset to merge, or disable repository squash and rebase merging.');
|
|
81
|
+
}
|
|
56
82
|
}
|
|
57
83
|
|
|
58
84
|
function githubApi(exec, cwd, host) {
|
package/scripts/run-tests.js
CHANGED
|
@@ -31,6 +31,15 @@ function main() {
|
|
|
31
31
|
process.exitCode = 2;
|
|
32
32
|
return;
|
|
33
33
|
}
|
|
34
|
+
const projectStatus = spawnSync(process.execPath, [require.resolve('./check-project.js')], {
|
|
35
|
+
cwd: config.root, stdio: 'inherit',
|
|
36
|
+
});
|
|
37
|
+
if (projectStatus.error) throw projectStatus.error;
|
|
38
|
+
if (projectStatus.status !== 0) {
|
|
39
|
+
fs.rmSync(receiptPath(config.root), { force: true });
|
|
40
|
+
process.exitCode = projectStatus.status ?? 1;
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
34
43
|
if (!force && !process.env.CI && hasReusableReceipt()) {
|
|
35
44
|
console.log(successMessage(config.root));
|
|
36
45
|
return;
|
|
@@ -12,7 +12,7 @@ try {
|
|
|
12
12
|
if (remote.status !== 0) throw new Error(remote.stderr);
|
|
13
13
|
const { host, slug } = originRepository(remote.stdout.trim());
|
|
14
14
|
const api = githubApi((command, args, options) => spawnSync(command, args, { ...options, encoding: 'utf8' }), cwd, host);
|
|
15
|
-
verifyProtection({ api, slug, branch:
|
|
15
|
+
verifyProtection({ api, slug, branch: api(`repos/${slug}`).default_branch, checks: requiredChecks(cwd) });
|
|
16
16
|
} catch (error) {
|
|
17
17
|
console.error(error.message);
|
|
18
18
|
process.exitCode = 1;
|