@bompus/house-rules 0.14.1 → 0.15.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 +1 -0
- package/docs/assets/banner-dark.png +0 -0
- package/docs/assets/banner-light.png +0 -0
- package/package.json +1 -1
- package/rules/core.md +8 -4
- package/rules/modifiers/compression-codecs.md +55 -0
- package/rules/modifiers/squash-landing.md +2 -2
- package/rules/modifiers/upstream-contributions.md +4 -2
- package/rules/references/landing.md +25 -16
- package/rules/references/squash-landing.md +5 -4
- package/skills/hr-pr-followup/references/review-helpers.md +27 -7
- package/skills/hr-pr-followup/scripts/check-landing.py +2 -1
- package/skills/hr-pr-followup/scripts/merge-pr.sh +18 -15
- package/skills/hr-pr-followup/scripts/verify-landed.sh +131 -91
package/README.md
CHANGED
|
@@ -236,6 +236,7 @@ in name order, after the modifiers.
|
|
|
236
236
|
| `backlog-outline` | A backlog report adds a Markdown outline of every open item grouped by what it waits on; nothing is rendered, so it reads the same on every host. |
|
|
237
237
|
| `backlog-visual` | A backlog report adds one dark panel of every open item grouped by what it waits on, with a table for waiting items and one row per decision; hosts without a render tool get the same outline in Markdown. |
|
|
238
238
|
| `coded-offers` | Offers use numbered questions and coded options (`1A`, `1B`) so one short reply answers every decision. |
|
|
239
|
+
| `compression-codecs` | Prefer zstd, then brotli, then gzip for data you write, with browser negotiation, gzip exceptions and measured levels. |
|
|
239
240
|
| `cross-session-relay` | Relayed work between sessions keeps its source and authorized scope, and an authorized push that changes inherited guidance or tooling is announced to the affected sessions. |
|
|
240
241
|
| `effort-estimates` | Options that differ in cost, or work that waits on CI, a build or a deploy, carry a wall-clock estimate based on comparable finished work. Estimates from workers, docs or other models are converted the same way or dropped. |
|
|
241
242
|
| `land-when-done` | Authorized repository work is not finished until it is in the remote default branch. |
|
|
Binary file
|
|
Binary file
|
package/package.json
CHANGED
package/rules/core.md
CHANGED
|
@@ -190,6 +190,8 @@ Transfer to another top-level conversation only at the user's direction, under a
|
|
|
190
190
|
|
|
191
191
|
Keep independent repository edits on separate branches and isolated worktrees when a checkout may be shared. Leave other people's and sessions' edits alone.
|
|
192
192
|
|
|
193
|
+
Before creating a branch or worktree, read the default branch of the repository the change will land in (`gh repo view <owner>/<repo> --json defaultBranchRef` or `git ls-remote --symref <remote> HEAD`), fetch it and base the work on its tip. That repository is your fork when the change lands there and upstream when a pull request targets upstream. A branch name shown by a tool or harness is not evidence of the default branch, which need not be `main`. If the repository's own guidance names another base, follow it and say so.
|
|
194
|
+
|
|
193
195
|
Before creating an issue or pull request, search the target repository's open and closed issues and pull requests for the same change. Reuse or coordinate with a match; otherwise link related work and say how yours differs. If you cannot search, say so instead of claiming no matches.
|
|
194
196
|
|
|
195
197
|
Before submitting a pull request to an external upstream project, review the
|
|
@@ -223,9 +225,11 @@ At task completion, unlanded or uncommitted task changes get one landing option,
|
|
|
223
225
|
Before integration, updating a default checkout or deleting a landed branch, read
|
|
224
226
|
[the landing procedure](references/landing.md).
|
|
225
227
|
|
|
226
|
-
|
|
227
|
-
session or job
|
|
228
|
-
|
|
228
|
+
Never reset, stash, switch or merge-commit a checkout to make it match the default.
|
|
229
|
+
A session or job running in it is not a reason to wait or ask. A checkout of the
|
|
230
|
+
default branch that is strictly behind, with no uncommitted changes to tracked files, is fast-forwarded
|
|
231
|
+
with `git merge --ff-only <sha>`. Leave a dirty, diverged or mid-operation one, report its path and reason,
|
|
232
|
+
and do not ask the user.
|
|
229
233
|
|
|
230
234
|
Before the first publish step of a version bump (tag, publish, release, announcement), review unreleased changelog entries and public materials (READMEs, charts, graphics with alt text, listing text, announcement) and fix what is stale first; an urgent release may limit only the materials to what it affects.
|
|
231
235
|
|
|
@@ -236,7 +240,7 @@ Remove a task worktree only when the default branch contains its work, its tree
|
|
|
236
240
|
## Coordination and isolation
|
|
237
241
|
|
|
238
242
|
A coordination roster, where there is one, may omit sessions. Check checkout and
|
|
239
|
-
branch ownership independently before treating them as free.
|
|
243
|
+
branch ownership independently before treating them as free to edit or remove.
|
|
240
244
|
|
|
241
245
|
Stop only processes you started, by the PID you recorded when starting them.
|
|
242
246
|
Never kill by pattern (`pkill -f`, `pgrep | kill`, or a PID found by matching
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Prefer zstd, then brotli, then gzip for data you write, with browser negotiation, gzip exceptions and measured levels.
|
|
3
|
+
after: Implementation economy
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
## Compression codecs
|
|
7
|
+
|
|
8
|
+
When you choose how to compress data a project writes itself (logs, snapshots,
|
|
9
|
+
caches, archives, API payloads), prefer zstd, then brotli, then gzip. Use the
|
|
10
|
+
first codec in that order that every writer and reader of the data supports. A
|
|
11
|
+
preference does not add a codec to a runtime. Node added `zlib.createZstdCompress`
|
|
12
|
+
in 22.15.0 and 23.8.0 and marks it experimental as of Node 26. Check the version
|
|
13
|
+
and the API before relying on it.
|
|
14
|
+
|
|
15
|
+
These figures are single runs on Node. They use three 10.5 MB log files. Zstd at level 9
|
|
16
|
+
compressed 10.0 to 13.2 times, brotli at 4 compressed 9.5 to 12.9 times and gzip at
|
|
17
|
+
6 compressed 4.9 to 9.8 times. Compression took 9 to 14 ms at zstd 3, 24 to 33 ms
|
|
18
|
+
at brotli 4 and 40 to 85 ms at gzip 6. The runs did not time zstd 9 or measure the
|
|
19
|
+
ratio at zstd 3, so they do not show zstd winning on both size and speed at one level.
|
|
20
|
+
|
|
21
|
+
On JSON payloads of 0.1 to 7 MB, also single runs, zstd 3 compressed 1.7 to 2.3
|
|
22
|
+
times faster than brotli 4. Its output was 1% to 4% larger up to 1 MB and 19% larger
|
|
23
|
+
at 7 MB. All of this comes from a few files in one project, not a general claim.
|
|
24
|
+
|
|
25
|
+
Treat these levels as starting points and verify them on your data. Use zstd 3 on hot
|
|
26
|
+
paths and zstd 9 for cold data kept at rest. Use brotli 4 for responses compressed on
|
|
27
|
+
request and brotli 11 for static files compressed ahead of time. The runs above did not
|
|
28
|
+
measure brotli 11.
|
|
29
|
+
|
|
30
|
+
For HTTP responses to browsers, negotiate by `Accept-Encoding` instead of sending
|
|
31
|
+
zstd to every client. Pick the first of zstd, brotli and gzip that the header accepts. A coding
|
|
32
|
+
listed with `q=0` is not acceptable, so skip it. If none is acceptable, send the
|
|
33
|
+
response uncompressed unless the header also rules out `identity`, as `identity;q=0` does.
|
|
34
|
+
Then answer 406 Not Acceptable. Zstd content encoding needs Chrome 123, Edge 123
|
|
35
|
+
or Firefox 126, or later. Safari takes its support from the operating system. It has
|
|
36
|
+
zstd from Safari 26.3 on the 26.3 releases of iOS, iPadOS, visionOS and macOS Tahoe.
|
|
37
|
+
Safari on older releases lacks it, and Can I use shows macOS Safari as only partial. Smaller output matters more on high-latency or slow links, where
|
|
38
|
+
brotli can win despite slower compression.
|
|
39
|
+
|
|
40
|
+
Keep gzip where another tool must read the file: `zcat`, tarballs, npm and release
|
|
41
|
+
assets. WebSocket compression stays on `permessage-deflate`, the only extension
|
|
42
|
+
browsers offer; IANA registers no zstd or brotli extension.
|
|
43
|
+
|
|
44
|
+
Before choosing between codecs or changing a working one, measure on the real data.
|
|
45
|
+
Use `hr-benchmarking`'s defaults: 3 runs per codec, and 2 more when the gap is within
|
|
46
|
+
the run-to-run variation. The gain is output size for cold data and compression time
|
|
47
|
+
for hot paths. The other measure is a constraint, and it must not get worse outside the
|
|
48
|
+
run-to-run variation. Change a working codec only when the gain lies outside that
|
|
49
|
+
variation. Converting existing data is outside a task unless the task asks
|
|
50
|
+
for it.
|
|
51
|
+
|
|
52
|
+
References: [Node zlib](https://nodejs.org/api/zlib.html),
|
|
53
|
+
[MDN Content-Encoding](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Content-Encoding),
|
|
54
|
+
[WebKit features for Safari 26.3](https://webkit.org/blog/17798/webkit-features-for-safari-26-3/) and
|
|
55
|
+
[Can I use zstd](https://caniuse.com/zstd).
|
|
@@ -13,10 +13,10 @@ When a review bot reviews pull requests, merge only after it finishes; fix or
|
|
|
13
13
|
answer all findings, resolve threads, and read the full review body.
|
|
14
14
|
|
|
15
15
|
During integration, never switch, reset, force-move or remove the session's
|
|
16
|
-
checkout. Default-branch updates must meet the fast-forward
|
|
16
|
+
checkout. Default-branch updates must meet the fast-forward
|
|
17
17
|
conditions in § Landing. After the merge, fetch first; move off a landed branch
|
|
18
18
|
only when its content is in the base and the tree is clean. When the base is
|
|
19
19
|
checked out elsewhere, detach at the fetched base. Never merge, reset or stash
|
|
20
20
|
to move the checkout. Leave app-managed checkouts for the app to retire.
|
|
21
|
-
If the tree is dirty
|
|
21
|
+
If the tree is dirty, leave the checkout where it is and record the merged commit, its path and the blocker; the landing stands.
|
|
22
22
|
Update the task record and follow § Cleanup after each landing.
|
|
@@ -19,8 +19,10 @@ issues invite, such as performance fixes or a reported defect.
|
|
|
19
19
|
request. A behavior change, or a project that asks for prior approval, starts
|
|
20
20
|
with a comment or discussion, not a pull request. Count the underlying
|
|
21
21
|
problems and split the submission when the guide asks for one per request.
|
|
22
|
-
3. Meet § Repository work first
|
|
23
|
-
|
|
22
|
+
3. Meet § Repository work first. Base the work on the default branch of the
|
|
23
|
+
repository the pull request targets. Search for existing reports, review the
|
|
24
|
+
diff, run the relevant checks, and follow the project's template and
|
|
25
|
+
disclosure ask.
|
|
24
26
|
4. Show the user the exact text and channel once before posting. Posting is
|
|
25
27
|
public and cannot be undone; this rule makes submitting the expected next
|
|
26
28
|
step, not a waiver of that look. Only a user instruction naming the kind of
|
|
@@ -9,25 +9,34 @@ recommended option includes it, is that direction.
|
|
|
9
9
|
|
|
10
10
|
At task completion, unlanded or uncommitted task changes get one landing option, `Land PR #<n>` or `Commit and land <change>`, never a bare done; only an explicit user deferral (recorded with branch, commits and reason) leaves it out. The option covers this section and § Cleanup, merge included; name only departures from that. Land through the repository's required path without bypassing checks. Landing is done when a fresh fetch shows the change in the default branch.
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
session or job
|
|
14
|
-
|
|
12
|
+
Never reset, stash, switch or merge-commit a checkout to make it match the default.
|
|
13
|
+
A session or job running in it is not a reason to wait or ask. A checkout of the
|
|
14
|
+
default branch that is strictly behind, with no uncommitted changes to tracked files, is fast-forwarded
|
|
15
|
+
with `git merge --ff-only <sha>`. Leave a dirty, diverged or mid-operation one, report its path and reason,
|
|
16
|
+
and do not ask the user.
|
|
17
|
+
Landing never waits on a local checkout, and a roster entry in one is not an input either: the
|
|
18
|
+
checkout's own state decides whether it can be fast-forwarded (below). Removing a worktree is
|
|
19
|
+
different; § Cleanup still waits on use.
|
|
15
20
|
|
|
16
21
|
After integration:
|
|
17
22
|
|
|
18
|
-
- Fetch, then bring the local default branch current
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
23
|
+
- Fetch, then bring the local default branch current, whoever else has it open.
|
|
24
|
+
When no checkout has it, fast-forward the branch ref. When one checkout has it
|
|
25
|
+
(your own, the primary or another linked worktree), run `git merge --ff-only <sha>`
|
|
26
|
+
there with the remote head you just fetched and verified, provided that checkout
|
|
27
|
+
is on the default branch, has no uncommitted changes to tracked files and no
|
|
28
|
+
unfinished Git operation (merge, rebase, cherry-pick, revert, bisect), and is
|
|
29
|
+
strictly behind. Read that state right before the merge. Untracked files may
|
|
30
|
+
stay, and Git refuses a merge that would overwrite one. Avoid `git pull` there: a fetch by another process can rewrite the shared
|
|
31
|
+
`FETCH_HEAD` mid-pull and fail it with "Cannot fast-forward to multiple
|
|
32
|
+
branches". Confirm the local and fetched remote heads match.
|
|
33
|
+
- If that checkout is dirty, diverged, mid-operation or no longer on the default
|
|
34
|
+
branch, or the merge refuses because local edits would be overwritten, leave it
|
|
35
|
+
and write down its path and the reason as a pending local sync. The landing
|
|
36
|
+
stands, so finish the task and do not ask the user to confirm or wait for the
|
|
37
|
+
checkout to go idle. Never merge, reset, stash or switch branches to make it
|
|
38
|
+
match, and never touch a separate clone or a worktree that does not hold the
|
|
39
|
+
default branch.
|
|
31
40
|
- Delete the merged remote branch when it is this task's own branch
|
|
32
41
|
(`git push origin --delete <branch>` when the merge did not); § Cleanup
|
|
33
42
|
covers branches others may rely on. Before merging a pull request that another open pull
|
|
@@ -26,8 +26,9 @@ During integration, never switch, reset or remove the session's own checkout: no
|
|
|
26
26
|
`checkout` or `switch` to another ref, no `reset` or branch force-move, and no
|
|
27
27
|
`worktree remove` on it. Updating the default branch it has checked out
|
|
28
28
|
(`merge --squash`, or `merge --ff-only` to the fetched remote head after a remote merge)
|
|
29
|
-
under the fast-forward conditions in § Landing is expected
|
|
30
|
-
|
|
29
|
+
under the fast-forward conditions in § Landing is expected, and so is fast-forwarding
|
|
30
|
+
the one other checkout that holds the default branch. Report a checkout that cannot be
|
|
31
|
+
fast-forwarded with its path and reason.
|
|
31
32
|
|
|
32
33
|
After the merge, do not leave the session's own checkout on a landed branch.
|
|
33
34
|
Fetch first, then:
|
|
@@ -41,8 +42,8 @@ Fetch first, then:
|
|
|
41
42
|
commits.
|
|
42
43
|
- When an app manages the checkout, leave it for the app to retire. Report it;
|
|
43
44
|
it is not a blocker.
|
|
44
|
-
- When the tree is dirty
|
|
45
|
-
|
|
45
|
+
- When the tree is dirty, leave the checkout where it is and record the merged
|
|
46
|
+
commit, the checkout path and the blocker. The landing stands.
|
|
46
47
|
|
|
47
48
|
After each landing, update the task list and any active handoff with the
|
|
48
49
|
commit, the verification result and the remaining work, then retire the task
|
|
@@ -108,18 +108,38 @@ It does not clear that gate's review-body findings or change the landing workflo
|
|
|
108
108
|
Three scripts in `scripts/` implement the landing steps § Landing describes. They never
|
|
109
109
|
reset, switch or delete a checkout.
|
|
110
110
|
|
|
111
|
-
- `merge-pr.sh <pr> [--merge] [--remote <name>]
|
|
111
|
+
- `merge-pr.sh <pr> [--merge] [--remote <name>]` squash-merges
|
|
112
112
|
(or merge-commits) an open pull request and deletes its remote head without
|
|
113
113
|
`gh pr merge --delete-branch`, which removes other sessions' linked worktrees. It
|
|
114
114
|
refuses while review feedback is open (`check-pr-review.ts`), while another open pull
|
|
115
115
|
request targets the head, and for a repository listed in `HR_PUSH_LANDED`
|
|
116
116
|
(space-separated `owner/repo`; those land by fast-forward push, which it prints). The
|
|
117
117
|
merge passes `--match-head-commit`. It lists worktrees still on the merged branch so
|
|
118
|
-
their owners clean up.
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
118
|
+
their owners clean up. The merge never waits on a local checkout; the local default is
|
|
119
|
+
synchronized afterwards as `verify-landed.sh` describes.
|
|
120
|
+
- `verify-landed.sh --commit <sha> [--ff]` fetches, checks that the default branch
|
|
121
|
+
contains the commit, and checks the local default. `--ff` brings the local default
|
|
122
|
+
current: it moves the branch ref when no checkout has it, and runs
|
|
123
|
+
`merge --ff-only <sha>` in the one checkout that has it when that checkout is on the
|
|
124
|
+
default branch, free of uncommitted changes to tracked files and of an unfinished Git
|
|
125
|
+
operation, and strictly behind. Untracked files may stay; Git refuses a fast-forward
|
|
126
|
+
that would overwrite one. Sessions,
|
|
127
|
+
processes and roster entries are never inputs. Any other checkout is left alone and
|
|
128
|
+
reported on one line, `sync skipped at <path>: <reason>`, with a reason of
|
|
129
|
+
`working tree dirty` (tracked changes), `diverged from <remote>/<default>`, `on branch
|
|
130
|
+
<name>, not <default>`, `unfinished Git operation: <name>`, `fast-forward refused: <why>`,
|
|
131
|
+
`cannot inspect Git state` or `cannot inspect working tree`.
|
|
132
|
+
Exit 0 means landed and current; 1 is a failure; 2 is usage; 3 means landed with a local
|
|
133
|
+
sync pending, which is not a failed landing and needs no question to the user: record
|
|
134
|
+
the path and reason in the task plan. Both scripts use these exit codes. After a
|
|
135
|
+
failure, rerun this script, not `merge-pr.sh`. Both scripts accept the old
|
|
136
|
+
`--idle-primary <path>` flag and ignore it with a note. It takes the next argument as its path unless that
|
|
137
|
+
argument is missing or starts with `-`. `verify-landed.sh` also accepts `--preflight`. Without
|
|
138
|
+
`--commit` and `--branch`, it prints a note and `preflight skipped: it no longer gates landing`, checks
|
|
139
|
+
nothing and exits 0. With either one it exits 2. `merge-pr.sh` rejects `--preflight` with exit 2. Both
|
|
140
|
+
flags will be removed.
|
|
123
141
|
- `check-landing.py [--path <checkout>] [--base <ref>]` reports whether a checkout's
|
|
124
142
|
work is in the base branch by content, so squash-merged branches count as landed, and
|
|
125
|
-
lists live processes whose working directory is inside the checkout (Linux only).
|
|
143
|
+
lists live processes whose working directory is inside the checkout (Linux only). That
|
|
144
|
+
list is a blocker for removing the worktree only. Landing and syncing the default
|
|
145
|
+
checkout never read it.
|
|
@@ -8,7 +8,8 @@ the base. Read-only; never fetches — the caller fetches first.
|
|
|
8
8
|
|
|
9
9
|
It also lists live processes whose working directory is inside the checkout
|
|
10
10
|
(Linux /proc only), excluding the checker's ancestors and its pipeline. Each listed process
|
|
11
|
-
blocks worktree removal unless its PID is one you recorded when you started it.
|
|
11
|
+
blocks worktree removal unless its PID is one you recorded when you started it. The list says nothing
|
|
12
|
+
about landing or about fast-forwarding a default checkout: neither reads it. `own_session`
|
|
12
13
|
is true only for a process that descends from the nearest agent CLI above the checker (a
|
|
13
14
|
process whose comm is one of AGENT_COMMS); a process under another session of the same agent
|
|
14
15
|
host's server, or any process when the checker has no agent ancestor, is false. A process
|
|
@@ -15,25 +15,26 @@
|
|
|
15
15
|
# empty by default): its own guidance lands by fast-forwarding the base to the head, because a GitHub
|
|
16
16
|
# merge writes the account's profile name into public history. It prints that push instead.
|
|
17
17
|
#
|
|
18
|
-
#
|
|
19
|
-
#
|
|
20
|
-
#
|
|
21
|
-
#
|
|
22
|
-
#
|
|
23
|
-
#
|
|
24
|
-
# If verification
|
|
25
|
-
# fails after "merged", rerun verify-landed.sh with the printed commit, not this script.
|
|
18
|
+
# Landing never waits on a local checkout. After the merge it fast-forwards the checkout that holds
|
|
19
|
+
# the default branch when that checkout is on the default branch, clean, free of an unfinished Git
|
|
20
|
+
# operation and strictly behind (verify-landed.sh), whoever else has it open. Otherwise it leaves the
|
|
21
|
+
# checkout alone, prints `sync skipped at <path>: <reason>` and exits 3: landed, local sync pending.
|
|
22
|
+
# Record the path and reason in the task plan; nothing needs the user. If verification fails after
|
|
23
|
+
# "merged" (exit 1), rerun verify-landed.sh with the printed commit, not this script.
|
|
26
24
|
#
|
|
27
25
|
# --merge lands with a merge commit instead of a squash. Use it only for a PR whose second parent
|
|
28
26
|
# must stay in history, such as an `-s ours` merge that records an upstream reconcile; a squash
|
|
29
27
|
# would drop that parent. Every other PR is squashed.
|
|
30
28
|
#
|
|
31
|
-
#
|
|
29
|
+
# Exit status: 0 landed and every local default current; 1 refused or failed; 2 usage; 3 landed, local sync pending.
|
|
30
|
+
# --idle-primary <path> is deprecated: accepted and ignored with a note on stderr, removed in a later release.
|
|
31
|
+
#
|
|
32
|
+
# Usage: bash scripts/merge-pr.sh <pr> [--merge] [--remote <name>]
|
|
32
33
|
set -euo pipefail
|
|
33
34
|
|
|
34
35
|
PUSH_LANDED=" ${HR_PUSH_LANDED:-} "
|
|
35
36
|
|
|
36
|
-
PR="${1:?usage: merge-pr.sh <pr> [--merge] [--remote <name>]
|
|
37
|
+
PR="${1:?usage: merge-pr.sh <pr> [--merge] [--remote <name>]}"
|
|
37
38
|
shift
|
|
38
39
|
REMOTE=origin
|
|
39
40
|
STRATEGY=--squash
|
|
@@ -42,7 +43,9 @@ while [ $# -gt 0 ]; do
|
|
|
42
43
|
case "$1" in
|
|
43
44
|
--merge) STRATEGY=--merge; shift;;
|
|
44
45
|
--remote) REMOTE="${2:?}"; shift 2;;
|
|
45
|
-
--idle-primary)
|
|
46
|
+
--idle-primary)
|
|
47
|
+
echo "note: --idle-primary is ignored; the checkout holding the default branch is fast-forwarded when it is clean and strictly behind. The flag will be removed in a later release." >&2
|
|
48
|
+
shift; case "${1:-}" in ''|-*) ;; *) shift;; esac;;
|
|
46
49
|
*) echo "unknown argument: $1" >&2; exit 2;;
|
|
47
50
|
esac
|
|
48
51
|
done
|
|
@@ -67,9 +70,6 @@ if [[ "$PUSH_LANDED" == *" $REPO "* ]]; then
|
|
|
67
70
|
exit 1
|
|
68
71
|
fi
|
|
69
72
|
|
|
70
|
-
bash "$(dirname "${BASH_SOURCE[0]}")/verify-landed.sh" --preflight \
|
|
71
|
-
--default "$BASE" --remote "$REMOTE" "${SYNC_ARGS[@]}"
|
|
72
|
-
|
|
73
73
|
gh pr merge "$PR" "$STRATEGY" --match-head-commit "$OID"
|
|
74
74
|
SHA="$(gh pr view "$PR" --json mergeCommit --jq '.mergeCommit.oid // empty')"
|
|
75
75
|
if ! [[ "$SHA" =~ ^([0-9a-f]{40}|[0-9a-f]{64})$ ]]; then
|
|
@@ -91,10 +91,13 @@ if [ "$FORK" != true ]; then
|
|
|
91
91
|
fi
|
|
92
92
|
fi
|
|
93
93
|
|
|
94
|
+
# Exit 3 (local sync pending) is not a failure: finish the worktree report, then pass it on.
|
|
95
|
+
STATUS=0
|
|
94
96
|
bash "$(dirname "${BASH_SOURCE[0]}")/verify-landed.sh" --commit "$SHA" --branch "$HEAD" \
|
|
95
|
-
--default "$BASE" --remote "$REMOTE" "${SYNC_ARGS[@]}"
|
|
97
|
+
--default "$BASE" --remote "$REMOTE" "${SYNC_ARGS[@]}" || STATUS=$?
|
|
96
98
|
|
|
97
99
|
git worktree list --porcelain | awk -v want="branch refs/heads/$HEAD" '
|
|
98
100
|
/^worktree / { path = substr($0, 10) }
|
|
99
101
|
$0 == want { print "still on " substr(want, 19) ": " path " (its owner retires it)" }
|
|
100
102
|
'
|
|
103
|
+
exit "$STATUS"
|
|
@@ -2,25 +2,35 @@
|
|
|
2
2
|
# Post-merge landing checks: containment, local-default state, leftover remote branches.
|
|
3
3
|
# Fetches and verifies both remote containment and local-default equality. A stale local default
|
|
4
4
|
# fails even when the remote contains the commit. Never resets, switches or deletes.
|
|
5
|
-
#
|
|
6
|
-
# --
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
5
|
+
#
|
|
6
|
+
# With --ff it brings the local default branch current, whoever else has that checkout open. Which
|
|
7
|
+
# processes, sessions or roster entries sit in a checkout is never an input. The branch moves when
|
|
8
|
+
# it is checked out nowhere (a ref update), or when the one checkout that holds it is on the default
|
|
9
|
+
# branch, free of uncommitted changes to tracked files and of an unfinished Git operation, and
|
|
10
|
+
# strictly behind: then
|
|
11
|
+
# `merge --ff-only <sha>` runs there. It uses the verified remote commit rather than `pull`, because
|
|
11
12
|
# `pull` re-fetches into the shared FETCH_HEAD, which a concurrent fetch in the same checkout can
|
|
12
|
-
# overwrite ("Cannot fast-forward to multiple branches").
|
|
13
|
-
#
|
|
14
|
-
#
|
|
15
|
-
#
|
|
16
|
-
#
|
|
17
|
-
#
|
|
18
|
-
#
|
|
13
|
+
# overwrite ("Cannot fast-forward to multiple branches"). Untracked files may stay: Git keeps them
|
|
14
|
+
# and refuses a fast-forward that would overwrite one.
|
|
15
|
+
#
|
|
16
|
+
# A checkout that fails any of those conditions is left alone and reported on one line:
|
|
17
|
+
# sync skipped at <path>: <reason>
|
|
18
|
+
# with a reason of "working tree dirty" (tracked changes), "diverged from <remote>/<default>", "on
|
|
19
|
+
# branch <name>, not <default>", "unfinished Git operation: <name>", "fast-forward refused: <why>",
|
|
20
|
+
# "cannot inspect Git state" or "cannot inspect working tree". A diverged default that no checkout
|
|
21
|
+
# holds is reported at refs/heads/<default>. The landing itself stands; record the path and reason
|
|
22
|
+
# where you track the task and do not ask the user.
|
|
23
|
+
#
|
|
24
|
+
# Exit status: 0 landed and every local default current; 1 a real failure (commit not contained,
|
|
25
|
+
# stale default with nothing skipped, leftover remote branch, fetch failure); 2 usage; 3 landed and
|
|
26
|
+
# verified, but a local checkout was skipped (local sync pending). After a failure, rerun this
|
|
27
|
+
# script, not merge-pr.sh: the PR is already merged.
|
|
28
|
+
#
|
|
29
|
+
# Deprecated, accepted and ignored with a note on stderr, and removed in a later release:
|
|
30
|
+
# --idle-primary <path> and --preflight. Landing no longer waits on any local checkout.
|
|
19
31
|
# Usage:
|
|
20
32
|
# bash scripts/verify-landed.sh --commit <sha> [--default <branch>]
|
|
21
|
-
# [--branch <merged-head>] [--remote <name>] [--ff
|
|
22
|
-
# bash scripts/verify-landed.sh --preflight --ff [--default <branch>]
|
|
23
|
-
# [--remote <name>] [--idle-primary <path>]
|
|
33
|
+
# [--branch <merged-head>] [--remote <name>] [--ff]
|
|
24
34
|
set -euo pipefail
|
|
25
35
|
|
|
26
36
|
REMOTE=origin
|
|
@@ -28,8 +38,7 @@ DEFAULT=""
|
|
|
28
38
|
COMMIT=""
|
|
29
39
|
BRANCH=""
|
|
30
40
|
FF=0
|
|
31
|
-
|
|
32
|
-
PREFLIGHT=0
|
|
41
|
+
LEGACY_PREFLIGHT=0
|
|
33
42
|
|
|
34
43
|
while [ $# -gt 0 ]; do
|
|
35
44
|
case "$1" in
|
|
@@ -38,24 +47,22 @@ while [ $# -gt 0 ]; do
|
|
|
38
47
|
--branch) BRANCH="${2:?}"; shift 2;;
|
|
39
48
|
--remote) REMOTE="${2:?}"; shift 2;;
|
|
40
49
|
--ff) FF=1; shift;;
|
|
41
|
-
--idle-primary)
|
|
42
|
-
|
|
50
|
+
--idle-primary)
|
|
51
|
+
echo "note: --idle-primary is ignored; the checkout holding the default branch is fast-forwarded when it is clean and strictly behind. The flag will be removed in a later release." >&2
|
|
52
|
+
shift; case "${1:-}" in ''|-*) ;; *) shift;; esac;;
|
|
53
|
+
--preflight)
|
|
54
|
+
LEGACY_PREFLIGHT=1
|
|
55
|
+
echo "note: --preflight is ignored; landing no longer waits on local checkouts. The flag will be removed in a later release." >&2
|
|
56
|
+
shift;;
|
|
43
57
|
*) echo "unknown argument: $1" >&2; exit 2;;
|
|
44
58
|
esac
|
|
45
59
|
done
|
|
46
|
-
if [ "$
|
|
47
|
-
[ "$
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
[ -n "$COMMIT" ] || { echo "missing --commit <sha>" >&2; exit 2; }
|
|
51
|
-
fi
|
|
52
|
-
if [ -n "$IDLE_PRIMARY" ]; then
|
|
53
|
-
[ "$FF" = 1 ] || { echo "--idle-primary requires --ff" >&2; exit 2; }
|
|
54
|
-
PRIMARY="$(git worktree list --porcelain | sed -n '1s/^worktree //p')"
|
|
55
|
-
IDLE_PRIMARY="$(git -C "$IDLE_PRIMARY" rev-parse --show-toplevel)"
|
|
56
|
-
[ "$IDLE_PRIMARY" = "$PRIMARY" ] \
|
|
57
|
-
|| { echo "--idle-primary is not this repository's primary checkout: $IDLE_PRIMARY" >&2; exit 2; }
|
|
60
|
+
if [ "$LEGACY_PREFLIGHT" = 1 ]; then
|
|
61
|
+
[ -z "$COMMIT$BRANCH" ] || { echo "--preflight excludes --commit/--branch" >&2; exit 2; }
|
|
62
|
+
echo "preflight skipped: it no longer gates landing"
|
|
63
|
+
exit 0
|
|
58
64
|
fi
|
|
65
|
+
[ -n "$COMMIT" ] || { echo "missing --commit <sha>" >&2; exit 2; }
|
|
59
66
|
|
|
60
67
|
if [ -z "$DEFAULT" ]; then
|
|
61
68
|
# The remote's own answer wins; the cached origin/HEAD goes stale when the default branch changes.
|
|
@@ -76,48 +83,64 @@ HOLDER="$(printf '%s\n' "$WTLS" | awk -v want="branch refs/heads/$DEFAULT" '
|
|
|
76
83
|
')"
|
|
77
84
|
HERE="$(git rev-parse --show-toplevel)"
|
|
78
85
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
86
|
+
# A rebase or bisect detaches HEAD while still reserving its original branch.
|
|
87
|
+
if [ -z "$HOLDER" ]; then
|
|
88
|
+
while IFS= read -r RECORD; do
|
|
89
|
+
case "$RECORD" in worktree\ *) CHECKOUT="${RECORD#worktree }";; *) continue;; esac
|
|
90
|
+
[ -d "$CHECKOUT" ] || continue
|
|
91
|
+
for ASSOCIATION in rebase-merge/head-name rebase-apply/head-name BISECT_START; do
|
|
92
|
+
# A checkout whose state cannot be read counts as holding the branch: holder_blocker then skips it.
|
|
93
|
+
ASSOCIATION_PATH="$(git -C "$CHECKOUT" rev-parse --path-format=absolute --git-path "$ASSOCIATION" 2>/dev/null)" || {
|
|
94
|
+
HOLDER="$CHECKOUT"
|
|
95
|
+
break 2
|
|
96
|
+
}
|
|
97
|
+
if [ -f "$ASSOCIATION_PATH" ]; then
|
|
98
|
+
ASSOCIATED_BRANCH=""
|
|
99
|
+
if ! { IFS= read -r ASSOCIATED_BRANCH < "$ASSOCIATION_PATH" || [ -n "$ASSOCIATED_BRANCH" ]; } 2>/dev/null; then
|
|
100
|
+
HOLDER="$CHECKOUT"
|
|
101
|
+
break 2
|
|
93
102
|
fi
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
echo "sync blocked at $HOLDER: that checkout has the default branch. If nothing is using it, rerun with --idle-primary <primary-path>" >&2
|
|
100
|
-
exit 1
|
|
101
|
-
fi
|
|
102
|
-
STATUS="$(git -C "$HOLDER" status --porcelain)" \
|
|
103
|
-
|| { echo "sync blocked at $HOLDER: cannot inspect working tree" >&2; exit 1; }
|
|
104
|
-
[ -z "$STATUS" ] \
|
|
105
|
-
|| { echo "sync blocked at $HOLDER: working tree dirty; leave it for its owner" >&2; exit 1; }
|
|
106
|
-
for OPERATION in MERGE_HEAD CHERRY_PICK_HEAD REVERT_HEAD rebase-merge rebase-apply sequencer BISECT_START; do
|
|
107
|
-
OPERATION_PATH="$(git -C "$HOLDER" rev-parse --path-format=absolute --git-path "$OPERATION")"
|
|
108
|
-
[ ! -e "$OPERATION_PATH" ] \
|
|
109
|
-
|| { echo "sync blocked at $HOLDER: unfinished Git operation: $OPERATION" >&2; exit 1; }
|
|
103
|
+
if [ "$ASSOCIATED_BRANCH" = "refs/heads/$DEFAULT" ] || [ "$ASSOCIATED_BRANCH" = "$DEFAULT" ]; then
|
|
104
|
+
HOLDER="$CHECKOUT"
|
|
105
|
+
break 2
|
|
106
|
+
fi
|
|
107
|
+
fi
|
|
110
108
|
done
|
|
111
|
-
|
|
112
|
-
if git show-ref --verify --quiet "refs/heads/$DEFAULT"; then
|
|
113
|
-
git merge-base --is-ancestor "$DEFAULT" "$REMOTE/$DEFAULT" \
|
|
114
|
-
|| { echo "sync blocked: $DEFAULT cannot fast-forward to $REMOTE/$DEFAULT" >&2; exit 1; }
|
|
115
|
-
fi
|
|
116
|
-
echo "synchronization preflight passed: $DEFAULT (${HOLDER:-not checked out})"
|
|
117
|
-
exit 0
|
|
109
|
+
done <<< "$WTLS"
|
|
118
110
|
fi
|
|
119
111
|
|
|
120
112
|
FAIL=0
|
|
113
|
+
SKIPPED=0
|
|
114
|
+
|
|
115
|
+
skip() {
|
|
116
|
+
echo "sync skipped at $1: $2"
|
|
117
|
+
SKIPPED=1
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
# Prints why a checkout cannot be fast-forwarded now, or nothing when it can.
|
|
121
|
+
holder_blocker() {
|
|
122
|
+
local path="$1" operation operation_path branch status
|
|
123
|
+
for operation in MERGE_HEAD CHERRY_PICK_HEAD REVERT_HEAD rebase-merge rebase-apply sequencer BISECT_START; do
|
|
124
|
+
operation_path="$(git -C "$path" rev-parse --path-format=absolute --git-path "$operation" 2>/dev/null)" \
|
|
125
|
+
|| { echo "cannot inspect Git state"; return; }
|
|
126
|
+
if [ -e "$operation_path" ]; then
|
|
127
|
+
echo "unfinished Git operation: $operation"
|
|
128
|
+
return
|
|
129
|
+
fi
|
|
130
|
+
done
|
|
131
|
+
branch="$(git -C "$path" symbolic-ref --quiet HEAD 2>/dev/null || true)"
|
|
132
|
+
if [ "$branch" != "refs/heads/$DEFAULT" ]; then
|
|
133
|
+
if [ -n "$branch" ]; then
|
|
134
|
+
echo "on branch ${branch#refs/heads/}, not $DEFAULT"
|
|
135
|
+
else
|
|
136
|
+
echo "on branch (detached HEAD), not $DEFAULT"
|
|
137
|
+
fi
|
|
138
|
+
return
|
|
139
|
+
fi
|
|
140
|
+
# Untracked files may stay: Git itself refuses a fast-forward that would overwrite one.
|
|
141
|
+
status="$(git -C "$path" status --porcelain --untracked-files=no 2>/dev/null)" || { echo "cannot inspect working tree"; return; }
|
|
142
|
+
[ -z "$status" ] || echo "working tree dirty"
|
|
143
|
+
}
|
|
121
144
|
|
|
122
145
|
if git merge-base --is-ancestor "$COMMIT" "$REMOTE/$DEFAULT" 2>/dev/null; then
|
|
123
146
|
echo "contained: $COMMIT in $REMOTE/$DEFAULT"
|
|
@@ -133,35 +156,45 @@ if git show-ref --verify --quiet "refs/heads/$DEFAULT"; then
|
|
|
133
156
|
echo "local $DEFAULT: current ($LOCAL)"
|
|
134
157
|
elif git merge-base --is-ancestor "$LOCAL" "$UPSTREAM" 2>/dev/null; then
|
|
135
158
|
BEHIND="$(git rev-list --count "$LOCAL..$UPSTREAM")"
|
|
136
|
-
if [
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
159
|
+
if [ "$FF" != 1 ] || [ "$FAIL" != 0 ]; then
|
|
160
|
+
WHY="not updated without --ff"
|
|
161
|
+
[ "$FF" != 1 ] || WHY="not updated: the landing check failed"
|
|
162
|
+
if [ -z "$HOLDER" ]; then
|
|
163
|
+
if [ "$FF" != 1 ]; then
|
|
164
|
+
echo "local $DEFAULT: $BEHIND behind, not checked out anywhere. Safe: git fetch $REMOTE $DEFAULT:$DEFAULT"
|
|
165
|
+
else
|
|
166
|
+
echo "local $DEFAULT: $BEHIND behind, not checked out anywhere; $WHY"
|
|
167
|
+
fi
|
|
140
168
|
else
|
|
141
|
-
echo "local $DEFAULT: $BEHIND behind,
|
|
169
|
+
echo "local $DEFAULT: $BEHIND behind, checked out at $HOLDER; $WHY"
|
|
142
170
|
fi
|
|
143
|
-
elif [ "$HOLDER"
|
|
144
|
-
if
|
|
145
|
-
|
|
146
|
-
|| { echo "primary checkout changed branches; refusing update" >&2; exit 1; }
|
|
147
|
-
git -C "$HOLDER" merge --quiet --ff-only "$UPSTREAM"
|
|
148
|
-
echo "local $DEFAULT: fast-forwarded $BEHIND commit(s) in confirmed idle primary $HOLDER"
|
|
171
|
+
elif [ -z "$HOLDER" ]; then
|
|
172
|
+
if REF_ERR="$(git update-ref -m "verify-landed: fast-forward $DEFAULT" "refs/heads/$DEFAULT" "$UPSTREAM" "$LOCAL" 2>&1)"; then
|
|
173
|
+
echo "local $DEFAULT: fast-forwarded $BEHIND commit(s) to $UPSTREAM (not checked out anywhere)"
|
|
149
174
|
else
|
|
150
|
-
|
|
151
|
-
echo "If nothing is using that checkout, rerun verify-landed.sh with --ff --idle-primary <primary-path>." >&2
|
|
175
|
+
skip "refs/heads/$DEFAULT" "fast-forward refused: $(printf '%s' "$REF_ERR" | head -n 1)"
|
|
152
176
|
fi
|
|
153
|
-
elif [ -n "$(git status --porcelain)" ]; then
|
|
154
|
-
echo "local $DEFAULT: $BEHIND behind but working tree dirty — report, do not touch"
|
|
155
|
-
elif [ "$FF" = 1 ] && [ "$FAIL" = 0 ]; then
|
|
156
|
-
git merge --quiet --ff-only "$UPSTREAM"
|
|
157
|
-
echo "local $DEFAULT: fast-forwarded $BEHIND commit(s) to $UPSTREAM (checked out here)"
|
|
158
177
|
else
|
|
159
|
-
|
|
178
|
+
# The state is read right before the merge; Git refuses it if edits would be overwritten.
|
|
179
|
+
REASON="$(holder_blocker "$HOLDER")"
|
|
180
|
+
if [ -n "$REASON" ]; then
|
|
181
|
+
skip "$HOLDER" "$REASON"
|
|
182
|
+
elif MERGE_ERR="$(git -C "$HOLDER" merge --quiet --ff-only "$UPSTREAM" 2>&1)"; then
|
|
183
|
+
if [ "$HOLDER" = "$HERE" ]; then
|
|
184
|
+
echo "local $DEFAULT: fast-forwarded $BEHIND commit(s) to $UPSTREAM (checked out here)"
|
|
185
|
+
else
|
|
186
|
+
echo "local $DEFAULT: fast-forwarded $BEHIND commit(s) to $UPSTREAM in $HOLDER"
|
|
187
|
+
fi
|
|
188
|
+
elif printf '%s' "$MERGE_ERR" | grep -q "would be overwritten"; then
|
|
189
|
+
skip "$HOLDER" "fast-forward refused: local edits would be overwritten"
|
|
190
|
+
else
|
|
191
|
+
skip "$HOLDER" "fast-forward refused: $(printf '%s' "$MERGE_ERR" | head -n 1)"
|
|
192
|
+
fi
|
|
160
193
|
fi
|
|
161
194
|
else
|
|
162
|
-
|
|
195
|
+
skip "${HOLDER:-refs/heads/$DEFAULT}" "diverged from $REMOTE/$DEFAULT"
|
|
163
196
|
fi
|
|
164
|
-
if [ "$(git rev-parse "$DEFAULT")" != "$(git rev-parse "$REMOTE/$DEFAULT")" ]; then
|
|
197
|
+
if [ "$(git rev-parse "$DEFAULT")" != "$(git rev-parse "$REMOTE/$DEFAULT")" ] && [ "$SKIPPED" = 0 ]; then
|
|
165
198
|
echo "LOCAL DEFAULT NOT CURRENT: $DEFAULT differs from $REMOTE/$DEFAULT; landing incomplete" >&2
|
|
166
199
|
FAIL=1
|
|
167
200
|
fi
|
|
@@ -181,4 +214,11 @@ if [ -n "$BRANCH" ]; then
|
|
|
181
214
|
fi
|
|
182
215
|
fi
|
|
183
216
|
|
|
184
|
-
|
|
217
|
+
if [ "$FAIL" != 0 ]; then
|
|
218
|
+
exit 1
|
|
219
|
+
fi
|
|
220
|
+
if [ "$SKIPPED" = 1 ]; then
|
|
221
|
+
echo "landed; local sync pending (exit 3): record the path and reason above in the task plan"
|
|
222
|
+
exit 3
|
|
223
|
+
fi
|
|
224
|
+
exit 0
|