@metasession.co/devaudit-cli 0.3.17 → 0.3.19
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 +36 -34
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/scripts/upload-evidence.sh +23 -19
- package/sdlc/files/_common/governance/incident-report.md.template +6 -1
- package/sdlc/files/_common/governance/nil-incident-report.md.template +5 -2
- package/sdlc/files/_common/scripts/check-host-deployment.sh +58 -8
- package/sdlc/files/_common/scripts/check-host-deployment.test.sh +35 -3
- package/sdlc/files/_common/scripts/check-release-pr-scope.sh +8 -1
- package/sdlc/files/_common/scripts/check-release-pr-scope.test.sh +61 -12
- package/sdlc/files/_common/scripts/check-self-hosted-runner.sh +103 -0
- package/sdlc/files/_common/scripts/check-self-hosted-runner.test.sh +74 -0
- package/sdlc/files/_common/scripts/derive-release-version.sh +7 -9
- package/sdlc/files/_common/scripts/derive-release-version.test.sh +18 -18
- package/sdlc/files/_common/scripts/generate-bundled-changes.sh +24 -3
- package/sdlc/files/_common/scripts/generate-bundled-changes.test.sh +9 -5
- package/sdlc/files/_common/scripts/reconcile-railway-deployment.sh +37 -0
- package/sdlc/files/_common/scripts/reconcile-railway-deployment.test.sh +30 -0
- package/sdlc/files/_common/scripts/record-uat-execution.sh +195 -0
- package/sdlc/files/_common/scripts/record-uat-execution.test.sh +144 -0
- package/sdlc/files/_common/scripts/render-test-executions.sh +146 -0
- package/sdlc/files/_common/scripts/{render-test-cycles.test.sh → render-test-executions.test.sh} +12 -18
- package/sdlc/files/_common/scripts/{report-test-cycle.sh → report-test-execution.sh} +66 -74
- package/sdlc/files/_common/scripts/report-test-execution.test.sh +177 -0
- package/sdlc/files/_common/scripts/submit-for-uat-review.sh +21 -0
- package/sdlc/files/_common/scripts/upload-evidence.sh +23 -19
- package/sdlc/files/_common/scripts/validate-commits.sh +6 -4
- package/sdlc/files/_common/scripts/validate-commits.test.sh +15 -0
- package/sdlc/files/_common/skills/e2e-test-engineer/SKILL.md +9 -7
- package/sdlc/files/_common/skills/e2e-test-engineer/references/e2e-regression-3-tier.yml +62 -10
- package/sdlc/files/_common/skills/sdlc-implementer/SKILL.md +19 -16
- package/sdlc/files/ci/ci.yml.template +86 -46
- package/sdlc/files/ci/compliance-evidence.yml.template +173 -49
- package/sdlc/files/ci/feature-e2e.yml.template +17 -17
- package/sdlc/files/ci/incident-export.yml.template +18 -4
- package/sdlc/files/ci/post-deploy-prod.yml.template +92 -48
- package/sdlc/files/ci/python/ci.yml.template +14 -14
- package/sdlc/files/ci/quality-gates-provenance.yml.template +3 -0
- package/sdlc/files/ci/reconcile-deployment.yml.template +59 -0
- package/sdlc/package.json +1 -1
- package/sdlc/src/blueprints/3-compile-evidence.raw.md +10 -10
- package/sdlc/src/blueprints/4-submit-for-review.raw.md +1 -1
- package/sdlc/src/blueprints/5-deploy-main.raw.md +1 -1
- package/sdlc/src/blueprints/implementing-an-sdlc-issue.raw.md +3 -3
- package/sdlc/files/_common/scripts/render-test-cycles.sh +0 -227
|
@@ -24,13 +24,11 @@
|
|
|
24
24
|
# -> empty/skip
|
|
25
25
|
# 5. Fallback: bare date -> v2026.05.17
|
|
26
26
|
#
|
|
27
|
-
#
|
|
28
|
-
#
|
|
29
|
-
#
|
|
30
|
-
#
|
|
31
|
-
#
|
|
32
|
-
# bare date — when exactly one ticket is open, attribute to it.
|
|
33
|
-
# Multiple open tickets stays ambiguous → bare-date fallback.
|
|
27
|
+
# A pending ticket or RTM row is not commit ownership. In particular, an
|
|
28
|
+
# independent main hotfix must not become part of an active UAT REQ merely
|
|
29
|
+
# because its mandatory back-merge lands while that REQ is the only pending
|
|
30
|
+
# ticket. The optional fallback below is disabled by default and may be used
|
|
31
|
+
# only for an explicitly reviewed integration exception.
|
|
34
32
|
#
|
|
35
33
|
# Note (DevAudit-Installer#220): `devaudit update` syncs now include
|
|
36
34
|
# `[skip ci]` in their commit message, so they no longer trigger CI
|
|
@@ -91,7 +89,7 @@ fi
|
|
|
91
89
|
# operator's explicit state says THIS is the in-flight release. Use it.
|
|
92
90
|
# Zero or multiple → ambiguous, fall through to the bare date.
|
|
93
91
|
# DevAudit-Installer#92.
|
|
94
|
-
if [ -d compliance/pending-releases ]; then
|
|
92
|
+
if [ "${DEVAUDIT_ALLOW_PENDING_TICKET_FALLBACK:-0}" = "1" ] && [ -d compliance/pending-releases ]; then
|
|
95
93
|
# NUL-delimited count so filenames with spaces don't trip us up.
|
|
96
94
|
TICKET_COUNT=$(find compliance/pending-releases -maxdepth 1 -type f \
|
|
97
95
|
-name 'RELEASE-TICKET-REQ-*.md' -print0 2>/dev/null \
|
|
@@ -113,7 +111,7 @@ fi
|
|
|
113
111
|
# multiple IN PROGRESS rows → ambiguous, fall through.
|
|
114
112
|
# DevAudit-Installer#95.
|
|
115
113
|
RTM_PATH="${RTM_PATH:-compliance/RTM.md}"
|
|
116
|
-
if [ -f "$RTM_PATH" ]; then
|
|
114
|
+
if [ "${DEVAUDIT_ALLOW_PENDING_TICKET_FALLBACK:-0}" = "1" ] && [ -f "$RTM_PATH" ]; then
|
|
117
115
|
# Match REQ rows whose status column starts with `IN PROGRESS`.
|
|
118
116
|
# `\|[[:space:]]+IN PROGRESS` requires a pipe followed by whitespace,
|
|
119
117
|
# so legend rows (`| \`IN PROGRESS\``) and prose mentions don't match.
|
|
@@ -111,12 +111,9 @@ make_fixture "$WORK/c8" "Merge pull request #7 from metasession-dev/feat/req-002
|
|
|
111
111
|
chore(deps): [REQ-002] dependency hardening — close R-001"
|
|
112
112
|
assert_eq "merge-commit body [REQ-002] -> REQ-002" "REQ-002" "$(run_helper)"
|
|
113
113
|
|
|
114
|
-
# Case 9
|
|
115
|
-
#
|
|
116
|
-
#
|
|
117
|
-
# Regression for REQ-051 / REQ-052 gate evidence landing on phantom date
|
|
118
|
-
# releases when a `chore: devaudit update to 0.1.x` commit landed between
|
|
119
|
-
# the feature merge and the release-PR open on wawagardenbar-app.
|
|
114
|
+
# Case 9: a pending ticket is not ownership of an unrelated commit. The
|
|
115
|
+
# default is a historical bare-date record; explicit operator exceptions may
|
|
116
|
+
# opt in with DEVAUDIT_ALLOW_PENDING_TICKET_FALLBACK=1.
|
|
120
117
|
make_fixture "$WORK/c9" "chore: devaudit update to 0.1.27"
|
|
121
118
|
mkdir -p compliance/pending-releases
|
|
122
119
|
cat > compliance/pending-releases/RELEASE-TICKET-REQ-051.md <<'TICKET'
|
|
@@ -124,7 +121,9 @@ cat > compliance/pending-releases/RELEASE-TICKET-REQ-051.md <<'TICKET'
|
|
|
124
121
|
|
|
125
122
|
**Status:** TESTED - PENDING SIGN-OFF
|
|
126
123
|
TICKET
|
|
127
|
-
assert_eq "chore: sync + single pending ticket
|
|
124
|
+
assert_eq "chore: sync + single pending ticket -> bare date $TODAY" "$TODAY" "$(run_helper)"
|
|
125
|
+
GOT=$(DEVAUDIT_ALLOW_PENDING_TICKET_FALLBACK=1 run_helper)
|
|
126
|
+
assert_eq "explicit pending-ticket exception -> REQ-051" "REQ-051" "$GOT"
|
|
128
127
|
|
|
129
128
|
# Case 10: a chore: sync commit with MULTIPLE pending tickets — ambiguous,
|
|
130
129
|
# stays at the bare-date fallback (don't guess between them).
|
|
@@ -154,11 +153,7 @@ cat > compliance/pending-releases/RELEASE-TICKET-REQ-051.md <<'TICKET'
|
|
|
154
153
|
TICKET
|
|
155
154
|
assert_eq "subject [REQ-099] beats pending REQ-051 -> REQ-099" "REQ-099" "$(run_helper)"
|
|
156
155
|
|
|
157
|
-
# Case 13
|
|
158
|
-
# no pending ticket, but RTM.md has exactly one IN PROGRESS row.
|
|
159
|
-
# Attribute to that REQ. Tests the zero-ceremony fallback that
|
|
160
|
-
# survives chore/docs/ci sync commits when no operator state file
|
|
161
|
-
# has been dropped.
|
|
156
|
+
# Case 13: an RTM IN PROGRESS row is likewise not automatic commit ownership.
|
|
162
157
|
make_fixture "$WORK/c13" "chore: devaudit update to 0.1.29"
|
|
163
158
|
mkdir -p compliance
|
|
164
159
|
cat > compliance/RTM.md <<'RTM'
|
|
@@ -168,7 +163,9 @@ cat > compliance/RTM.md <<'RTM'
|
|
|
168
163
|
| REQ-100 | #10 | LOW | compliance/evidence/REQ-100/ | APPROVED - DEPLOYED | dev | 2026-05-30 |
|
|
169
164
|
| REQ-101 | #11 | MED | compliance/evidence/REQ-101/ | IN PROGRESS | dev | 2026-06-01 |
|
|
170
165
|
RTM
|
|
171
|
-
assert_eq "RTM single IN PROGRESS row ->
|
|
166
|
+
assert_eq "RTM single IN PROGRESS row -> bare date $TODAY" "$TODAY" "$(run_helper)"
|
|
167
|
+
GOT=$(DEVAUDIT_ALLOW_PENDING_TICKET_FALLBACK=1 run_helper)
|
|
168
|
+
assert_eq "explicit RTM fallback exception -> REQ-101" "REQ-101" "$GOT"
|
|
172
169
|
|
|
173
170
|
# Case 14: step-4-bis ambiguity guard. Two IN PROGRESS rows → falls
|
|
174
171
|
# through to the bare date rather than guessing.
|
|
@@ -210,7 +207,8 @@ cat > compliance/RTM.md <<'RTM'
|
|
|
210
207
|
| ------- | ----- | ----------- | ---------------------------- | --------------------------------------------------------------------- | ---------- | ---------- |
|
|
211
208
|
| REQ-056 | #117 | MEDIUM-HIGH | compliance/evidence/REQ-056/ | IN PROGRESS (WhatsApp inbound-message router; many details follow...) | ostendo-io | 2026-06-01 |
|
|
212
209
|
RTM
|
|
213
|
-
|
|
210
|
+
GOT=$(DEVAUDIT_ALLOW_PENDING_TICKET_FALLBACK=1 run_helper)
|
|
211
|
+
assert_eq "explicit RTM long-status fallback -> REQ-056" "REQ-056" "$GOT"
|
|
214
212
|
|
|
215
213
|
# Case 17: step-4-bis must NOT win over a pending ticket on disk.
|
|
216
214
|
# Step 4 returns first.
|
|
@@ -224,7 +222,8 @@ cat > compliance/RTM.md <<'RTM'
|
|
|
224
222
|
| ------- | ----------- |
|
|
225
223
|
| REQ-302 | IN PROGRESS |
|
|
226
224
|
RTM
|
|
227
|
-
|
|
225
|
+
GOT=$(DEVAUDIT_ALLOW_PENDING_TICKET_FALLBACK=1 run_helper)
|
|
226
|
+
assert_eq "explicit pending ticket beats RTM fallback" "REQ-301" "$GOT"
|
|
228
227
|
|
|
229
228
|
# Case 18: step-4-bis respects RTM_PATH env override.
|
|
230
229
|
make_fixture "$WORK/c18" "chore: devaudit update to 0.1.29"
|
|
@@ -234,8 +233,8 @@ cat > docs/custom-RTM.md <<'RTM'
|
|
|
234
233
|
| ------- | ----------- |
|
|
235
234
|
| REQ-400 | IN PROGRESS |
|
|
236
235
|
RTM
|
|
237
|
-
GOT=$(RTM_PATH=docs/custom-RTM.md run_helper)
|
|
238
|
-
assert_eq "RTM_PATH
|
|
236
|
+
GOT=$(RTM_PATH=docs/custom-RTM.md DEVAUDIT_ALLOW_PENDING_TICKET_FALLBACK=1 run_helper)
|
|
237
|
+
assert_eq "explicit RTM_PATH fallback exception -> REQ-400" "REQ-400" "$GOT"
|
|
239
238
|
|
|
240
239
|
# Case 19: step-4-bis with escaped pipes (\|) in the Status column.
|
|
241
240
|
# The Status cell contains literal pipe characters escaped as \| (markdown
|
|
@@ -251,7 +250,8 @@ cat > compliance/RTM.md <<'RTM'
|
|
|
251
250
|
| REQ-056 | #117 | MEDIUM-HIGH | compliance/evidence/REQ-056/ | IN PROGRESS (regex: /^\s*(stop\|unsubscribe\|opt[-\s]?out)\s*$/i) | ostendo-io | 2026-06-01 |
|
|
252
251
|
| REQ-064 | #121 | MEDIUM | compliance/evidence/REQ-064/ | RELEASED (enum: open\|in_progress\|awaiting_customer\|resolved\|closed) | dev | 2026-06-02 |
|
|
253
252
|
RTM
|
|
254
|
-
|
|
253
|
+
GOT=$(DEVAUDIT_ALLOW_PENDING_TICKET_FALLBACK=1 run_helper)
|
|
254
|
+
assert_eq "explicit RTM escaped-pipe fallback -> REQ-056" "REQ-056" "$GOT"
|
|
255
255
|
|
|
256
256
|
# Case 20: step-4-bis with escaped pipes — ambiguity guard still works.
|
|
257
257
|
# Two IN PROGRESS rows, both with escaped pipes in status → falls through
|
|
@@ -250,8 +250,29 @@ for version in "${EXPLICIT_PREDECESSORS[@]}"; do
|
|
|
250
250
|
PREDECESSOR_LINES+=("- \`${version}\` (${role}/${relationship}) — ${title:-Untitled release ticket}")
|
|
251
251
|
done
|
|
252
252
|
|
|
253
|
-
|
|
254
|
-
|
|
253
|
+
# A tracked release only absorbs genuinely REQ-free housekeeping performed
|
|
254
|
+
# during its own implementation window. Do not recast earlier history, or a
|
|
255
|
+
# conventional commit explicitly owned by any REQ, as generic bundle work.
|
|
256
|
+
SCAN_FROM="$SINCE_REF"
|
|
257
|
+
if [[ "$VERSION" =~ ^REQ-[0-9]+$ ]]; then
|
|
258
|
+
FIRST_REQ_SHA="$(git log --reverse --format='%H' --grep="\\[${VERSION}\\]\\|Ref: ${VERSION}" 2>/dev/null | head -1 || true)"
|
|
259
|
+
if [ -n "$FIRST_REQ_SHA" ] && git rev-parse --verify "${FIRST_REQ_SHA}^" >/dev/null 2>&1; then
|
|
260
|
+
SCAN_FROM="${FIRST_REQ_SHA}^"
|
|
261
|
+
fi
|
|
262
|
+
fi
|
|
263
|
+
COMMITS="$(git log "$SCAN_FROM"..HEAD --format='%h%x09%s' 2>/dev/null || true)"
|
|
264
|
+
BUNDLED=""
|
|
265
|
+
while IFS=$'\t' read -r sha subject; do
|
|
266
|
+
[ -n "$sha" ] || continue
|
|
267
|
+
if ! printf '%s\t%s\n' "$sha" "$subject" | grep -Eq "$HOUSEKEEPING_TYPES"; then
|
|
268
|
+
continue
|
|
269
|
+
fi
|
|
270
|
+
if git log -1 --format='%s%n%b' "$sha" | grep -Eq '\[REQ-[0-9]+\]|Ref:[[:space:]]*REQ-[0-9]+'; then
|
|
271
|
+
continue
|
|
272
|
+
fi
|
|
273
|
+
BUNDLED+="${sha}"$'\t'"${subject}"$'\n'
|
|
274
|
+
done <<<"$COMMITS"
|
|
275
|
+
BUNDLED="${BUNDLED%$'\n'}"
|
|
255
276
|
NON_RELEASE_ITEMS='[]'
|
|
256
277
|
NON_RELEASE_LINES=()
|
|
257
278
|
if [ -n "$BUNDLED" ]; then
|
|
@@ -324,7 +345,7 @@ echo "- **Why bundled here:** Explicit predecessor release tickets and non-relea
|
|
|
324
345
|
echo "- **Evidence impact:** Evidence ownership remains on the source releases; the bundle manifest provides lineage and inherited visibility only."
|
|
325
346
|
echo "- **Reviewer impact:** Approval scope includes the core tracked release plus the explicit predecessor releases and non-release work listed below."
|
|
326
347
|
echo "- **Security / risk impact:** No additional security/risk impact identified automatically; reviewer must confirm in the canonical release artifacts."
|
|
327
|
-
echo "- **Reference:** commit range \`${
|
|
348
|
+
echo "- **Reference:** commit range \`${SCAN_FROM}..HEAD\`"
|
|
328
349
|
echo ""
|
|
329
350
|
|
|
330
351
|
echo "### Explicit Constituent Releases"
|
|
@@ -109,25 +109,29 @@ assert_file_exists() {
|
|
|
109
109
|
fi
|
|
110
110
|
}
|
|
111
111
|
|
|
112
|
-
# Test 1: housekeeping
|
|
113
|
-
echo "Test 1:
|
|
112
|
+
# Test 1: only REQ-free housekeeping inside the tracked REQ window is bundled.
|
|
113
|
+
echo "Test 1: release ownership excludes tagged and historical work"
|
|
114
114
|
DIR1="$TMPDIR_BASE/test1"
|
|
115
115
|
make_fixture "$DIR1"
|
|
116
116
|
add_commit "chore: sync DevAudit templates from v0.1.69 to v0.1.70 [skip ci]"
|
|
117
117
|
add_commit "docs: update API reference for /bookings endpoint"
|
|
118
118
|
add_commit "feat: add booking widget [REQ-042]"
|
|
119
|
+
add_commit "test: verify booking widget [REQ-042]"
|
|
119
120
|
add_commit "chore(deps): bump eslint 9.0.5 to 9.0.6"
|
|
121
|
+
add_commit "docs: document booking widget\n\nRef: REQ-042"
|
|
120
122
|
add_commit "fix: resolve null pointer in booking service [REQ-042]"
|
|
121
123
|
SINCE=$(git rev-list --max-parents=0 HEAD)
|
|
122
124
|
JSON_OUT="$DIR1/bundle.json"
|
|
123
125
|
OUTPUT=$(bash "$HELPER" "$SINCE" "REQ-042" --json-out "$JSON_OUT" 2>&1)
|
|
124
|
-
assert_contains "markdown includes chore commit" "chore: sync DevAudit templates" "$OUTPUT"
|
|
125
|
-
assert_contains "markdown includes docs commit" "docs: update API reference" "$OUTPUT"
|
|
126
126
|
assert_contains "markdown includes scoped chore commit" "chore(deps): bump eslint" "$OUTPUT"
|
|
127
|
+
assert_not_contains "markdown excludes historical chore commit" "chore: sync DevAudit templates" "$OUTPUT"
|
|
128
|
+
assert_not_contains "markdown excludes historical docs commit" "docs: update API reference" "$OUTPUT"
|
|
129
|
+
assert_not_contains "markdown excludes tagged test commit" "test: verify booking widget" "$OUTPUT"
|
|
130
|
+
assert_not_contains "markdown excludes Ref-tagged docs commit" "docs: document booking widget" "$OUTPUT"
|
|
127
131
|
assert_not_contains "markdown excludes feat commit" "feat: add booking widget" "$OUTPUT"
|
|
128
132
|
assert_not_contains "markdown excludes fix commit" "fix: resolve null pointer" "$OUTPUT"
|
|
129
133
|
assert_file_exists "json manifest emitted" "$JSON_OUT"
|
|
130
|
-
assert_eq "non-release work count" "
|
|
134
|
+
assert_eq "non-release work count" "1" "$(jq -r '.nonReleaseWorkItems | length' "$JSON_OUT")"
|
|
131
135
|
assert_eq "member count with no tickets" "0" "$(jq -r '.members | length' "$JSON_OUT")"
|
|
132
136
|
assert_eq "first housekeeping kind" "housekeeping_commit" "$(jq -r '.nonReleaseWorkItems[0].kind' "$JSON_OUT")"
|
|
133
137
|
echo
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Reconcile a verified Railway recovery deployment to its GitHub deployment.
|
|
3
|
+
set -euo pipefail
|
|
4
|
+
|
|
5
|
+
REPO=""; SHA=""; GITHUB_DEPLOYMENT_ID=""; RAILWAY_DEPLOYMENT_ID=""
|
|
6
|
+
RAILWAY_PROJECT=""; RAILWAY_ENVIRONMENT="production"; RAILWAY_SERVICE=""; HEALTH_URL=""
|
|
7
|
+
for arg in "$@"; do
|
|
8
|
+
case "$arg" in
|
|
9
|
+
--repo=*) REPO="${arg#*=}" ;; --sha=*) SHA="${arg#*=}" ;;
|
|
10
|
+
--github-deployment-id=*) GITHUB_DEPLOYMENT_ID="${arg#*=}" ;;
|
|
11
|
+
--railway-deployment-id=*) RAILWAY_DEPLOYMENT_ID="${arg#*=}" ;;
|
|
12
|
+
--railway-project=*) RAILWAY_PROJECT="${arg#*=}" ;;
|
|
13
|
+
--railway-environment=*) RAILWAY_ENVIRONMENT="${arg#*=}" ;;
|
|
14
|
+
--railway-service=*) RAILWAY_SERVICE="${arg#*=}" ;;
|
|
15
|
+
--health-url=*) HEALTH_URL="${arg#*=}" ;;
|
|
16
|
+
*) echo "Unknown argument: $arg" >&2; exit 2 ;;
|
|
17
|
+
esac
|
|
18
|
+
done
|
|
19
|
+
[ -n "$REPO" ] && [ -n "$SHA" ] && [ -n "$GITHUB_DEPLOYMENT_ID" ] && [ -n "$RAILWAY_DEPLOYMENT_ID" ] && [ -n "$RAILWAY_PROJECT" ] && [ -n "$RAILWAY_SERVICE" ] && [ -n "$HEALTH_URL" ] || { echo "Missing required reconciliation arguments" >&2; exit 2; }
|
|
20
|
+
|
|
21
|
+
DEPLOYMENTS=$(railway deployment list --json --project "$RAILWAY_PROJECT" --environment "$RAILWAY_ENVIRONMENT" --service "$RAILWAY_SERVICE" --limit 100)
|
|
22
|
+
MATCH=$(jq -cer --arg id "$RAILWAY_DEPLOYMENT_ID" '.[] | select(.id == $id)' <<<"$DEPLOYMENTS") || { echo "::error::Railway deployment ${RAILWAY_DEPLOYMENT_ID} was not found."; exit 1; }
|
|
23
|
+
STATUS=$(jq -r '.status // empty' <<<"$MATCH")
|
|
24
|
+
PROVIDER_SHA=$(jq -r '.meta.commitHash // empty' <<<"$MATCH")
|
|
25
|
+
BRANCH=$(jq -r '.meta.branch // empty' <<<"$MATCH")
|
|
26
|
+
PROVIDER_REPO=$(jq -r '.meta.repo // empty' <<<"$MATCH")
|
|
27
|
+
[ "$STATUS" = "SUCCESS" ] || { echo "::error::Railway deployment is not terminal-successful: ${STATUS:-missing}"; exit 1; }
|
|
28
|
+
[ "$PROVIDER_SHA" = "$SHA" ] && [ "$BRANCH" = "main" ] && [ "$PROVIDER_REPO" = "$REPO" ] || { echo "::error::Railway provenance mismatch: repo=$PROVIDER_REPO branch=$BRANCH sha=$PROVIDER_SHA"; exit 1; }
|
|
29
|
+
HTTP=$(curl -sS -o /dev/null -w '%{http_code}' --connect-timeout 5 --max-time 15 "$HEALTH_URL" || echo 000)
|
|
30
|
+
[[ "$HTTP" =~ ^[23][0-9][0-9]$ ]] || { echo "::error::Health probe failed for ${HEALTH_URL}: HTTP ${HTTP}"; exit 1; }
|
|
31
|
+
DESCRIPTION="manual_reconciliation; railway_deployment=${RAILWAY_DEPLOYMENT_ID}; sha=${SHA}; health_http=${HTTP}; verified_at=$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
|
32
|
+
EXISTING=$(gh api "/repos/${REPO}/deployments/${GITHUB_DEPLOYMENT_ID}/statuses?per_page=20" --jq '[.[] | select(.state == "success" and (.description // "" | contains("railway_deployment='"$RAILWAY_DEPLOYMENT_ID"'")))] | length')
|
|
33
|
+
if [ "$EXISTING" = "0" ]; then
|
|
34
|
+
gh api --method POST "/repos/${REPO}/deployments/${GITHUB_DEPLOYMENT_ID}/statuses" -f state=success -f environment=production -f description="$DESCRIPTION" -f environment_url="$HEALTH_URL" >/dev/null
|
|
35
|
+
fi
|
|
36
|
+
jq -n --arg provenance manual_reconciliation --arg provider railway --arg providerDeploymentId "$RAILWAY_DEPLOYMENT_ID" --arg sha "$SHA" --arg healthUrl "$HEALTH_URL" --arg healthHttp "$HTTP" --arg actor "${GITHUB_ACTOR:-operator}" --arg verifiedAt "$(date -u +%Y-%m-%dT%H:%M:%SZ)" '{provenance:$provenance,provider:$provider,providerDeploymentId:$providerDeploymentId,sha:$sha,healthUrl:$healthUrl,healthHttp:($healthHttp|tonumber),actor:$actor,verifiedAt:$verifiedAt}' > deployment-reconciliation.json
|
|
37
|
+
echo "Verified Railway deployment ${RAILWAY_DEPLOYMENT_ID}; GitHub deployment ${GITHUB_DEPLOYMENT_ID} reconciled."
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
ROOT="$(cd "$(dirname "$0")" && pwd)"; HELPER="$ROOT/reconcile-railway-deployment.sh"
|
|
4
|
+
WORK="$(mktemp -d)"; trap 'rm -rf "$WORK"' EXIT; PASS=0; FAIL=0
|
|
5
|
+
ok(){ echo " ok $1"; PASS=$((PASS+1)); }; no(){ echo " FAIL $1"; FAIL=$((FAIL+1)); }
|
|
6
|
+
mock() { mkdir -p "$1/bin"; cat >"$1/bin/railway" <<'EOF'
|
|
7
|
+
#!/usr/bin/env bash
|
|
8
|
+
printf '%s' "$MOCK_RAILWAY_JSON"
|
|
9
|
+
EOF
|
|
10
|
+
cat >"$1/bin/curl" <<'EOF'
|
|
11
|
+
#!/usr/bin/env bash
|
|
12
|
+
printf '%s' "${MOCK_HTTP:-200}"
|
|
13
|
+
EOF
|
|
14
|
+
cat >"$1/bin/gh" <<'EOF'
|
|
15
|
+
#!/usr/bin/env bash
|
|
16
|
+
if [[ "$*" == *"--jq"* ]]; then printf '%s' "${MOCK_EXISTING:-[]}"; else echo "$*" >> "$MOCK_GH_LOG"; fi
|
|
17
|
+
EOF
|
|
18
|
+
chmod +x "$1/bin/"*; }
|
|
19
|
+
run(){ local d="$1"; shift; (cd "$d"; PATH="$d/bin:$PATH" MOCK_GH_LOG="$d/gh.log" "$@" bash "$HELPER" --repo=org/app --sha=abc --github-deployment-id=42 --railway-deployment-id=r1 --railway-project=p --railway-service=s --health-url=https://health.test); }
|
|
20
|
+
mock "$WORK"
|
|
21
|
+
GOOD='[{"id":"r1","status":"SUCCESS","meta":{"repo":"org/app","branch":"main","commitHash":"abc"}}]'
|
|
22
|
+
if run "$WORK" env MOCK_RAILWAY_JSON="$GOOD"; then ok valid; else no valid; fi
|
|
23
|
+
grep -q '"provenance": "manual_reconciliation"' "$WORK/deployment-reconciliation.json" && ok provenance_evidence || no provenance_evidence
|
|
24
|
+
if run "$WORK" env MOCK_RAILWAY_JSON='[{"id":"r1","status":"SUCCESS","meta":{"repo":"org/app","branch":"main","commitHash":"wrong"}}]'; then no sha_mismatch; else ok sha_mismatch; fi
|
|
25
|
+
if run "$WORK" env MOCK_RAILWAY_JSON='[{"id":"r1","status":"FAILED","meta":{"repo":"org/app","branch":"main","commitHash":"abc"}}]'; then no provider_failure; else ok provider_failure; fi
|
|
26
|
+
if run "$WORK" env MOCK_RAILWAY_JSON="$GOOD" MOCK_HTTP=500; then no health_failure; else ok health_failure; fi
|
|
27
|
+
rm -f "$WORK/gh.log"
|
|
28
|
+
if run "$WORK" env MOCK_RAILWAY_JSON="$GOOD" MOCK_EXISTING='[{"state":"success","description":"railway_deployment=r1"}]'; then ok idempotent; else no idempotent; fi
|
|
29
|
+
[ ! -f "$WORK/gh.log" ] && ok no_duplicate_post || no no_duplicate_post
|
|
30
|
+
echo "$PASS passed, $FAIL failed"; [ "$FAIL" -eq 0 ]
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# record-uat-execution.sh - Record an explicit Stage 4 UAT execution.
|
|
3
|
+
#
|
|
4
|
+
# This script does not submit, approve, or release anything. It records the
|
|
5
|
+
# manual UAT execution that happened while the release was under UAT review.
|
|
6
|
+
#
|
|
7
|
+
# Usage:
|
|
8
|
+
# ./scripts/record-uat-execution.sh \
|
|
9
|
+
# --project-slug <slug> \
|
|
10
|
+
# --release <REQ-XXX|version> \
|
|
11
|
+
# --outcome <passed|failed|cancelled|skipped|timed_out|action_required> \
|
|
12
|
+
# --executor <identity> \
|
|
13
|
+
# [--tested-sha <sha>] \
|
|
14
|
+
# [--build-version <version>] \
|
|
15
|
+
# [--checklist-ref <path-or-url>] \
|
|
16
|
+
# [--evidence-ref <path-or-url>] \
|
|
17
|
+
# [--remediation-ref <issue-or-url>] \
|
|
18
|
+
# [--executed-at <iso8601>] \
|
|
19
|
+
# [--execution-id <stable-id>] \
|
|
20
|
+
# [--output-file <path>]
|
|
21
|
+
#
|
|
22
|
+
# Required environment:
|
|
23
|
+
# DEVAUDIT_BASE_URL
|
|
24
|
+
# DEVAUDIT_API_KEY
|
|
25
|
+
|
|
26
|
+
set -euo pipefail
|
|
27
|
+
|
|
28
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
29
|
+
REPORT_TEST_EXECUTION="${REPORT_TEST_EXECUTION_HELPER:-${SCRIPT_DIR}/report-test-execution.sh}"
|
|
30
|
+
|
|
31
|
+
usage() {
|
|
32
|
+
sed -n '1,38p' "$0" >&2
|
|
33
|
+
exit 1
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
PROJECT_SLUG=""
|
|
37
|
+
RELEASE_VERSION=""
|
|
38
|
+
OUTCOME=""
|
|
39
|
+
EXECUTOR=""
|
|
40
|
+
TESTED_SHA="${GITHUB_SHA:-}"
|
|
41
|
+
BUILD_VERSION=""
|
|
42
|
+
CHECKLIST_REF=""
|
|
43
|
+
EVIDENCE_REF=""
|
|
44
|
+
REMEDIATION_REF=""
|
|
45
|
+
EXECUTED_AT=""
|
|
46
|
+
EXECUTION_ID=""
|
|
47
|
+
OUTPUT_FILE=""
|
|
48
|
+
|
|
49
|
+
while [ "$#" -gt 0 ]; do
|
|
50
|
+
case "$1" in
|
|
51
|
+
--project-slug) PROJECT_SLUG="${2:-}"; shift 2 ;;
|
|
52
|
+
--release) RELEASE_VERSION="${2:-}"; shift 2 ;;
|
|
53
|
+
--outcome) OUTCOME="${2:-}"; shift 2 ;;
|
|
54
|
+
--executor) EXECUTOR="${2:-}"; shift 2 ;;
|
|
55
|
+
--tested-sha) TESTED_SHA="${2:-}"; shift 2 ;;
|
|
56
|
+
--build-version) BUILD_VERSION="${2:-}"; shift 2 ;;
|
|
57
|
+
--checklist-ref) CHECKLIST_REF="${2:-}"; shift 2 ;;
|
|
58
|
+
--evidence-ref) EVIDENCE_REF="${2:-}"; shift 2 ;;
|
|
59
|
+
--remediation-ref) REMEDIATION_REF="${2:-}"; shift 2 ;;
|
|
60
|
+
--executed-at) EXECUTED_AT="${2:-}"; shift 2 ;;
|
|
61
|
+
--execution-id) EXECUTION_ID="${2:-}"; shift 2 ;;
|
|
62
|
+
--output-file) OUTPUT_FILE="${2:-}"; shift 2 ;;
|
|
63
|
+
-h|--help) usage ;;
|
|
64
|
+
*) echo "Unknown option: $1" >&2; usage ;;
|
|
65
|
+
esac
|
|
66
|
+
done
|
|
67
|
+
|
|
68
|
+
require_arg() {
|
|
69
|
+
local value="$1" name="$2"
|
|
70
|
+
if [ -z "$value" ]; then
|
|
71
|
+
echo "Error: ${name} is required" >&2
|
|
72
|
+
exit 1
|
|
73
|
+
fi
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
require_arg "$PROJECT_SLUG" "--project-slug"
|
|
77
|
+
require_arg "$RELEASE_VERSION" "--release"
|
|
78
|
+
require_arg "$OUTCOME" "--outcome"
|
|
79
|
+
require_arg "$EXECUTOR" "--executor"
|
|
80
|
+
|
|
81
|
+
case "$OUTCOME" in
|
|
82
|
+
passed|failed|cancelled|skipped|timed_out|action_required) ;;
|
|
83
|
+
*)
|
|
84
|
+
echo "Error: --outcome must be passed, failed, cancelled, skipped, timed_out, or action_required" >&2
|
|
85
|
+
exit 1
|
|
86
|
+
;;
|
|
87
|
+
esac
|
|
88
|
+
if [ "$OUTCOME" = "failed" ] && [ -z "$REMEDIATION_REF" ]; then
|
|
89
|
+
echo "Error: --remediation-ref is required when --outcome failed" >&2
|
|
90
|
+
exit 1
|
|
91
|
+
fi
|
|
92
|
+
|
|
93
|
+
if [ -z "${DEVAUDIT_BASE_URL:-}" ]; then
|
|
94
|
+
echo "Error: DEVAUDIT_BASE_URL environment variable is required" >&2
|
|
95
|
+
exit 1
|
|
96
|
+
fi
|
|
97
|
+
if [ -z "${DEVAUDIT_API_KEY:-}" ]; then
|
|
98
|
+
echo "Error: DEVAUDIT_API_KEY environment variable is required" >&2
|
|
99
|
+
exit 1
|
|
100
|
+
fi
|
|
101
|
+
if [ ! -x "$REPORT_TEST_EXECUTION" ]; then
|
|
102
|
+
echo "Error: report-test-execution helper not found or not executable: $REPORT_TEST_EXECUTION" >&2
|
|
103
|
+
exit 1
|
|
104
|
+
fi
|
|
105
|
+
|
|
106
|
+
if [ -z "$TESTED_SHA" ] && git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
|
|
107
|
+
TESTED_SHA="$(git rev-parse HEAD)"
|
|
108
|
+
fi
|
|
109
|
+
if [ -z "$TESTED_SHA" ]; then
|
|
110
|
+
echo "Error: --tested-sha is required when GITHUB_SHA is unset and git HEAD cannot be resolved" >&2
|
|
111
|
+
exit 1
|
|
112
|
+
fi
|
|
113
|
+
|
|
114
|
+
iso_now() {
|
|
115
|
+
date -u +"%Y-%m-%dT%H:%M:%SZ"
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
hash_value() {
|
|
119
|
+
if command -v sha256sum >/dev/null 2>&1; then
|
|
120
|
+
printf '%s' "$1" | sha256sum | awk '{print $1}'
|
|
121
|
+
else
|
|
122
|
+
printf '%s' "$1" | shasum -a 256 | awk '{print $1}'
|
|
123
|
+
fi
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
sanitize_id_part() {
|
|
127
|
+
printf '%s' "$1" | tr -cs 'A-Za-z0-9._:-' '-'
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
[ -n "$EXECUTED_AT" ] || EXECUTED_AT="$(iso_now)"
|
|
131
|
+
|
|
132
|
+
if [ -z "$EXECUTION_ID" ]; then
|
|
133
|
+
KEY_SOURCE="${PROJECT_SLUG}|${RELEASE_VERSION}|${TESTED_SHA}|${BUILD_VERSION}|${EXECUTOR}|${OUTCOME}|${CHECKLIST_REF}|${EVIDENCE_REF}|${REMEDIATION_REF}"
|
|
134
|
+
KEY_HASH="$(hash_value "$KEY_SOURCE" | cut -c1-16)"
|
|
135
|
+
EXECUTION_ID="manual-uat:$(sanitize_id_part "$RELEASE_VERSION"):$(sanitize_id_part "$TESTED_SHA" | cut -c1-12):${KEY_HASH}"
|
|
136
|
+
fi
|
|
137
|
+
|
|
138
|
+
IDEMPOTENCY_KEY="manual-uat:${PROJECT_SLUG}:${RELEASE_VERSION}:${EXECUTION_ID}"
|
|
139
|
+
OUTCOME_REASON="executor=${EXECUTOR}; tested_sha=${TESTED_SHA}"
|
|
140
|
+
[ -z "$BUILD_VERSION" ] || OUTCOME_REASON="${OUTCOME_REASON}; build=${BUILD_VERSION}"
|
|
141
|
+
[ -z "$CHECKLIST_REF" ] || OUTCOME_REASON="${OUTCOME_REASON}; checklist=${CHECKLIST_REF}"
|
|
142
|
+
[ -z "$EVIDENCE_REF" ] || OUTCOME_REASON="${OUTCOME_REASON}; evidence=${EVIDENCE_REF}"
|
|
143
|
+
[ -z "$REMEDIATION_REF" ] || OUTCOME_REASON="${OUTCOME_REASON}; remediation=${REMEDIATION_REF}"
|
|
144
|
+
|
|
145
|
+
START_OUTPUT="$(mktemp)"
|
|
146
|
+
COMPLETE_OUTPUT="$(mktemp)"
|
|
147
|
+
cleanup() {
|
|
148
|
+
rm -f "$START_OUTPUT" "$COMPLETE_OUTPUT"
|
|
149
|
+
}
|
|
150
|
+
trap cleanup EXIT
|
|
151
|
+
|
|
152
|
+
COMMON_ARGS=(
|
|
153
|
+
--project-slug "$PROJECT_SLUG"
|
|
154
|
+
--release "$RELEASE_VERSION"
|
|
155
|
+
--sdlc-stage 4
|
|
156
|
+
--environment uat
|
|
157
|
+
--suite-kind uat
|
|
158
|
+
--iteration-key "uat:${RELEASE_VERSION}:${TESTED_SHA}"
|
|
159
|
+
--provider manual_uat
|
|
160
|
+
--external-run-id "$EXECUTION_ID"
|
|
161
|
+
--external-job-id "manual-uat-execution"
|
|
162
|
+
--commit-sha "$TESTED_SHA"
|
|
163
|
+
--branch "uat"
|
|
164
|
+
--workflow-name "Manual UAT execution"
|
|
165
|
+
--workflow-url "${DEVAUDIT_BASE_URL%/}/projects/${PROJECT_SLUG}/requirements/${RELEASE_VERSION}"
|
|
166
|
+
--idempotency-key "$IDEMPOTENCY_KEY"
|
|
167
|
+
--started-at "$EXECUTED_AT"
|
|
168
|
+
)
|
|
169
|
+
COMPLETE_ARGS=(
|
|
170
|
+
"${COMMON_ARGS[@]}"
|
|
171
|
+
--completed-at "$EXECUTED_AT"
|
|
172
|
+
--outcome "$OUTCOME"
|
|
173
|
+
--outcome-reason "$OUTCOME_REASON"
|
|
174
|
+
)
|
|
175
|
+
if [ -n "$REMEDIATION_REF" ]; then
|
|
176
|
+
COMPLETE_ARGS+=(--incident-reference "$REMEDIATION_REF")
|
|
177
|
+
fi
|
|
178
|
+
|
|
179
|
+
"$REPORT_TEST_EXECUTION" start "${COMMON_ARGS[@]}" --output-file "$START_OUTPUT"
|
|
180
|
+
"$REPORT_TEST_EXECUTION" complete "${COMPLETE_ARGS[@]}" --output-file "$COMPLETE_OUTPUT"
|
|
181
|
+
|
|
182
|
+
if [ -n "$OUTPUT_FILE" ]; then
|
|
183
|
+
{
|
|
184
|
+
printf 'uat_execution_id=%s\n' "$EXECUTION_ID"
|
|
185
|
+
printf 'uat_idempotency_key=%s\n' "$IDEMPOTENCY_KEY"
|
|
186
|
+
printf 'uat_outcome=%s\n' "$OUTCOME"
|
|
187
|
+
printf 'uat_executed_at=%s\n' "$EXECUTED_AT"
|
|
188
|
+
grep '^execution_' "$COMPLETE_OUTPUT" || true
|
|
189
|
+
} >> "$OUTPUT_FILE"
|
|
190
|
+
fi
|
|
191
|
+
|
|
192
|
+
echo "Recorded Stage 4 UAT execution for ${RELEASE_VERSION}: ${OUTCOME}"
|
|
193
|
+
echo " Execution ID: ${EXECUTION_ID}"
|
|
194
|
+
echo " Tested SHA: ${TESTED_SHA}"
|
|
195
|
+
echo " Executor: ${EXECUTOR}"
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# record-uat-execution.test.sh - Regression tests for the manual UAT execution helper.
|
|
3
|
+
|
|
4
|
+
set -euo pipefail
|
|
5
|
+
|
|
6
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
7
|
+
HELPER="$SCRIPT_DIR/record-uat-execution.sh"
|
|
8
|
+
[ -x "$HELPER" ] || chmod +x "$HELPER"
|
|
9
|
+
|
|
10
|
+
PASS=0
|
|
11
|
+
FAIL=0
|
|
12
|
+
|
|
13
|
+
ok() { echo " PASS: $1"; PASS=$((PASS + 1)); }
|
|
14
|
+
no() { echo " FAIL: $1"; FAIL=$((FAIL + 1)); }
|
|
15
|
+
|
|
16
|
+
make_fixture() {
|
|
17
|
+
local dir="$1"
|
|
18
|
+
rm -rf "$dir"
|
|
19
|
+
mkdir -p "$dir/bin"
|
|
20
|
+
cat > "$dir/bin/report-test-execution.sh" <<'EOF'
|
|
21
|
+
#!/usr/bin/env bash
|
|
22
|
+
set -euo pipefail
|
|
23
|
+
LOG_FILE="${UAT_TEST_LOG:?}"
|
|
24
|
+
printf 'CALL:%s\n' "$*" >> "$LOG_FILE"
|
|
25
|
+
mode="$1"
|
|
26
|
+
shift
|
|
27
|
+
out=""
|
|
28
|
+
while [ "$#" -gt 0 ]; do
|
|
29
|
+
case "$1" in
|
|
30
|
+
--output-file) out="$2"; shift 2 ;;
|
|
31
|
+
*) shift ;;
|
|
32
|
+
esac
|
|
33
|
+
done
|
|
34
|
+
if [ -n "$out" ]; then
|
|
35
|
+
{
|
|
36
|
+
printf 'execution_supported=true\n'
|
|
37
|
+
printf 'execution_record_id=execution-%s\n' "$mode"
|
|
38
|
+
printf 'execution_idempotency_key=stub\n'
|
|
39
|
+
} >> "$out"
|
|
40
|
+
fi
|
|
41
|
+
EOF
|
|
42
|
+
chmod +x "$dir/bin/report-test-execution.sh"
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
run_helper() {
|
|
46
|
+
UAT_TEST_LOG="$1/uat.log" \
|
|
47
|
+
DEVAUDIT_BASE_URL="https://devaudit.example.test" \
|
|
48
|
+
DEVAUDIT_API_KEY="mc_test_dummy" \
|
|
49
|
+
REPORT_TEST_EXECUTION_HELPER="$1/bin/report-test-execution.sh" \
|
|
50
|
+
"$HELPER" "${@:2}"
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
assert_contains() {
|
|
54
|
+
local label="$1" needle="$2" file="$3"
|
|
55
|
+
if grep -Fq -- "$needle" "$file"; then ok "$label"; else no "$label"; fi
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
echo "=== record-uat-execution.sh tests ==="
|
|
59
|
+
|
|
60
|
+
case_passed_records_stage_four_pair() {
|
|
61
|
+
echo "case: passed UAT records Stage 4 start and complete"
|
|
62
|
+
local dir out exit_code
|
|
63
|
+
dir=$(mktemp -d)
|
|
64
|
+
make_fixture "$dir"
|
|
65
|
+
out="$dir/out.env"
|
|
66
|
+
run_helper "$dir" \
|
|
67
|
+
--project-slug fixture-app \
|
|
68
|
+
--release REQ-100 \
|
|
69
|
+
--outcome passed \
|
|
70
|
+
--executor "Jane Reviewer" \
|
|
71
|
+
--tested-sha abc123def456 \
|
|
72
|
+
--build-version build-42 \
|
|
73
|
+
--checklist-ref compliance/evidence/REQ-100/uat-checklist.md \
|
|
74
|
+
--evidence-ref https://devaudit.example.test/projects/fixture-app/releases/release-1 \
|
|
75
|
+
--executed-at 2026-07-20T12:00:00Z \
|
|
76
|
+
--output-file "$out" >"$dir/stdout.log" 2>"$dir/stderr.log" && exit_code=0 || exit_code=$?
|
|
77
|
+
if [ "$exit_code" -eq 0 ]; then ok "exit code 0"; else no "expected exit 0"; fi
|
|
78
|
+
assert_contains "called start" "CALL:start" "$dir/uat.log"
|
|
79
|
+
assert_contains "called complete" "CALL:complete" "$dir/uat.log"
|
|
80
|
+
assert_contains "stage 4" "--sdlc-stage 4" "$dir/uat.log"
|
|
81
|
+
assert_contains "uat environment" "--environment uat" "$dir/uat.log"
|
|
82
|
+
assert_contains "uat kind" "--suite-kind uat" "$dir/uat.log"
|
|
83
|
+
assert_contains "manual provider" "--provider manual_uat" "$dir/uat.log"
|
|
84
|
+
assert_contains "tested sha" "--commit-sha abc123def456" "$dir/uat.log"
|
|
85
|
+
assert_contains "passed outcome" "--outcome passed" "$dir/uat.log"
|
|
86
|
+
assert_contains "executor provenance" "executor=Jane Reviewer" "$dir/uat.log"
|
|
87
|
+
if grep -q '^uat_execution_id=' "$out"; then ok "output execution id"; else no "missing output execution id"; fi
|
|
88
|
+
rm -rf "$dir"
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
case_idempotency_is_stable() {
|
|
92
|
+
echo "case: derived execution id is stable for repeat command"
|
|
93
|
+
local dir out1 out2 id1 id2
|
|
94
|
+
dir=$(mktemp -d)
|
|
95
|
+
make_fixture "$dir"
|
|
96
|
+
out1="$dir/out1.env"
|
|
97
|
+
out2="$dir/out2.env"
|
|
98
|
+
run_helper "$dir" --project-slug fixture-app --release REQ-100 --outcome passed --executor "Jane Reviewer" --tested-sha abc123def456 --executed-at 2026-07-20T12:00:00Z --output-file "$out1" >/dev/null
|
|
99
|
+
run_helper "$dir" --project-slug fixture-app --release REQ-100 --outcome passed --executor "Jane Reviewer" --tested-sha abc123def456 --executed-at 2026-07-20T13:00:00Z --output-file "$out2" >/dev/null
|
|
100
|
+
id1=$(grep '^uat_idempotency_key=' "$out1")
|
|
101
|
+
id2=$(grep '^uat_idempotency_key=' "$out2")
|
|
102
|
+
if [ "$id1" = "$id2" ]; then ok "stable idempotency key"; else no "idempotency key changed"; fi
|
|
103
|
+
rm -rf "$dir"
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
case_failed_records_remediation() {
|
|
107
|
+
echo "case: failed UAT includes remediation reference"
|
|
108
|
+
local dir
|
|
109
|
+
dir=$(mktemp -d)
|
|
110
|
+
make_fixture "$dir"
|
|
111
|
+
run_helper "$dir" \
|
|
112
|
+
--project-slug fixture-app \
|
|
113
|
+
--release REQ-100 \
|
|
114
|
+
--outcome failed \
|
|
115
|
+
--executor "Jane Reviewer" \
|
|
116
|
+
--tested-sha abc123def456 \
|
|
117
|
+
--remediation-ref https://github.com/example/repo/issues/10 \
|
|
118
|
+
>/dev/null
|
|
119
|
+
assert_contains "failed outcome" "--outcome failed" "$dir/uat.log"
|
|
120
|
+
assert_contains "incident reference" "--incident-reference https://github.com/example/repo/issues/10" "$dir/uat.log"
|
|
121
|
+
rm -rf "$dir"
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
case_invalid_outcome_fails_before_calls() {
|
|
125
|
+
echo "case: invalid outcome fails before lifecycle calls"
|
|
126
|
+
local dir exit_code
|
|
127
|
+
dir=$(mktemp -d)
|
|
128
|
+
make_fixture "$dir"
|
|
129
|
+
run_helper "$dir" --project-slug fixture-app --release REQ-100 --outcome green --executor Jane --tested-sha abc123 >/dev/null 2>"$dir/stderr.log" && exit_code=0 || exit_code=$?
|
|
130
|
+
if [ "$exit_code" -ne 0 ]; then ok "exit code non-zero"; else no "expected non-zero"; fi
|
|
131
|
+
if [ ! -f "$dir/uat.log" ]; then ok "no lifecycle call"; else no "unexpected lifecycle call"; fi
|
|
132
|
+
rm -rf "$dir"
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
case_passed_records_stage_four_pair
|
|
136
|
+
case_idempotency_is_stable
|
|
137
|
+
case_failed_records_remediation
|
|
138
|
+
case_invalid_outcome_fails_before_calls
|
|
139
|
+
|
|
140
|
+
echo ""
|
|
141
|
+
echo "=== record-uat-execution.test.sh: $PASS passed, $FAIL failed ==="
|
|
142
|
+
if [ "$FAIL" -gt 0 ]; then
|
|
143
|
+
exit 1
|
|
144
|
+
fi
|