* fix(release-sdk): skip cherry-picks whose target context is absent at base
When auto_cherry_pick processed a fix:/chore: commit whose patch modified
code that didn't exist at the hotfix base tag — typically because the
surrounding infrastructure was added later in a feat/refactor commit
excluded by the filter — `git cherry-pick` failed with a conflict that
no operator could meaningfully resolve, and the loop bricked the run.
Discovered re-running the 1.39.1 dry-run after #2965 merged: cherry-pick
of `a3467792` (the #2965 merge itself) failed because the auto_cherry_pick
block it modifies was added in #2956 ("Add automated cherry-pick + SDK-
bundle parity to hotfix flow") — an Add/feat commit, so the fix/chore
filter excludes it. v1.39.0 has no such block, so the patch had no
anchor.
The conflict is unmistakably distinguishable from a real content conflict:
git emits marker blocks where every `<<<<<<< HEAD ... =======` HEAD
section is empty (no anchor lines to reconcile against), while real
conflicts have content on both sides.
After cherry-pick fails:
1. List unmerged paths via `git diff --diff-filter=U`.
2. For each, scan conflict markers with awk. If every HEAD section is
blank/whitespace-only across every block, classify as
context-missing.
3. Context-missing → `git cherry-pick --skip` and append to SKIPPED
list with reason "(context absent at base)".
4. Otherwise fall through to the existing abort/push-partial/error
path that surfaces the conflict for operator resolution.
Real conflicts still surface with the same workflow as before.
Tests in tests/bug-2966-cherry-pick-context-missing.test.cjs cover:
- Static — extracts the "Prepare hotfix branch" run block via
indentation-aware YAML parsing (no raw-text grep) and asserts the
classification predicate, --skip call, and skipped-reason annotation
are present.
- Behavioral — synthetic repo reproducing the real shape of the
failure, asserts cherry-pick exits non-zero and produces the
empty-HEAD marker shape.
- Predicate — pulls the awk script out of the deployed workflow and
feeds it sample conflict shapes (empty-HEAD, real, mixed,
whitespace-only); asserts each is classified as the workflow will
behave.
Local: `node --test tests/bug-2966-...test.cjs` → 3/3 pass.
Local: `npm run lint:tests` → 0 violations.
https://claude.ai/code/session_01LApueb9PVs2uSBhsLprVzG
* fix(release-sdk): pin merge.conflictStyle=merge on hotfix cherry-pick
CodeRabbit flagged on #2967 that the awk classifier introduced for #2966
assumes default conflict-marker style (plain `<<<<<<< HEAD ... ======= ...
>>>>>>>`). If a runner has merge.conflictStyle=diff3 or zdiff3 set
(globally, repo-config, or via git defaults shift), the marker emits an
extra `||||||| ancestor` section between HEAD and =======. The awk's
`in_head` mode would accumulate that ancestor content into the HEAD
buffer, and a context-missing conflict would misclassify as real —
sending the workflow into the abort path on a pick that should be
silently skipped.
Pass `-c merge.conflictStyle=merge` on the cherry-pick command itself
(scoped to that one git invocation; doesn't leak to other commands).
This guarantees marker shape regardless of the runner's git config.
Updated the existing static assertion in
tests/bug-2966-cherry-pick-context-missing.test.cjs to require the pin —
a future edit dropping it fails the test.
Local: `node --test tests/bug-2966-...test.cjs` → 3/3 pass.
https://claude.ai/code/session_01LApueb9PVs2uSBhsLprVzG
* test(#2964): allow git options between `git` and `cherry-pick`
The previous commit on this branch (d6530190) added
`git -c merge.conflictStyle=merge cherry-pick ...` to release-sdk.yml.
The bug-2964 static test's regex `/git cherry-pick[^\n]*"\$SHA"/`
required `cherry-pick` to be the literal next token after `git`, so it
no longer matched the line and CI failed on Node 22 / Node 24 / macOS.
Loosen to `/git\b[^\n]*?cherry-pick[^\n]*"\$SHA"/` so any options
between `git` and `cherry-pick` (e.g. `-c key=value`) are tolerated.
The flag assertions on the matched line still verify --allow-empty and
--keep-redundant-commits are present, which is what bug-2964 actually
guards.
Local: `node --test tests/bug-2964-...test.cjs tests/bug-2966-...test.cjs`
→ 5/5 pass.
https://claude.ai/code/session_01LApueb9PVs2uSBhsLprVzG
* test(#2966): pin merge.conflictStyle in test git wrapper, assert awk status
CodeRabbit raised two issues on PR #2967:
1. The synthetic-repo cherry-pick reproducer asserted `<<<<<<< HEAD ...`
blocks have empty HEAD sections, but the cherry-pick itself didn't
pin `merge.conflictStyle`. A developer or CI runner with global
diff3/zdiff3 config would inject `||||||| ancestor` lines into the
HEAD scan and the test would fail for environment reasons rather
than the bug premise. Pin the style on the test's `git()` wrapper
so every git operation in the test is deterministic regardless of
user config.
2. `classify()` ran awk and consumed `r.stdout.trim()` without checking
`r.status` or `r.error`. A failed awk invocation (missing binary,
syntax error, signal) returns empty stdout, which would falsely
classify as "context-missing" and the test would silently pass on
broken predicates. Add `assert.ok(!r.error, ...)` and
`assert.equal(r.status, 0, ...)` before reading stdout.
Local: `node --test tests/bug-2966-...test.cjs tests/bug-2964-...test.cjs`
→ 5/5 pass.
https://claude.ai/code/session_01LApueb9PVs2uSBhsLprVzG
---------
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>