@metasession.co/devaudit-cli 0.3.8 → 0.3.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/index.js +3 -2
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/scripts/upload-evidence.sh +23 -3
- package/sdlc/files/_common/joining-an-existing-project.md +5 -4
- package/sdlc/files/_common/scripts/close-out-contract.test.sh +71 -0
- package/sdlc/files/_common/scripts/derive-release-version.test.sh +48 -0
- package/sdlc/files/_common/scripts/extract-release-metadata.test.sh +85 -0
- package/sdlc/files/_common/scripts/prepare-release-pr.sh +99 -0
- package/sdlc/files/_common/scripts/upload-evidence.sh +593 -0
- package/sdlc/files/_common/scripts/validate-commits.sh +32 -7
- package/sdlc/files/_common/scripts/validate-commits.test.sh +143 -0
- package/sdlc/files/_common/skills/sdlc-implementer/SKILL.md +39 -3
- package/sdlc/files/ci/ci.yml.template +2 -2
- package/sdlc/files/ci/compliance-evidence.yml.template +49 -4
- package/sdlc/files/ci/incident-export.yml.template +7 -3
- package/sdlc/files/ci/label-retention.yml.template +3 -1
- package/sdlc/files/ci/periodic-review.yml.template +5 -3
- package/sdlc/files/ci/post-deploy-prod.yml.template +6 -1
- package/sdlc/files/ci/quality-gates-provenance.yml.template +1 -1
- package/sdlc/files/stacks/node/hooks/pre-push +13 -13
- package/sdlc/package.json +1 -1
- package/sdlc/src/bin/devaudit-sdlc.js +490 -54
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@metasession.co/devaudit-cli",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.10",
|
|
4
4
|
"description": "DevAudit CLI — installs, syncs, and operates the Metasession SDLC across consumer projects.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
35
|
"@clack/prompts": "^0.8.2",
|
|
36
|
-
"@metasession.co/devaudit-plugin-sdk": "^0.3.
|
|
36
|
+
"@metasession.co/devaudit-plugin-sdk": "^0.3.10",
|
|
37
37
|
"ajv": "^8.20.0",
|
|
38
38
|
"commander": "^12.1.0",
|
|
39
39
|
"consola": "^3.2.3",
|
|
@@ -54,6 +54,14 @@
|
|
|
54
54
|
# Issue from: Project Settings → API Keys in
|
|
55
55
|
# META-COMPLY's web UI.
|
|
56
56
|
#
|
|
57
|
+
# Large-file uploads (>=25MB):
|
|
58
|
+
# Files above 25MB use a presigned R2 upload flow (3-step: request URL →
|
|
59
|
+
# PUT to R2 → notify portal) instead of multipart POST. This requires
|
|
60
|
+
# the Portal to be configured with R2 evidence storage
|
|
61
|
+
# (R2_EVIDENCE_BUCKET and related env vars). If R2 is not configured,
|
|
62
|
+
# the Portal returns a clear error which is surfaced to the operator.
|
|
63
|
+
# #298 — the Portal response body is included in the error output.
|
|
64
|
+
#
|
|
57
65
|
# Examples:
|
|
58
66
|
# ./scripts/upload-evidence.sh meta-ats REQ-001 screenshot \
|
|
59
67
|
# compliance/evidence/REQ-001/screenshots/
|
|
@@ -345,9 +353,9 @@ upload_presigned() {
|
|
|
345
353
|
echo -n "(portal did not return presigned URL, falling back to multipart) "
|
|
346
354
|
return 255
|
|
347
355
|
fi
|
|
348
|
-
rm -f "$resp_body"
|
|
349
356
|
if [ "$curl_exit" -ne 0 ] || [ "$http_code" = "429" ] || { [ "$http_code" -ge 500 ] && [ "$http_code" -lt 600 ]; }; then
|
|
350
357
|
if [ "$attempt" -lt "$PRESIGNED_MAX_ATTEMPTS" ]; then
|
|
358
|
+
rm -f "$resp_body"
|
|
351
359
|
echo -n "(step 1: HTTP ${http_code}, retry in ${backoff}s) "
|
|
352
360
|
sleep "$backoff"
|
|
353
361
|
attempt=$((attempt + 1))
|
|
@@ -355,8 +363,20 @@ upload_presigned() {
|
|
|
355
363
|
continue
|
|
356
364
|
fi
|
|
357
365
|
fi
|
|
358
|
-
# Non-retriable error (4xx other than 429).
|
|
359
|
-
|
|
366
|
+
# Non-retriable error (4xx other than 429) or exhausted retries.
|
|
367
|
+
# #298 — Surface the Portal response body so operators see WHY it
|
|
368
|
+
# failed (e.g. "Presigned uploads require R2 storage configuration")
|
|
369
|
+
# instead of just "HTTP 501". Include up to 500 chars of the body.
|
|
370
|
+
local err_excerpt=""
|
|
371
|
+
if [ -s "$resp_body" ]; then
|
|
372
|
+
err_excerpt=$(head -c 500 "$resp_body")
|
|
373
|
+
fi
|
|
374
|
+
rm -f "$resp_body"
|
|
375
|
+
echo -n "(step 1 failed: HTTP ${http_code}"
|
|
376
|
+
if [ -n "$err_excerpt" ]; then
|
|
377
|
+
echo -n " — ${err_excerpt}"
|
|
378
|
+
fi
|
|
379
|
+
echo ") "
|
|
360
380
|
return 1
|
|
361
381
|
done
|
|
362
382
|
|
|
@@ -152,9 +152,10 @@ Two distinct credentials exist; conflating them is what causes the silent-CI-tok
|
|
|
152
152
|
|---|---|---|---|---|
|
|
153
153
|
| **Personal PAT** | `mctok_…` | `~/.config/devaudit/auth.json` (per developer) | You (the user) | Your local CLI commands |
|
|
154
154
|
| **Project API key** | `dak_…` | Repo secret `DEVAUDIT_API_KEY` | The project | CI's `devaudit push` calls |
|
|
155
|
-
| **Operator's PAT** | `mctok_…` | Repo secret `DEVAUDIT_USER_TOKEN` | The operator (singular) | CI's
|
|
155
|
+
| **Operator's PAT** | `mctok_…` | Repo secret `DEVAUDIT_USER_TOKEN` | The operator (singular) | CI's DevAudit portal calls that need human attribution (release submit / approval flows) |
|
|
156
|
+
| **GitHub workflow token** | `${{ github.token }}` | Issued automatically per workflow run | The GitHub Actions workflow | CI's GitHub repo mutations (checkout, branch push, PR, issue, comment, label, check-run updates) |
|
|
156
157
|
|
|
157
|
-
**Never paste your personal PAT into a repo secret.** Repo `DEVAUDIT_USER_TOKEN` is operator-owned. CI portal mutations are attributed to whoever's PAT is there — if it's yours, your name shows up against every release the team ships, and the moment your PAT expires CI
|
|
158
|
+
**Never paste your personal PAT into a repo secret.** Repo `DEVAUDIT_USER_TOKEN` is operator-owned and is not the default GitHub auth path for workflow repo mutations. CI portal mutations are attributed to whoever's PAT is there — if it's yours, your name shows up against every release the team ships, and the moment your PAT expires CI portal actions break. Rotation belongs to the operator: `devaudit install --force-team-config` on their machine.
|
|
158
159
|
|
|
159
160
|
If `devaudit auth status` shows the wrong user, run `devaudit auth logout && devaudit auth login` and paste the right PAT.
|
|
160
161
|
|
|
@@ -181,10 +182,10 @@ The synced CI gates expect a specific environment. Here's what you need locally
|
|
|
181
182
|
|
|
182
183
|
| Surface | CI | Local (your machine) |
|
|
183
184
|
|---|---|---|
|
|
184
|
-
| Personal identity | `secrets.DEVAUDIT_USER_TOKEN` (operator's) | `~/.config/devaudit/auth.json` (yours) — `devaudit auth login` |
|
|
185
|
+
| Personal identity | `secrets.DEVAUDIT_USER_TOKEN` (operator's, portal-only) | `~/.config/devaudit/auth.json` (yours) — `devaudit auth login` |
|
|
185
186
|
| Project API key | `secrets.DEVAUDIT_API_KEY` | Usually unset locally — only needed if you're testing `devaudit push` against the live portal; ask the operator if you need to debug it |
|
|
186
187
|
| Portal URL | `vars.DEVAUDIT_BASE_URL` | `~/.config/devaudit/auth.json` (set by `auth login`) or `$DEVAUDIT_BASE_URL` env |
|
|
187
|
-
| GitHub auth | `${{ github.token }}` (auto) | `gh auth login` |
|
|
188
|
+
| GitHub auth | `${{ github.token }}` (auto, repo mutations) | `gh auth login` |
|
|
188
189
|
| Node | matrix-pinned to project's `node_version` | nvm / volta / whatever — `devaudit doctor` checks ≥ 22 |
|
|
189
190
|
| `jq` / `curl` | always present on GH runners | install via package manager — `devaudit doctor` flags absence |
|
|
190
191
|
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# close-out-contract.test.sh — Tests for the close-out marker helper.
|
|
3
|
+
#
|
|
4
|
+
# Usage:
|
|
5
|
+
# ./scripts/close-out-contract.test.sh
|
|
6
|
+
|
|
7
|
+
set -euo pipefail
|
|
8
|
+
|
|
9
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
10
|
+
# shellcheck source=/dev/null
|
|
11
|
+
source "$SCRIPT_DIR/close-out-contract.sh"
|
|
12
|
+
|
|
13
|
+
PASS=0
|
|
14
|
+
FAIL=0
|
|
15
|
+
|
|
16
|
+
assert_eq() {
|
|
17
|
+
local desc="$1" want="$2" got="$3"
|
|
18
|
+
if [ "$got" = "$want" ]; then
|
|
19
|
+
echo " PASS: $desc"
|
|
20
|
+
PASS=$((PASS + 1))
|
|
21
|
+
else
|
|
22
|
+
echo " FAIL: $desc"
|
|
23
|
+
echo " want: $want"
|
|
24
|
+
echo " got: $got"
|
|
25
|
+
FAIL=$((FAIL + 1))
|
|
26
|
+
fi
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
assert_true() {
|
|
30
|
+
local desc="$1"
|
|
31
|
+
shift
|
|
32
|
+
if "$@"; then
|
|
33
|
+
echo " PASS: $desc"
|
|
34
|
+
PASS=$((PASS + 1))
|
|
35
|
+
else
|
|
36
|
+
echo " FAIL: $desc"
|
|
37
|
+
FAIL=$((FAIL + 1))
|
|
38
|
+
fi
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
assert_false() {
|
|
42
|
+
local desc="$1"
|
|
43
|
+
shift
|
|
44
|
+
if "$@"; then
|
|
45
|
+
echo " FAIL: $desc"
|
|
46
|
+
FAIL=$((FAIL + 1))
|
|
47
|
+
else
|
|
48
|
+
echo " PASS: $desc"
|
|
49
|
+
PASS=$((PASS + 1))
|
|
50
|
+
fi
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
echo "=== close-out-contract.sh tests ==="
|
|
54
|
+
|
|
55
|
+
assert_eq "branch prefix constant" "chore/close-out-" "$CLOSEOUT_BRANCH_PREFIX"
|
|
56
|
+
assert_eq "marker prefix constant" "Release-Closeout:" "$CLOSEOUT_MARKER_PREFIX"
|
|
57
|
+
assert_eq "closeout_marker emits structured line" "Release-Closeout: REQ-090" "$(closeout_marker REQ-090)"
|
|
58
|
+
|
|
59
|
+
assert_true "is_closeout_marker accepts canonical marker" is_closeout_marker "Release-Closeout: REQ-090"
|
|
60
|
+
assert_true "is_closeout_marker accepts extra whitespace" is_closeout_marker "Release-Closeout: REQ-123"
|
|
61
|
+
assert_false "is_closeout_marker rejects wrong prefix" is_closeout_marker "Closeout: REQ-090"
|
|
62
|
+
assert_false "is_closeout_marker rejects short req ids" is_closeout_marker "Release-Closeout: REQ-9"
|
|
63
|
+
assert_false "is_closeout_marker rejects bare prose mention" is_closeout_marker "This closes REQ-090 after release."
|
|
64
|
+
|
|
65
|
+
assert_eq "closeout_marker_req extracts req id" "REQ-090" "$(closeout_marker_req "Release-Closeout: REQ-090")"
|
|
66
|
+
assert_eq "closeout_marker_req extracts first req id from mixed text" "REQ-111" "$(closeout_marker_req $'x\nRelease-Closeout: REQ-111\nmore')"
|
|
67
|
+
assert_eq "closeout_marker_req returns empty when marker absent" "" "$(closeout_marker_req "no marker here" || true)"
|
|
68
|
+
|
|
69
|
+
echo
|
|
70
|
+
echo "Result: $PASS passed, $FAIL failed"
|
|
71
|
+
[ "$FAIL" = "0" ]
|
|
@@ -275,6 +275,54 @@ make_fixture "$WORK/c21" "chore(release): reconcile REQ-089 close-out
|
|
|
275
275
|
Release-Closeout: REQ-089"
|
|
276
276
|
assert_eq "close-out marker -> empty version" "" "$(run_helper)"
|
|
277
277
|
|
|
278
|
+
# Case 22 (#571 Gap 3): close-out marker with no REQ digits — must NOT
|
|
279
|
+
# suppress the bare-date fallback. The regex requires REQ-[0-9]{3,} so
|
|
280
|
+
# a malformed marker like "Release-Closeout: REQ-AB" should fall through
|
|
281
|
+
# to the bare date.
|
|
282
|
+
make_fixture "$WORK/c22" "chore(release): reconcile close-out
|
|
283
|
+
|
|
284
|
+
Release-Closeout: REQ-AB"
|
|
285
|
+
assert_eq "malformed close-out marker -> bare date $TODAY" "$TODAY" "$(run_helper)"
|
|
286
|
+
|
|
287
|
+
# Case 23 (#571 Gap 3): close-out marker with extra whitespace — must
|
|
288
|
+
# still suppress (regex allows leading whitespace after the colon).
|
|
289
|
+
make_fixture "$WORK/c23" "chore(release): reconcile REQ-100 close-out
|
|
290
|
+
|
|
291
|
+
Release-Closeout: REQ-100"
|
|
292
|
+
assert_eq "close-out marker with extra whitespace -> empty" "" "$(run_helper)"
|
|
293
|
+
|
|
294
|
+
# Case 24 (#571 Gap 3): no ticket file, no RTM, no git tags, no close-out
|
|
295
|
+
# marker — the ultimate fallback chain reaches the bare date. This tests
|
|
296
|
+
# that every prior step's `exit 0` guard correctly falls through when
|
|
297
|
+
# its condition is not met.
|
|
298
|
+
make_fixture "$WORK/c24" "chore: misc cleanup with no compliance state"
|
|
299
|
+
assert_eq "no compliance state at all -> bare date $TODAY" "$TODAY" "$(run_helper)"
|
|
300
|
+
|
|
301
|
+
# Case 25 (#571 Gap 3): pending-releases dir exists but is empty — step 4
|
|
302
|
+
# must not crash and must fall through to step 4-bis / bare date.
|
|
303
|
+
make_fixture "$WORK/c25" "chore: devaudit update to 0.1.30"
|
|
304
|
+
mkdir -p compliance/pending-releases
|
|
305
|
+
assert_eq "empty pending-releases dir -> bare date $TODAY" "$TODAY" "$(run_helper)"
|
|
306
|
+
|
|
307
|
+
# Case 26 (#571 Gap 3): RTM.md exists but has no IN PROGRESS rows —
|
|
308
|
+
# step 4-bis must fall through to the bare date.
|
|
309
|
+
make_fixture "$WORK/c26" "chore: devaudit update to 0.1.30"
|
|
310
|
+
mkdir -p compliance
|
|
311
|
+
cat > compliance/RTM.md <<'RTM'
|
|
312
|
+
| REQ-ID | Status |
|
|
313
|
+
| ------- | ------------- |
|
|
314
|
+
| REQ-001 | RELEASED |
|
|
315
|
+
| REQ-002 | DRAFT |
|
|
316
|
+
RTM
|
|
317
|
+
assert_eq "RTM with no IN PROGRESS -> bare date $TODAY" "$TODAY" "$(run_helper)"
|
|
318
|
+
|
|
319
|
+
# Case 27 (#571 Gap 3): RTM.md exists but is empty (no rows at all) —
|
|
320
|
+
# step 4-bis must not crash and must fall through.
|
|
321
|
+
make_fixture "$WORK/c27" "chore: devaudit update to 0.1.30"
|
|
322
|
+
mkdir -p compliance
|
|
323
|
+
echo "# Requirements Traceability Matrix" > compliance/RTM.md
|
|
324
|
+
assert_eq "empty RTM.md -> bare date $TODAY" "$TODAY" "$(run_helper)"
|
|
325
|
+
|
|
278
326
|
echo ""
|
|
279
327
|
echo "=== Summary: $PASS pass / $FAIL fail ==="
|
|
280
328
|
|
|
@@ -273,6 +273,91 @@ assert_eq "RTM issue title fallback without ticket" "Issue title from RTM" "$REL
|
|
|
273
273
|
assert_empty "Summary empty without ticket" "$RELEASE_SUMMARY"
|
|
274
274
|
echo ""
|
|
275
275
|
|
|
276
|
+
# --- Test 12 (#571 Gap 3): No ticket, no RTM, no gh CLI — all fallbacks empty ---
|
|
277
|
+
echo "--- Test 12: All fallbacks unreachable ---"
|
|
278
|
+
make_fixture "$WORK/test12"
|
|
279
|
+
source "$HELPER"
|
|
280
|
+
extract_release_metadata "REQ-999"
|
|
281
|
+
assert_empty "Title empty when no ticket, no RTM, no gh" "$RELEASE_TITLE"
|
|
282
|
+
assert_empty "Summary empty when no ticket" "$RELEASE_SUMMARY"
|
|
283
|
+
echo ""
|
|
284
|
+
|
|
285
|
+
# --- Test 13 (#571 Gap 3): Ticket exists but has no **Requirement:** line,
|
|
286
|
+
# no H1, and no ## Summary — all extraction targets are unreachable ---
|
|
287
|
+
echo "--- Test 13: Ticket with no extractable content ---"
|
|
288
|
+
make_fixture "$WORK/test13"
|
|
289
|
+
cat > "compliance/pending-releases/RELEASE-TICKET-REQ-012.md" <<'TICKET'
|
|
290
|
+
Some prose without any standard headings.
|
|
291
|
+
|
|
292
|
+
Just a paragraph.
|
|
293
|
+
TICKET
|
|
294
|
+
|
|
295
|
+
source "$HELPER"
|
|
296
|
+
extract_release_metadata "REQ-012"
|
|
297
|
+
assert_empty "Title empty when no Requirement line or H1" "$RELEASE_TITLE"
|
|
298
|
+
assert_empty "Summary empty when no ## Summary section" "$RELEASE_SUMMARY"
|
|
299
|
+
echo ""
|
|
300
|
+
|
|
301
|
+
# --- Test 14 (#571 Gap 3): RTM row exists but has no issue number,
|
|
302
|
+
# so the gh CLI fallback is unreachable. Title should be empty. ---
|
|
303
|
+
echo "--- Test 14: RTM row without issue number ---"
|
|
304
|
+
make_fixture "$WORK/test14"
|
|
305
|
+
cat > compliance/RTM.md <<'RTM'
|
|
306
|
+
| REQ-ID | Issue | Risk | Evidence | Status |
|
|
307
|
+
| ------- | ----- | ---- | -------- | ------ |
|
|
308
|
+
| REQ-013 | | LOW | n/a | DRAFT |
|
|
309
|
+
RTM
|
|
310
|
+
source "$HELPER"
|
|
311
|
+
extract_release_metadata "REQ-013"
|
|
312
|
+
assert_empty "Title empty when RTM row has no issue number" "$RELEASE_TITLE"
|
|
313
|
+
echo ""
|
|
314
|
+
|
|
315
|
+
# --- Test 15 (#571 Gap 3): gh CLI exists but issue lookup fails —
|
|
316
|
+
# must fall through to H1 fallback, not error out ---
|
|
317
|
+
echo "--- Test 15: gh CLI fails, falls through to H1 ---"
|
|
318
|
+
make_fixture "$WORK/test15"
|
|
319
|
+
cat > "compliance/pending-releases/RELEASE-TICKET-REQ-014.md" <<'TICKET'
|
|
320
|
+
# Fix critical payment bug
|
|
321
|
+
|
|
322
|
+
## Summary
|
|
323
|
+
Fixes the payment redirect issue.
|
|
324
|
+
TICKET
|
|
325
|
+
cat > compliance/RTM.md <<'RTM'
|
|
326
|
+
| REQ-ID | Issue | Risk | Evidence | Status |
|
|
327
|
+
| ------- | ----- | ---- | -------- | ------ |
|
|
328
|
+
| REQ-014 | #999 | LOW | n/a | DRAFT |
|
|
329
|
+
RTM
|
|
330
|
+
mkdir -p bin
|
|
331
|
+
cat > bin/gh <<'GH'
|
|
332
|
+
#!/usr/bin/env bash
|
|
333
|
+
# Simulate gh CLI failing (e.g. rate limited, issue not found)
|
|
334
|
+
exit 1
|
|
335
|
+
GH
|
|
336
|
+
chmod +x bin/gh
|
|
337
|
+
PATH="$PWD/bin:$PATH"
|
|
338
|
+
source "$HELPER"
|
|
339
|
+
extract_release_metadata "REQ-014"
|
|
340
|
+
assert_eq "H1 fallback when gh CLI fails" "Fix critical payment bug" "$RELEASE_TITLE"
|
|
341
|
+
echo ""
|
|
342
|
+
|
|
343
|
+
# --- Test 16 (#571 Gap 3): Summary section exists but is only whitespace ---
|
|
344
|
+
echo "--- Test 16: Whitespace-only summary is cleared ---"
|
|
345
|
+
make_fixture "$WORK/test16"
|
|
346
|
+
cat > "compliance/pending-releases/RELEASE-TICKET-REQ-015.md" <<'TICKET'
|
|
347
|
+
**Requirement:** REQ-015 — Feature fifteen
|
|
348
|
+
|
|
349
|
+
## Summary
|
|
350
|
+
|
|
351
|
+
## Changes
|
|
352
|
+
- Change A
|
|
353
|
+
TICKET
|
|
354
|
+
|
|
355
|
+
source "$HELPER"
|
|
356
|
+
extract_release_metadata "REQ-015"
|
|
357
|
+
assert_eq "Title extracted" "Feature fifteen" "$RELEASE_TITLE"
|
|
358
|
+
assert_empty "Whitespace-only summary cleared" "$RELEASE_SUMMARY"
|
|
359
|
+
echo ""
|
|
360
|
+
|
|
276
361
|
echo ""
|
|
277
362
|
echo "=== Results: $PASS passed, $FAIL failed ==="
|
|
278
363
|
exit $FAIL
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# prepare-release-pr.sh — ensure there is one truthful release PR.
|
|
3
|
+
#
|
|
4
|
+
# Usage:
|
|
5
|
+
# ./scripts/prepare-release-pr.sh [--apply] [--mode=update|recreate]
|
|
6
|
+
#
|
|
7
|
+
# Default mode is dry-run + update. Dry-run prints the action it would take.
|
|
8
|
+
# With --apply:
|
|
9
|
+
# - no existing PR: creates one
|
|
10
|
+
# - current PR: updates title/body only when needed
|
|
11
|
+
# - stale PR + mode=update: rewrites title/body and comments
|
|
12
|
+
# - stale PR + mode=recreate: closes old PR and opens a fresh one
|
|
13
|
+
|
|
14
|
+
set -euo pipefail
|
|
15
|
+
|
|
16
|
+
APPLY=false
|
|
17
|
+
MODE="update"
|
|
18
|
+
for arg in "$@"; do
|
|
19
|
+
case "$arg" in
|
|
20
|
+
--apply) APPLY=true ;;
|
|
21
|
+
--mode=update) MODE="update" ;;
|
|
22
|
+
--mode=recreate) MODE="recreate" ;;
|
|
23
|
+
*) echo "Unknown argument: $arg" >&2; exit 2 ;;
|
|
24
|
+
esac
|
|
25
|
+
done
|
|
26
|
+
|
|
27
|
+
INTEGRATION_BRANCH=$(jq -r '.integration_branch // "develop"' sdlc-config.json 2>/dev/null || echo "develop")
|
|
28
|
+
RELEASE_BRANCH=$(jq -r '.release_branch // "main"' sdlc-config.json 2>/dev/null || echo "main")
|
|
29
|
+
|
|
30
|
+
if [ ! -x scripts/derive-release-version.sh ]; then
|
|
31
|
+
chmod +x scripts/derive-release-version.sh 2>/dev/null || true
|
|
32
|
+
fi
|
|
33
|
+
CURRENT_RELEASE=$(./scripts/derive-release-version.sh)
|
|
34
|
+
if [ -z "$CURRENT_RELEASE" ]; then
|
|
35
|
+
echo "No active release version derived; close-out reconciliation appears in progress." >&2
|
|
36
|
+
exit 1
|
|
37
|
+
fi
|
|
38
|
+
|
|
39
|
+
TITLE="Release: ${CURRENT_RELEASE}"
|
|
40
|
+
BODY=$(cat <<EOF
|
|
41
|
+
## Release
|
|
42
|
+
|
|
43
|
+
- Release: ${CURRENT_RELEASE}
|
|
44
|
+
- Base: ${RELEASE_BRANCH}
|
|
45
|
+
- Head: ${INTEGRATION_BRANCH}
|
|
46
|
+
|
|
47
|
+
## DevAudit
|
|
48
|
+
|
|
49
|
+
- Release context is derived from the current ${INTEGRATION_BRANCH} head.
|
|
50
|
+
- If this PR replaced an older release PR, the older PR was superseded because it no longer matched the governing release context.
|
|
51
|
+
EOF
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
OPEN_PRS=$(gh pr list --base "$RELEASE_BRANCH" --head "$INTEGRATION_BRANCH" --state open --json number,title,body,url --limit 10)
|
|
55
|
+
COUNT=$(echo "$OPEN_PRS" | jq 'length')
|
|
56
|
+
|
|
57
|
+
if [ "$COUNT" -eq 0 ]; then
|
|
58
|
+
echo "No open ${INTEGRATION_BRANCH}->${RELEASE_BRANCH} PR found."
|
|
59
|
+
if [ "$APPLY" = "true" ]; then
|
|
60
|
+
gh pr create --base "$RELEASE_BRANCH" --head "$INTEGRATION_BRANCH" --title "$TITLE" --body "$BODY"
|
|
61
|
+
else
|
|
62
|
+
echo "Dry run: would create release PR titled '$TITLE'."
|
|
63
|
+
fi
|
|
64
|
+
exit 0
|
|
65
|
+
fi
|
|
66
|
+
|
|
67
|
+
if [ "$COUNT" -gt 1 ]; then
|
|
68
|
+
echo "ERROR: ${COUNT} open ${INTEGRATION_BRANCH}->${RELEASE_BRANCH} PRs found. Resolve duplicates manually." >&2
|
|
69
|
+
echo "$OPEN_PRS" | jq -r '.[] | " #\(.number) \(.title) \(.url)"' >&2
|
|
70
|
+
exit 1
|
|
71
|
+
fi
|
|
72
|
+
|
|
73
|
+
PR_NUMBER=$(echo "$OPEN_PRS" | jq -r '.[0].number')
|
|
74
|
+
PR_TITLE=$(echo "$OPEN_PRS" | jq -r '.[0].title // ""')
|
|
75
|
+
PR_BODY=$(echo "$OPEN_PRS" | jq -r '.[0].body // ""')
|
|
76
|
+
PR_URL=$(echo "$OPEN_PRS" | jq -r '.[0].url')
|
|
77
|
+
|
|
78
|
+
if printf '%s\n%s\n' "$PR_TITLE" "$PR_BODY" | grep -q "$CURRENT_RELEASE"; then
|
|
79
|
+
echo "Open release PR #${PR_NUMBER} already matches ${CURRENT_RELEASE}: ${PR_URL}"
|
|
80
|
+
exit 0
|
|
81
|
+
fi
|
|
82
|
+
|
|
83
|
+
echo "Open release PR #${PR_NUMBER} is stale for current release ${CURRENT_RELEASE}: ${PR_URL}"
|
|
84
|
+
echo "Current title: ${PR_TITLE}"
|
|
85
|
+
|
|
86
|
+
if [ "$APPLY" != "true" ]; then
|
|
87
|
+
echo "Dry run: would ${MODE} stale PR #${PR_NUMBER} for ${CURRENT_RELEASE}."
|
|
88
|
+
exit 0
|
|
89
|
+
fi
|
|
90
|
+
|
|
91
|
+
if [ "$MODE" = "recreate" ]; then
|
|
92
|
+
gh pr comment "$PR_NUMBER" --body "Superseded: current ${INTEGRATION_BRANCH} head now derives ${CURRENT_RELEASE}; this PR no longer matches the governing release context."
|
|
93
|
+
gh pr close "$PR_NUMBER" --comment "Closed as superseded by ${CURRENT_RELEASE} release context."
|
|
94
|
+
gh pr create --base "$RELEASE_BRANCH" --head "$INTEGRATION_BRANCH" --title "$TITLE" --body "$BODY"
|
|
95
|
+
else
|
|
96
|
+
gh pr edit "$PR_NUMBER" --title "$TITLE" --body "$BODY"
|
|
97
|
+
gh pr comment "$PR_NUMBER" --body "Release PR context refreshed: current ${INTEGRATION_BRANCH} head derives ${CURRENT_RELEASE}."
|
|
98
|
+
echo "Updated PR #${PR_NUMBER} to ${CURRENT_RELEASE}: ${PR_URL}"
|
|
99
|
+
fi
|