forgedock 1.18.0 → 1.20.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.
Files changed (135) hide show
  1. package/README.md +2 -0
  2. package/bin/engine/outcome-codes.mjs +13 -0
  3. package/bin/engine/phases.mjs +54 -4
  4. package/bin/engine/runlog.mjs +10 -2
  5. package/bin/engine/telemetry-seam.mjs +3 -0
  6. package/bin/engine/telemetry.test.mjs +3 -2
  7. package/bin/engine-cli.mjs +32 -2
  8. package/bin/engine.mjs +187 -10
  9. package/bin/runner.mjs +89 -13
  10. package/bin/stats/aggregate.mjs +7 -3
  11. package/bin/stats/cache.mjs +7 -3
  12. package/bin/stats/github-source.mjs +136 -28
  13. package/bin/stats/index.mjs +46 -9
  14. package/bin/stats/render.mjs +1 -1
  15. package/bin/stats/stats.test.mjs +201 -40
  16. package/commands/orchestrate/phase-4-execution.md +1 -1
  17. package/commands/review-pr-agents/protocols.md +19 -0
  18. package/commands/review-pr-staging.md +4 -4
  19. package/commands/review-pr.md +31 -20
  20. package/commands/work-on/build/architect.md +54 -1
  21. package/commands/work-on/build/implement.md +28 -1
  22. package/commands/work-on/build/validate.md +321 -29
  23. package/commands/work-on/build.md +184 -17
  24. package/commands/work-on/close.md +9 -4
  25. package/commands/work-on/investigate.md +7 -0
  26. package/commands/work-on/remediate.md +43 -17
  27. package/commands/work-on/review.md +43 -22
  28. package/commands/work-on.md +4 -4
  29. package/docs/CONFIG.md +93 -2
  30. package/docs/OUTCOME-CODES.md +5 -1
  31. package/docs/RELEASE-NOTES.md +15 -0
  32. package/docs/STATS.md +11 -4
  33. package/docs/TELEMETRY.md +5 -2
  34. package/docs/WORK-ON-RUNTIME.md +29 -3
  35. package/package.json +2 -2
  36. package/packages/protocol/src/types.js +1 -1
  37. package/scripts/acceptance-red-green.sh +90 -0
  38. package/scripts/acceptance-red-green.test.sh +74 -0
  39. package/scripts/architect-resume-match.test.sh +12 -3
  40. package/scripts/build-knowledge-index.mjs +8 -5
  41. package/scripts/builder-resume-match.test.sh +12 -3
  42. package/scripts/calibration.mjs +5 -2
  43. package/scripts/cap-findings.test.sh +2 -0
  44. package/scripts/check-contract-scope.test.sh +2 -0
  45. package/scripts/check-edge-cases.sh +143 -0
  46. package/scripts/check-edge-cases.test.sh +175 -0
  47. package/scripts/check-spec-markers.sh +4 -0
  48. package/scripts/check-test-tmp-cleanup.sh +56 -0
  49. package/scripts/check-test-tmp-cleanup.test.sh +69 -0
  50. package/scripts/ci-local/adapters/azure-pipelines.mjs +113 -0
  51. package/scripts/ci-local/adapters/bitbucket.mjs +64 -0
  52. package/scripts/ci-local/adapters/buildkite.mjs +58 -0
  53. package/scripts/ci-local/adapters/circleci.mjs +101 -0
  54. package/scripts/ci-local/adapters/common.mjs +115 -0
  55. package/scripts/ci-local/adapters/explicit.mjs +21 -0
  56. package/scripts/ci-local/adapters/github-actions.mjs +349 -0
  57. package/scripts/ci-local/adapters/gitlab-ci.mjs +184 -0
  58. package/scripts/ci-local/adapters/portability.mjs +104 -0
  59. package/scripts/ci-local/adapters/task-runners.mjs +67 -0
  60. package/scripts/ci-local/cli.mjs +50 -0
  61. package/scripts/ci-local/lib.mjs +379 -0
  62. package/scripts/ci-local/plan.mjs +186 -0
  63. package/scripts/ci-local/run.mjs +451 -0
  64. package/scripts/ci-local/scoping.mjs +531 -0
  65. package/scripts/ci-local.sh +98 -0
  66. package/scripts/ci-local.test.sh +1108 -0
  67. package/scripts/classify-finding.sh +33 -7
  68. package/scripts/classify-finding.test.sh +23 -0
  69. package/scripts/classify-test-run.sh +69 -0
  70. package/scripts/classify-test-run.test.sh +90 -0
  71. package/scripts/close-parent-c3.test.sh +2 -0
  72. package/scripts/consumer-sweep.sh +569 -0
  73. package/scripts/consumer-sweep.test.sh +357 -0
  74. package/scripts/detect-append-registries.sh +106 -0
  75. package/scripts/detect-append-registries.test.sh +64 -0
  76. package/scripts/file-deferred-followups.sh +91 -30
  77. package/scripts/file-deferred-followups.test.sh +101 -8
  78. package/scripts/fixtures/ci-local/azure/azure-pipelines.yml +11 -0
  79. package/scripts/fixtures/ci-local/bitbucket/bitbucket-pipelines.yml +12 -0
  80. package/scripts/fixtures/ci-local/buildkite/.buildkite/pipeline.yml +10 -0
  81. package/scripts/fixtures/ci-local/circleci/.circleci/config.yml +29 -0
  82. package/scripts/fixtures/ci-local/gha-basic/.github/workflows/ci.yml +59 -0
  83. package/scripts/fixtures/ci-local/gha-basic/.github/workflows/docs.yml +10 -0
  84. package/scripts/fixtures/ci-local/gha-basic/.github/workflows/nightly.yml +10 -0
  85. package/scripts/fixtures/ci-local/gha-basic/docs/index.md +1 -0
  86. package/scripts/fixtures/ci-local/gha-basic/src/greet.sh +2 -0
  87. package/scripts/fixtures/ci-local/gha-basic/tests/greet.test.sh +3 -0
  88. package/scripts/fixtures/ci-local/gha-dorny/.github/workflows/ci.yml +51 -0
  89. package/scripts/fixtures/ci-local/gha-dorny/backend/app.py +1 -0
  90. package/scripts/fixtures/ci-local/gha-dorny/docs/a.md +1 -0
  91. package/scripts/fixtures/ci-local/gha-dorny/frontend/app.js +1 -0
  92. package/scripts/fixtures/ci-local/gha-run/.github/workflows/ci.yml +39 -0
  93. package/scripts/fixtures/ci-local/gha-run/src/x.txt +1 -0
  94. package/scripts/fixtures/ci-local/gitlab/.gitlab-ci.yml +43 -0
  95. package/scripts/fixtures/ci-local/gitlab/docs/d.md +1 -0
  96. package/scripts/fixtures/ci-local/gitlab/src/a.txt +1 -0
  97. package/scripts/fixtures/ci-local/monorepo/.github/workflows/ci.yml +13 -0
  98. package/scripts/fixtures/ci-local/monorepo/package-lock.json +1 -0
  99. package/scripts/fixtures/ci-local/monorepo/packages/a/src/a.sh +2 -0
  100. package/scripts/fixtures/ci-local/monorepo/packages/a/tests/a.test.sh +3 -0
  101. package/scripts/fixtures/ci-local/monorepo/packages/b/src/b.sh +2 -0
  102. package/scripts/fixtures/ci-local/monorepo/packages/b/tests/b.test.sh +3 -0
  103. package/scripts/fixtures/ci-local/no-ci/Makefile +6 -0
  104. package/scripts/fixtures/ci-local/no-ci/package.json +1 -0
  105. package/scripts/fixtures/ci-local/no-ci/src/a.js +1 -0
  106. package/scripts/fixtures/ci-local/no-ci/src/b.js +1 -0
  107. package/scripts/fixtures/ci-local/no-ci/tests/a.test.js +2 -0
  108. package/scripts/fixtures/ci-local/no-ci/tests/b.test.js +2 -0
  109. package/scripts/forge-root.test.sh +1 -1
  110. package/scripts/lib/telemetry.sh +3 -0
  111. package/scripts/remediate-approved-head.test.sh +2 -0
  112. package/scripts/remediate-basesync.test.sh +1 -1
  113. package/scripts/review-acceptance-gate.sh +2 -2
  114. package/scripts/review-acceptance-gate.test.sh +20 -0
  115. package/scripts/review-delta.sh +8 -1
  116. package/scripts/review-delta.test.sh +19 -0
  117. package/scripts/review-finalize.sh +294 -53
  118. package/scripts/review-finalize.test.sh +347 -9
  119. package/scripts/review-pr-delta-wiring.test.sh +13 -0
  120. package/scripts/review-prepare.sh +5 -3
  121. package/scripts/review-prepare.test.sh +18 -2
  122. package/scripts/review-roster.sh +18 -5
  123. package/scripts/review-roster.test.sh +57 -1
  124. package/scripts/run-shell-tests.sh +5 -0
  125. package/scripts/selfreview-plan.sh +86 -0
  126. package/scripts/selfreview-plan.test.sh +90 -0
  127. package/scripts/spawn-depth-check.sh +5 -4
  128. package/scripts/spawn-depth-check.test.sh +3 -0
  129. package/scripts/telemetry.test.sh +10 -0
  130. package/scripts/transition-label.sh +26 -0
  131. package/scripts/trusted-comments.test.sh +2 -1
  132. package/scripts/verify-phase-trail.test.sh +14 -0
  133. package/scripts/wait-ci-green.sh +61 -9
  134. package/scripts/wait-ci-green.test.sh +41 -3
  135. package/scripts/with-run-tmpdir.sh +20 -0
@@ -4,8 +4,9 @@
4
4
  # SPDX-License-Identifier: AGPL-3.0-or-later
5
5
  #
6
6
  # The builder decides fold-vs-defer (judgment, build.md B2.0); this script does the mechanics for the items
7
- # that stay deferred: trusted lookup of an open follow-up carrying <!-- FORGE:DEFERRED_FROM: #N -->, append the
8
- # missing items to it as checklist entries (exact path match), or create ONE issue with a checklist.
7
+ # that stay deferred: trusted lookup (claim marker <!-- FORGE:DEFERRED_ISSUE: #M --> on the parent, then a state-all search) of a follow-up carrying
8
+ # <!-- FORGE:DEFERRED_FROM: #N -->, append the missing items (exact path match; a closed one is reopened), or create ONE issue
9
+ # with a checklist, claim it on the parent and re-check once for a concurrent lower-numbered duplicate.
9
10
  #
10
11
  # Usage: file-deferred-followups.sh --parent N --repo OWNER/REPO [--items FILE|-] [--folded N]
11
12
  # items TSV, one per line: <path> TAB <why> TAB <known-flaw flag: known-flaw|1|yes|true, else empty>
@@ -71,27 +72,60 @@ finish() { # $1 = batched_into number or empty, $2 = filed count
71
72
  }
72
73
 
73
74
  # --- Trusted lookup (author predicate applied before path match), one retry ---------------------------------
74
- do_lookup() {
75
+ # Sources, in order: (1) claim markers `<!-- FORGE:DEFERRED_ISSUE: #M -->` on the parent's REST comments (read-after-write
76
+ # consistent), (2) a search over ALL issue states (eventually consistent, so only a fallback). Every candidate is then
77
+ # verified by number over REST (trusted author, DEFERRED_FROM marker, not a PR); the lowest number is canonical.
78
+ JQ_PRE='def lst: split(",") | map(gsub("^\\s+|\\s+$"; "")) | map(select(length > 0));
79
+ def trusted: ((.author_association // "") as $x | ($assoc | lst) | index($x) != null)
80
+ or ((.user.type // "") == "Bot")
81
+ or ((.user.login // "") as $x | ($logins | lst) | index($x) != null);'
82
+ CLAIM_RE='^\s*<!-- FORGE:DEFERRED_ISSUE: #(?<n>[0-9]+) -->'
83
+ claim_nums() {
84
+ gh api --paginate "repos/${REPO}/issues/${PARENT}/comments" 2>/dev/null \
85
+ | jq -s -r --arg assoc "$TRUSTED_ASSOC" --arg logins "$TRUSTED_LOGINS" --arg re "$CLAIM_RE" "$JQ_PRE"'
86
+ [.[][] | select(trusted) | (.body // "") | capture($re) | .n | tonumber] | unique | .[]' 2>/dev/null
87
+ }
88
+ search_nums() {
75
89
  gh api -X GET search/issues -f per_page=50 \
76
- -f q="\"FORGE:DEFERRED_FROM: #${PARENT}\" in:body repo:${REPO} is:issue is:open" 2>/dev/null \
77
- | jq -c --arg assoc "$TRUSTED_ASSOC" --arg logins "$TRUSTED_LOGINS" --arg m "$MARKER" '
78
- ($assoc | split(",") | map(gsub("^\\s+|\\s+$"; "")) | map(select(length > 0))) as $A
79
- | ($logins | split(",") | map(gsub("^\\s+|\\s+$"; "")) | map(select(length > 0))) as $L
80
- | [.items[]
81
- | select(((.author_association // "") as $x | $A | index($x) != null)
82
- or ((.user.type // "") == "Bot")
83
- or ((.user.login // "") as $x | $L | index($x) != null))
84
- | select((.body // "") | contains($m))
85
- | {number, body: (.body // "")}]
86
- | sort_by(.number)' 2>/dev/null
90
+ -f q="\"FORGE:DEFERRED_FROM: #${PARENT}\" in:body repo:${REPO} is:issue" 2>/dev/null \
91
+ | jq -r --arg assoc "$TRUSTED_ASSOC" --arg logins "$TRUSTED_LOGINS" --arg m "$MARKER" "$JQ_PRE"'
92
+ [.items[] | select(trusted) | select((.body // "") | contains($m)) | .number] | sort | .[]' 2>/dev/null
93
+ }
94
+ verify_num() { # number -> `number<TAB>state` when a trusted, non-PR issue carries the marker; empty when not (a 404 is "not it")
95
+ local out rc
96
+ out=$(gh api "repos/${REPO}/issues/$1" 2>"$TMP/verr"); rc=$?
97
+ if [ "$rc" -ne 0 ]; then grep -qE '404|Not Found|410' "$TMP/verr" 2>/dev/null && return 0; return 1; fi
98
+ printf '%s' "$out" | jq -r --arg assoc "$TRUSTED_ASSOC" --arg logins "$TRUSTED_LOGINS" --arg m "$MARKER" "$JQ_PRE"'
99
+ select(.pull_request == null) | select(trusted) | select((.body // "") | contains($m))
100
+ | "\(.number)\t\(.state)"' 2>/dev/null
101
+ }
102
+ CANON=""; CSTATE=""
103
+ pick_verified() { # numbers... ; sets CANON/CSTATE from the first verified one; non-zero only when a fetch failed
104
+ local n r
105
+ for n in "$@"; do
106
+ r=$(verify_num "$n") || return 1
107
+ if [ -n "$r" ]; then CANON="${r%%$'\t'*}"; CSTATE="${r#*$'\t'}"; return 0; fi
108
+ done
109
+ return 0
87
110
  }
88
- LOOKUP=""
111
+ find_canonical() {
112
+ local cands
113
+ CANON=""; CSTATE=""
114
+ cands=$(claim_nums) || return 1
115
+ # shellcheck disable=SC2086
116
+ pick_verified $cands || return 1
117
+ [ -n "$CANON" ] && return 0
118
+ cands=$(search_nums) || return 1
119
+ # shellcheck disable=SC2086
120
+ pick_verified $cands || return 1
121
+ return 0
122
+ }
123
+ LOOKUP_OK=""
89
124
  for attempt in 1 2; do
90
- LOOKUP=$(do_lookup) && [ -n "$LOOKUP" ] && break
91
- LOOKUP=""
125
+ if find_canonical; then LOOKUP_OK=1; break; fi
92
126
  [ "$attempt" = 1 ] && sleep "${FORGE_DEFERRED_RETRY_SLEEP:-2}"
93
127
  done
94
- if [ -z "$LOOKUP" ]; then
128
+ if [ -z "$LOOKUP_OK" ]; then
95
129
  echo "DEFERRED_LOOKUP=FAILED"
96
130
  exit 3
97
131
  fi
@@ -108,27 +142,35 @@ has_entry() { # body-file path : exact checklist-line match, not substring
108
142
  grep -F -- "\`$2\`" "$1" | grep -qE "^- \[[ xX]\] \`$(printf '%s' "$2" | sed 's/[][\.*^$+?(){}|]/\\&/g')\`( |\$)"
109
143
  }
110
144
 
111
- NUM=$(printf '%s' "$LOOKUP" | jq -r 'first | .number // empty' 2>/dev/null)
112
-
113
- if [ -n "$NUM" ]; then
145
+ # Append the missing items to follow-up $1 (state $2), then reopen it when closed (review-finalize needs it open).
146
+ append_items() { # number state ; non-zero = failure
147
+ local num="$1" state="$2" note=""
114
148
  # Re-read the body immediately before editing (parallel builds of the same parent), then append what is missing.
115
- if ! gh api "repos/${REPO}/issues/${NUM}" --jq '.body // ""' > "$TMP/body" 2>/dev/null; then
116
- echo "DEFERRED_FILE=FAILED"; exit 4
117
- fi
149
+ gh api "repos/${REPO}/issues/${num}" --jq '.body // ""' > "$TMP/body" 2>/dev/null || return 1
118
150
  : > "$TMP/add"
119
151
  while IFS="$SEP" read -r p w k; do
120
152
  has_entry "$TMP/body" "$p" || entry "$p" "$w" "$k" >> "$TMP/add"
121
153
  done < "$TMP/items"
154
+ [ "$state" = "closed" ] && note=" (follow-up was closed; reopened)"
122
155
  if [ -s "$TMP/add" ]; then
123
- { cat "$TMP/body"; printf '\n### Added by a later build of #%s\n\n' "$PARENT"; cat "$TMP/add"; } > "$TMP/newbody"
156
+ { cat "$TMP/body"; printf '\n### Added by a later build of #%s%s\n\n' "$PARENT" "$note"; cat "$TMP/add"; } > "$TMP/newbody"
124
157
  if [ -n "${DRY_RUN:-}" ]; then
125
- echo "DRY_RUN: gh issue edit $NUM (append $(wc -l < "$TMP/add" | tr -d ' ') item(s))" >&2
126
- elif ! gh issue edit "$NUM" -R "$REPO" --body-file "$TMP/newbody" >/dev/null 2>&1; then
127
- echo "DEFERRED_FILE=FAILED"; exit 4
158
+ echo "DRY_RUN: gh issue edit $num (append $(wc -l < "$TMP/add" | tr -d ' ') item(s))" >&2
159
+ else
160
+ gh issue edit "$num" -R "$REPO" --body-file "$TMP/newbody" >/dev/null 2>&1 || return 1
128
161
  fi
129
162
  fi
130
- while IFS="$SEP" read -r p w k; do printf '%s\t%s\n' "$p" "$NUM"; done < "$TMP/items"
131
- finish "$NUM" 0
163
+ if [ "$state" = "closed" ]; then
164
+ if [ -n "${DRY_RUN:-}" ]; then echo "DRY_RUN: gh issue reopen $num" >&2
165
+ else gh issue reopen "$num" -R "$REPO" >/dev/null 2>&1 || return 1; fi
166
+ fi
167
+ return 0
168
+ }
169
+
170
+ if [ -n "$CANON" ]; then
171
+ append_items "$CANON" "$CSTATE" || { echo "DEFERRED_FILE=FAILED"; exit 4; }
172
+ while IFS="$SEP" read -r p w k; do printf '%s\t%s\n' "$p" "$CANON"; done < "$TMP/items"
173
+ finish "$CANON" 0
132
174
  exit 0
133
175
  fi
134
176
 
@@ -154,6 +196,25 @@ else
154
196
  NUM=$(printf '%s' "$URL" | grep -oE '[0-9]+$' | tail -n 1)
155
197
  [ -n "$NUM" ] || { echo "DEFERRED_FILE=FAILED"; exit 4; }
156
198
  fi
199
+ if [ -z "${DRY_RUN:-}" ]; then
200
+ # Claim the follow-up on the parent (REST comments are read-after-write consistent; best effort, the search still finds it).
201
+ gh issue comment "$PARENT" -R "$REPO" --body "<!-- FORGE:DEFERRED_ISSUE: #${NUM} -->" >/dev/null 2>&1 \
202
+ || echo "file-deferred-followups: warning: could not post claim marker for #${NUM} on #${PARENT}" >&2
203
+ # Post-create re-check: a concurrent build may have filed first. The lowest trusted number is canonical; the newer one is closed.
204
+ if find_canonical; then
205
+ if [ -n "$CANON" ] && [ "$CANON" -lt "$NUM" ]; then
206
+ if append_items "$CANON" "$CSTATE"; then
207
+ gh issue close "$NUM" -R "$REPO" --reason "not planned" --comment "Duplicate of #${CANON}: deferred items of #${PARENT} are tracked there." >/dev/null 2>&1 \
208
+ || echo "file-deferred-followups: warning: could not close duplicate #${NUM}" >&2
209
+ NUM="$CANON"
210
+ else
211
+ echo "file-deferred-followups: warning: could not move items to #${CANON}; keeping #${NUM}" >&2
212
+ fi
213
+ fi
214
+ else
215
+ echo "file-deferred-followups: warning: post-create re-check failed; keeping #${NUM}" >&2
216
+ fi
217
+ fi
157
218
  while IFS="$SEP" read -r p w k; do printf '%s\t%s\n' "$p" "$NUM"; done < "$TMP/items"
158
219
  finish "$NUM" 1
159
220
  exit 0
@@ -8,19 +8,27 @@ S="$HERE/file-deferred-followups.sh"
8
8
  T=$(mktemp -d); trap 'rm -rf "$T"' EXIT
9
9
  mkdir "$T/bin"
10
10
  export FORGE_DEFERRED_RETRY_SLEEP=0
11
- export GH_LOG="$T/gh.log" SEARCH_JSON="$T/search.json" ISSUE_BODY="$T/issue.body"
11
+ export GH_LOG="$T/gh.log" SEARCH_JSON="$T/search.json" ISSUE_BODY="$T/issue.body" COMMENTS_JSON="$T/comments.json" T_DIR="$T"
12
12
 
13
13
  cat > "$T/bin/gh" <<'MOCK'
14
14
  #!/usr/bin/env bash
15
15
  echo "$*" >> "$GH_LOG"
16
16
  case "$1 $2" in
17
17
  "api -X") [ -n "${LOOKUP_FAIL:-}" ] && exit 1; cat "$SEARCH_JSON"; exit 0 ;;
18
+ "api --paginate") [ -n "${CLAIM_FAIL:-}" ] && exit 1; cat "$COMMENTS_JSON"; exit 0 ;;
18
19
  "issue create") [ -n "${CREATE_FAIL:-}" ] && exit 1; echo "https://github.com/o/r/issues/901"; exit 0 ;;
20
+ "issue comment") [ -n "${COMMENT_FAIL:-}" ] && exit 1; exit 0 ;;
21
+ "issue reopen") [ -n "${REOPEN_FAIL:-}" ] && exit 1; exit 0 ;;
22
+ "issue close") [ -n "${CLOSE_FAIL:-}" ] && exit 1; exit 0 ;;
19
23
  "issue edit")
20
24
  [ -n "${EDIT_FAIL:-}" ] && exit 1
21
25
  while [ $# -gt 0 ]; do [ "$1" = "--body-file" ] && cp "$2" "$ISSUE_BODY.edited"; shift; done; exit 0 ;;
22
26
  esac
23
- case "$1" in api) cat "$ISSUE_BODY"; exit 0 ;; esac
27
+ if [ "$1" = api ]; then
28
+ case "$*" in *--jq*) cat "$ISSUE_BODY"; exit 0 ;; esac
29
+ n="${2##*/}"; [ -f "$T_DIR/issue.$n.json" ] && { cat "$T_DIR/issue.$n.json"; exit 0; }
30
+ echo "HTTP 404: Not Found" >&2; exit 1
31
+ fi
24
32
  exit 1
25
33
  MOCK
26
34
  chmod +x "$T/bin/gh"; export PATH="$T/bin:$PATH"
@@ -29,7 +37,12 @@ PASS=0; FAIL=0
29
37
  ok() { PASS=$((PASS+1)); echo "PASS: $1"; }
30
38
  bad() { FAIL=$((FAIL+1)); echo "FAIL: $1"; }
31
39
  check() { if eval "$2"; then ok "$1"; else bad "$1"; fi; }
32
- reset() { : > "$GH_LOG"; rm -f "$ISSUE_BODY.edited"; unset LOOKUP_FAIL CREATE_FAIL EDIT_FAIL DRY_RUN; echo '{"items":[]}' > "$SEARCH_JSON"; }
40
+ reset() { : > "$GH_LOG"; rm -f "$ISSUE_BODY.edited" "$T"/issue.*.json; unset LOOKUP_FAIL CREATE_FAIL EDIT_FAIL CLAIM_FAIL COMMENT_FAIL REOPEN_FAIL CLOSE_FAIL DRY_RUN
41
+ echo '{"items":[]}' > "$SEARCH_JSON"; echo '[]' > "$COMMENTS_JSON"; }
42
+ # issue_json NUMBER STATE ASSOCIATION [PARENT]: REST view of a follow-up carrying the DEFERRED_FROM marker
43
+ issue_json() { printf '{"number":%s,"state":"%s","author_association":"%s","user":{"type":"User"},"body":"<!-- FORGE:DEFERRED_FROM: #%s -->"}\n' "$1" "$2" "$3" "${4:-10}" > "$T/issue.$1.json"; }
44
+ # claim_json ASSOCIATION NUMBER...: parent comments carrying claim markers
45
+ claim_json() { local a="$1" out="" n; shift; for n in "$@"; do out="${out:+$out,}{\"author_association\":\"$a\",\"user\":{\"type\":\"User\"},\"body\":\"<!-- FORGE:DEFERRED_ISSUE: #$n -->\"}"; done; echo "[$out]" > "$COMMENTS_JSON"; }
33
46
  printf 'a/x.sh\twhy a\t\nb/y.sh\twhy b\tknown-flaw\nc/z.sh\twhy c\t\n' > "$T/items.tsv"
34
47
 
35
48
  # 1. N items -> exactly one create
@@ -43,7 +56,7 @@ check "create: telemetry" "printf '%s' \"\$OUT\" | grep -q 'DEFERRED_TELEMETRY d
43
56
  # 2. append to existing trusted open issue; exact path match
44
57
  reset
45
58
  printf '## Affected Files\n- `a/x.sh`\n\n- [ ] `a/x.sh.bak` — other\n<!-- FORGE:DEFERRED_FROM: #10 -->\n' > "$ISSUE_BODY"
46
- echo '{"items":[{"number":77,"author_association":"OWNER","user":{"type":"User"},"body":"<!-- FORGE:DEFERRED_FROM: #10 -->"}]}' > "$SEARCH_JSON"
59
+ echo '{"items":[{"number":77,"author_association":"OWNER","user":{"type":"User"},"body":"<!-- FORGE:DEFERRED_FROM: #10 -->"}]}' > "$SEARCH_JSON"; issue_json 77 open OWNER
47
60
  OUT=$(bash "$S" --parent 10 --repo o/r --items "$T/items.tsv"); RC=$?
48
61
  check "append: rc 0, no create" "[ $RC -eq 0 ] && ! grep -q '^issue create' '$GH_LOG'"
49
62
  check "append: telemetry batched into 77 filed=0" "printf '%s' \"\$OUT\" | grep -q 'batched_into=#77 filed=0'"
@@ -55,7 +68,7 @@ reset
55
68
  printf 'd/w.sh\t\tknown-flaw\n' > "$T/emptywhy.tsv"
56
69
  bash "$S" --parent 10 --repo o/r --items "$T/emptywhy.tsv" >/dev/null; RC=$?
57
70
  rm -f "$ISSUE_BODY.edited"; printf 'x\n' > "$ISSUE_BODY"
58
- echo '{"items":[{"number":77,"author_association":"OWNER","user":{"type":"User"},"body":"<!-- FORGE:DEFERRED_FROM: #10 -->"}]}' > "$SEARCH_JSON"
71
+ echo '{"items":[{"number":77,"author_association":"OWNER","user":{"type":"User"},"body":"<!-- FORGE:DEFERRED_FROM: #10 -->"}]}' > "$SEARCH_JSON"; issue_json 77 open OWNER
59
72
  OUT=$(bash "$S" --parent 10 --repo o/r --items "$T/emptywhy.tsv"); RC=$?
60
73
  check "empty-why: append keeps known-flaw flag" "[ $RC -eq 0 ] && grep -q 'd/w.sh. \*\*known-flaw\*\*' '$ISSUE_BODY.edited'"
61
74
 
@@ -66,13 +79,13 @@ check "missing option value: rc 2" "[ $RC -eq 2 ]"
66
79
  # 3. already-listed path is not duplicated; nothing to add -> no edit
67
80
  reset
68
81
  printf -- '- [ ] `a/x.sh` — w\n- [x] `b/y.sh`\n- [ ] `c/z.sh`\n' > "$ISSUE_BODY"
69
- echo '{"items":[{"number":77,"author_association":"MEMBER","user":{"type":"User"},"body":"<!-- FORGE:DEFERRED_FROM: #10 -->"}]}' > "$SEARCH_JSON"
82
+ echo '{"items":[{"number":77,"author_association":"MEMBER","user":{"type":"User"},"body":"<!-- FORGE:DEFERRED_FROM: #10 -->"}]}' > "$SEARCH_JSON"; issue_json 77 open OWNER
70
83
  bash "$S" --parent 10 --repo o/r --items "$T/items.tsv" >/dev/null; RC=$?
71
84
  check "idempotent: no edit, no create on resume" "[ $RC -eq 0 ] && ! grep -qE '^issue (edit|create)' '$GH_LOG'"
72
85
 
73
86
  # 4. untrusted match ignored -> new issue
74
87
  reset
75
- echo '{"items":[{"number":66,"author_association":"NONE","user":{"type":"User"},"body":"<!-- FORGE:DEFERRED_FROM: #10 -->"}]}' > "$SEARCH_JSON"
88
+ echo '{"items":[{"number":66,"author_association":"NONE","user":{"type":"User"},"body":"<!-- FORGE:DEFERRED_FROM: #10 -->"}]}' > "$SEARCH_JSON"; issue_json 66 open NONE; issue_json 77 open OWNER
76
89
  OUT=$(bash "$S" --parent 10 --repo o/r --items "$T/items.tsv")
77
90
  check "untrusted: ignored, create filed" "grep -q '^issue create' '$GH_LOG' && printf '%s' \"\$OUT\" | grep -q 'filed=1'"
78
91
 
@@ -87,7 +100,7 @@ OUT=$(bash "$S" --parent 10 --repo o/r --items "$T/items.tsv"); RC=$?
87
100
  check "create failure: rc 4, no telemetry" "[ $RC -eq 4 ] && ! printf '%s' \"\$OUT\" | grep -q DEFERRED_TELEMETRY"
88
101
  reset; export EDIT_FAIL=1
89
102
  printf 'x\n' > "$ISSUE_BODY"
90
- echo '{"items":[{"number":77,"author_association":"OWNER","user":{"type":"User"},"body":"<!-- FORGE:DEFERRED_FROM: #10 -->"}]}' > "$SEARCH_JSON"
103
+ echo '{"items":[{"number":77,"author_association":"OWNER","user":{"type":"User"},"body":"<!-- FORGE:DEFERRED_FROM: #10 -->"}]}' > "$SEARCH_JSON"; issue_json 77 open OWNER
91
104
  OUT=$(bash "$S" --parent 10 --repo o/r --items "$T/items.tsv"); RC=$?
92
105
  check "edit failure: rc 4, no telemetry" "[ $RC -eq 4 ] && ! printf '%s' \"\$OUT\" | grep -q DEFERRED_TELEMETRY"
93
106
 
@@ -102,5 +115,85 @@ OUT=$(printf '' | bash "$S" --parent 10 --repo o/r)
102
115
  check "empty: no create, deferred_items=0" "! grep -q '^issue create' '$GH_LOG' && printf '%s' \"\$OUT\" | grep -q 'deferred_items=0'"
103
116
  bash "$S" --repo o/r >/dev/null 2>&1; check "usage: missing parent rc 2" "[ $? -eq 2 ]"
104
117
 
118
+ # 9. e1: search index lags (returns nothing) but the claim marker on the parent finds the follow-up
119
+ reset
120
+ printf 'x\n' > "$ISSUE_BODY"; issue_json 77 open OWNER; claim_json OWNER 77
121
+ OUT=$(bash "$S" --parent 10 --repo o/r --items "$T/items.tsv"); RC=$?
122
+ check "index-lag: claim found, no duplicate create" "[ $RC -eq 0 ] && ! grep -q '^issue create' '$GH_LOG' && printf '%s' \"\$OUT\" | grep -q 'batched_into=#77 filed=0'"
123
+ check "index-lag: search never needed" "! grep -q search/issues '$GH_LOG'"
124
+
125
+ # 10. e2: closed follow-up (found only by a state-all search) is reused and reopened
126
+ reset
127
+ printf 'x\n' > "$ISSUE_BODY"; issue_json 77 closed OWNER
128
+ echo '{"items":[{"number":77,"author_association":"OWNER","user":{"type":"User"},"body":"<!-- FORGE:DEFERRED_FROM: #10 -->"}]}' > "$SEARCH_JSON"
129
+ OUT=$(bash "$S" --parent 10 --repo o/r --items "$T/items.tsv"); RC=$?
130
+ check "closed: reused, reopened, no create" "[ $RC -eq 0 ] && grep -q '^issue reopen 77' '$GH_LOG' && ! grep -q '^issue create' '$GH_LOG'"
131
+ check "closed: search query covers all states" "grep -q 'is:issue' '$GH_LOG' && ! grep 'api -X' '$GH_LOG' | grep -q 'is:open'"
132
+ reset; export REOPEN_FAIL=1
133
+ printf 'x\n' > "$ISSUE_BODY"; issue_json 77 closed OWNER; claim_json OWNER 77
134
+ bash "$S" --parent 10 --repo o/r --items "$T/items.tsv" >/dev/null; RC=$?
135
+ check "closed: reopen failure is rc 4" "[ $RC -eq 4 ]"
136
+
137
+ # 11. create posts the claim marker on the parent
138
+ reset
139
+ OUT=$(bash "$S" --parent 10 --repo o/r --items "$T/items.tsv"); RC=$?
140
+ check "claim: posted on parent after create" "[ $RC -eq 0 ] && grep -q '^issue comment 10 .*FORGE:DEFERRED_ISSUE: #901' '$GH_LOG'"
141
+ reset; export COMMENT_FAIL=1
142
+ OUT=$(bash "$S" --parent 10 --repo o/r --items "$T/items.tsv" 2>/dev/null); RC=$?
143
+ check "claim: post failure only warns" "[ $RC -eq 0 ] && printf '%s' \"\$OUT\" | grep -q 'batched_into=#901 filed=1'"
144
+
145
+ # 12. e3: a concurrent build filed #800 first; the first claim read (before our create) misses it, the post-create re-check sees it
146
+ race_setup() { # first comments read returns [] (the other build has not claimed yet), later reads return the real listing
147
+ CALLS="$T/claim.calls"; : > "$CALLS"; export CALLS
148
+ mv "$T/bin/gh" "$T/bin/gh.real"
149
+ cat > "$T/bin/gh" <<'WRAP'
150
+ #!/usr/bin/env bash
151
+ if [ "$1 $2" = "api --paginate" ]; then
152
+ echo x >> "$CALLS"
153
+ if [ "$(wc -l < "$CALLS" | tr -d ' ')" -le 1 ]; then echo "$*" >> "$GH_LOG"; echo '[]'; exit 0; fi
154
+ fi
155
+ exec "$(dirname "$0")/gh.real" "$@"
156
+ WRAP
157
+ chmod +x "$T/bin/gh"
158
+ }
159
+ race_teardown() { mv "$T/bin/gh.real" "$T/bin/gh"; }
160
+ reset
161
+ printf 'x\n' > "$ISSUE_BODY"; issue_json 800 open OWNER; claim_json OWNER 800
162
+ race_setup
163
+ OUT=$(bash "$S" --parent 10 --repo o/r --items "$T/items.tsv"); RC=$?
164
+ race_teardown
165
+ check "race: newer duplicate closed, items moved to lowest" "[ $RC -eq 0 ] && grep -q '^issue close 901' '$GH_LOG' && grep -q '^issue edit 800' '$GH_LOG' && printf '%s' \"\$OUT\" | grep -q 'batched_into=#800 filed=1'"
166
+ check "race: rows point at canonical issue" "[ \$(printf '%s\n' \"\$OUT\" | grep -c '800\$') -ge 3 ]"
167
+ # e5: duplicate-close failure only warns
168
+ reset; export CLOSE_FAIL=1
169
+ printf 'x\n' > "$ISSUE_BODY"; issue_json 800 open OWNER; claim_json OWNER 800
170
+ race_setup
171
+ OUT=$(bash "$S" --parent 10 --repo o/r --items "$T/items.tsv" 2>/dev/null); RC=$?
172
+ race_teardown
173
+ check "race: duplicate-close failure still rc 0" "[ $RC -eq 0 ] && printf '%s' \"\$OUT\" | grep -q 'batched_into=#800'"
174
+
175
+ # 13. e4: claim listing failure is a failed lookup, never "none found"
176
+ reset; export CLAIM_FAIL=1
177
+ OUT=$(bash "$S" --parent 10 --repo o/r --items "$T/items.tsv"); RC=$?
178
+ check "claim-failure: rc 3, no create" "[ $RC -eq 3 ] && printf '%s' \"\$OUT\" | grep -q DEFERRED_LOOKUP=FAILED && ! grep -q '^issue create' '$GH_LOG'"
179
+
180
+ # 15. e6: untrusted claim comment is ignored (falls through to create)
181
+ reset
182
+ issue_json 77 open OWNER; claim_json NONE 77
183
+ OUT=$(bash "$S" --parent 10 --repo o/r --items "$T/items.tsv" 2>/dev/null); RC=$?
184
+ check "untrusted-claim: ignored, create filed" "[ $RC -eq 0 ] && grep -q '^issue create' '$GH_LOG'"
185
+
186
+ # 16. e7: several claims, lowest valid wins; a claim to a missing issue falls through
187
+ reset
188
+ printf 'x\n' > "$ISSUE_BODY"; issue_json 80 open OWNER; issue_json 90 open OWNER; claim_json OWNER 70 90 80
189
+ OUT=$(bash "$S" --parent 10 --repo o/r --items "$T/items.tsv"); RC=$?
190
+ check "multi-claim: lowest valid (80) wins, missing 70 skipped" "[ $RC -eq 0 ] && printf '%s' \"\$OUT\" | grep -q 'batched_into=#80 filed=0'"
191
+
192
+ # 17. DRY_RUN: no claim, reopen or close
193
+ reset; export DRY_RUN=1
194
+ printf 'x\n' > "$ISSUE_BODY"; issue_json 77 closed OWNER; claim_json OWNER 77
195
+ bash "$S" --parent 10 --repo o/r --items "$T/items.tsv" >/dev/null 2>&1
196
+ check "dry-run: no comment/reopen/close/edit" "! grep -qE '^issue (comment|reopen|close|edit|create)' '$GH_LOG'"
197
+
105
198
  echo "passed=$PASS failed=$FAIL"
106
199
  [ "$FAIL" -eq 0 ]
@@ -0,0 +1,11 @@
1
+ pr:
2
+ branches:
3
+ include: [main]
4
+ pool:
5
+ vmImage: ubuntu-latest
6
+ variables:
7
+ greeting: hi
8
+ steps:
9
+ - script: echo "$(greeting) azure"
10
+ displayName: say
11
+ - task: SomeUnknownTask@1
@@ -0,0 +1,12 @@
1
+ image: atlassian/default-image:4
2
+ pipelines:
3
+ default:
4
+ - step:
5
+ name: Build and test
6
+ script:
7
+ - echo bitbucket-test
8
+ - step:
9
+ name: Deploy
10
+ deployment: production
11
+ script:
12
+ - echo deploy
@@ -0,0 +1,10 @@
1
+ steps:
2
+ - label: ":hammer: test"
3
+ command: echo bk-test
4
+ - wait
5
+ - label: "docker plugin"
6
+ command: echo bk-docker
7
+ plugins:
8
+ - docker#v5.0.0:
9
+ image: node:20
10
+ - block: ":rocket: release"
@@ -0,0 +1,29 @@
1
+ version: 2.1
2
+ jobs:
3
+ test:
4
+ docker:
5
+ - image: cimg/base:stable
6
+ steps:
7
+ - checkout
8
+ - run:
9
+ name: unit
10
+ command: echo circle-unit
11
+ secured:
12
+ docker:
13
+ - image: cimg/base:stable
14
+ steps:
15
+ - run: echo secured
16
+ deploy-prod:
17
+ docker:
18
+ - image: cimg/base:stable
19
+ steps:
20
+ - run: echo deploy
21
+ workflows:
22
+ main:
23
+ jobs:
24
+ - test
25
+ - secured:
26
+ context: org-secrets
27
+ requires: [test]
28
+ - deploy-prod:
29
+ requires: [test]
@@ -0,0 +1,59 @@
1
+ name: CI
2
+ on:
3
+ pull_request:
4
+ branches: [main]
5
+ paths:
6
+ - 'src/**'
7
+ - 'tests/**'
8
+ - '.github/workflows/ci.yml'
9
+ jobs:
10
+ unit:
11
+ name: Unit
12
+ runs-on: ubuntu-latest
13
+ steps:
14
+ - uses: actions/checkout@v4
15
+ - uses: actions/setup-node@v4
16
+ with:
17
+ node-version: 20
18
+ - name: unit tests
19
+ run: bash tests/greet.test.sh
20
+ matrix:
21
+ runs-on: ${{ matrix.os }}
22
+ strategy:
23
+ matrix:
24
+ os: [ubuntu-latest, macos-14, windows-latest]
25
+ steps:
26
+ - uses: actions/checkout@v4
27
+ - run: echo matrix-ok
28
+ withservice:
29
+ runs-on: ubuntu-latest
30
+ services:
31
+ postgres:
32
+ image: postgres:16
33
+ env:
34
+ POSTGRES_PASSWORD: example
35
+ ports:
36
+ - 5432:5432
37
+ options: --health-cmd pg_isready --health-interval 5s
38
+ steps:
39
+ - run: echo service-ok
40
+ needsecret:
41
+ runs-on: ubuntu-latest
42
+ env:
43
+ API_TOKEN: ${{ secrets.API_TOKEN }}
44
+ steps:
45
+ - run: echo uses-secret
46
+ unknownaction:
47
+ runs-on: ubuntu-latest
48
+ steps:
49
+ - uses: some-org/mystery-action@v9
50
+ deployprod:
51
+ runs-on: ubuntu-latest
52
+ environment: production
53
+ steps:
54
+ - run: echo deploy
55
+ conditional:
56
+ runs-on: ubuntu-latest
57
+ if: github.event_name == 'push'
58
+ steps:
59
+ - run: echo push-only
@@ -0,0 +1,10 @@
1
+ name: Docs
2
+ on:
3
+ pull_request:
4
+ paths:
5
+ - 'docs/**'
6
+ jobs:
7
+ docs:
8
+ runs-on: ubuntu-latest
9
+ steps:
10
+ - run: echo docs-ok
@@ -0,0 +1,10 @@
1
+ name: Nightly
2
+ on:
3
+ schedule:
4
+ - cron: '0 3 * * *'
5
+ workflow_dispatch:
6
+ jobs:
7
+ nightly:
8
+ runs-on: ubuntu-latest
9
+ steps:
10
+ - run: echo nightly
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env bash
2
+ greet() { echo "hello $1"; }
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env bash
2
+ . "$(dirname "$0")/../src/greet.sh"
3
+ [ "$(greet x)" = "hello x" ] || exit 1
@@ -0,0 +1,51 @@
1
+ name: CI
2
+ on:
3
+ pull_request:
4
+ branches: [staging, main]
5
+ jobs:
6
+ changes:
7
+ runs-on: ubuntu-latest
8
+ outputs:
9
+ backend: ${{ steps.filter.outputs.backend }}
10
+ frontend: ${{ steps.filter.outputs.frontend }}
11
+ steps:
12
+ - uses: actions/checkout@v4
13
+ - uses: dorny/paths-filter@v3
14
+ id: filter
15
+ with:
16
+ filters: |
17
+ backend:
18
+ - 'backend/**'
19
+ frontend:
20
+ - 'frontend/**'
21
+ backend:
22
+ needs: changes
23
+ if: needs.changes.outputs.backend == 'true'
24
+ runs-on: ubuntu-latest
25
+ services:
26
+ redis:
27
+ image: redis:7
28
+ ports:
29
+ - 6379:6379
30
+ steps:
31
+ - run: echo backend-ok
32
+ frontend:
33
+ needs: changes
34
+ if: needs.changes.outputs.frontend == 'true'
35
+ runs-on: ubuntu-latest
36
+ steps:
37
+ - run: echo frontend-ok
38
+ integration:
39
+ needs: [changes]
40
+ if: needs.changes.outputs.backend == 'true'
41
+ runs-on: ubuntu-latest
42
+ env:
43
+ DB_PASSWORD: ${{ secrets.DB_PASSWORD }}
44
+ steps:
45
+ - run: echo integration
46
+ deploy-staging:
47
+ needs: [backend]
48
+ runs-on: ubuntu-latest
49
+ environment: staging
50
+ steps:
51
+ - run: echo deploy
@@ -0,0 +1 @@
1
+ print("a")
@@ -0,0 +1 @@
1
+ console.log(1)
@@ -0,0 +1,39 @@
1
+ name: CI
2
+ on:
3
+ pull_request:
4
+ jobs:
5
+ first:
6
+ runs-on: ubuntu-latest
7
+ steps:
8
+ - run: echo first >> "$RUN_LOG"
9
+ - name: reads GITHUB_ENV
10
+ run: |
11
+ echo "FROM_STEP=carried" >> "$GITHUB_ENV"
12
+ - run: test "$FROM_STEP" = carried
13
+ env:
14
+ RUN_LOG: run.log
15
+ second:
16
+ needs: first
17
+ runs-on: ubuntu-latest
18
+ steps:
19
+ - run: echo second
20
+ failing:
21
+ runs-on: ubuntu-latest
22
+ steps:
23
+ - run: |
24
+ echo "about to fail"
25
+ exit 3
26
+ skippedbyneed:
27
+ needs: failing
28
+ runs-on: ubuntu-latest
29
+ steps:
30
+ - run: echo never
31
+ slow:
32
+ runs-on: ubuntu-latest
33
+ timeout-minutes: 1
34
+ steps:
35
+ - run: echo slow-ok
36
+ broken-install:
37
+ runs-on: ubuntu-latest
38
+ steps:
39
+ - run: npm ci --no-audit --registry=http://127.0.0.1:9
@@ -0,0 +1,43 @@
1
+ stages: [build, test, deploy]
2
+ variables:
3
+ GREETING: hello
4
+ .template:
5
+ script:
6
+ - echo template
7
+ lint:
8
+ stage: test
9
+ image: node:20
10
+ rules:
11
+ - if: $CI_PIPELINE_SOURCE == "merge_request_event"
12
+ changes:
13
+ - src/**/*
14
+ script:
15
+ - echo "$GREETING lint"
16
+ unit:
17
+ stage: test
18
+ extends: .template
19
+ only:
20
+ - merge_requests
21
+ withdb:
22
+ stage: test
23
+ services:
24
+ - postgres:16
25
+ script:
26
+ - echo db
27
+ manualjob:
28
+ stage: test
29
+ when: manual
30
+ script:
31
+ - echo manual
32
+ release:
33
+ stage: deploy
34
+ environment: production
35
+ script:
36
+ - echo release
37
+ docsonly:
38
+ stage: test
39
+ rules:
40
+ - changes:
41
+ - docs/**/*
42
+ script:
43
+ - echo docs