@jakobm/peacekeeper 0.13.0 → 0.14.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -57,7 +57,9 @@ values cause failure without changing the file. Repeating the command on a
57
57
  complete valid file leaves it unchanged.
58
58
 
59
59
  All CLI commands validate configuration before executing, except help,
60
- `check-project --explain`, and `update-config`. An incomplete existing file fails
60
+ `check-project --explain`, and `update-config`. Checkpoint hooks instead validate
61
+ the event worktree configuration when the event requires instructions or a check.
62
+ An incomplete existing file fails
61
63
  with missing field paths and a repair hint. Updates introducing new fields
62
64
  therefore require `peacekeeper update-config` before ordinary commands can run.
63
65
 
@@ -92,6 +94,8 @@ Complete example:
92
94
 
93
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.
94
96
 
97
+ Checkpoint hook failures write the cause to stderr and exit with code 2 instead of reporting success. For `Stop`, this requests continued work under the Codex hook protocol. Invocation logging is advisory: a logging failure is reported on stderr, but the checkpoint instructions or check still run and retain their result. Behavior configuration comes from the event's worktree, while log settings come from the primary checkout. A logging warning alone does not change the exit code.
98
+
95
99
  To publish checkpoint branches automatically, set `git.checkpointPushRemotes` (fragment):
96
100
 
97
101
  ```json
@@ -277,7 +281,8 @@ with **Require branches to be up to date before merging** enabled. Classic branc
277
281
  protection must enforce checks for administrators too. Alternatively, active
278
282
  rulesets must require strict checks and have no bypass actors. Rules inherited
279
283
  from the organization are included. Merge queues are not currently an alternative
280
- to this strict policy.
284
+ to this strict policy. Repositories must also enable **Automatically delete head
285
+ branches** (`delete_branch_on_merge=true`).
281
286
 
282
287
  The PR test job runs `peacekeeper verify-release-protection` before its tests.
283
288
  The command checks `GITHUB_BASE_REF`, or `git.defaultBranch` outside PR CI.
@@ -291,8 +296,9 @@ jobs in its latest PR workflow run. Manual tags without that PR are rejected.
291
296
  API failures, missing permissions, and unverifiable rules stop the commands.
292
297
  The workflow expects a `RELEASE_POLICY_TOKEN` Actions secret, exposed to the
293
298
  commands as `GH_TOKEN`. Use a fine-grained PAT or GitHub App token with access to
294
- this repository and Administration read, Contents read, Pull requests read and
295
- Actions read permissions. GitHub only exposes ruleset bypass actors to callers
299
+ this repository and Administration read, Contents read/write, Pull requests read and
300
+ Actions read permissions. Contents write is required even to read merge-related
301
+ settings such as `delete_branch_on_merge`. GitHub only exposes ruleset bypass actors to callers
296
302
  with write access to the ruleset; ruleset users must additionally grant that
297
303
  access (Administration write for repository rulesets, or the appropriate access
298
304
  for inherited organization rulesets). Missing bypass information also fails.
@@ -303,8 +309,8 @@ by executing fork code with `pull_request_target`.
303
309
 
304
310
  To set this up for this repository:
305
311
 
306
- 1. Open the [token creation form with the required read permissions prefilled](https://github.com/settings/personal-access-tokens/new?name=peacekeeper-release-policy&target_name=jakobmattsson&expires_in=90&administration=read&contents=read&pull_requests=read&actions=read).
307
- For repository rulesets, use the [form with Administration write instead](https://github.com/settings/personal-access-tokens/new?name=peacekeeper-release-policy&target_name=jakobmattsson&expires_in=90&administration=write&contents=read&pull_requests=read&actions=read)
312
+ 1. Open the [token creation form with the required permissions prefilled](https://github.com/settings/personal-access-tokens/new?name=peacekeeper-release-policy&target_name=jakobmattsson&expires_in=90&administration=read&contents=write&pull_requests=read&actions=read).
313
+ For repository rulesets, use the [form with Administration write instead](https://github.com/settings/personal-access-tokens/new?name=peacekeeper-release-policy&target_name=jakobmattsson&expires_in=90&administration=write&contents=write&pull_requests=read&actions=read)
308
314
  so GitHub exposes bypass actors. For another repository, change the resource
309
315
  owner. Select **Only select repositories**, choose `peacekeeper`, review
310
316
  the permissions and expiration, and generate the token. If the owner requires
@@ -45,7 +45,8 @@ function main() {
45
45
  }
46
46
  const informational = (args.length === 1 && ['--help', '-h'].includes(args[0]))
47
47
  || (command === 'check-project' && args.length === 1 && args[0] === '--explain');
48
- if (command !== 'update-config' && !informational) require('../scripts/project-config').projectConfig();
48
+ const checkpointHook = ['checkpoint-codex-session-start', 'checkpoint-codex-stop'].includes(command);
49
+ if (command !== 'update-config' && !checkpointHook && !informational) require('../scripts/project-config').projectConfig();
49
50
  const script = path.resolve(__dirname, '..', 'scripts', `${command}.js`);
50
51
  const result = spawnSync(process.execPath, [script, ...args], {
51
52
  cwd: process.cwd(),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jakobm/peacekeeper",
3
- "version": "0.13.0",
3
+ "version": "0.14.1",
4
4
  "description": "Shared repository test, Git worktree, and Codex hook commands",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -184,6 +184,9 @@ Online release policy and GitHub token setup
184
184
  local declarations above. Use verify-release-protection to check the branch
185
185
  policy and verify-release before publishing to check the release itself.
186
186
 
187
+ Enable "Automatically delete head branches" (delete_branch_on_merge=true) in
188
+ the repository settings. The online release policy commands require it.
189
+
187
190
  Configure the default branch from git.defaultBranch with every exact job name
188
191
  in git.releaseRequiredChecks mandatory and "Require branches to be up to date
189
192
  before merging" enabled. Choose either:
@@ -197,7 +200,7 @@ Online release policy and GitHub token setup
197
200
  Create a fine-grained GitHub PAT or GitHub App installation token with access
198
201
  to this repository and these repository permissions:
199
202
  Administration: read
200
- Contents: read
203
+ Contents: read/write
201
204
  Pull requests: read
202
205
  Actions: read
203
206
  For repository rulesets, Administration: write is additionally needed because
@@ -78,8 +78,18 @@ function runLoggedCheckpointHook(buildResponse, errorPrefix) {
78
78
  let logPath;
79
79
  let event;
80
80
  let response;
81
+ function log(action) {
82
+ try {
83
+ action();
84
+ } catch (error) {
85
+ process.stderr.write(`${errorPrefix}: logging failed: ${error.message}\n`);
86
+ }
87
+ }
88
+ log(() => { logPath = logScriptInvocation(); });
89
+ function record(value) {
90
+ if (logPath) log(() => fs.appendFileSync(logPath, `${JSON.stringify(value)}\n`));
91
+ }
81
92
  try {
82
- logPath = logScriptInvocation();
83
93
  const input = fs.readFileSync(0, 'utf8').trim();
84
94
  event = input === '' ? null : JSON.parse(input);
85
95
  const details = {
@@ -88,16 +98,20 @@ function runLoggedCheckpointHook(buildResponse, errorPrefix) {
88
98
  source: event?.source,
89
99
  stopHookActive: event?.stop_hook_active,
90
100
  };
91
- if (logPath) fs.appendFileSync(logPath, `${JSON.stringify({ event: details })}\n`);
101
+ record({ event: details });
92
102
  if (typeof event?.cwd === 'string') {
93
103
  const primaryWorktree = isPrimaryWorktree(event.cwd);
94
- if (logPath) fs.appendFileSync(logPath, `${JSON.stringify({ primaryWorktree })}\n`);
104
+ record({ primaryWorktree });
95
105
  }
96
106
  response = buildResponse(event);
97
107
  } catch (error) {
98
- response = { systemMessage: `${errorPrefix}: ${error.message}` };
108
+ const message = `${errorPrefix}: ${error.message}`;
109
+ process.stderr.write(`${message}\n`);
110
+ process.exitCode = 2;
111
+ record({ error: message });
112
+ return;
99
113
  }
100
- if (logPath) fs.appendFileSync(logPath, `${JSON.stringify({ response })}\n`);
114
+ record({ response });
101
115
  process.stdout.write(`${JSON.stringify(response)}\n`);
102
116
  }
103
117
 
@@ -11,6 +11,9 @@ 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
+ if (api(base)?.delete_branch_on_merge !== true) {
15
+ 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
+ }
14
17
  let protection;
15
18
  try {
16
19
  protection = api(`${base}/branches/${encodeURIComponent(branch)}/protection`);
@@ -56,7 +59,7 @@ function githubApi(exec, cwd, host) {
56
59
  return endpoint => {
57
60
  const response = exec('gh', ['api', '--hostname', host, '--method', 'GET', endpoint], { cwd });
58
61
  if (response.status !== 0) {
59
- const error = new Error(`Cannot verify GitHub release policy (${endpoint}): ${response.stderr?.trim() || 'API request failed'}. Grant the token Administration read, Contents read, Pull requests read and Actions read permissions, then rerun.`);
62
+ const error = new Error(`Cannot verify GitHub release policy (${endpoint}): ${response.stderr?.trim() || 'API request failed'}. Grant the token Administration read, Contents read/write, Pull requests read and Actions read permissions, then rerun.`);
60
63
  error.status = Number(/HTTP (\d{3})/u.exec(response.stderr || '')?.[1]);
61
64
  throw error;
62
65
  }