forgedock 1.29.1 → 1.31.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 +5 -2
- package/bin/engine/phases.mjs +20 -3
- package/bin/engine/runlog.mjs +82 -12
- package/bin/engine/telemetry-seam.mjs +11 -4
- package/bin/engine/telemetry-seam.test.mjs +18 -0
- package/bin/engine-cli.mjs +453 -66
- package/bin/engine.mjs +50 -38
- package/bin/forgedock.mjs +35 -3
- package/bin/gc.mjs +50 -17
- package/bin/labels.json +5 -0
- package/bin/lean/apishape.mjs +170 -0
- package/bin/lean/apishape.test.mjs +187 -0
- package/bin/lean/batch.mjs +32 -0
- package/bin/lean/batch.test.mjs +356 -2
- package/bin/lean/budget.mjs +89 -0
- package/bin/lean/budget.test.mjs +106 -0
- package/bin/lean/checks.mjs +32 -9
- package/bin/lean/checks.test.mjs +78 -1
- package/bin/lean/conformance.e2e.test.mjs +27 -5
- package/bin/lean/conformance.mjs +278 -22
- package/bin/lean/conformance.test.mjs +251 -5
- package/bin/lean/context.mjs +133 -55
- package/bin/lean/context.test.mjs +175 -9
- package/bin/lean/decompose.mjs +90 -0
- package/bin/lean/decompose.test.mjs +105 -0
- package/bin/lean/depcache.mjs +11 -7
- package/bin/lean/depcache.test.mjs +81 -0
- package/bin/lean/e2e.mjs +102 -0
- package/bin/lean/e2e.test.mjs +86 -0
- package/bin/lean/effects.mjs +55 -19
- package/bin/lean/enumerating.mjs +189 -0
- package/bin/lean/enumerating.test.mjs +97 -0
- package/bin/lean/explore.mjs +47 -0
- package/bin/lean/explore.test.mjs +61 -0
- package/bin/lean/facts.mjs +84 -0
- package/bin/lean/facts.test.mjs +66 -0
- package/bin/lean/fixround.mjs +90 -0
- package/bin/lean/fixround.test.mjs +117 -0
- package/bin/lean/heavy.mjs +35 -2
- package/bin/lean/heavy.test.mjs +32 -2
- package/bin/lean/lean.mjs +799 -127
- package/bin/lean/lean.test.mjs +1749 -37
- package/bin/lean/ledger.mjs +5 -3
- package/bin/lean/ledger.test.mjs +18 -0
- package/bin/lean/light.mjs +135 -0
- package/bin/lean/light.test.mjs +173 -0
- package/bin/lean/limits.test.mjs +36 -1
- package/bin/lean/orchestrate.mjs +341 -99
- package/bin/lean/pitfalls.mjs +136 -0
- package/bin/lean/pitfalls.test.mjs +117 -0
- package/bin/lean/postmerge.mjs +85 -0
- package/bin/lean/postmerge.test.mjs +79 -0
- package/bin/lean/precheck.mjs +180 -0
- package/bin/lean/precheck.test.mjs +205 -0
- package/bin/lean/prefilter.mjs +180 -9
- package/bin/lean/prefilter.test.mjs +175 -3
- package/bin/lean/preflight.mjs +238 -0
- package/bin/lean/preflight.test.mjs +108 -0
- package/bin/lean/probe.mjs +297 -0
- package/bin/lean/probe.test.mjs +210 -0
- package/bin/lean/prompts.mjs +134 -12
- package/bin/lean/recovery.test.mjs +75 -0
- package/bin/lean/render.mjs +7 -3
- package/bin/lean/resume.mjs +67 -0
- package/bin/lean/resume.test.mjs +84 -0
- package/bin/lean/review-trigger.mjs +12 -0
- package/bin/lean/review-trigger.test.mjs +17 -1
- package/bin/lean/routing.mjs +74 -0
- package/bin/lean/routing.test.mjs +67 -0
- package/bin/lean/sched.test.mjs +127 -1
- package/bin/lean/schedd.mjs +55 -9
- package/bin/lean/schedd.test.mjs +149 -3
- package/bin/lean/selfreview.mjs +37 -0
- package/bin/lean/selfreview.test.mjs +51 -0
- package/bin/lean/seqlease.mjs +40 -0
- package/bin/lean/seqlease.test.mjs +101 -0
- package/bin/lean/sequence.mjs +385 -0
- package/bin/lean/sequence.test.mjs +373 -0
- package/bin/lean/testmap.mjs +43 -6
- package/bin/lean/testmap.test.mjs +38 -1
- package/bin/lean/toolchain.mjs +60 -15
- package/bin/lean/toolchain.test.mjs +120 -3
- package/bin/lean/toolstats.mjs +72 -0
- package/bin/lean/toolstats.test.mjs +47 -0
- package/bin/lean/verifier.mjs +232 -0
- package/bin/lean/verifier.test.mjs +163 -0
- package/bin/lean/verify.mjs +77 -10
- package/bin/lean/verify.test.mjs +74 -3
- package/bin/review-staging.mjs +533 -0
- package/bin/runner.mjs +82 -13
- package/bin/stats/aggregate.mjs +87 -2
- package/bin/stats/efficiency.mjs +319 -0
- package/bin/stats/efficiency.test.mjs +233 -0
- package/bin/stats/github-source.mjs +8 -3
- package/bin/stats/index.mjs +69 -6
- package/bin/stats/local-source.mjs +86 -3
- package/bin/stats/query.mjs +48 -0
- package/bin/stats/render.mjs +22 -0
- package/bin/stats/stats.test.mjs +224 -5
- package/commands/orchestrate.md +6 -2
- package/commands/review-pr-agents/protocols.md +1 -1
- package/commands/review-pr-staging.md +84 -831
- package/commands/review-pr.md +1 -1
- package/commands/work-on/build/implement.md +1 -0
- package/commands/work-on/build/validate.md +11 -1
- package/docs/CONFIG.md +88 -6
- package/docs/OUTCOME-CODES.md +7 -4
- package/docs/RELEASE-NOTES.md +10 -0
- package/docs/STATS.md +53 -1
- package/docs/TELEMETRY.md +1 -1
- package/docs/spec/review-protocol.md +1 -1
- package/package.json +1 -1
- package/scripts/acceptance-red-green.sh +13 -4
- package/scripts/acceptance-red-green.test.sh +14 -0
- package/scripts/ci-local/lib.mjs +20 -0
- package/scripts/ci-local/plan.mjs +2 -12
- package/scripts/ci-local/scoping.mjs +10 -1
- package/scripts/ci-local.test.sh +2 -0
- package/scripts/fixtures/apishape/other/go-change/base/x.go +3 -0
- package/scripts/fixtures/apishape/other/go-change/head/x.go +3 -0
- package/scripts/fixtures/apishape/python/generator-function/base/seq.py +3 -0
- package/scripts/fixtures/apishape/python/generator-function/head/seq.py +2 -0
- package/scripts/fixtures/apishape/python/ichunked/base/more.py +17 -0
- package/scripts/fixtures/apishape/python/ichunked/head/more.py +20 -0
- package/scripts/fixtures/apishape/python/private-edit/base/calc.py +6 -0
- package/scripts/fixtures/apishape/python/private-edit/head/calc.py +9 -0
- package/scripts/fixtures/apishape/python/removed/base/ops.py +6 -0
- package/scripts/fixtures/apishape/python/removed/head/ops.py +2 -0
- package/scripts/fixtures/apishape/python/renamed/base/text.py +2 -0
- package/scripts/fixtures/apishape/python/renamed/head/text.py +2 -0
- package/scripts/fixtures/apishape/python/signature/base/shapes.py +2 -0
- package/scripts/fixtures/apishape/python/signature/head/shapes.py +2 -0
- package/scripts/fixtures/apishape/python/sync-async/base/client.py +7 -0
- package/scripts/fixtures/apishape/python/sync-async/head/client.py +7 -0
- package/scripts/fixtures/apishape/ts/generator-function/base/seq.js +3 -0
- package/scripts/fixtures/apishape/ts/generator-function/head/seq.js +5 -0
- package/scripts/fixtures/apishape/ts/private-edit/base/calc.ts +7 -0
- package/scripts/fixtures/apishape/ts/private-edit/head/calc.ts +9 -0
- package/scripts/fixtures/apishape/ts/removed/base/ops.ts +7 -0
- package/scripts/fixtures/apishape/ts/removed/head/ops.ts +3 -0
- package/scripts/fixtures/apishape/ts/renamed/base/text.ts +3 -0
- package/scripts/fixtures/apishape/ts/renamed/head/text.ts +3 -0
- package/scripts/fixtures/apishape/ts/signature/base/shapes.ts +3 -0
- package/scripts/fixtures/apishape/ts/signature/head/shapes.ts +3 -0
- package/scripts/fixtures/apishape/ts/sync-async/base/client.mjs +5 -0
- package/scripts/fixtures/apishape/ts/sync-async/head/client.mjs +5 -0
- package/scripts/forge-root.test.sh +2 -2
- package/scripts/publish-staging-sync.test.sh +62 -4
- package/scripts/review-finalize.sh +17 -6
- package/scripts/review-finalize.test.sh +47 -0
- package/scripts/review-pr-arguments.test.sh +13 -1
- package/scripts/review-pr-delta-wiring.test.sh +2 -2
- package/scripts/review-pr-gate2.test.sh +6 -14
- package/scripts/review-pr-staging-finding-filter.test.sh +4 -4
- package/scripts/review-pr-staging-scope.test.sh +39 -33
- package/scripts/review-prepare.sh +73 -6
- package/scripts/review-prepare.test.sh +85 -0
- package/scripts/review-staging-post.sh +293 -0
- package/scripts/review-staging-post.test.sh +143 -0
- package/scripts/review-staging-pre.sh +401 -0
- package/scripts/review-staging-pre.test.sh +150 -0
- package/scripts/symbol-index.mjs +26 -7
- package/scripts/symbol-index.py +55 -9
- package/scripts/test-map.sh +63 -17
- package/scripts/test-map.test.sh +55 -0
- package/scripts/trusted-comments.test.sh +1 -1
|
@@ -589,6 +589,8 @@ function scopeTsc(rest, cmd, cwdRel, sctx, join) {
|
|
|
589
589
|
return scoped(join([rest[0], '-b', ...owners.map((d) => fromCwd(cwdRel, d)), ...flags.filter((f) => f !== '--noEmit')]), owners.length, null, 'tsc-project', { projects: owners });
|
|
590
590
|
}
|
|
591
591
|
|
|
592
|
+
const isLintTask = (t) => t === 'lint' || t.startsWith('lint:') || t.endsWith(':lint');
|
|
593
|
+
|
|
592
594
|
function scopeMono(tool, rest, cmd, sctx, join) {
|
|
593
595
|
if (sctx.widen) return full(cmd, sctx.widen, { selector: `${tool}-affected` });
|
|
594
596
|
const base = sctx.from || sctx.baseRef;
|
|
@@ -597,7 +599,14 @@ function scopeMono(tool, rest, cmd, sctx, join) {
|
|
|
597
599
|
if (rest[1] === 'run-many' && !out.some((t) => t === '-t' || t.startsWith('--target'))) return full(cmd, '', { selector: 'unscoped:nx-no-target' });
|
|
598
600
|
return scoped(join([...out, `--base=${base}`]), null, null, 'nx-affected');
|
|
599
601
|
}
|
|
600
|
-
if (tool === 'turbo' && rest[1] === 'run') {
|
|
602
|
+
if (tool === 'turbo' && rest[1] === 'run') {
|
|
603
|
+
if (rest.some((t) => t.startsWith('--filter'))) return full(cmd, '', { selector: 'unscoped:explicit-filter' });
|
|
604
|
+
// #4381: lint is per-file, so a lint-only run covers the changed packages, not their dependents (`...[base]`).
|
|
605
|
+
const end = rest.findIndex((t, i) => i > 1 && t.startsWith('-'));
|
|
606
|
+
const tasks = rest.slice(2, end < 0 ? rest.length : end);
|
|
607
|
+
if (tasks.length && tasks.every(isLintTask)) return scoped(join([...rest, `--filter=[${base}]`]), null, null, 'turbo-changed');
|
|
608
|
+
return scoped(join([...rest, `--filter=...[${base}]`]), null, null, 'turbo-affected');
|
|
609
|
+
}
|
|
601
610
|
return null;
|
|
602
611
|
}
|
|
603
612
|
|
package/scripts/ci-local.test.sh
CHANGED
|
@@ -23,6 +23,8 @@ export FORGE_TELEMETRY=off
|
|
|
23
23
|
export FORGE_CI_LOCAL_HOME="$T/home"
|
|
24
24
|
export FORGE_CI_LOCAL_SLOTS=4
|
|
25
25
|
unset FORGE_CI_LOCAL_SERVICES FORGE_CI_LOCAL_NO_YQ FORGE_CI_LOCAL_BUDGET_SECONDS FORGE_CI_LOCAL_JOB_TIMEOUT FORGE_CI_LOCAL_NO_CACHE
|
|
26
|
+
# A lane's FORGE_HOST_PARALLELISM cap (#4182) would serialise the parallelism fixtures below; capParallel has its own test.
|
|
27
|
+
unset FORGE_HOST_PARALLELISM
|
|
26
28
|
HOST=$(uname -s)
|
|
27
29
|
|
|
28
30
|
pass=0; fail=0
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from itertools import islice
|
|
2
|
+
|
|
3
|
+
__all__ = ["ichunked"]
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def _take(iterator, n):
|
|
7
|
+
return list(islice(iterator, n))
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def ichunked(iterable, n):
|
|
11
|
+
"""Yield successive chunks of length n from iterable."""
|
|
12
|
+
iterator = iter(iterable)
|
|
13
|
+
while True:
|
|
14
|
+
chunk = _take(iterator, n)
|
|
15
|
+
if not chunk:
|
|
16
|
+
return
|
|
17
|
+
yield iter(chunk)
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
from itertools import islice
|
|
2
|
+
|
|
3
|
+
__all__ = ["ichunked"]
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def _take(iterator, n):
|
|
7
|
+
return list(islice(iterator, n))
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _chunks(iterator, n):
|
|
11
|
+
while True:
|
|
12
|
+
chunk = _take(iterator, n)
|
|
13
|
+
if not chunk:
|
|
14
|
+
return
|
|
15
|
+
yield iter(chunk)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def ichunked(iterable, n):
|
|
19
|
+
"""Return an iterator over successive chunks of length n from iterable."""
|
|
20
|
+
return _chunks(iter(iterable), n)
|
|
@@ -416,9 +416,9 @@ FR="$ROOT/scripts/lib/forge-resolve.sh"; FB="$ROOT/scripts/lib/fence-bootstrap.s
|
|
|
416
416
|
[ -f "$FR" ] && [ -f "$FB" ] && ok || bad "scripts/lib/forge-resolve.sh and scripts/lib/fence-bootstrap.sh must exist"
|
|
417
417
|
sed 's/^[[:space:]]*//' "$STG_SPEC" | grep '^type forge_resolve >/dev/null' | sort | uniq -c > "$T/loc_uniq"
|
|
418
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"
|
|
419
|
-
[ "$(awk '{print $1}' "$T/loc_uniq")" -ge
|
|
419
|
+
[ "$(awk '{print $1}' "$T/loc_uniq")" -ge 12 ] && ok || bad "expected a locator on every bootstrap (10) and resolver (2) stub (the pre/post-dispatch fences moved to forgedock review-staging, #4359)"
|
|
420
420
|
grep -qE 'PWD|\$\(pwd\)|REPO_PATH' "$T/locator.sh" && bad "stub locator must not consult the cwd or repo under review" || ok
|
|
421
|
-
[ "$(grep -c '# FENCE_BOOTSTRAP$' "$STG_SPEC")" =
|
|
421
|
+
[ "$(grep -c '# FENCE_BOOTSTRAP$' "$STG_SPEC")" = 10 ] && ok || bad "expected 10 fence bootstrap stubs"
|
|
422
422
|
[ "$(grep '# FENCE_BOOTSTRAP$' "$STG_SPEC" | sort -u | wc -l | tr -d ' ')" = 1 ] && ok || bad "fence bootstrap stubs must be byte-identical"
|
|
423
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"
|
|
424
424
|
# the spec carries none of the old inlined blocks
|
|
@@ -1,6 +1,6 @@
|
|
|
1
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.
|
|
2
|
+
# publish-staging-sync.test.sh — guards the publish.yml "Sync version to staging" step (forge#3695, forge#4357)
|
|
3
|
+
# and the deploy-pr Phase 2A.5 version-sync pre-flight. Static assertions plus a local-only run of the step (bare origin, stub gh); no network.
|
|
4
4
|
# Usage: bash scripts/publish-staging-sync.test.sh
|
|
5
5
|
# SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios
|
|
6
6
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
@@ -29,14 +29,72 @@ else bad "sync step sits between Push version bump and Create GitHub Release"; f
|
|
|
29
29
|
|
|
30
30
|
stepHas() { if printf '%s\n' "$STEP" | grep -qF -e "$2"; then ok "$1"; else bad "$1 (missing '$2')"; fi; }
|
|
31
31
|
stepHas "step fetches origin/staging" "origin/staging"
|
|
32
|
-
stepHas "step pushes HEAD to staging" "git push origin HEAD:staging"
|
|
33
32
|
stepHas "step is non-fatal" "continue-on-error: true"
|
|
34
33
|
stepHas "step is guarded by SKIP_PUBLISH" "env.SKIP_PUBLISH != 'true'"
|
|
35
|
-
|
|
34
|
+
# forge#4357: staging is protected (GH006), so the sync goes through a PR, never a direct push.
|
|
35
|
+
stepHas "step pushes to a chore/sync-version-X.Y.Z branch" 'SYNC_BRANCH="chore/sync-version-${VERSION}"'
|
|
36
|
+
stepHas "step force-pushes the sync branch" 'git push -f origin "HEAD:refs/heads/$SYNC_BRANCH"'
|
|
37
|
+
stepHas "step looks up an existing sync PR" 'gh pr list --head "$SYNC_BRANCH" --base staging'
|
|
38
|
+
stepHas "step opens a PR to staging" 'gh pr create --base staging --head "$SYNC_BRANCH"'
|
|
39
|
+
stepHas "step authenticates gh" 'GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}'
|
|
40
|
+
stepHas "step skips when staging is at or above the version" "nothing to sync"
|
|
41
|
+
if printf '%s\n' "$STEP" | grep -E 'git push' | grep -qE 'HEAD:staging|refs/heads/staging|origin staging'; then
|
|
42
|
+
bad "step must not push directly to staging"; else ok "step does not push directly to staging"; fi
|
|
43
|
+
if printf '%s\n' "$STEP" | grep -E 'git commit' | grep -qF '[skip ci]'; then
|
|
44
|
+
bad "sync commit (PR head) must not carry [skip ci]"; else ok "sync commit does not carry [skip ci]"; fi
|
|
45
|
+
PERMS=$(awk '/^ permissions:/{f=1;next} f&&/^ [a-z]/{exit} f{print}' "$PUB")
|
|
46
|
+
if printf '%s\n' "$PERMS" | grep -qE '^ +pull-requests: write'; then ok "publish job grants pull-requests: write"; else bad "publish job grants pull-requests: write"; fi
|
|
36
47
|
stepHas "only version files are staged" "git add package.json .claude-plugin/plugin.json"
|
|
37
48
|
stepHas "failure emits a warning" "::warning::"
|
|
38
49
|
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
50
|
|
|
51
|
+
# Behavioural: run the step's script against a local bare origin with a stub gh.
|
|
52
|
+
RUN=$(printf '%s\n' "$STEP" | awk '/^ run: \|/{f=1;next} f{sub(/^ /,"");print}')
|
|
53
|
+
SB=$(mktemp -d "${TMPDIR:-/tmp}/pubsync.XXXXXX")
|
|
54
|
+
trap 'rm -rf "$SB"' EXIT
|
|
55
|
+
(
|
|
56
|
+
set -e
|
|
57
|
+
export GIT_AUTHOR_NAME=t GIT_AUTHOR_EMAIL=t@t GIT_COMMITTER_NAME=t GIT_COMMITTER_EMAIL=t@t
|
|
58
|
+
git init -q --bare "$SB/origin.git"
|
|
59
|
+
git init -q -b staging "$SB/seed"; cd "$SB/seed"
|
|
60
|
+
mkdir .claude-plugin
|
|
61
|
+
echo '{"name":"x","version":"1.9.0"}' > package.json
|
|
62
|
+
echo '{"name":"x","version":"1.9.0"}' > .claude-plugin/plugin.json
|
|
63
|
+
echo keep > other.txt
|
|
64
|
+
git add -A; git commit -qm seed; git push -q "$SB/origin.git" staging
|
|
65
|
+
git clone -q -b staging "$SB/origin.git" "$SB/work"
|
|
66
|
+
mkdir "$SB/bin"
|
|
67
|
+
printf '#!/bin/sh\necho "$*" >> "%s/gh.log"\ncase "$2" in create) echo https://example.test/pr/1;; list) [ -z "$GH_STUB_LIST" ] || echo "$GH_STUB_LIST";; esac\n' "$SB" > "$SB/bin/gh"
|
|
68
|
+
chmod +x "$SB/bin/gh"
|
|
69
|
+
) >/dev/null 2>&1 || bad "behavioural sandbox setup"
|
|
70
|
+
# The workflow's bump step sets git user.name/email; give the step that identity here
|
|
71
|
+
# (CI runners have no global git config).
|
|
72
|
+
runStep() { (cd "$SB/work" && PATH="$SB/bin:$PATH" RUNNER_TEMP="$SB/rt" VERSION="$1" \
|
|
73
|
+
GIT_AUTHOR_NAME=t GIT_AUTHOR_EMAIL=t@t GIT_COMMITTER_NAME=t GIT_COMMITTER_EMAIL=t@t bash -c "$RUN") >"$SB/out" 2>&1; }
|
|
74
|
+
mkdir -p "$SB/rt"
|
|
75
|
+
STAGING_BEFORE=$(git -C "$SB/origin.git" rev-parse staging 2>/dev/null)
|
|
76
|
+
if runStep 1.10.0; then ok "step exits 0"; else bad "step exits 0"; fi
|
|
77
|
+
[ "$(git -C "$SB/origin.git" rev-parse staging 2>/dev/null)" = "$STAGING_BEFORE" ] && ok "staging is not pushed to" || bad "staging is not pushed to"
|
|
78
|
+
SYNCV=$(git -C "$SB/origin.git" show chore/sync-version-1.10.0:package.json 2>/dev/null | node -e "process.stdout.write(JSON.parse(require('fs').readFileSync(0,'utf8')).version)" 2>/dev/null)
|
|
79
|
+
[ "$SYNCV" = "1.10.0" ] && ok "sync branch carries the new version" || bad "sync branch carries the new version (got '$SYNCV')"
|
|
80
|
+
CHANGED=$(git -C "$SB/origin.git" diff --name-only staging chore/sync-version-1.10.0 2>/dev/null | sort | tr '\n' ' ')
|
|
81
|
+
[ "$CHANGED" = ".claude-plugin/plugin.json package.json " ] && ok "sync commit is version-only" || bad "sync commit is version-only (got '$CHANGED')"
|
|
82
|
+
grep -q -- 'pr create --base staging --head chore/sync-version-1.10.0' "$SB/gh.log" 2>/dev/null && ok "gh pr create targets staging" || bad "gh pr create targets staging"
|
|
83
|
+
: > "$SB/gh.log"
|
|
84
|
+
if GH_STUB_LIST=https://example.test/pr/7 runStep 1.10.0 && grep -q 'Updated existing staging sync PR https://example.test/pr/7' "$SB/out" \
|
|
85
|
+
&& ! grep -q 'pr create' "$SB/gh.log"; then
|
|
86
|
+
ok "an open sync PR is updated, not duplicated"; else bad "an open sync PR is updated, not duplicated"; fi
|
|
87
|
+
: > "$SB/gh.log"
|
|
88
|
+
if runStep 1.9.0 && grep -q 'nothing to sync' "$SB/out" && [ ! -s "$SB/gh.log" ]; then
|
|
89
|
+
ok "skipped when staging is already at the version"; else bad "skipped when staging is already at the version"; fi
|
|
90
|
+
# A failed commit (no identity) must not push an unchanged branch or open a PR.
|
|
91
|
+
: > "$SB/gh.log"
|
|
92
|
+
git -C "$SB/origin.git" branch -D chore/sync-version-1.11.0 >/dev/null 2>&1 || true
|
|
93
|
+
(cd "$SB/work" && PATH="$SB/bin:$PATH" RUNNER_TEMP="$SB/rt" VERSION=1.11.0 GIT_CONFIG_GLOBAL=/dev/null GIT_CONFIG_NOSYSTEM=1 \
|
|
94
|
+
GIT_AUTHOR_NAME='' GIT_COMMITTER_NAME='' EMAIL='' bash -c "$RUN") >"$SB/out" 2>&1
|
|
95
|
+
if ! git -C "$SB/origin.git" rev-parse -q --verify chore/sync-version-1.11.0 >/dev/null && [ ! -s "$SB/gh.log" ]; then
|
|
96
|
+
ok "failed commit pushes nothing and opens no PR"; else bad "failed commit pushes nothing and opens no PR"; fi
|
|
97
|
+
|
|
40
98
|
has "deploy-pr has the 2A.5 pre-flight" "### 2A.5: Version Sync Pre-flight" "$DEP"
|
|
41
99
|
has "deploy-pr pre-flight compares package.json" "package.json" "$DEP"
|
|
42
100
|
|
|
@@ -364,12 +364,18 @@ do_extract() {
|
|
|
364
364
|
}
|
|
365
365
|
}' "$BODY_FILE" > "$SECTION_FILE" 2>/dev/null || : > "$SECTION_FILE"
|
|
366
366
|
# Failure scenario: present / absent when the section was found, unknown when the carrying comment or section is unreadable.
|
|
367
|
+
# Read from the prose (#4356): a `**Failure scenario**` anywhere on a line, or a line that starts (after list/quote/bold markup, any
|
|
368
|
+
# case) with a `failure scenario`, `scenario`, `repro`, `reproduction` or `steps to reproduce` label followed by `:` or a closing `**`.
|
|
369
|
+
# The content sits on that line or the next non-blank one; a mid-sentence use of the word is not a label.
|
|
367
370
|
SCENARIO="unknown"
|
|
368
371
|
if [ -s "$SECTION_FILE" ]; then
|
|
369
372
|
SCENARIO=$(awk '
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
+
want { t = $0; gsub(/[[:space:]]/, "", t); if (t != "") { print "present"; found = 1; exit } next }
|
|
374
|
+
{ l = tolower($0); s = "" }
|
|
375
|
+
l ~ /\*\*failure scenario\*\*/ { s = l; sub(/^.*\*\*failure scenario\*\*/, "", s) }
|
|
376
|
+
s == "" { m = l; sub(/^[[:space:]]*([-*+>_][[:space:]]*)*/, "", m)
|
|
377
|
+
if (match(m, /^(failure scenario|steps to reproduce|reproduction|repro|scenario)((\*\*|__)?[[:space:]]*:|\*\*|__)/)) s = substr(m, RLENGTH + 1); else next }
|
|
378
|
+
{ gsub(/[*_:[:space:]]/, "", s); if (s != "") { print "present"; found = 1; exit } else want = 1 }
|
|
373
379
|
END { if (!found) print "absent" }' "$SECTION_FILE")
|
|
374
380
|
fi
|
|
375
381
|
FIX_EFF=$(grep -iE '^[[:space:]]*(\*\*Fix efficacy\*\*:?|FIX_EFFICACY:)[[:space:]]*\**[[:space:]]*(fails|regresses)' "$SECTION_FILE" 2>/dev/null | head -n 1 \
|
|
@@ -531,10 +537,15 @@ SYMEOF
|
|
|
531
537
|
case "$SEV" in MEDIUM|LOW|INFO) ROUTE="pre-existing" ;; esac
|
|
532
538
|
fi
|
|
533
539
|
fi
|
|
534
|
-
# Gate 3 (concrete failure): MEDIUM or lower without a
|
|
535
|
-
# CRITICAL/HIGH are exempt
|
|
540
|
+
# Gate 3 (concrete failure): MEDIUM or lower without a failure scenario in its prose is speculative hardening, demoted to a NOTE.
|
|
541
|
+
# CRITICAL/HIGH (any confidence) and CONFIRMED MEDIUM are exempt (#4356): a confirmed finding is filed through the classifier with its
|
|
542
|
+
# agent and prose as evidence. An unreadable carrying comment keeps the finding.
|
|
536
543
|
if [ "$ROUTE" = "file" ] && [ "$SCEN" = "absent" ]; then
|
|
537
|
-
|
|
544
|
+
CONF=$(upper "$(printf '%s' "$F" | jq -r '.confidence // ""')")
|
|
545
|
+
case "$SEV" in
|
|
546
|
+
MEDIUM) [ "$CONF" = "CONFIRMED" ] || ROUTE="demoted-no-scenario" ;;
|
|
547
|
+
LOW|INFO) ROUTE="demoted-no-scenario" ;;
|
|
548
|
+
esac
|
|
538
549
|
fi
|
|
539
550
|
case "$ROUTE" in pre-existing) N_PREEXISTING=$((N_PREEXISTING + 1)) ;; demoted-no-scenario) N_NO_SCENARIO=$((N_NO_SCENARIO + 1)) ;; esac
|
|
540
551
|
N_KEPT=$((N_KEPT + 1))
|
|
@@ -286,6 +286,45 @@ want "scope: HIGH near an added line" '.findings[] | select(.id == "N-2") | .sco
|
|
|
286
286
|
want "scope: MEDIUM near an added line" '.findings[] | select(.id == "N-3") | .scope' findings.json "introduced"
|
|
287
287
|
want "scope: every finding carries a valid scope" '[.findings[].scope | select(. == "introduced" or . == "pre-existing" or . == "unknown")] | length' findings.json "$(jq '.findings | length' "$WORK/out/findings.json")"
|
|
288
288
|
want "scope: introduced count" '.counts.findings_introduced' findings.json "$(jq '[.findings[] | select(.scope == "introduced")] | length' "$WORK/out/findings.json")"
|
|
289
|
+
# Gate 3 reads the scenario from the prose and never demotes a CONFIRMED MEDIUM+ for a missing field (#4356); the damper stays for the rest
|
|
290
|
+
reset
|
|
291
|
+
for f in a b c d e f g h; do printf '{"filename":"%s.py","status":"modified","patch":"@@ -9,0 +10,1 @@\\n+new line"}\n' "$f"; done | jq -s . > "$FIX/pr-files.json"
|
|
292
|
+
cm MEMBER User "$(agent_body security "$SHA" '### C-1 confirmed
|
|
293
|
+
Real bug described in prose only.
|
|
294
|
+
### C-2 confirmed lower case
|
|
295
|
+
Real bug described in prose only.
|
|
296
|
+
### C-3 confirmed low
|
|
297
|
+
Prose only.
|
|
298
|
+
### C-4 plain label
|
|
299
|
+
Failure scenario: a missing key crashes the loader
|
|
300
|
+
### C-5 repro label
|
|
301
|
+
- **Repro**: call load() twice
|
|
302
|
+
### C-6 no scenario
|
|
303
|
+
Some prose without the field.
|
|
304
|
+
### C-7 mid-sentence word
|
|
305
|
+
no scenario written here
|
|
306
|
+
### C-8 label on next line
|
|
307
|
+
**Steps to reproduce**:
|
|
308
|
+
|
|
309
|
+
1. run it
|
|
310
|
+
' '<!-- FINDING:C-1|CONFIRMED|MEDIUM|a.py:10|Confirmed medium without a scenario field -->
|
|
311
|
+
<!-- FINDING:C-2|confirmed|medium|b.py:10|Lower case confirmed medium without a scenario field -->
|
|
312
|
+
<!-- FINDING:C-3|CONFIRMED|LOW|c.py:10|Confirmed low without a scenario field -->
|
|
313
|
+
<!-- FINDING:C-4|LIKELY|MEDIUM|d.py:10|Plain failure scenario label in prose -->
|
|
314
|
+
<!-- FINDING:C-5|POSSIBLE|MEDIUM|e.py:10|Bold repro label in a list item -->
|
|
315
|
+
<!-- FINDING:C-6|POSSIBLE|MEDIUM|f.py:10|Possible medium with no scenario anywhere -->
|
|
316
|
+
<!-- FINDING:C-7|LIKELY|MEDIUM|g.py:10|Scenario word mid sentence only -->
|
|
317
|
+
<!-- FINDING:C-8|LIKELY|MEDIUM|h.py:10|Scenario content on the next line -->')"
|
|
318
|
+
flush; run "$GOOD" extract; rc_is "gate3 prose" "$RC" 0
|
|
319
|
+
want "gate3: CONFIRMED MEDIUM without a scenario is filed" '.findings[] | select(.id == "C-1") | [.scenario, .route] | join(",")' findings.json "absent,file"
|
|
320
|
+
want "gate3: lower-case confirmed medium is filed" '.findings[] | select(.id == "C-2") | .route' findings.json "file"
|
|
321
|
+
want "gate3: CONFIRMED LOW without a scenario is still demoted" '.findings[] | select(.id == "C-3") | .route' findings.json "demoted-no-scenario"
|
|
322
|
+
want "gate3: plain Failure scenario label counts" '.findings[] | select(.id == "C-4") | [.scenario, .route] | join(",")' findings.json "present,file"
|
|
323
|
+
want "gate3: bold Repro label counts" '.findings[] | select(.id == "C-5") | [.scenario, .route] | join(",")' findings.json "present,file"
|
|
324
|
+
want "gate3: POSSIBLE MEDIUM with no scenario is demoted" '.findings[] | select(.id == "C-6") | .route' findings.json "demoted-no-scenario"
|
|
325
|
+
want "gate3: a mid-sentence scenario word is not a label" '.findings[] | select(.id == "C-7") | [.scenario, .route] | join(",")' findings.json "absent,demoted-no-scenario"
|
|
326
|
+
want "gate3: label content on the next line counts" '.findings[] | select(.id == "C-8") | .scenario' findings.json "present"
|
|
327
|
+
want "gate3: demoted count" '.counts.notes_demoted_no_scenario' findings.json "3"
|
|
289
328
|
# an unreadable files API classifies nothing as pre-existing
|
|
290
329
|
reset; echo '[]' > "$FIX/pr-files.json"
|
|
291
330
|
cm MEMBER User "$(agent_body security "$SHA" '### P-1 pre
|
|
@@ -625,6 +664,14 @@ want "lineage damps LIKELY medium" '.findings[] | select(.id == "L-1") | .class'
|
|
|
625
664
|
want "no-scenario note" '.findings[] | select(.id == "L-2") | .disposition' classified.json "NOTE no-scenario"
|
|
626
665
|
posted_has "no-scenario counter" "notes_demoted_no_scenario=1"
|
|
627
666
|
posted_has "no-scenario listed" "- L-2 a.py:9 [LIKELY] Demoted for lack of a scenario: no-scenario"
|
|
667
|
+
# a CONFIRMED MEDIUM that Gate 3 kept (scenario absent, route file) is classified, never a no-scenario NOTE (#4356); marker fields unchanged
|
|
668
|
+
reset
|
|
669
|
+
mkfind K-1 CONFIRMED MEDIUM a.py 5 "Confirmed medium kept without a scenario field"
|
|
670
|
+
FJ=$(printf '%s' "$FJ" | jq -c '.scenario = "absent"'); putfind 1
|
|
671
|
+
run "$GOOD" classify; rc_is "confirmed medium classify" "$RC" 0
|
|
672
|
+
want "confirmed medium without scenario is an issue" '.findings[] | select(.id == "K-1") | .class' classified.json "ISSUE"
|
|
673
|
+
has "confirmed medium without scenario is filed" "FINDINGS_FILED=1"
|
|
674
|
+
posted_has "confirmed medium: marker field set unchanged" "<!-- FORGE:NOTE_DISPOSITION: notes_fixed=0 notes_listed=0 notes_dropped=0 findings_filed=1 inpr_fix=0 contract_demoted=0 findings_dropped_stale=0 findings_preexisting=0 introduced=0 notes_demoted_no_scenario=0 lineage=none classifier=script notes_reflag=0 notes_capped=0 reviewed_sha=${SHA} -->"
|
|
628
675
|
# the PR-body issue in the context alone never sets lineage: only the invoker's --issue does (#3952 SEC-1/QA-2)
|
|
629
676
|
reset; echo review-finding > "$FIX/labels"; mkctx 1 FULL "" 12
|
|
630
677
|
mkfind L-1 LIKELY MEDIUM a.py 2 "Lineage damped medium finding"
|
|
@@ -151,6 +151,17 @@ touch $SENTINEL"; do
|
|
|
151
151
|
else bad "staging injection not rejected: '$got'"; fi
|
|
152
152
|
done
|
|
153
153
|
|
|
154
|
+
# Test 6b (forge#4360): `--delta <full sha>` is forwarded to the CLI; anything but a full lowercase hex SHA is dropped
|
|
155
|
+
DELTA_LINE=$(grep -m1 '^STAGING_DELTA=' "$STAGING")
|
|
156
|
+
if [ -n "$DELTA_LINE" ] && grep -qF '${STAGING_DELTA:+--delta "$STAGING_DELTA"}' "$STAGING"; then ok; else bad "staging --delta parse/forward not found"; fi
|
|
157
|
+
run_delta() { STAGING_ARGS_RAW="$1" "${BASH:-bash}" -c "$DELTA_LINE"$'\n''printf "%s" "$STAGING_DELTA"'; }
|
|
158
|
+
D40=0123456789abcdef0123456789abcdef01234567
|
|
159
|
+
got=$(run_delta "3401 --delta $D40"); if [ "$got" = "$D40" ]; then ok; else bad "staging --delta not parsed: '$got'"; fi
|
|
160
|
+
got=$(run_delta "3401 --delta $D40 --thorough"); if [ "$got" = "$D40" ]; then ok; else bad "staging --delta before a flag: '$got'"; fi
|
|
161
|
+
for v in "abc123" "${D40}0" "XYZ" ""; do
|
|
162
|
+
got=$(run_delta "3401 --delta $v"); if [ -z "$got" ]; then ok; else bad "staging --delta '$v' not dropped: '$got'"; fi
|
|
163
|
+
done
|
|
164
|
+
|
|
154
165
|
# Test 7: Phase -1 stops on an empty PR_NUMBER before any `gh pr view` (gh falls back to the current-branch PR)
|
|
155
166
|
GUARD=$(awk '/^ if \[ -z "\$PR_NUMBER" \]; then$/{p=1} p{print} p && /^ fi$/{exit}' "$SPEC")
|
|
156
167
|
if [ -n "$GUARD" ]; then ok; else bad "Phase -1 empty-PR_NUMBER guard not found"; fi
|
|
@@ -167,7 +178,8 @@ done
|
|
|
167
178
|
got=$(run_route "3401 --auto-merge --issue 3398"); if [ "$got" = REACHED ]; then ok; else bad "route stopped a valid PR number: '$got'"; fi
|
|
168
179
|
|
|
169
180
|
# Test 8: the whole staging fence stops (non-zero, no REVIEW_ROUTE post) on a rejected string
|
|
170
|
-
|
|
181
|
+
# Cut before the CLI resolution (#4359): the fence ends by running `forgedock review-staging`, which this test never runs.
|
|
182
|
+
STAGING_FENCE=$(awk '/^```bash/{buf=""; in_f=1; next} /^```$/{if (in_f && buf ~ /STAGING_ARGS_RAW/) {printf "%s", buf; exit}; in_f=0; next} in_f && /^FD_ROOT=/{if (buf ~ /STAGING_ARGS_RAW/) {printf "%s", buf; exit}} in_f{buf=buf $0 "\n"}' "$STAGING")
|
|
171
183
|
if [ -n "$STAGING_FENCE" ]; then ok; else bad "staging fence not found"; fi
|
|
172
184
|
run_stg_fence() { gen_script "$STAGING_FENCE" "$1" "$WORK/stgf.sh" 'echo REACHED'; : > "$ERR"; (cd "$WORK" && "${BASH:-bash}" "$WORK/stgf.sh" 2>"$ERR"); }
|
|
173
185
|
got=$(run_stg_fence "\$(touch $SENTINEL)"); rc=$?
|
|
@@ -235,8 +235,8 @@ case "$(p4 FULL 'security auth')" in "PANEL_STATUS: SHORTFALL"*) ok ;; *) bad "u
|
|
|
235
235
|
# a verdict without a findings block is undelivered (#3976): not complete, never counted
|
|
236
236
|
{ cm "$(printf '<!-- FORGE:REVIEW-AGENT:security -->\nReviewed-SHA: %s\n\nVerdict: APPROVE, no findings.' "$HEAD")" User MEMBER; cm "$(body_agent auth "$HEAD")" User MEMBER; } | jq -s '.' > "$FAKE_COMMENTS"
|
|
237
237
|
case "$(p4 FULL 'security auth')" in "PANEL_STATUS: SHORTFALL (ACTUAL_AGENT_COUNT=1)"*) ok ;; *) bad "a block-less reviewer comment must not satisfy Phase 4: $(p4 FULL 'security auth')" ;; esac
|
|
238
|
-
# the 3C pending-agent count and the staging 6.75 delivery count carry the same block lookahead as Phase 4
|
|
239
|
-
STAGING="$ROOT/
|
|
238
|
+
# the 3C pending-agent count and the staging 6.75 delivery count (scripts/review-staging-post.sh) carry the same block lookahead as Phase 4
|
|
239
|
+
STAGING="$ROOT/scripts/review-staging-post.sh" # owner of the staging 6.75 delivery count (forge#4359)
|
|
240
240
|
for spec in "$SPEC" "$STAGING"; do
|
|
241
241
|
n=$(grep -F 'count "^(?=[\\s\\S]*<!-- REVIEW-FINDINGS-START -->[\\s\\S]*<!-- REVIEW-FINDINGS-END -->)<!-- FORGE:REVIEW-AGENT:${AGENT_DOMAIN} -->' "$spec" | wc -l)
|
|
242
242
|
want=2; [ "$spec" = "$STAGING" ] && want=1
|
|
@@ -37,20 +37,12 @@ for p in infra security scraper; do
|
|
|
37
37
|
if [ "$reads" -ge 1 ] && [ "$reads" = "$decls" ]; then ok; else bad "$p.md: $reads fences read SLICE_FILE, $decls re-declare it"; fi
|
|
38
38
|
done
|
|
39
39
|
|
|
40
|
-
# Staging slice path:
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
if grep -qF '
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
got=$(REVIEW_SHA_FULL="$full"; eval "$SHORT_LINE"; printf '%s' "$REVIEW_SHA_SHORT")
|
|
47
|
-
case "$full" in
|
|
48
|
-
0123*) want="0123456" ;;
|
|
49
|
-
*) want="unresolved" ;;
|
|
50
|
-
esac
|
|
51
|
-
if [ "$got" = "$want" ]; then ok; else bad "REVIEW_SHA_SHORT for '$full' = '$got' (want '$want')"; fi
|
|
52
|
-
case "$got" in */*) bad "REVIEW_SHA_SHORT contains a slash for '$full'" ;; *) ok ;; esac
|
|
53
|
-
done
|
|
40
|
+
# Staging slice path (#4359): `forgedock review-staging` renders [REVIEW_SHA_SHORT] into the briefs. It derives it only from a validated
|
|
41
|
+
# full head SHA and stops before any brief when none resolves, so the slice path can never carry a / or an unresolved sentinel.
|
|
42
|
+
STAGING_CLI="$ROOT/bin/review-staging.mjs"
|
|
43
|
+
if grep -qF 'const SHA_RE = /^(?:[0-9a-f]{40}|[0-9a-f]{64})$/;' "$STAGING_CLI" && grep -qF 'if (!SHA_RE.test(head)) throw new Stop(1' "$STAGING_CLI"; then ok; else bad "staging CLI does not refuse an unvalidated head SHA"; fi
|
|
44
|
+
if grep -qF 'const sha7 = head.slice(0, 7);' "$STAGING_CLI" && grep -qF 'REVIEW_SHA_SHORT: sha7' "$STAGING_CLI"; then ok; else bad "staging CLI does not bind [REVIEW_SHA_SHORT] to the validated head's first 7 hex"; fi
|
|
45
|
+
if grep -q 'REVIEW_SHA_SHORT' "$ROOT/commands/review-pr-staging.md"; then bad "staging spec still binds [REVIEW_SHA_SHORT] by hand (the CLI renders the briefs)"; else ok; fi
|
|
54
46
|
|
|
55
47
|
echo "review-pr-gate2.test.sh: passed=$PASS failed=$FAILN"
|
|
56
48
|
[ "$FAILN" -eq 0 ]
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
2
|
# review-pr-staging-finding-filter.test.sh — fixtures for the anchored open-finding filter (forge#3800).
|
|
3
|
-
# The
|
|
3
|
+
# The staging gate's `# FINDING_PR_FILTER` line (scripts/review-staging-pre.sh since #4359) is extracted and run against issue shapes: the gate must keep a
|
|
4
4
|
# hit only when its title suffix or `**Source**:` line names exactly PR #N; hidden markers never count.
|
|
5
5
|
# SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios
|
|
6
6
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
@@ -12,10 +12,10 @@ bad() { FAILN=$((FAILN+1)); echo "FAIL: $1"; }
|
|
|
12
12
|
command -v jq >/dev/null 2>&1 || { echo "review-pr-staging-finding-filter.test.sh: jq missing, skipped"; exit 0; }
|
|
13
13
|
|
|
14
14
|
extract() { grep -E '# FINDING_PR_FILTER$' "$1" | sed 's/[[:space:]]*# FINDING_PR_FILTER$//; s/^[[:space:]]*//'; }
|
|
15
|
-
SPEC="$ROOT/
|
|
15
|
+
SPEC="$ROOT/scripts/review-staging-pre.sh" # owner of the staging 0A gate (forge#4359)
|
|
16
16
|
DEF="$(extract "$SPEC" | sort -u)"
|
|
17
|
-
[ "$(printf '%s\n' "$DEF" | grep -c .)" -eq 1 ] && ok || bad "review-
|
|
18
|
-
[ "$(extract "$ROOT/commands/deploy-info.md" | sort -u)" = "$DEF" ] && ok || bad "deploy-info.md filter differs from review-
|
|
17
|
+
[ "$(printf '%s\n' "$DEF" | grep -c .)" -eq 1 ] && ok || bad "review-staging-pre.sh must carry one identical FINDING_PR_FILTER definition"
|
|
18
|
+
[ "$(extract "$ROOT/commands/deploy-info.md" | sort -u)" = "$DEF" ] && ok || bad "deploy-info.md filter differs from review-staging-pre.sh"
|
|
19
19
|
[ "$(grep -c -F 'FINDING_PR_FILTER' "$SPEC")" -ge 3 ] && ok || bad "filter must be referenced by all three queries"
|
|
20
20
|
grep -q -F 'OVERRIDE: shipping with open findings' "$SPEC" && ok || bad "OVERRIDE text missing"
|
|
21
21
|
eval "$DEF"
|