@metasession.co/devaudit-cli 0.3.9 → 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/dist/index.js +3 -2
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- 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/prepare-release-pr.sh +99 -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",
|
|
@@ -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" ]
|
|
@@ -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
|
|
@@ -37,6 +37,23 @@ fi
|
|
|
37
37
|
TOTAL=0
|
|
38
38
|
FAILED=0
|
|
39
39
|
|
|
40
|
+
ACTIVE_RELEASE_REQS=""
|
|
41
|
+
if [ -d compliance/pending-releases ]; then
|
|
42
|
+
ACTIVE_RELEASE_REQS=$(find compliance/pending-releases -maxdepth 1 -type f \
|
|
43
|
+
-name 'RELEASE-TICKET-REQ-*.md' -print 2>/dev/null \
|
|
44
|
+
| sed -E 's|.*/RELEASE-TICKET-(REQ-[0-9]+)\.md$|\1|' | sort -u || true)
|
|
45
|
+
fi
|
|
46
|
+
if [ -z "$ACTIVE_RELEASE_REQS" ] && [ -f compliance/RTM.md ]; then
|
|
47
|
+
ACTIVE_RELEASE_REQS=$(sed 's/\\|/ /g' compliance/RTM.md 2>/dev/null \
|
|
48
|
+
| grep -E '\|[[:space:]]+IN PROGRESS|\|[[:space:]]+TESTED - PENDING SIGN-OFF' \
|
|
49
|
+
| grep -oE '^\|[[:space:]]*REQ-[0-9]+' \
|
|
50
|
+
| grep -oE 'REQ-[0-9]+' | sort -u || true)
|
|
51
|
+
fi
|
|
52
|
+
ACTIVE_RELEASE_REQ_COUNT=0
|
|
53
|
+
if [ -n "$ACTIVE_RELEASE_REQS" ]; then
|
|
54
|
+
ACTIVE_RELEASE_REQ_COUNT=$(printf '%s\n' "$ACTIVE_RELEASE_REQS" | grep -c . || true)
|
|
55
|
+
fi
|
|
56
|
+
|
|
40
57
|
while IFS= read -r sha; do
|
|
41
58
|
TOTAL=$((TOTAL + 1))
|
|
42
59
|
SUBJECT=$(git log -1 --format='%s' "$sha")
|
|
@@ -66,13 +83,21 @@ while IFS= read -r sha; do
|
|
|
66
83
|
feat|fix|refactor|perf)
|
|
67
84
|
if ! echo "$SUBJECT" | grep -qP '\[REQ-\d{3,}\]' \
|
|
68
85
|
&& ! echo "$BODY" | grep -qiP 'Ref:\s*REQ-\d{3,}'; then
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
86
|
+
if [ "$ACTIVE_RELEASE_REQ_COUNT" -eq 1 ]; then
|
|
87
|
+
ACTIVE_REQ=$(printf '%s\n' "$ACTIVE_RELEASE_REQS" | head -1)
|
|
88
|
+
echo "WARNING [$SHORT]: '$TYPE' cites no requirement, but the branch has one active tracked release (${ACTIVE_REQ})."
|
|
89
|
+
echo " Treating the release ticket / RTM context as authoritative for this already-merged history."
|
|
90
|
+
echo " Add a follow-up compliance note if this was not an intentional recovery path."
|
|
91
|
+
WARN_COUNT=$((WARN_COUNT + 1))
|
|
92
|
+
else
|
|
93
|
+
echo "ERROR [$SHORT]: '$TYPE' is an implementation commit but cites no requirement."
|
|
94
|
+
echo " Add [REQ-XXX] to the subject or a 'Ref: REQ-XXX' trailer. Start work"
|
|
95
|
+
echo " from a requirement via the sdlc-implementer skill (it assigns the REQ"
|
|
96
|
+
echo " from the originating issue in Phase 1)."
|
|
97
|
+
FAILED=$((FAILED + 1))
|
|
98
|
+
EXIT_CODE=1
|
|
99
|
+
continue
|
|
100
|
+
fi
|
|
76
101
|
fi
|
|
77
102
|
|
|
78
103
|
# RTM provenance check (devaudit-installer#226): verify the REQ-XXX
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# validate-commits.test.sh — Fixture-based tests for validate-commits.sh.
|
|
3
|
+
#
|
|
4
|
+
# Covers the relaxed "exactly one active tracked release" behavior:
|
|
5
|
+
# implementation commits without REQ tags downgrade to warnings only when
|
|
6
|
+
# release context is unambiguous; otherwise they remain hard errors.
|
|
7
|
+
#
|
|
8
|
+
# Usage:
|
|
9
|
+
# ./scripts/validate-commits.test.sh
|
|
10
|
+
|
|
11
|
+
set -euo pipefail
|
|
12
|
+
|
|
13
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
14
|
+
VALIDATOR="$SCRIPT_DIR/validate-commits.sh"
|
|
15
|
+
[ -x "$VALIDATOR" ] || chmod +x "$VALIDATOR"
|
|
16
|
+
|
|
17
|
+
PASS=0
|
|
18
|
+
FAIL=0
|
|
19
|
+
|
|
20
|
+
make_fixture() {
|
|
21
|
+
local dir="$1" subject="$2" body="${3:-}"
|
|
22
|
+
rm -rf "$dir"
|
|
23
|
+
mkdir -p "$dir"
|
|
24
|
+
cd "$dir"
|
|
25
|
+
git init -q --initial-branch=main
|
|
26
|
+
git config user.email "test@example.com"
|
|
27
|
+
git config user.name "test"
|
|
28
|
+
echo "base" > base.txt
|
|
29
|
+
git add base.txt
|
|
30
|
+
git commit -q -m "chore: base"
|
|
31
|
+
|
|
32
|
+
git checkout -q -b feature
|
|
33
|
+
echo "feature" > app.ts
|
|
34
|
+
git add app.ts
|
|
35
|
+
if [ -n "$body" ]; then
|
|
36
|
+
git commit -q -m "$subject" -m "$body"
|
|
37
|
+
else
|
|
38
|
+
git commit -q -m "$subject"
|
|
39
|
+
fi
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
run_validator() {
|
|
43
|
+
set +e
|
|
44
|
+
OUT_FILE=$(mktemp)
|
|
45
|
+
bash "$VALIDATOR" main > "$OUT_FILE" 2>&1
|
|
46
|
+
LAST_EXIT=$?
|
|
47
|
+
set -e
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
assert_exit() {
|
|
51
|
+
local desc="$1" want="$2"
|
|
52
|
+
if [ "$LAST_EXIT" = "$want" ]; then
|
|
53
|
+
echo " PASS: $desc (exit=$LAST_EXIT)"
|
|
54
|
+
PASS=$((PASS + 1))
|
|
55
|
+
else
|
|
56
|
+
echo " FAIL: $desc (want=$want, got=$LAST_EXIT)"
|
|
57
|
+
sed 's/^/ /' "$OUT_FILE"
|
|
58
|
+
FAIL=$((FAIL + 1))
|
|
59
|
+
fi
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
assert_grep() {
|
|
63
|
+
local desc="$1" pattern="$2" want_match="$3"
|
|
64
|
+
local found=0
|
|
65
|
+
if grep -qE "$pattern" "$OUT_FILE"; then
|
|
66
|
+
found=1
|
|
67
|
+
fi
|
|
68
|
+
if [ "$found" = "$want_match" ]; then
|
|
69
|
+
echo " PASS: $desc"
|
|
70
|
+
PASS=$((PASS + 1))
|
|
71
|
+
else
|
|
72
|
+
echo " FAIL: $desc (want match=$want_match, got=$found, pattern=$pattern)"
|
|
73
|
+
sed 's/^/ /' "$OUT_FILE"
|
|
74
|
+
FAIL=$((FAIL + 1))
|
|
75
|
+
fi
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
WORKDIR=$(mktemp -d -t validate-commits-test-XXXX)
|
|
79
|
+
trap 'rm -rf "$WORKDIR"' EXIT
|
|
80
|
+
|
|
81
|
+
echo "=== validate-commits.sh tests ==="
|
|
82
|
+
|
|
83
|
+
# Case 1: one active pending ticket makes a missing-REQ implementation
|
|
84
|
+
# commit a warning, not a hard failure.
|
|
85
|
+
echo "Case 1: single active pending release downgrades missing REQ to warning"
|
|
86
|
+
make_fixture "$WORKDIR/case1" "feat: implement kitchen workflow" "Co-Authored-By: Test <test@example.com>"
|
|
87
|
+
mkdir -p compliance/pending-releases
|
|
88
|
+
cat > compliance/pending-releases/RELEASE-TICKET-REQ-123.md <<'EOF'
|
|
89
|
+
# Release Ticket: REQ-123
|
|
90
|
+
EOF
|
|
91
|
+
git add compliance/pending-releases
|
|
92
|
+
git commit -q --amend --no-edit
|
|
93
|
+
run_validator
|
|
94
|
+
assert_exit "single active pending release exits 0" 0
|
|
95
|
+
assert_grep "warning emitted for missing REQ with one active release" 'WARNING .*one active tracked release \(REQ-123\)' 1
|
|
96
|
+
assert_grep "no hard error for missing requirement" "ERROR .*implementation commit but cites no requirement" 0
|
|
97
|
+
|
|
98
|
+
# Case 2: ambiguous pending release context keeps the missing-REQ path as
|
|
99
|
+
# a hard error.
|
|
100
|
+
echo "Case 2: two active pending releases keep missing REQ as hard error"
|
|
101
|
+
make_fixture "$WORKDIR/case2" "feat: implement kitchen workflow" "Co-Authored-By: Test <test@example.com>"
|
|
102
|
+
mkdir -p compliance/pending-releases
|
|
103
|
+
cat > compliance/pending-releases/RELEASE-TICKET-REQ-123.md <<'EOF'
|
|
104
|
+
# Release Ticket: REQ-123
|
|
105
|
+
EOF
|
|
106
|
+
cat > compliance/pending-releases/RELEASE-TICKET-REQ-124.md <<'EOF'
|
|
107
|
+
# Release Ticket: REQ-124
|
|
108
|
+
EOF
|
|
109
|
+
git add compliance/pending-releases
|
|
110
|
+
git commit -q --amend --no-edit
|
|
111
|
+
run_validator
|
|
112
|
+
assert_exit "ambiguous pending releases exits 1" 1
|
|
113
|
+
assert_grep "hard error emitted when active context is ambiguous" "ERROR .*implementation commit but cites no requirement" 1
|
|
114
|
+
|
|
115
|
+
# Case 3: no pending ticket, but exactly one active RTM row in the
|
|
116
|
+
# accepted statuses also downgrades to a warning.
|
|
117
|
+
echo "Case 3: RTM fallback with one active row downgrades missing REQ to warning"
|
|
118
|
+
make_fixture "$WORKDIR/case3" "fix: recover already-merged history" "Co-Authored-By: Test <test@example.com>"
|
|
119
|
+
mkdir -p compliance
|
|
120
|
+
cat > compliance/RTM.md <<'EOF'
|
|
121
|
+
# RTM
|
|
122
|
+
| REQ-ID | Title | Status |
|
|
123
|
+
| ------- | ----- | ------------------------- |
|
|
124
|
+
| REQ-200 | Done | APPROVED - DEPLOYED |
|
|
125
|
+
| REQ-201 | Live | TESTED - PENDING SIGN-OFF |
|
|
126
|
+
EOF
|
|
127
|
+
git add compliance/RTM.md
|
|
128
|
+
git commit -q --amend --no-edit
|
|
129
|
+
run_validator
|
|
130
|
+
assert_exit "single active RTM row exits 0" 0
|
|
131
|
+
assert_grep "warning emitted for RTM fallback context" 'WARNING .*one active tracked release \(REQ-201\)' 1
|
|
132
|
+
assert_grep "no hard error under single RTM fallback context" "ERROR .*implementation commit but cites no requirement" 0
|
|
133
|
+
|
|
134
|
+
# Case 4: zero active context remains a hard error.
|
|
135
|
+
echo "Case 4: no active release context keeps missing REQ as hard error"
|
|
136
|
+
make_fixture "$WORKDIR/case4" "perf: tune kitchen workflow" "Co-Authored-By: Test <test@example.com>"
|
|
137
|
+
run_validator
|
|
138
|
+
assert_exit "no active context exits 1" 1
|
|
139
|
+
assert_grep "hard error emitted without any active release context" "ERROR .*implementation commit but cites no requirement" 1
|
|
140
|
+
|
|
141
|
+
echo
|
|
142
|
+
echo "Result: $PASS passed, $FAIL failed"
|
|
143
|
+
[ "$FAIL" = "0" ]
|
|
@@ -553,7 +553,17 @@ Reached only on the **tracked** route from Phase 0 (the issue is already fetched
|
|
|
553
553
|
|
|
554
554
|
### Phase 4 — Submit for UAT review (SDLC stage 4)
|
|
555
555
|
|
|
556
|
-
1. **
|
|
556
|
+
1. **Prepare the truthful release PR** — the PR that carries the UAT four-eyes approval gate (`check-release-approval.yml`), always into `$RELEASE_BRANCH` from `$INTEGRATION_BRANCH` (e.g. `develop → main`). The implementation already landed on `$INTEGRATION_BRANCH` in Phase 2; this promotes it. (Note: if other work is also waiting on `$INTEGRATION_BRANCH`, this is a bundled release — every in-scope REQ keeps its own release record and Production approval.)
|
|
557
|
+
|
|
558
|
+
Before creating a PR manually, run the synced helper:
|
|
559
|
+
|
|
560
|
+
```bash
|
|
561
|
+
./scripts/prepare-release-pr.sh --apply --mode=update
|
|
562
|
+
```
|
|
563
|
+
|
|
564
|
+
This is mandatory for devaudit-installer#312. It detects an existing open `$INTEGRATION_BRANCH → $RELEASE_BRANCH` PR, derives the current governing release from the current head, and ensures there is exactly one truthful release PR. If the existing PR is stale, the default `update` mode rewrites the title/body and comments with the refresh. Use `--mode=recreate` only when the operator explicitly wants the older PR closed and a new PR opened.
|
|
565
|
+
|
|
566
|
+
If the helper reports multiple open release PRs, halt with: "Multiple open release PRs for `$INTEGRATION_BRANCH → $RELEASE_BRANCH`; operator action — close duplicates or choose the authoritative PR, then resume REQ-XXX." Do not open another PR.
|
|
557
567
|
|
|
558
568
|
PR body per the SDLC PR template (see [`.github/pull_request_template.md`](../../../../../.github/pull_request_template.md)):
|
|
559
569
|
- Closes #N
|
|
@@ -571,8 +581,30 @@ Reached only on the **tracked** route from Phase 0 (the issue is already fetched
|
|
|
571
581
|
|
|
572
582
|
3. **Apply labels** — `awaiting-uat-review`, `risk:<class>`.
|
|
573
583
|
4. **Comment on the issue**: "Implementation complete. PR #M opened. Evidence on portal: <link>. UAT review requested. Resume with `resume REQ-XXX` once UAT approval is granted on the portal."
|
|
574
|
-
5. **
|
|
575
|
-
|
|
584
|
+
5. **Inspect PR blockers before handoff (devaudit-installer#304).** Run `gh pr view <N> --json mergeStateStatus,reviewDecision,statusCheckRollup,url` and categorize the state:
|
|
585
|
+
- `reviewDecision` not approved → human approval blocker; hand off to the reviewer.
|
|
586
|
+
- Release Approval Gate failing while portal state is not `uat_approved` → portal approval blocker; hand off to portal UAT reviewer.
|
|
587
|
+
- Release Approval Gate failing while portal state is `uat_approved` → retry path below.
|
|
588
|
+
- Compliance validation / evidence completeness failing → real workflow or evidence defect; fix or create a follow-up PR before handoff.
|
|
589
|
+
- Checks pending → wait/poll until terminal before declaring the PR ready.
|
|
590
|
+
- Stale PR context detected by `prepare-release-pr.sh` → rerun the helper in `--apply` mode and repeat this blocker inspection.
|
|
591
|
+
|
|
592
|
+
**Executable loop (devaudit-installer#304).** Use the bundled watcher rather than a one-shot `gh` read when the PR is blocked or waiting:
|
|
593
|
+
|
|
594
|
+
```bash
|
|
595
|
+
node SDLC/bin/devaudit-sdlc.js \
|
|
596
|
+
--watch-pr=<N> \
|
|
597
|
+
--repo <owner/name> \
|
|
598
|
+
--req XXX \
|
|
599
|
+
--release REQ-XXX \
|
|
600
|
+
--project-slug <project-slug> \
|
|
601
|
+
--base-url <devaudit-base-url>
|
|
602
|
+
```
|
|
603
|
+
|
|
604
|
+
The watcher persists retry state in `.sdlc-pr-watch.json`, polls `gh pr view` + `gh pr checks`, re-runs likely flaky workflows automatically, and re-runs the Release Approval Gate when the portal is already approved but GitHub has not converged yet. Use `--once` when you only need a single classification pass.
|
|
605
|
+
|
|
606
|
+
6. **Hard stop only after blocker classification.** Phase 4 ends here. Do not proceed to merge; the human's next action is reviewing on the portal or clearing the categorized blocker above.
|
|
607
|
+
7. **Update SDLC status sticky** before halting: `bash scripts/update-sdlc-status.sh "$ISSUE_NUM" "Phase 4 — truthful release PR #<N> prepared against $RELEASE_BRANCH; blockers classified" "Operator action — review PR #<N> + approve UAT release on the portal; sdlc-implementer halts until you ping resume REQ-XXX"`. This is a critical handoff — the sticky must reflect that the agent has stopped + the operator is on the hook.
|
|
576
608
|
|
|
577
609
|
**When an external gate hangs or fails for unrelated reasons.** A required gate may fail for reasons outside the change's scope — flaky infra, an unrelated regression test that hangs at hour-plus runtime with no log activity, a known-failing suite. When this happens:
|
|
578
610
|
|
|
@@ -685,6 +717,10 @@ The native agent must NOT continue to the next phase (Phase 3 → Phase 4, Phase
|
|
|
685
717
|
|
|
686
718
|
**PR merged to main ≠ done.** After the release PR is merged to `main`, the native agent must re-invoke the skill for Phase 5 close-out (see Phase 5 step 0 below). The merge is not the end of the workflow — post-merge compliance steps are mandatory.
|
|
687
719
|
|
|
720
|
+
### Commit-history hygiene recovery (devaudit-installer#315)
|
|
721
|
+
|
|
722
|
+
Commit-level `[REQ-XXX]` hygiene is preventive, not the strongest source of release truth after history has already merged to a protected integration branch. If CI reports a missing commit-level REQ after the branch has exactly one active tracked release ticket or one matching RTM row, treat the release ticket / RTM context as authoritative and add a follow-up compliance note instead of rewriting protected history. The generated `validate-commits.sh` implements this as a warning in the exactly-one-active-release case. If there are zero or multiple active tracked releases, the validation remains a hard error because the release context is ambiguous.
|
|
723
|
+
|
|
688
724
|
### Commit-scoping rule for SRS updates (devaudit-installer#226)
|
|
689
725
|
|
|
690
726
|
When a commit is authored as part of REQ-XXX's SDLC flow, the commit subject must cite `[REQ-XXX]` — the **active REQ** driving the current SDLC flow — regardless of which other REQs the content touches. Other REQs may appear in the body for traceability, but never in the subject.
|
|
@@ -783,13 +783,13 @@ jobs:
|
|
|
783
783
|
# "zero traceable evidence" even though well-written, REQ-tagged
|
|
784
784
|
# specs exist on disk. If specs are found but weren't run in
|
|
785
785
|
# this tier, emit a non-blocking warning instead of an error.
|
|
786
|
-
SPECS_ON_DISK=$(grep -rl --include='*.spec.ts' -e "@requirement ${REQ}" -e "tagTest.*${REQ}" -e "'${REQ}'" e2e/ 2>/dev/null | wc -l)
|
|
786
|
+
SPECS_ON_DISK=$( { grep -rl --include='*.spec.ts' -e "@requirement ${REQ}" -e "tagTest.*${REQ}" -e "'${REQ}'" e2e/ 2>/dev/null || true; } | wc -l | tr -d ' ')
|
|
787
787
|
|
|
788
788
|
# devaudit-installer#237 — also scan unit test files for
|
|
789
789
|
# @requirement REQ-XXX annotations. A REQ with thorough unit
|
|
790
790
|
# test coverage but no E2E specs (legitimate for API-only or
|
|
791
791
|
# backend-only changes) should not be hard-blocked.
|
|
792
|
-
UNIT_TESTS_ON_DISK=$(grep -rl --include='*.test.ts' --include='*.test.tsx' -e "@requirement ${REQ}" -e "'${REQ}'" e2e/ __tests__/ tests/ src/ 2>/dev/null | wc -l)
|
|
792
|
+
UNIT_TESTS_ON_DISK=$( { grep -rl --include='*.test.ts' --include='*.test.tsx' -e "@requirement ${REQ}" -e "'${REQ}'" e2e/ __tests__/ tests/ src/ 2>/dev/null || true; } | wc -l | tr -d ' ')
|
|
793
793
|
|
|
794
794
|
# devaudit-installer#237 — check for test-execution-summary.md
|
|
795
795
|
# as a secondary signal. This is a human-authored document and
|
|
@@ -638,6 +638,7 @@ jobs:
|
|
|
638
638
|
permissions:
|
|
639
639
|
contents: read
|
|
640
640
|
actions: read
|
|
641
|
+
issues: write
|
|
641
642
|
env:
|
|
642
643
|
DEVAUDIT_BASE_URL_VAR: ${{ vars.DEVAUDIT_BASE_URL }}
|
|
643
644
|
DEVAUDIT_API_KEY: ${{ secrets.DEVAUDIT_API_KEY }}
|
|
@@ -682,7 +683,7 @@ jobs:
|
|
|
682
683
|
name: e2e-regression-report
|
|
683
684
|
path: e2e-artifacts/
|
|
684
685
|
run-id: ${{ github.event.workflow_run.id }}
|
|
685
|
-
github-token: ${{
|
|
686
|
+
github-token: ${{ github.token }}
|
|
686
687
|
continue-on-error: true
|
|
687
688
|
|
|
688
689
|
- name: Derive release version
|
|
@@ -726,8 +727,9 @@ jobs:
|
|
|
726
727
|
FLAGS="${FLAGS} --test-cycle ${{ github.event.workflow_run.id }}"
|
|
727
728
|
|
|
728
729
|
# In-scope REQs from pending release tickets at the triggering
|
|
729
|
-
# SHA.
|
|
730
|
-
# the
|
|
730
|
+
# SHA. If the ticket was created after the triggering E2E run,
|
|
731
|
+
# recover REQs from the Playwright JSON artifact before falling
|
|
732
|
+
# back to `_compliance-docs` (DevAudit-Installer#311).
|
|
731
733
|
REQS=()
|
|
732
734
|
if [ -d compliance/pending-releases ]; then
|
|
733
735
|
for TICKET in compliance/pending-releases/RELEASE-TICKET-REQ-*.md; do
|
|
@@ -742,6 +744,47 @@ jobs:
|
|
|
742
744
|
REQS+=("$REQ_ID")
|
|
743
745
|
done
|
|
744
746
|
fi
|
|
747
|
+
|
|
748
|
+
if [ "${#REQS[@]}" -eq 0 ] && [ -f e2e-artifacts/e2e-regression-results.json ]; then
|
|
749
|
+
while IFS= read -r REQ_ID; do
|
|
750
|
+
[ -n "$REQ_ID" ] || continue
|
|
751
|
+
REQS+=("$REQ_ID")
|
|
752
|
+
done < <(python3 - <<'PY'
|
|
753
|
+
import json
|
|
754
|
+
import re
|
|
755
|
+
import sys
|
|
756
|
+
|
|
757
|
+
try:
|
|
758
|
+
with open('e2e-artifacts/e2e-regression-results.json', 'r', encoding='utf-8') as handle:
|
|
759
|
+
data = json.load(handle)
|
|
760
|
+
except Exception:
|
|
761
|
+
sys.exit(0)
|
|
762
|
+
|
|
763
|
+
seen = set()
|
|
764
|
+
|
|
765
|
+
def walk(value):
|
|
766
|
+
if isinstance(value, str):
|
|
767
|
+
for req in re.findall(r'REQ-[0-9]+', value):
|
|
768
|
+
seen.add(req)
|
|
769
|
+
elif isinstance(value, dict):
|
|
770
|
+
for item in value.values():
|
|
771
|
+
walk(item)
|
|
772
|
+
elif isinstance(value, list):
|
|
773
|
+
for item in value:
|
|
774
|
+
walk(item)
|
|
775
|
+
|
|
776
|
+
walk(data)
|
|
777
|
+
for req in sorted(seen):
|
|
778
|
+
print(req)
|
|
779
|
+
PY
|
|
780
|
+
)
|
|
781
|
+
fi
|
|
782
|
+
|
|
783
|
+
if [ "${#REQS[@]}" -eq 1 ] && echo "$DERIVED_RELEASE" | grep -qE '^v[0-9]{4}\.[0-9]{2}\.[0-9]{2}([.][0-9]+)?$'; then
|
|
784
|
+
echo "E2E upload recovered tracked release ${REQS[0]} from artifact metadata; overriding bare-date release ${DERIVED_RELEASE}."
|
|
785
|
+
DERIVED_RELEASE="${REQS[0]}"
|
|
786
|
+
fi
|
|
787
|
+
|
|
745
788
|
if [ "${#REQS[@]}" -eq 0 ]; then
|
|
746
789
|
REQS=(_compliance-docs)
|
|
747
790
|
fi
|
|
@@ -812,7 +855,9 @@ jobs:
|
|
|
812
855
|
- name: Triage and file incidents on E2E Regression failure
|
|
813
856
|
if: steps.resolve.outputs.skip != 'true' && github.event.workflow_run.conclusion == 'failure'
|
|
814
857
|
env:
|
|
815
|
-
|
|
858
|
+
# GitHub issue/label mutations use the workflow token. DevAudit
|
|
859
|
+
# user tokens are portal PATs, not guaranteed GitHub PATs (#305).
|
|
860
|
+
GH_TOKEN: ${{ github.token }}
|
|
816
861
|
CI_RUN_ID: ${{ github.event.workflow_run.id }}
|
|
817
862
|
WORKFLOW_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.event.workflow_run.id }}
|
|
818
863
|
GIT_SHA: ${{ github.event.workflow_run.head_sha }}
|
|
@@ -49,7 +49,9 @@ jobs:
|
|
|
49
49
|
- uses: actions/checkout@v6
|
|
50
50
|
with:
|
|
51
51
|
fetch-depth: 0
|
|
52
|
-
|
|
52
|
+
# GitHub repo mutations use the workflow token. The DevAudit
|
|
53
|
+
# user token is for portal attribution, not GitHub checkout/push.
|
|
54
|
+
token: ${{ github.token }}
|
|
53
55
|
|
|
54
56
|
- name: Classify routing (Path A vs Path B)
|
|
55
57
|
id: route
|
|
@@ -436,7 +438,8 @@ jobs:
|
|
|
436
438
|
- name: Direct commit to develop (Path A)
|
|
437
439
|
if: steps.route.outputs.path == 'A'
|
|
438
440
|
env:
|
|
439
|
-
|
|
441
|
+
# GitHub push uses the workflow token plus job permissions.
|
|
442
|
+
GH_TOKEN: ${{ github.token }}
|
|
440
443
|
OUT: ${{ steps.paths.outputs.out }}
|
|
441
444
|
ISSUE_NUMBER: ${{ github.event.issue.number }}
|
|
442
445
|
ISSUE_URL: ${{ github.event.issue.html_url }}
|
|
@@ -454,7 +457,8 @@ jobs:
|
|
|
454
457
|
- name: Open export PR (Path B)
|
|
455
458
|
if: steps.route.outputs.path == 'B'
|
|
456
459
|
env:
|
|
457
|
-
|
|
460
|
+
# GitHub branch push + PR creation use the workflow token.
|
|
461
|
+
GH_TOKEN: ${{ github.token }}
|
|
458
462
|
BRANCH: ${{ steps.paths.outputs.branch }}
|
|
459
463
|
OUT: ${{ steps.paths.outputs.out }}
|
|
460
464
|
ISSUE_NUMBER: ${{ github.event.issue.number }}
|
|
@@ -56,7 +56,9 @@ jobs:
|
|
|
56
56
|
- name: Enforce label retention
|
|
57
57
|
if: steps.opt.outputs.enabled == 'true'
|
|
58
58
|
env:
|
|
59
|
-
|
|
59
|
+
# GitHub issue/comment mutations use the workflow token. The
|
|
60
|
+
# DevAudit user token is reserved for portal attribution flows.
|
|
61
|
+
GH_TOKEN: ${{ github.token }}
|
|
60
62
|
EVENT_NAME: ${{ github.event_name }}
|
|
61
63
|
EVENT_ACTION: ${{ github.event.action }}
|
|
62
64
|
ISSUE_NUMBER: ${{ github.event.issue.number }}
|
|
@@ -37,8 +37,9 @@ jobs:
|
|
|
37
37
|
- uses: actions/checkout@v6
|
|
38
38
|
with:
|
|
39
39
|
fetch-depth: 0
|
|
40
|
-
# Need write access for the chore branch.
|
|
41
|
-
token
|
|
40
|
+
# Need write access for the chore branch. GitHub repo mutations
|
|
41
|
+
# use the workflow token; the DevAudit user token is portal auth.
|
|
42
|
+
token: ${{ github.token }}
|
|
42
43
|
|
|
43
44
|
- name: Compute review period
|
|
44
45
|
id: period
|
|
@@ -188,7 +189,8 @@ jobs:
|
|
|
188
189
|
|
|
189
190
|
- name: Open / update review PR
|
|
190
191
|
env:
|
|
191
|
-
|
|
192
|
+
# GitHub branch push + PR creation use the workflow token.
|
|
193
|
+
GH_TOKEN: ${{ github.token }}
|
|
192
194
|
BRANCH: ${{ steps.period.outputs.branch }}
|
|
193
195
|
REVIEW_ID: ${{ steps.period.outputs.review_id }}
|
|
194
196
|
run: |
|