@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.
Files changed (45) hide show
  1. package/dist/index.js +36 -34
  2. package/dist/index.js.map +1 -1
  3. package/package.json +2 -2
  4. package/scripts/upload-evidence.sh +23 -19
  5. package/sdlc/files/_common/governance/incident-report.md.template +6 -1
  6. package/sdlc/files/_common/governance/nil-incident-report.md.template +5 -2
  7. package/sdlc/files/_common/scripts/check-host-deployment.sh +58 -8
  8. package/sdlc/files/_common/scripts/check-host-deployment.test.sh +35 -3
  9. package/sdlc/files/_common/scripts/check-release-pr-scope.sh +8 -1
  10. package/sdlc/files/_common/scripts/check-release-pr-scope.test.sh +61 -12
  11. package/sdlc/files/_common/scripts/check-self-hosted-runner.sh +103 -0
  12. package/sdlc/files/_common/scripts/check-self-hosted-runner.test.sh +74 -0
  13. package/sdlc/files/_common/scripts/derive-release-version.sh +7 -9
  14. package/sdlc/files/_common/scripts/derive-release-version.test.sh +18 -18
  15. package/sdlc/files/_common/scripts/generate-bundled-changes.sh +24 -3
  16. package/sdlc/files/_common/scripts/generate-bundled-changes.test.sh +9 -5
  17. package/sdlc/files/_common/scripts/reconcile-railway-deployment.sh +37 -0
  18. package/sdlc/files/_common/scripts/reconcile-railway-deployment.test.sh +30 -0
  19. package/sdlc/files/_common/scripts/record-uat-execution.sh +195 -0
  20. package/sdlc/files/_common/scripts/record-uat-execution.test.sh +144 -0
  21. package/sdlc/files/_common/scripts/render-test-executions.sh +146 -0
  22. package/sdlc/files/_common/scripts/{render-test-cycles.test.sh → render-test-executions.test.sh} +12 -18
  23. package/sdlc/files/_common/scripts/{report-test-cycle.sh → report-test-execution.sh} +66 -74
  24. package/sdlc/files/_common/scripts/report-test-execution.test.sh +177 -0
  25. package/sdlc/files/_common/scripts/submit-for-uat-review.sh +21 -0
  26. package/sdlc/files/_common/scripts/upload-evidence.sh +23 -19
  27. package/sdlc/files/_common/scripts/validate-commits.sh +6 -4
  28. package/sdlc/files/_common/scripts/validate-commits.test.sh +15 -0
  29. package/sdlc/files/_common/skills/e2e-test-engineer/SKILL.md +9 -7
  30. package/sdlc/files/_common/skills/e2e-test-engineer/references/e2e-regression-3-tier.yml +62 -10
  31. package/sdlc/files/_common/skills/sdlc-implementer/SKILL.md +19 -16
  32. package/sdlc/files/ci/ci.yml.template +86 -46
  33. package/sdlc/files/ci/compliance-evidence.yml.template +173 -49
  34. package/sdlc/files/ci/feature-e2e.yml.template +17 -17
  35. package/sdlc/files/ci/incident-export.yml.template +18 -4
  36. package/sdlc/files/ci/post-deploy-prod.yml.template +92 -48
  37. package/sdlc/files/ci/python/ci.yml.template +14 -14
  38. package/sdlc/files/ci/quality-gates-provenance.yml.template +3 -0
  39. package/sdlc/files/ci/reconcile-deployment.yml.template +59 -0
  40. package/sdlc/package.json +1 -1
  41. package/sdlc/src/blueprints/3-compile-evidence.raw.md +10 -10
  42. package/sdlc/src/blueprints/4-submit-for-review.raw.md +1 -1
  43. package/sdlc/src/blueprints/5-deploy-main.raw.md +1 -1
  44. package/sdlc/src/blueprints/implementing-an-sdlc-issue.raw.md +3 -3
  45. 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
- # Step 4 (DevAudit-Installer#92) handles `chore:` / `docs:` / `ci:`
28
- # commits (e.g. a `devaudit update` sync) landing on the integration
29
- # branch between feature merge and release-PR open. Such a commit has
30
- # no REQ tag in its message steps 1-3 fall through. The release
31
- # ticket on disk is a stronger explicit-operator-state signal than the
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 (DevAudit-Installer#92): a chore: sync commit has no REQ tag in
115
- # its message but a pending release ticket exists on disk. Must attribute
116
- # to the REQ from that ticket instead of falling through to the bare date.
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 REQ-051 -> REQ-051" "REQ-051" "$(run_helper)"
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 (DevAudit-Installer#95): step-4-bis. No subject/body tag,
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 -> REQ-101" "REQ-101" "$(run_helper)"
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
- assert_eq "RTM long parenthetical status -> REQ-056" "REQ-056" "$(run_helper)"
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
- assert_eq "pending ticket REQ-301 beats RTM IN PROGRESS REQ-302" "REQ-301" "$(run_helper)"
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=docs/custom-RTM.md -> REQ-400" "REQ-400" "$GOT"
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
- assert_eq "RTM escaped pipes -> REQ-056 (single IN PROGRESS)" "REQ-056" "$(run_helper)"
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
- COMMITS="$(git log "$SINCE_REF"..HEAD --format='%h%x09%s' 2>/dev/null || true)"
254
- BUNDLED="$(printf '%s\n' "$COMMITS" | grep -E "$HOUSEKEEPING_TYPES" || true)"
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 \`${SINCE_REF}..HEAD\`"
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 commits appear in markdown + JSON.
113
- echo "Test 1: housekeeping commits render as non-release work"
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" "3" "$(jq -r '.nonReleaseWorkItems | length' "$JSON_OUT")"
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