forgedock 1.17.0 → 1.19.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/bin/engine/outcome-codes.mjs +37 -2
- package/bin/engine/phases.mjs +207 -106
- package/bin/engine/runlog.mjs +17 -2
- package/bin/engine/telemetry-seam.mjs +3 -0
- package/bin/engine/telemetry.test.mjs +3 -2
- package/bin/engine-cli.mjs +37 -2
- package/bin/engine.mjs +221 -15
- package/bin/forgedock.mjs +1 -1
- package/bin/hooks/pre-tool-use.mjs +65 -32
- package/bin/runner.mjs +125 -6
- package/bin/stats/aggregate.mjs +7 -3
- package/bin/stats/cache.mjs +7 -3
- package/bin/stats/github-source.mjs +203 -24
- package/bin/stats/index.mjs +46 -9
- package/bin/stats/render.mjs +1 -1
- package/bin/stats/stats.test.mjs +264 -15
- package/commands/cleanup.md +10 -3
- package/commands/deploy-pr.md +64 -0
- package/commands/fix-ci.md +20 -6
- package/commands/milestone.md +34 -17
- package/commands/orchestrate/phase-4-execution.md +11 -4
- package/commands/review-pr-staging.md +145 -522
- package/commands/review-pr.md +496 -3124
- package/commands/test-gate.md +25 -10
- package/commands/work-on/build/architect.md +66 -1
- package/commands/work-on/build/implement.md +21 -1
- package/commands/work-on/build/validate.md +322 -28
- package/commands/work-on/build.md +173 -95
- package/commands/work-on/close.md +50 -15
- package/commands/work-on/decompose.md +72 -34
- package/commands/work-on/investigate.md +16 -12
- package/commands/work-on/remediate.md +58 -25
- package/commands/work-on/review.md +36 -21
- package/commands/work-on.md +1 -2
- package/docs/CONFIG.md +132 -26
- package/docs/OUTCOME-CODES.md +12 -3
- package/docs/RELEASE-NOTES.md +12 -0
- package/docs/STATS.md +13 -5
- package/docs/TELEMETRY.md +7 -4
- package/docs/WORK-ON-RUNTIME.md +24 -0
- package/docs/spec/forge-protocol-v1.md +2 -2
- package/package.json +2 -2
- package/packages/protocol/src/phases.js +5 -5
- package/packages/protocol/src/types.js +1 -1
- package/scripts/acceptance-red-green.sh +90 -0
- package/scripts/acceptance-red-green.test.sh +74 -0
- package/scripts/cap-findings.sh +1 -1
- package/scripts/cap-findings.test.sh +3 -1
- package/scripts/check-contract-scope.test.sh +2 -0
- package/scripts/check-edge-cases.sh +143 -0
- package/scripts/check-edge-cases.test.sh +175 -0
- package/scripts/check-spec-bash.sh +5 -1
- package/scripts/check-spec-bash.test.sh +20 -0
- package/scripts/check-spec-markers.sh +2 -0
- package/scripts/check-test-tmp-cleanup.sh +56 -0
- package/scripts/check-test-tmp-cleanup.test.sh +69 -0
- package/scripts/ci-local/adapters/azure-pipelines.mjs +113 -0
- package/scripts/ci-local/adapters/bitbucket.mjs +64 -0
- package/scripts/ci-local/adapters/buildkite.mjs +58 -0
- package/scripts/ci-local/adapters/circleci.mjs +101 -0
- package/scripts/ci-local/adapters/common.mjs +115 -0
- package/scripts/ci-local/adapters/explicit.mjs +21 -0
- package/scripts/ci-local/adapters/github-actions.mjs +349 -0
- package/scripts/ci-local/adapters/gitlab-ci.mjs +184 -0
- package/scripts/ci-local/adapters/portability.mjs +104 -0
- package/scripts/ci-local/adapters/task-runners.mjs +67 -0
- package/scripts/ci-local/cli.mjs +50 -0
- package/scripts/ci-local/lib.mjs +371 -0
- package/scripts/ci-local/plan.mjs +186 -0
- package/scripts/ci-local/run.mjs +345 -0
- package/scripts/ci-local/scoping.mjs +508 -0
- package/scripts/ci-local.sh +97 -0
- package/scripts/ci-local.test.sh +919 -0
- package/scripts/classify-finding.sh +32 -6
- package/scripts/classify-finding.test.sh +23 -0
- package/scripts/classify-test-run.sh +69 -0
- package/scripts/classify-test-run.test.sh +90 -0
- package/scripts/close-parent-c3.test.sh +71 -0
- package/scripts/consumer-sweep.sh +569 -0
- package/scripts/consumer-sweep.test.sh +357 -0
- package/scripts/detect-append-registries.sh +106 -0
- package/scripts/detect-append-registries.test.sh +64 -0
- package/scripts/diff-size.sh +93 -4
- package/scripts/diff-size.test.sh +59 -2
- package/scripts/fixtures/ci-local/azure/azure-pipelines.yml +11 -0
- package/scripts/fixtures/ci-local/bitbucket/bitbucket-pipelines.yml +12 -0
- package/scripts/fixtures/ci-local/buildkite/.buildkite/pipeline.yml +10 -0
- package/scripts/fixtures/ci-local/circleci/.circleci/config.yml +29 -0
- package/scripts/fixtures/ci-local/gha-basic/.github/workflows/ci.yml +59 -0
- package/scripts/fixtures/ci-local/gha-basic/.github/workflows/docs.yml +10 -0
- package/scripts/fixtures/ci-local/gha-basic/.github/workflows/nightly.yml +10 -0
- package/scripts/fixtures/ci-local/gha-basic/docs/index.md +1 -0
- package/scripts/fixtures/ci-local/gha-basic/src/greet.sh +2 -0
- package/scripts/fixtures/ci-local/gha-basic/tests/greet.test.sh +3 -0
- package/scripts/fixtures/ci-local/gha-dorny/.github/workflows/ci.yml +51 -0
- package/scripts/fixtures/ci-local/gha-dorny/backend/app.py +1 -0
- package/scripts/fixtures/ci-local/gha-dorny/docs/a.md +1 -0
- package/scripts/fixtures/ci-local/gha-dorny/frontend/app.js +1 -0
- package/scripts/fixtures/ci-local/gha-run/.github/workflows/ci.yml +39 -0
- package/scripts/fixtures/ci-local/gha-run/src/x.txt +1 -0
- package/scripts/fixtures/ci-local/gitlab/.gitlab-ci.yml +43 -0
- package/scripts/fixtures/ci-local/gitlab/docs/d.md +1 -0
- package/scripts/fixtures/ci-local/gitlab/src/a.txt +1 -0
- package/scripts/fixtures/ci-local/monorepo/.github/workflows/ci.yml +13 -0
- package/scripts/fixtures/ci-local/monorepo/package-lock.json +1 -0
- package/scripts/fixtures/ci-local/monorepo/packages/a/src/a.sh +2 -0
- package/scripts/fixtures/ci-local/monorepo/packages/a/tests/a.test.sh +3 -0
- package/scripts/fixtures/ci-local/monorepo/packages/b/src/b.sh +2 -0
- package/scripts/fixtures/ci-local/monorepo/packages/b/tests/b.test.sh +3 -0
- package/scripts/fixtures/ci-local/no-ci/Makefile +6 -0
- package/scripts/fixtures/ci-local/no-ci/package.json +1 -0
- package/scripts/fixtures/ci-local/no-ci/src/a.js +1 -0
- package/scripts/fixtures/ci-local/no-ci/src/b.js +1 -0
- package/scripts/fixtures/ci-local/no-ci/tests/a.test.js +2 -0
- package/scripts/fixtures/ci-local/no-ci/tests/b.test.js +2 -0
- package/scripts/forge-root.test.sh +14 -12
- package/scripts/lib/telemetry.sh +1 -0
- package/scripts/publish-staging-sync.test.sh +48 -0
- package/scripts/remediate-approved-head.test.sh +2 -0
- package/scripts/remediate-basesync.test.sh +1 -1
- package/scripts/resolve-deploy-gate.sh +32 -6
- package/scripts/resolve-deploy-gate.test.sh +42 -1
- package/scripts/review-acceptance-gate.sh +2 -2
- package/scripts/review-acceptance-gate.test.sh +20 -0
- package/scripts/review-delta.sh +8 -1
- package/scripts/review-delta.test.sh +19 -0
- package/scripts/review-finalize.sh +1579 -0
- package/scripts/review-finalize.test.sh +1040 -0
- package/scripts/review-pr-delta-wiring.test.sh +32 -72
- package/scripts/review-pr-domain-map.test.sh +18 -13
- package/scripts/review-pr-gate2.test.sh +9 -65
- package/scripts/review-pr-staging-scope.test.sh +14 -98
- package/scripts/review-prepare.sh +53 -15
- package/scripts/review-prepare.test.sh +37 -3
- package/scripts/review-roster.sh +16 -2
- package/scripts/review-roster.test.sh +22 -0
- package/scripts/run-shell-tests.sh +5 -0
- package/scripts/telemetry.test.sh +2 -0
- package/scripts/trusted-comments.test.sh +11 -3
- package/scripts/verify-phase-trail.test.sh +8 -0
- package/scripts/wait-ci-green.sh +61 -9
- package/scripts/wait-ci-green.test.sh +44 -6
- package/scripts/with-run-tmpdir.sh +20 -0
|
@@ -95,8 +95,8 @@ fi
|
|
|
95
95
|
# forge#3758 (hgate is a fixture of its own: the loop runs once per shell and h14 is reused below with other versions): deploy-gate script resolves on a plugin-only install (cache only, no symlink, no FORGE_* env).
|
|
96
96
|
# The gate-script line is taken verbatim from the spec so the test exercises exactly what ships.
|
|
97
97
|
mkscripts "$T/hgate/.claude/plugins/cache/forgedock/forgedock/1.0.0"; : > "$T/hgate/.claude/plugins/cache/forgedock/forgedock/1.0.0/scripts/resolve-deploy-gate.sh"
|
|
98
|
-
# review-pr.md resolves the gate script through
|
|
99
|
-
sed 's/^[[:space:]]*//' "$ROOT/
|
|
98
|
+
# review-finalize.sh (the review-pr.md Phase 8 gate) resolves the gate script through forge_resolve (#3780): the locator and the call line are taken verbatim from the script.
|
|
99
|
+
sed 's/^[[:space:]]*//' "$ROOT/scripts/review-finalize.sh" | grep '^GATE_SCRIPT=\$(forge_resolve resolve-deploy-gate.sh)$' | sort -u > "$T/gs"
|
|
100
100
|
expect "gate-script resolver line is identical across sites" 1 "$(wc -l < "$T/gs" | tr -d ' ')"
|
|
101
101
|
{ sed 's/^[[:space:]]*//' "$ROOT/commands/review-pr.md" | grep -m1 '^type forge_resolve >/dev/null'; cat "$T/gs"; } > "$T/gs_full"
|
|
102
102
|
gate_run() { ( cd "$T/consumer" && env -i PATH="$PATH" HOME="$1" "$SH" -c "$(cat "$T/gs_full"); printf %s \"\$GATE_SCRIPT\"" ); }
|
|
@@ -271,11 +271,13 @@ extract_helper_block() { # file var -> block on stdout
|
|
|
271
271
|
extract_helper_fn() { # file -> function on stdout
|
|
272
272
|
awk '/^resolve_extract_affected_files\(\) \{/ {inb=1} inb {print} inb && /^\}$/ {exit}' "$1"
|
|
273
273
|
}
|
|
274
|
-
# review-pr.md resolves its helpers through the forge_resolve stub (#3780)
|
|
274
|
+
# review-pr.md resolves its helpers through the forge_resolve stub (#3780); since #3841 the classify, cap, milestone, priority and dedup helpers are resolved inside
|
|
275
|
+
# scripts/review-finalize.sh and the code-index helper inside scripts/review-prepare.sh: the pinned locator plus the call line, taken from those scripts.
|
|
275
276
|
stub_block() { # file var -> locator + the first `var=$(forge_resolve ...)` line, indentation stripped
|
|
276
277
|
sed 's/^[[:space:]]*//' "$1" | grep -m1 '^type forge_resolve >/dev/null'; sed 's/^[[:space:]]*//' "$1" | grep -m1 "^$2=\$(forge_resolve "
|
|
277
278
|
}
|
|
278
|
-
HR_CLASSIFY="$ROOT/
|
|
279
|
+
HR_CLASSIFY="$ROOT/scripts/review-finalize.sh"
|
|
280
|
+
HR_PREPARE="$ROOT/scripts/review-prepare.sh"
|
|
279
281
|
HR_AMP="$ROOT/commands/orchestrate/phase-4-execution.md"
|
|
280
282
|
HR_EX3="$ROOT/commands/orchestrate/phase-3-dependency.md"
|
|
281
283
|
stub_block "$HR_CLASSIFY" CLASSIFY_SCRIPT > "$T/hr_classify.sh"
|
|
@@ -285,7 +287,7 @@ extract_helper_fn "$HR_AMP" > "$T/hr_ex4.sh"
|
|
|
285
287
|
extract_helper_fn "$HR_EX3" > "$T/hr_ex3.sh"
|
|
286
288
|
expect "CLASSIFY_SCRIPT resolver stub found" 1 "$(grep -c '^CLASSIFY_SCRIPT=\$(forge_resolve classify-finding.sh)$' "$T/hr_classify.sh")"
|
|
287
289
|
expect "CAP_SCRIPT resolver stub found" 1 "$(grep -c '^CAP_SCRIPT=\$(forge_resolve cap-findings.sh)$' "$T/hr_cap.sh")"
|
|
288
|
-
expect "CAP_SCRIPT resolver stub
|
|
290
|
+
expect "review-pr-staging.md no longer carries a CAP_SCRIPT resolver stub (review-finalize.sh owns the cap helper, #3841)" 0 "$(sed 's/^[[:space:]]*//' "$ROOT/commands/review-pr-staging.md" | grep -c '^CAP_SCRIPT=\$(forge_resolve cap-findings.sh)$')"
|
|
289
291
|
expect "AMP_SCRIPT resolver block found" 1 "$(grep -c '^AMP_SCRIPT=""' "$T/hr_amp.sh")"
|
|
290
292
|
expect "extract-affected-files resolver found in phase-4" 1 "$(grep -c 'resolve_extract_affected_files() {' "$T/hr_ex4.sh")"
|
|
291
293
|
expect "extract-affected-files resolver found in phase-3" 1 "$(grep -c 'resolve_extract_affected_files() {' "$T/hr_ex3.sh")"
|
|
@@ -337,7 +339,7 @@ hr_check_stub() { # label file var script: the spec calls the script through the
|
|
|
337
339
|
}
|
|
338
340
|
hr_check_stub review-pr "$HR_CLASSIFY" DERIVE_MILESTONE_SCRIPT derive-finding-milestone.sh
|
|
339
341
|
hr_check_stub review-pr "$HR_CLASSIFY" PRIORITY_SCRIPT severity-to-priority.sh
|
|
340
|
-
hr_check_stub review-
|
|
342
|
+
hr_check_stub review-prepare "$HR_PREPARE" CODE_INDEX_SCRIPT code-index.sh
|
|
341
343
|
hr_check_stub review-pr "$HR_CLASSIFY" DEDUP_SCRIPT issue-dedup.sh
|
|
342
344
|
hr_check_block issue "$ROOT/commands/issue.md" DEDUP_SCRIPT issue-dedup.sh
|
|
343
345
|
stub_block "$HR_CLASSIFY" DERIVE_MILESTONE_SCRIPT > "$T/hr_dm.sh"
|
|
@@ -414,9 +416,9 @@ FR="$ROOT/scripts/lib/forge-resolve.sh"; FB="$ROOT/scripts/lib/fence-bootstrap.s
|
|
|
414
416
|
[ -f "$FR" ] && [ -f "$FB" ] && ok || bad "scripts/lib/forge-resolve.sh and scripts/lib/fence-bootstrap.sh must exist"
|
|
415
417
|
sed 's/^[[:space:]]*//' "$STG_SPEC" | grep '^type forge_resolve >/dev/null' | sort | uniq -c > "$T/loc_uniq"
|
|
416
418
|
[ "$(wc -l < "$T/loc_uniq" | tr -d ' ')" = 1 ] && ok || bad "forge_resolve locator line must be byte-identical across every stub in review-pr-staging.md"
|
|
417
|
-
[ "$(awk '{print $1}' "$T/loc_uniq")" -ge
|
|
419
|
+
[ "$(awk '{print $1}' "$T/loc_uniq")" -ge 25 ] && ok || bad "expected a locator on every bootstrap (21) and resolver (5) stub"
|
|
418
420
|
grep -qE 'PWD|\$\(pwd\)|REPO_PATH' "$T/locator.sh" && bad "stub locator must not consult the cwd or repo under review" || ok
|
|
419
|
-
[ "$(grep -c '# FENCE_BOOTSTRAP$' "$STG_SPEC")" =
|
|
421
|
+
[ "$(grep -c '# FENCE_BOOTSTRAP$' "$STG_SPEC")" = 21 ] && ok || bad "expected 21 fence bootstrap stubs"
|
|
420
422
|
[ "$(grep '# FENCE_BOOTSTRAP$' "$STG_SPEC" | sort -u | wc -l | tr -d ' ')" = 1 ] && ok || bad "fence bootstrap stubs must be byte-identical"
|
|
421
423
|
grep '# FENCE_BOOTSTRAP$' "$STG_SPEC" | sed 1q | grep -qF 'exit 1' && ok || bad "fence bootstrap stub must fail closed (exit 1) when the helper is unresolved"
|
|
422
424
|
# the spec carries none of the old inlined blocks
|
|
@@ -424,11 +426,11 @@ grep -qE '^[[:space:]]*(TRUSTED_SCRIPT|[A-Z_]+_SCRIPT)=""$|_tc="\$\(printf' "$ST
|
|
|
424
426
|
# no $PWD / $(pwd) tier in the helper (comments aside)
|
|
425
427
|
grep -nE '\$PWD|\$\(pwd\)|\$\{PWD' "$FR" | grep -vE '^[0-9]+:[[:space:]]*#' | grep -q . && bad "forge-resolve.sh must have no PWD tier" || ok
|
|
426
428
|
|
|
427
|
-
# Guard (#3779): remediate.md carries the same byte-identical locator stub (
|
|
429
|
+
# Guard (#3779): remediate.md carries the same byte-identical locator stub (9 fences: M4 ancestry, merged-first transition-label (the merge-branch one reuses the CI-gate fence locator), 5 gate sites, trusted-comments, CI gate) and none of the old inlined resolvers.
|
|
428
430
|
REM_SPEC="$ROOT/commands/work-on/remediate.md"
|
|
429
431
|
sed 's/^[[:space:]]*//' "$REM_SPEC" | grep '^type forge_resolve >/dev/null' | sort | uniq -c > "$T/rem_uniq"
|
|
430
432
|
[ "$(wc -l < "$T/rem_uniq" | tr -d ' ')" = 1 ] && ok || bad "remediate.md locator lines must be byte-identical"
|
|
431
|
-
expect "remediate.md locator count (one per fence that resolves a script)"
|
|
433
|
+
expect "remediate.md locator count (one per fence that resolves a script)" 9 "$(awk '{print $1}' "$T/rem_uniq")"
|
|
432
434
|
[ "$(sed 's/^[[:space:]]*//' "$REM_SPEC" | grep '^type forge_resolve >/dev/null' | sed 1q)" = "$(cat "$T/locator.sh")" ] && ok || bad "remediate.md locator differs from the pinned review-pr-staging.md locator"
|
|
433
435
|
expect "remediate.md inlined FORGE_ROOT bootstrap copies" 0 "$(grep -c '# FORGE_ROOT bootstrap' "$REM_SPEC" || true)"
|
|
434
436
|
grep -qE '^[[:space:]]*(TRUSTED_SCRIPT|[A-Z_]+_SCRIPT)=""|_tc="\$\(printf|^[[:space:]]*GATE_SCRIPT=""' "$REM_SPEC" && bad "remediate.md still carries an inlined resolver block" || ok
|
|
@@ -441,7 +443,7 @@ expect "remediate.md: forge_resolve calls without a preceding locator in the sam
|
|
|
441
443
|
RP_SPEC="$ROOT/commands/review-pr.md"
|
|
442
444
|
sed 's/^[[:space:]]*//' "$RP_SPEC" | grep '^type forge_resolve >/dev/null' | sort | uniq -c > "$T/rp_uniq"
|
|
443
445
|
[ "$(wc -l < "$T/rp_uniq" | tr -d ' ')" = 1 ] && ok || bad "review-pr.md locator lines must be byte-identical"
|
|
444
|
-
[ "$(awk '{print $1}' "$T/rp_uniq")" -ge
|
|
446
|
+
[ "$(awk '{print $1}' "$T/rp_uniq")" -ge 10 ] && ok || bad "expected a locator in every review-pr.md fence that resolves a script (>=10; the classify/file/prepare helpers resolve inside the review scripts since #3841)"
|
|
445
447
|
[ "$(sed 's/^[[:space:]]*//' "$RP_SPEC" | grep '^type forge_resolve >/dev/null' | sed 1q)" = "$(cat "$T/locator.sh")" ] && ok || bad "review-pr.md locator differs from the pinned review-pr-staging.md locator"
|
|
446
448
|
expect "review-pr.md inlined FORGE_ROOT bootstrap copies" 0 "$(grep -c 'FORGE_ROOT bootstrap' "$RP_SPEC" || true)"
|
|
447
449
|
# (SCOPE_SCRIPT="" is a sibling path derived from the resolved trusted script's own directory, not a resolver accumulator)
|
|
@@ -462,7 +464,7 @@ for f in $STUB_SITES; do
|
|
|
462
464
|
expect "$f: forge_root calls without a preceding locator in the same fence" 0 "$(awk '/^[ ]*```/{loc=0} /^[ ]*type forge_resolve >\/dev\/null/{loc=1} /FORGE_ROOT="\$\(forge_root /&&!loc{bad++} END{print bad+0}' "$ROOT/$f")"
|
|
463
465
|
done
|
|
464
466
|
# the spec-count of stub resolutions pins the conversion (a copy dropped or re-inlined changes it)
|
|
465
|
-
for pair in work-on.md:3 work-on/review.md:2 work-on/build/validate.md:
|
|
467
|
+
for pair in work-on.md:3 work-on/review.md:2 work-on/build/validate.md:5 work-on/investigate.md:1 work-on/decompose.md:1 work-on/close.md:1 work-on/build.md:1 quality-gate.md:4 orchestrate/phase-1-resolve.md:2 orchestrate/phase-4-execution.md:8; do
|
|
466
468
|
f="commands/${pair%%:*}"; n="${pair##*:}"
|
|
467
469
|
expect "$f: FORGE_ROOT stub resolutions" "$n" "$(grep -cF "$FR_LINE" "$ROOT/$f" || true)"
|
|
468
470
|
done
|
package/scripts/lib/telemetry.sh
CHANGED
|
@@ -51,6 +51,7 @@ _forge_tel_record() { # event k=v... -> one JSON line on stdout
|
|
|
51
51
|
run: ($run | str), phase: ($phase | str), event: $ev, eid: $eid} + $p' --args "${args[@]}"
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
+
# finding_disposition payload is generic key=value (incl. scope=introduced|pre-existing|unknown); no per-field allowlist applies.
|
|
54
55
|
# Prints "<number> <repo>" of the comment target (issue or PR) for this event, or nothing when none is usable.
|
|
55
56
|
_forge_tel_target() {
|
|
56
57
|
local ev="$1" repo="$2" n
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# publish-staging-sync.test.sh — guards the publish.yml "Sync version to staging" step (forge#3695)
|
|
3
|
+
# and the deploy-pr Phase 2A.5 version-sync pre-flight. Static assertions, no network.
|
|
4
|
+
# Usage: bash scripts/publish-staging-sync.test.sh
|
|
5
|
+
# SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios
|
|
6
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
7
|
+
|
|
8
|
+
set -uo pipefail
|
|
9
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
10
|
+
ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
|
11
|
+
PUB="$ROOT/.github/workflows/publish.yml"
|
|
12
|
+
DEP="$ROOT/commands/deploy-pr.md"
|
|
13
|
+
|
|
14
|
+
PASS=0; FAILN=0
|
|
15
|
+
ok() { PASS=$((PASS+1)); echo "ok - $1"; }
|
|
16
|
+
bad() { FAILN=$((FAILN+1)); echo "FAIL - $1"; }
|
|
17
|
+
has() { if grep -qF -e "$2" "$3"; then ok "$1"; else bad "$1 (missing '$2')"; fi; }
|
|
18
|
+
|
|
19
|
+
# Extract the sync step block (from its name line to the next step's name line).
|
|
20
|
+
STEP=$(awk '/- name: Sync version to staging/{f=1;print;next} f&&/^ - name: /{exit} f{print}' "$PUB")
|
|
21
|
+
[ -n "$STEP" ] && ok "publish.yml has the Sync version to staging step" || bad "publish.yml has the Sync version to staging step"
|
|
22
|
+
|
|
23
|
+
L_PUSH=$(grep -n -- '- name: Push version bump' "$PUB" | head -1 | cut -d: -f1)
|
|
24
|
+
L_SYNC=$(grep -n -- '- name: Sync version to staging' "$PUB" | head -1 | cut -d: -f1)
|
|
25
|
+
L_REL=$(grep -n -- '- name: Create GitHub Release' "$PUB" | head -1 | cut -d: -f1)
|
|
26
|
+
if [ -n "$L_PUSH" ] && [ -n "$L_SYNC" ] && [ -n "$L_REL" ] && [ "$L_PUSH" -lt "$L_SYNC" ] && [ "$L_SYNC" -lt "$L_REL" ]; then
|
|
27
|
+
ok "sync step sits between Push version bump and Create GitHub Release"
|
|
28
|
+
else bad "sync step sits between Push version bump and Create GitHub Release"; fi
|
|
29
|
+
|
|
30
|
+
stepHas() { if printf '%s\n' "$STEP" | grep -qF -e "$2"; then ok "$1"; else bad "$1 (missing '$2')"; fi; }
|
|
31
|
+
stepHas "step fetches origin/staging" "origin/staging"
|
|
32
|
+
stepHas "step pushes HEAD to staging" "git push origin HEAD:staging"
|
|
33
|
+
stepHas "step is non-fatal" "continue-on-error: true"
|
|
34
|
+
stepHas "step is guarded by SKIP_PUBLISH" "env.SKIP_PUBLISH != 'true'"
|
|
35
|
+
stepHas "commit carries [skip ci]" "[skip ci]"
|
|
36
|
+
stepHas "only version files are staged" "git add package.json .claude-plugin/plugin.json"
|
|
37
|
+
stepHas "failure emits a warning" "::warning::"
|
|
38
|
+
if printf '%s\n' "$STEP" | grep -qE 'git (merge|pull)'; then bad "step must not merge main (version-only commit)"; else ok "step does not merge main"; fi
|
|
39
|
+
|
|
40
|
+
has "deploy-pr has the 2A.5 pre-flight" "### 2A.5: Version Sync Pre-flight" "$DEP"
|
|
41
|
+
has "deploy-pr pre-flight compares package.json" "package.json" "$DEP"
|
|
42
|
+
|
|
43
|
+
if git -C "$ROOT" rev-parse --verify -q origin/main >/dev/null 2>&1; then
|
|
44
|
+
if git -C "$ROOT" diff --quiet origin/main -- .github/workflows/version-guard.yml; then ok "version-guard.yml unchanged"; else bad "version-guard.yml unchanged"; fi
|
|
45
|
+
fi
|
|
46
|
+
|
|
47
|
+
echo "passed=$PASS failed=$FAILN"
|
|
48
|
+
[ "$FAILN" -eq 0 ]
|
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
6
6
|
|
|
7
7
|
set -uo pipefail
|
|
8
|
+
# Every mktemp below lands in one private dir that the EXIT trap removes (forge#4016).
|
|
9
|
+
_SUITE_TMP=$(mktemp -d); export TMPDIR="$_SUITE_TMP"; trap 'rm -rf "$_SUITE_TMP"' EXIT
|
|
8
10
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
9
11
|
ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
|
10
12
|
SPEC="$ROOT/commands/work-on/remediate.md"
|
|
@@ -66,7 +66,7 @@ eq "MERGE_RC/MERGE_OUT read only in the fence that sets them" "$FENCE_USE" "$FEN
|
|
|
66
66
|
has "refused park posts needs-human from the sync fence" 'echo "BLOCKED: base-sync: merge refused'
|
|
67
67
|
|
|
68
68
|
# Behavioural: dirty tree + base change -> refused (rc!=0, no MERGE_HEAD, empty U-list); real conflict -> MERGE_HEAD present.
|
|
69
|
-
T="$(mktemp -d)"; trap 'rm -rf "$T"' EXIT
|
|
69
|
+
T="$(mktemp -d)"; trap 'rm -rf "$T" "$CAPDIR"' EXIT
|
|
70
70
|
(
|
|
71
71
|
cd "$T" && git init -q -b main . && git config user.email t@t && git config user.name t
|
|
72
72
|
echo a > f && git add f && git commit -qm base
|
|
@@ -11,7 +11,9 @@
|
|
|
11
11
|
# A base is the deploy gate when it is empty/`null`, the literal `main` (hard
|
|
12
12
|
# floor), the repository's GitHub default branch (`default_branch`), or the
|
|
13
13
|
# `branches.default` of the default branch's COMMITTED forge.yaml
|
|
14
|
-
# (`origin/<default>:forge.yaml`, falling back to the contents API
|
|
14
|
+
# (`refs/remotes/origin/<default>:forge.yaml`, falling back to the contents API;
|
|
15
|
+
# if neither source is readable the result is `true`, never `false`; a source that
|
|
16
|
+
# cleanly reports forge.yaml absent is "no config" and falls through) or of an
|
|
15
17
|
# explicit trusted config (FORGE_TRUSTED_CONFIG, orchestrator-supplied).
|
|
16
18
|
#
|
|
17
19
|
# It NEVER reads forge.yaml from $PWD: the PR worktree is author-controlled, so
|
|
@@ -34,13 +36,37 @@ GH_DEFAULT=$(gh api "repos/${REPO}" --jq .default_branch 2>/dev/null || echo "")
|
|
|
34
36
|
|
|
35
37
|
_cfg_default() { yq '.branches.default // ""' 2>/dev/null || true; }
|
|
36
38
|
|
|
39
|
+
# Parse stdin's branches.default. Returns non-zero when yq is missing or the
|
|
40
|
+
# parse fails, so "config unreadable" is distinguishable from "no branches.default".
|
|
41
|
+
_cfg_parse() { command -v yq >/dev/null 2>&1 && yq '.branches.default // ""' 2>/dev/null; }
|
|
42
|
+
|
|
37
43
|
CFG_DEFAULT=""
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
44
|
+
READ_OK=""
|
|
45
|
+
# Committed default-branch config only (a ref, never the working tree). The
|
|
46
|
+
# fully-qualified ref cannot be shadowed by a local tag/branch named origin/<default>.
|
|
47
|
+
# A present ref with no forge.yaml at that path is a clean "no config" read (READ_OK,
|
|
48
|
+
# empty default); an unfetched ref, missing yq or parse error stays unreadable.
|
|
49
|
+
CFG_REF="refs/remotes/origin/${GH_DEFAULT}"
|
|
50
|
+
if git rev-parse -q --verify "${CFG_REF}^{commit}" >/dev/null 2>&1; then
|
|
51
|
+
if git cat-file -e "${CFG_REF}:forge.yaml" 2>/dev/null; then
|
|
52
|
+
CFG_RAW=$(git show "${CFG_REF}:forge.yaml" 2>/dev/null) && [ -n "$CFG_RAW" ] \
|
|
53
|
+
&& CFG_DEFAULT=$(printf '%s\n' "$CFG_RAW" | _cfg_parse) && READ_OK=1
|
|
54
|
+
else
|
|
55
|
+
READ_OK=1
|
|
56
|
+
fi
|
|
57
|
+
fi
|
|
58
|
+
if [ -z "$READ_OK" ]; then
|
|
59
|
+
CFG_DEFAULT=""
|
|
60
|
+
API_OUT=$(gh api "repos/${REPO}/contents/forge.yaml?ref=${GH_DEFAULT}" --jq .content 2>&1); API_RC=$?
|
|
61
|
+
if [ "$API_RC" -eq 0 ]; then
|
|
62
|
+
CFG_RAW=$(printf '%s' "$API_OUT" | base64 -d 2>/dev/null) && [ -n "$CFG_RAW" ] \
|
|
63
|
+
&& CFG_DEFAULT=$(printf '%s\n' "$CFG_RAW" | _cfg_parse) && READ_OK=1
|
|
64
|
+
elif printf '%s' "$API_OUT" | grep -q 'HTTP 404'; then
|
|
65
|
+
READ_OK=1 # API says the file does not exist: clean "no config", not a read failure
|
|
66
|
+
fi
|
|
43
67
|
fi
|
|
68
|
+
# Config unreadable from every source (no yq, ref unfetched, API failing): fail closed.
|
|
69
|
+
[ -n "$READ_OK" ] || emit true
|
|
44
70
|
[ -n "$CFG_DEFAULT" ] && [ "$CFG_DEFAULT" != "null" ] && [ "$BASE" = "$CFG_DEFAULT" ] && emit true
|
|
45
71
|
|
|
46
72
|
if [ -n "${FORGE_TRUSTED_CONFIG:-}" ] && [ -f "$FORGE_TRUSTED_CONFIG" ]; then
|
|
@@ -9,7 +9,7 @@ TMP="$(mktemp -d)"; trap 'rm -rf "$TMP"' EXIT
|
|
|
9
9
|
ok() { PASS=$((PASS+1)); }
|
|
10
10
|
bad() { FAILN=$((FAILN+1)); echo "FAIL: $1"; }
|
|
11
11
|
|
|
12
|
-
command -v yq >/dev/null 2>&1 ||
|
|
12
|
+
HAVE_YQ=1; command -v yq >/dev/null 2>&1 || HAVE_YQ=0
|
|
13
13
|
|
|
14
14
|
# Stub gh: GH_DEFAULT_STUB drives `repos/<r> --jq .default_branch`; contents API unused (fails).
|
|
15
15
|
mkdir -p "$TMP/bin"
|
|
@@ -17,6 +17,7 @@ cat > "$TMP/bin/gh" <<'EOF'
|
|
|
17
17
|
#!/usr/bin/env bash
|
|
18
18
|
case "$*" in
|
|
19
19
|
*"--jq .default_branch"*) [ -n "${GH_FAIL:-}" ] && exit 1; printf '%s\n' "${GH_DEFAULT_STUB:-}" ;;
|
|
20
|
+
*"contents/forge.yaml"*) [ -n "${GH_CONTENTS_404:-}" ] && { echo "gh: Not Found (HTTP 404)" >&2; exit 1; }; exit 1 ;;
|
|
20
21
|
*) exit 1 ;;
|
|
21
22
|
esac
|
|
22
23
|
EOF
|
|
@@ -40,6 +41,7 @@ run() { # base default_stub [env...]
|
|
|
40
41
|
}
|
|
41
42
|
expect() { [ "$1" = "$2" ] && ok || bad "$3: got '$1' want '$2'"; }
|
|
42
43
|
|
|
44
|
+
if [ "$HAVE_YQ" = 1 ]; then
|
|
43
45
|
expect "$(run main trunk)" true "main with worktree release"
|
|
44
46
|
expect "$(run release trunk)" false "worktree-declared branch is not a gate"
|
|
45
47
|
expect "$(run staging trunk)" false "staging is fast lane"
|
|
@@ -55,5 +57,44 @@ printf 'branches:\n default: prod\n' > "$TMP/trusted.yaml"
|
|
|
55
57
|
expect "$(run prod trunk FORGE_TRUSTED_CONFIG="$TMP/trusted.yaml")" true "trusted config default"
|
|
56
58
|
expect "$(run prod trunk)" false "prod without trusted config"
|
|
57
59
|
|
|
60
|
+
|
|
61
|
+
# Shadowing: a local tag named origin/trunk carrying a config without `develop`
|
|
62
|
+
# must not displace the committed refs/remotes/origin/trunk config.
|
|
63
|
+
printf 'branches:\n default: other\n' > "$REPO/forge.yaml"
|
|
64
|
+
git -C "$REPO" add forge.yaml; git -C "$REPO" commit -qm shadow
|
|
65
|
+
git -C "$REPO" tag origin/trunk
|
|
66
|
+
expect "$(run develop trunk)" true "shadow tag origin/trunk does not displace committed config"
|
|
67
|
+
expect "$(run other trunk)" false "shadow tag config is not honoured"
|
|
68
|
+
git -C "$REPO" tag -d origin/trunk >/dev/null
|
|
69
|
+
fi
|
|
70
|
+
|
|
71
|
+
# Config read fail: no origin ref and failing contents API -> true (fail closed).
|
|
72
|
+
NOREF="$TMP/noref"; git init -q "$NOREF"
|
|
73
|
+
run_in() { # dir base [env...]
|
|
74
|
+
local dir="$1" base="$2"; shift 2
|
|
75
|
+
(cd "$dir" && env PATH="$TMP/bin:$PATH" GH_DEFAULT_STUB=trunk "$@" bash "$S" "$base" owner/repo)
|
|
76
|
+
}
|
|
77
|
+
expect "$(run_in "$NOREF" staging)" true "config read fail (no origin ref, API failing) fails closed"
|
|
78
|
+
|
|
79
|
+
# Missing forge.yaml is a clean "no config" read, not a read failure (falls through to false).
|
|
80
|
+
NOCFG="$TMP/nocfg.git"; NOCFGW="$TMP/nocfgw"
|
|
81
|
+
git init -q --bare "$NOCFG"; git init -q "$NOCFGW"
|
|
82
|
+
git -C "$NOCFGW" config user.email t@t; git -C "$NOCFGW" config user.name t
|
|
83
|
+
echo x > "$NOCFGW/README"; git -C "$NOCFGW" add README; git -C "$NOCFGW" commit -qm init
|
|
84
|
+
git -C "$NOCFGW" branch -M trunk; git -C "$NOCFGW" remote add origin "$NOCFG"
|
|
85
|
+
git -C "$NOCFGW" push -q origin trunk 2>/dev/null; git -C "$NOCFGW" fetch -q origin
|
|
86
|
+
expect "$(run_in "$NOCFGW" staging)" false "forge.yaml absent on default branch ref is no-config (fast lane)"
|
|
87
|
+
expect "$(run_in "$NOCFGW" main)" true "main stays strict with no config"
|
|
88
|
+
expect "$(run_in "$NOCFGW" trunk)" true "github default stays strict with no config"
|
|
89
|
+
# API 404 (ref not fetched, file absent) is also a clean no-config read; other API failures stay closed.
|
|
90
|
+
expect "$(run_in "$NOREF" staging GH_CONTENTS_404=1)" false "contents API 404 is no-config"
|
|
91
|
+
expect "$(run_in "$NOREF" staging)" true "non-404 contents API failure fails closed"
|
|
92
|
+
|
|
93
|
+
# Config unreadable: yq unusable (a failing stub shadows any real yq; portable to Git Bash, unlike a stripped PATH) even though the ref is readable.
|
|
94
|
+
NOYQ="$TMP/noyqbin"; mkdir -p "$NOYQ"
|
|
95
|
+
printf '#!/usr/bin/env bash\nexit 127\n' > "$NOYQ/yq"; chmod +x "$NOYQ/yq"
|
|
96
|
+
cp "$TMP/bin/gh" "$NOYQ/gh"
|
|
97
|
+
expect "$(cd "$REPO" && env PATH="$NOYQ:$PATH" GH_DEFAULT_STUB=trunk bash "$S" staging owner/repo)" true "config unreadable (yq failing) fails closed"
|
|
98
|
+
|
|
58
99
|
echo "resolve-deploy-gate.test.sh: $PASS passed, $FAILN failed"
|
|
59
100
|
[ "$FAILN" -eq 0 ]
|
|
@@ -79,12 +79,12 @@ while IFS= read -r check_line; do
|
|
|
79
79
|
ERR=$(eval "$TARGET" 2>&1 >/dev/null); RC=$?
|
|
80
80
|
# grep exit >= 2 is a usage error, not a verdict; only a positively identified usage error (classify-grep-exit.sh) is malformed; a missing operand stays a real FAIL even with stderr suppressed.
|
|
81
81
|
[ "$RC" -ge 2 ] && MALFORMED=$(bash "$GREP_CLS" "$RC" "$ERR" "$TARGET" 2>/dev/null)
|
|
82
|
-
[ "$RC" -eq 127 ] && [ -z "$MALFORMED" ] && printf '%s' "$ERR" | grep -
|
|
82
|
+
[ "$RC" -eq 127 ] && [ -z "$MALFORMED" ] && { FIRST=${TARGET#"${TARGET%%[![:space:]]*}"}; FIRST=${FIRST%%[[:space:]]*}; [ -n "$FIRST" ] && [ "${FIRST#*/}" = "$FIRST" ] && ! command -v -- "$FIRST" >/dev/null 2>&1 && printf '%s\n' "$ERR" | grep -qE "(^|: )$(printf '%s' "$FIRST" | sed 's/[][\.*^$(){}?+|/]/\\&/g'):"; } && MALFORMED="command not found (rc 127): the target is not an executable command"
|
|
83
83
|
[ "$RC" -eq 0 ] || [ -n "$MALFORMED" ] || { RESULT="FAIL"; DETAIL="command exited non-zero: $TARGET"; }
|
|
84
84
|
else
|
|
85
85
|
ERRF=$(mktemp); OUTPUT=$(eval "$TARGET" 2>"$ERRF"); RC=$?; ERR=$(cat "$ERRF"); rm -f "$ERRF"; [ -z "$ERR" ] || OUTPUT="${OUTPUT}${OUTPUT:+$'\n'}${ERR}" # classify on stderr only, match on both
|
|
86
86
|
[ "$RC" -ge 2 ] && MALFORMED=$(bash "$GREP_CLS" "$RC" "$ERR" "$TARGET" 2>/dev/null)
|
|
87
|
-
[ "$RC" -eq 127 ] && [ -z "$MALFORMED" ] && printf '%s' "$ERR" | grep -
|
|
87
|
+
[ "$RC" -eq 127 ] && [ -z "$MALFORMED" ] && { FIRST=${TARGET#"${TARGET%%[![:space:]]*}"}; FIRST=${FIRST%%[[:space:]]*}; [ -n "$FIRST" ] && [ "${FIRST#*/}" = "$FIRST" ] && ! command -v -- "$FIRST" >/dev/null 2>&1 && printf '%s\n' "$ERR" | grep -qE "(^|: )$(printf '%s' "$FIRST" | sed 's/[][\.*^$(){}?+|/]/\\&/g'):"; } && MALFORMED="command not found (rc 127): the target is not an executable command"
|
|
88
88
|
if [ -n "$MALFORMED" ]; then :
|
|
89
89
|
else
|
|
90
90
|
printf '%s\n' "$OUTPUT" | grep -qE "$MATCHER" || printf '%s\n' "$OUTPUT" | grep -qF -- "$MATCHER" \
|
|
@@ -90,6 +90,23 @@ inv "$(chk ac-1 true)" "$(chk ac-7 "echo 'unbalanced")"; rc=$(run); eq "mixed pa
|
|
|
90
90
|
grep -q ':PASSED:' "$TMP/out" && grep -qi 'malformed' "$TMP/out" && ok "mixed: PASSED with malformed listed" || bad "mixed: wrong output"
|
|
91
91
|
inv "$(chk ac-1 false)" "$(chk ac-7 "echo 'unbalanced")"; rc=$(run); eq "mixed fail+malformed: fails closed" "$rc" 1
|
|
92
92
|
|
|
93
|
+
# rc 127 downgrade only when the target's own first word is the missing command (forge#3842).
|
|
94
|
+
printf '#!/bin/bash\nno-such-tool-xyzzy\n' > "$TMP/wt/inner.sh"
|
|
95
|
+
inv "$(chk ac-8 'no-such-cmd-xyzzy --help')"; rc=$(run); eq "first word missing: malformed, rc 0" "$rc" 0
|
|
96
|
+
grep -qi 'malformed' "$TMP/out" && ok "first word missing reported malformed" || bad "first word missing not reported"
|
|
97
|
+
inv "$(chk ac-8 'bash inner.sh')"; rc=$(run); eq "script with missing inner tool is a real fail" "$rc" 1
|
|
98
|
+
inv "$(chk ac-8 'true && no-such-cmd-xyzzy')"; rc=$(run); eq "missing command not first word is a real fail" "$rc" 1
|
|
99
|
+
export LANG=de_DE.UTF-8 LC_MESSAGES=de_DE.UTF-8
|
|
100
|
+
inv "$(chk ac-8 'no-such-cmd-xyzzy --help')"; rc=$(run); eq "non-English locale: first word missing still malformed" "$rc" 0
|
|
101
|
+
grep -qi 'malformed' "$TMP/out" && ok "locale: reported malformed" || bad "locale: not reported malformed"
|
|
102
|
+
unset LANG LC_MESSAGES
|
|
103
|
+
# target runs in its native locale: a locale-dependent verdict (wc -m on multibyte) must not be forced to C
|
|
104
|
+
export LC_ALL=C.UTF-8
|
|
105
|
+
inv "$(chk ac-8 'printf "\303\251" | wc -m | grep -qx 1')"; rc=$(run); eq "target keeps native locale (wc -m multibyte)" "$rc" 0
|
|
106
|
+
unset LC_ALL
|
|
107
|
+
inv 'ACCEPTANCE_CHECK: id=ac-9 type=command target="no-such-cmd-xyzzy" matcher="QQQ" phase=review description=d'; rc=$(run); eq "matcher branch: first word missing malformed" "$rc" 0
|
|
108
|
+
inv 'ACCEPTANCE_CHECK: id=ac-9 type=command target="bash inner.sh" matcher="QQQ" phase=review description=d'; rc=$(run); eq "matcher branch: inner missing tool is a real fail" "$rc" 1
|
|
109
|
+
|
|
93
110
|
# Selection parity with B6.5 (forge#3809): non-review checks are selected by the `gh pr` target only, never by a phase= text match.
|
|
94
111
|
bt() { printf 'ACCEPTANCE_CHECK: id=%s type=command target="%s" matcher="exit_0" phase=%s description=%s' "$1" "$2" "$3" "$4"; }
|
|
95
112
|
inv "$(bt ac-1 'gh pr view 1 && false' build d)"; rc=$(run); eq "phase=build gh pr check is evaluated (fails)" "$rc" 1
|
|
@@ -99,5 +116,8 @@ inv "$(bt ac-1 'false' build d)"; rc=$(run); eq "phase=build non-gh-pr check is
|
|
|
99
116
|
inv "$(bt ac-1 'gh pr view 1 && false' build 'x phase=review y')"; rc=$(run); eq "phase=review in description does not change selection" "$rc" 1
|
|
100
117
|
inv "$(bt ac-1 'false' build 'x phase=review y')"; rc=$(run); eq "phase=review in description does not select a non-gh-pr check" "$rc" 0
|
|
101
118
|
inv 'ACCEPTANCE_CHECK: id=ac-1 type=command target="gh pr view 1 && false" matcher="exit_0" description=untagged'; rc=$(run); eq "untagged gh pr check is evaluated" "$rc" 1
|
|
119
|
+
# test= tolerance (forge#3630): a test="..." token between phase= and description= does not change phase handling or selection.
|
|
120
|
+
inv 'ACCEPTANCE_CHECK: id=ac-1 type=command target="gh pr view 1 && false" matcher="exit_0" phase=review test="node --test x.mjs" description=d'; rc=$(run); eq "test= token on a phase=review check is tolerated (evaluated, fails)" "$rc" 1
|
|
121
|
+
inv 'ACCEPTANCE_CHECK: id=ac-1 type=command target="false" matcher="exit_0" phase=build test="node --test x.mjs" description=d'; rc=$(run); eq "test= token on a phase=build non-gh-pr check is not selected" "$rc" 0
|
|
102
122
|
|
|
103
123
|
echo "$PASS passed, $FAILN failed"; [ "$FAILN" -eq 0 ]
|
package/scripts/review-delta.sh
CHANGED
|
@@ -172,7 +172,14 @@ while IFS= read -r c; do
|
|
|
172
172
|
if [ "$rc" -ne 0 ]; then SYNC=0; break; fi # second parent is not base history
|
|
173
173
|
CC=$(G diff-tree --cc --no-commit-id -r "$c" 2>/dev/null); rc=$?
|
|
174
174
|
[ "$rc" -eq 0 ] || full
|
|
175
|
-
if [ -n "$CC" ]; then
|
|
175
|
+
if [ -n "$CC" ]; then
|
|
176
|
+
# Non-empty --cc is either hand-written resolution content (not mechanical) or a merge=union result (forge#3969).
|
|
177
|
+
# Accept only when re-running the merge mechanically reproduces the committed tree exactly: a clean
|
|
178
|
+
# `merge-tree` (rc 0) whose tree equals the merge commit's. A hand-resolved or hand-edited merge never matches.
|
|
179
|
+
MT=$(G merge-tree --write-tree "$2" "$3" 2>/dev/null); rc=$?
|
|
180
|
+
MT=$(printf '%s\n' "$MT" | sed -n 1p)
|
|
181
|
+
if [ "$rc" -ne 0 ] || [ -z "$MT" ] || [ "$MT" != "$(G rev-parse "${c}^{tree}" 2>/dev/null)" ]; then SYNC=0; break; fi
|
|
182
|
+
fi
|
|
176
183
|
done <<< "$CHAIN"
|
|
177
184
|
|
|
178
185
|
# per-path shape of a diff: modes, status and path for every file, plus both blob ids for binary files
|
|
@@ -83,6 +83,25 @@ commit shared.txt "feat side" "feat edits shared"; R=$(sha HEAD); panel "$R"
|
|
|
83
83
|
g merge -q --no-ff origin/main >/dev/null 2>&1; printf 'resolved\n' > "$W/shared.txt"; g add -A >/dev/null; g commit -q -m "merge main (resolved)" >/dev/null; HEAD=$(sha HEAD)
|
|
84
84
|
out=$(run_rd | sed -n 1p); case "$out" in BASE_SYNC_ONLY*) bad "conflict-resolution merge must not be BASE_SYNC_ONLY (got $out)" ;; *) ok ;; esac
|
|
85
85
|
|
|
86
|
+
# 5b. union-merged base merge (forge#3969): both sides append a different line to a union-attributed registry.
|
|
87
|
+
# merge=union makes the same-spot append conflict-free, so the sync merge has no resolution edit and stays BASE_SYNC_ONLY.
|
|
88
|
+
mkrepo; g checkout -q main; printf 'e1\ne2\n' > "$W/list.txt"; g add -A >/dev/null; g commit -q -m "registry" >/dev/null; g push -q origin main
|
|
89
|
+
g checkout -q feat; g merge -q --no-ff -m "sync" main >/dev/null 2>&1
|
|
90
|
+
printf 'list.txt merge=union\n' > "$W/.git/info/attributes"
|
|
91
|
+
printf 'e-pr\n' >> "$W/list.txt"; g add -A >/dev/null; g commit -q -m "pr appends" >/dev/null; R=$(sha HEAD); panel "$R"
|
|
92
|
+
g checkout -q main; printf 'e-base\n' >> "$W/list.txt"; g add -A >/dev/null; g commit -q -m "base appends" >/dev/null; g push -q origin main; g checkout -q feat; g fetch -q origin
|
|
93
|
+
if g merge -q --no-ff -m "merge main" origin/main >/dev/null 2>&1; then
|
|
94
|
+
HEAD=$(sha HEAD)
|
|
95
|
+
grep -qxF e-pr "$W/list.txt" && grep -qxF e-base "$W/list.txt" && ok || bad "union-merged base merge lost a line"
|
|
96
|
+
expect "union-merged base merge" "BASE_SYNC_ONLY $R" 1
|
|
97
|
+
else
|
|
98
|
+
bad "union-merged base merge should be conflict-free"
|
|
99
|
+
fi
|
|
100
|
+
# same union merge, then hand-edited before the merge commit: the mechanical re-merge no longer matches -> not BASE_SYNC_ONLY
|
|
101
|
+
g reset -q --hard "$R"; g merge -q --no-ff --no-commit origin/main >/dev/null 2>&1; printf 'e-hand\n' >> "$W/list.txt"; g add -A >/dev/null; g commit -q -m "merge main (edited)" >/dev/null; HEAD=$(sha HEAD)
|
|
102
|
+
out=$(run_rd | sed -n 1p); case "$out" in BASE_SYNC_ONLY*) bad "hand-edited union merge must not be BASE_SYNC_ONLY (got $out)" ;; *) ok ;; esac
|
|
103
|
+
rm -f "$W/.git/info/attributes"
|
|
104
|
+
|
|
86
105
|
# 6. mixed: base merge plus a regular commit -> DELTA
|
|
87
106
|
mkrepo; R=$(sha HEAD); panel "$R"; base_advance b.txt "b1"; g merge -q --no-ff -m "merge main" origin/main >/dev/null 2>&1; commit pr.txt "pr2" "pr2"; HEAD=$(sha HEAD)
|
|
88
107
|
expect "base merge plus regular commit" "DELTA $R..$HEAD" 1
|