@metasession.co/devaudit-cli 0.3.17 → 0.3.18

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.
@@ -2,8 +2,8 @@
2
2
  #
3
3
  # Copy this into your consumer-owned .github/workflows/e2e-regression.yml
4
4
  # to adopt the 3-tier model: smoke (every develop push, fast) / critical
5
- # (consumer-enabled release PR, ~10-15 min target) / regression (post-merge
6
- # or scheduled only when the consumer opts in, plus dispatch).
5
+ # (consumer-enabled release PR, ~10-15 min target) / regression after a
6
+ # successful production deployment status (or manual dispatch).
7
7
  #
8
8
  # The framework does NOT sync this file automatically — your consumer
9
9
  # owns its e2e-regression.yml. Apply the patterns below to your own
@@ -17,14 +17,18 @@
17
17
  # playwright.config.ts must define the `critical` project for this to
18
18
  # fire; if it doesn't, the gate falls back to the existing `smoke`
19
19
  # project so PR-to-main stays green during migration.
20
+ #
21
+ # A post-merge regression is release evidence, not a best-effort background
22
+ # task. Keep its timeout below the job timeout so the workflow can retain
23
+ # partial evidence and report a terminal timeout outcome to DevAudit.
20
24
 
21
25
  name: E2E Regression
22
26
 
23
27
  on:
24
28
  pull_request:
25
29
  branches: [main] # critical-tier gate before merge
26
- push:
27
- branches: [main] # full regression after merge; auto-issues on failure
30
+ deployment_status:
31
+ types: [created] # full regression only after successful production deploy
28
32
  workflow_dispatch:
29
33
  inputs:
30
34
  specs:
@@ -42,11 +46,19 @@ concurrency:
42
46
  jobs:
43
47
  e2e:
44
48
  name: E2E Regression Tests
49
+ if: >-
50
+ github.event_name != 'deployment_status' ||
51
+ (github.event.deployment_status.state == 'success' &&
52
+ (github.event.deployment.environment == 'production' || github.event.deployment.environment == 'prod'))
45
53
  runs-on: ubuntu-latest # adapt to your runner; e.g. self-hosted, ubuntu-24.04
54
+ # The full regression target is about 35 minutes. Leave time to archive
55
+ # partial output before GitHub terminates the job.
56
+ timeout-minutes: 55
46
57
  steps:
47
58
  - uses: actions/checkout@v6
48
59
  with:
49
60
  fetch-depth: 0 # for E2E_NEW_SPECS computation
61
+ ref: ${{ github.event.deployment.sha || github.sha }}
50
62
 
51
63
  - uses: actions/setup-node@v6
52
64
  with:
@@ -79,7 +91,7 @@ jobs:
79
91
  fi
80
92
  echo "specs=" >> "$GITHUB_OUTPUT"
81
93
  ;;
82
- push|schedule)
94
+ deployment_status|schedule)
83
95
  echo "project=regression" >> "$GITHUB_OUTPUT"
84
96
  echo "specs=" >> "$GITHUB_OUTPUT"
85
97
  echo "Running full regression project"
@@ -93,6 +105,25 @@ jobs:
93
105
  ;;
94
106
  esac
95
107
 
108
+ - name: Record E2E execution context
109
+ id: context
110
+ env:
111
+ E2E_TARGET_URL: ${{ vars.E2E_TARGET_URL }}
112
+ run: |
113
+ set -euo pipefail
114
+ TARGET_URL="${E2E_TARGET_URL:-${PLAYWRIGHT_BASE_URL:-${BASE_URL:-http://localhost:3000}}}"
115
+ jq -n \
116
+ --arg targetUrl "$TARGET_URL" \
117
+ --arg project "${{ steps.select.outputs.project }}" \
118
+ --arg specs "${{ steps.select.outputs.specs }}" \
119
+ --arg serverStart "consumer-managed-or-external" \
120
+ --arg serverStop "consumer-managed-or-external" \
121
+ --arg startedAt "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
122
+ --argjson timeoutMinutes 40 \
123
+ '{target_url: $targetUrl, project: $project, selected_specs: $specs, test_server_start: $serverStart, test_server_stop: $serverStop, timeout_minutes: $timeoutMinutes, started_at: $startedAt, outcome: "running"}' \
124
+ > e2e-regression-metadata.json
125
+ echo "target_url=$TARGET_URL" >> "$GITHUB_OUTPUT"
126
+
96
127
  - name: Run E2E suite
97
128
  id: run
98
129
  env:
@@ -100,14 +131,28 @@ jobs:
100
131
  PLAYWRIGHT_JSON_OUTPUT_NAME: e2e-regression-results.json
101
132
  # Add your e2e_env values here as needed (DEVAUDIT_BASE_URL etc.)
102
133
  run: |
103
- set -euo pipefail
134
+ set -uo pipefail
104
135
  PROJECT="${{ steps.select.outputs.project }}"
105
136
  SPECS="${{ steps.select.outputs.specs }}"
106
137
  if [ -n "$SPECS" ]; then
107
- npx playwright test --project="$PROJECT" --reporter=json,html $SPECS
138
+ timeout --signal=TERM --kill-after=60s 40m npx playwright test --project="$PROJECT" --reporter=json,html $SPECS
139
+ else
140
+ timeout --signal=TERM --kill-after=60s 40m npx playwright test --project="$PROJECT" --reporter=json,html
141
+ fi
142
+ STATUS=$?
143
+ if [ "$STATUS" -eq 124 ]; then
144
+ OUTCOME="timed_out"
145
+ echo "::error::E2E regression exceeded its 40-minute execution budget. Partial evidence will be uploaded."
146
+ elif [ "$STATUS" -eq 0 ]; then
147
+ OUTCOME="passed"
108
148
  else
109
- npx playwright test --project="$PROJECT" --reporter=json,html
149
+ OUTCOME="failed"
110
150
  fi
151
+ jq --arg outcome "$OUTCOME" --arg completedAt "$(date -u +%Y-%m-%dT%H:%M:%SZ)" --argjson exitCode "$STATUS" \
152
+ '. + {outcome: $outcome, completed_at: $completedAt, exit_code: $exitCode}' \
153
+ e2e-regression-metadata.json > e2e-regression-metadata.tmp
154
+ mv e2e-regression-metadata.tmp e2e-regression-metadata.json
155
+ exit "$STATUS"
111
156
 
112
157
  - uses: actions/upload-artifact@v7
113
158
  if: always()
@@ -115,11 +160,15 @@ jobs:
115
160
  name: e2e-regression-report
116
161
  path: |
117
162
  e2e-regression-results.json
163
+ e2e-regression-metadata.json
118
164
  playwright-report/
119
165
  test-results/
166
+ e2e-server.log
167
+ server.log
168
+ if-no-files-found: warn
120
169
 
121
170
  # ─────────────────────────────────────────────────────────────
122
- # Post-merge auto-issue on regression failure (push:branches:[main])
171
+ # Post-deployment auto-issue on regression failure.
123
172
  #
124
173
  # Catches regressions that slipped past the critical-tier PR gate.
125
174
  # Opens a high-priority issue tagging the merge commit + the
@@ -127,7 +176,7 @@ jobs:
127
176
  # No auto-revert — that's intentionally an operator decision.
128
177
  # ─────────────────────────────────────────────────────────────
129
178
  - name: Open hotfix issue on post-merge regression
130
- if: failure() && github.event_name == 'push' && github.ref == 'refs/heads/main'
179
+ if: failure() && github.event_name == 'deployment_status' && github.event.deployment_status.state == 'success'
131
180
  env:
132
181
  GH_TOKEN: ${{ github.token }}
133
182
  run: |
@@ -342,7 +342,7 @@ Reached from Phase 0 for non-tracked change-types. The skill drives this end-to-
342
342
 
343
343
  Either way, never bypass a gate (no `--no-verify`, no `--admin` merge of a red required check); the only difference is **where** you wait for the gate to fire — before merge vs. after merge.
344
344
  8. **Guide review -> merge.** A human still reviews the PR. Merge only after all required checks are terminal green on the current PR head SHA; queued, running, stale, cancelled, unexpectedly skipped, or failed checks are not green. There is no portal release approval, UAT four-eyes, Production gate, or standalone close-out on the normal lightweight path.
345
- 9. **Record the correct release meaning.** A housekeeping push produces at most a bare-date release (`vYYYY.MM.DD`) that is integration/history, not an active approval by default. It waits on `$INTEGRATION_BRANCH` until the next tracked release absorbs it through the required bundled-changes markdown/JSON manifest and portal lineage. A doc-only push attaches documents to the existing `REQ-XXX` release.
345
+ 9. **Record the correct release meaning.** A housekeeping push produces at most a bare-date release (`vYYYY.MM.DD`) that is integration/history, not an active approval by default. It waits on `$INTEGRATION_BRANCH` until a later tracked release explicitly absorbs it through the required bundled-changes markdown/JSON manifest and portal lineage. Never infer that an unrelated hotfix or mandatory back-merge belongs to the only pending REQ. A submitted REQ bundle is frozen approval scope; changing it returns that REQ to evidence compilation and UAT resubmission. A doc-only push attaches documents to the existing `REQ-XXX` release.
346
346
  10. **Standalone exception.** Only when housekeeping cannot reasonably wait for the next tracked release may the skill prepare a standalone promotion. It must state `Standalone housekeeping promotion` and why in the release PR, add `compliance/standalone-housekeeping/STANDALONE-HOUSEKEEPING-vYYYY.MM.DD.json` with `schemaVersion: 1`, the exact derived bare-date version, `releaseMode: standalone_housekeeping`, and a specific reason. It must obtain terminal-green CI and PR review. The installed release-scope check validates the declaration before merge; post-deploy uploads it as release-ticket evidence and changes the portal row to standalone housekeeping. Use portal approval only if project policy explicitly opts in. If the installed workflow cannot implement that policy, halt and keep the change on the normal integration path.
347
347
 
348
348
  ### Phase 1 — Plan (SDLC stage 1)
@@ -693,8 +693,9 @@ Invoked separately by the user after UAT activity on the portal. Trigger: "resum
693
693
  - **Enumerate all in-scope REQs** from `compliance/pending-releases/RELEASE-TICKET-REQ-*.md` and check each one's portal state before merging. If ANY are `uat_changes_requested`: halt — "REQ-YYY has changes requested on the portal. Cannot merge bundled PR until all in-scope REQs are approved." If ANY are still `uat_review`: halt — "REQ-YYY is still pending UAT review." If ALL are `uat_approved` (or `prod_review`/`prod_approved`/`released`): proceed with merge.
694
694
  - `gh pr merge <M> --merge` (merge commit; `--squash` and `--rebase` are blocked by branch protection on SDLC repos and would break the audit trail).
695
695
  - **Before watching `post-deploy-prod.yml`**, verify the workflow exists and has triggered: `gh run list --workflow post-deploy-prod.yml --limit 1 --json status,conclusion`. If no run appears within 5 minutes of the merge, halt — "post-deploy-prod.yml did not trigger after merge. Check workflow configuration, secrets, and deployment platform. Operator action — run `gh workflow run post-deploy-prod.yml` manually, then ping `resume REQ-XXX`."
696
+ - **Gate topology:** when a host waits for CI before deploy, only pre-deploy eligibility belongs to the main-push suite. Post-deploy evidence and full regression trigger from a successful production `deployment_status`, not that same push. If the provider skipped the SHA due to CI gating, redeploy that exact SHA before running post-deploy verification.
696
697
  - Watch `post-deploy-prod.yml` via `gh run watch` — block until the workflow reaches a terminal state. Wrap in a timeout (30 minutes default, configurable via `sdlc-config.json:post_deploy.timeout_minutes`). If the timeout fires: halt — "post-deploy-prod.yml has not reached a terminal state in N minutes. The deployment may be stuck. Operator action — check hosting platform logs, decide whether to wait or rollback, then ping `resume REQ-XXX`."
697
- - Treat `post-deploy-prod.yml` success as necessary but not sufficient. The workflow must also confirm the hosting platform's GitHub deployment status for the merged SHA reached terminal `success`. If the host deployment is still `queued` / `in_progress`, remain blocked. If it reaches `failure`, halt — "The host deployment for <sha> failed after merge. Do not approve Production or mark Released. Operator action — inspect hosting logs / rollback / resume once the production state is understood."
698
+ - Treat `post-deploy-prod.yml` success as necessary but not sufficient. The workflow must also confirm the hosting platform's GitHub deployment status for the merged SHA reached terminal `success`. If it reports `deployment_status_timeout`, `deployment_status_missing`, or terminal failure, remain blocked and preserve the deployment ID, SHA, environment, final observed state, target URL, elapsed time, and independent health-probe result. A successful health probe is corroboration only, never deployment-status proof. Halt — "The host deployment for <sha> is not terminal-successful after merge. Do not approve Production or mark Released. Operator action — inspect provider logs, fix forward or rollback, then rerun post-deploy verification once the production state is understood."
698
699
  - Verify production smoke evidence uploaded (`--environment production`) at `https://devaudit.metasession.co/projects/<slug>/releases/<version>`.
699
700
  - **Read `production_review.terminal_status` from `sdlc-config.json`** (default: `prod_review`). Branch:
700
701
  - If `released` (Option B): PATCH directly to `released` — `PATCH /releases/<version>` with `{"status": "released"}`.
@@ -646,18 +646,46 @@ jobs:
646
646
  --environment uat --sdlc-stage 2
647
647
  fi
648
648
 
649
- # devaudit-installer#200 Fix 4 — Per-REQ gate evidence fan-out.
649
+ # Per-REQ gate evidence fan-out.
650
650
  # The gate evidence above was uploaded to the single commit-derived
651
651
  # release (FLAGS carries --release <derived>). When multiple REQs
652
652
  # have pending release tickets, only that one REQ gets gate evidence
653
653
  # on the portal — other in-scope REQs show "no gate evidence" and
654
654
  # "no tests tagged with this REQ" even though the gates ran and
655
- # passed. Fan out gate-outcomes.json + e2e-results.json to each
656
- # in-scope REQ's release so the portal's per-REQ test parser can
657
- # extract REQ-tagged tests from the e2e-results.json associated with
658
- # that REQ's release. Heavy run-context artefacts (SAST, dep-audit,
659
- # playwright zip, coverage) remain on the commit-derived release
660
- # only — they are run-level, not per-REQ.
655
+ # passed. Gate outcomes can be attached to every active REQ, but an
656
+ # E2E JSON result is REQ-scoped proof only when it actually contains
657
+ # an executed test tagged for that REQ. A smoke run must never
658
+ # overwrite or obscure a prior targeted feature execution merely
659
+ # because a pending ticket exists (DevAudit-Installer#435).
660
+ has_req_tagged_e2e_result() {
661
+ python3 - "$1" "$2" <<'PY'
662
+ import json, re, sys
663
+
664
+ req_id, filename = sys.argv[1:]
665
+ def values(value):
666
+ if isinstance(value, str): return [value]
667
+ if isinstance(value, dict): return [str(value.get('type', '')), str(value.get('description', ''))]
668
+ return []
669
+ def tagged(node, ancestry):
670
+ text = [*ancestry, str(node.get('title', ''))]
671
+ for field in ('annotations', 'tags'):
672
+ for value in node.get(field, []): text.extend(values(value))
673
+ return any(req_id in value for value in text)
674
+ def executed(test):
675
+ return any(result.get('status') not in {'skipped', 'interrupted'} for result in test.get('results', []))
676
+ def contains(node, ancestry):
677
+ title = str(node.get('title', ''))
678
+ path = [*ancestry, title] if title else ancestry
679
+ for spec in node.get('specs', []):
680
+ if tagged(spec, path) and any(executed(test) for test in spec.get('tests', [])): return True
681
+ for test in node.get('tests', []):
682
+ if tagged(test, path) and executed(test): return True
683
+ return any(contains(suite, path) for suite in node.get('suites', []))
684
+ with open(filename, encoding='utf-8') as handle:
685
+ report = json.load(handle)
686
+ raise SystemExit(0 if re.fullmatch(r'REQ-[A-Z0-9-]+', req_id) and contains(report, []) else 1)
687
+ PY
688
+ }
661
689
  COMMIT_REQ="${{ needs.register-release.outputs.version }}"
662
690
  FANOUT_REQS=()
663
691
  if [ -d compliance/pending-releases ]; then
@@ -688,10 +716,12 @@ jobs:
688
716
  {{PROJECT_SLUG}} "${REQ_ID}" gate_outcome ci-evidence/gate-outcomes.json \
689
717
  --category ci_pipeline ${FANOUT_FLAGS}
690
718
  fi
691
- if [ -f ci-evidence/e2e-results.json ]; then
719
+ if [ -f ci-evidence/e2e-results.json ] && has_req_tagged_e2e_result "$REQ_ID" ci-evidence/e2e-results.json; then
692
720
  upload "e2e-results.json -> ${REQ_ID}" \
693
721
  {{PROJECT_SLUG}} "${REQ_ID}" e2e_result ci-evidence/e2e-results.json \
694
722
  --category e2e_result ${FANOUT_FLAGS}
723
+ elif [ -f ci-evidence/e2e-results.json ]; then
724
+ echo "::notice::Not attaching generic E2E JSON to ${REQ_ID}: this CI tier executed no tagged ${REQ_ID} test. Run-level evidence remains on _compliance-docs."
695
725
  fi
696
726
  done
697
727
  fi
@@ -823,6 +823,17 @@ jobs:
823
823
  if [ "${#REQS[@]}" -eq 0 ]; then
824
824
  REQS=(_compliance-docs)
825
825
  fi
826
+ # A bounded Playwright process exits the workflow with `failure`
827
+ # even when its machine-readable execution result is specifically
828
+ # `timed_out`. Preserve that distinction for the portal lifecycle.
829
+ EXECUTION_OUTCOME=""
830
+ if [ -f e2e-artifacts/e2e-regression-metadata.json ]; then
831
+ EXECUTION_OUTCOME="$(jq -r '.outcome // empty' e2e-artifacts/e2e-regression-metadata.json 2>/dev/null || true)"
832
+ case "$EXECUTION_OUTCOME" in
833
+ passed|failed|timed_out) ;;
834
+ *) EXECUTION_OUTCOME="" ;;
835
+ esac
836
+ fi
826
837
  echo "Uploading E2E ${TIER}-tier evidence to: ${REQS[*]} (release: ${DERIVED_RELEASE})"
827
838
 
828
839
  UPLOAD_FAILURES=0
@@ -868,7 +879,24 @@ jobs:
868
879
  REQ_FAILURES=$((REQ_FAILURES + 1))
869
880
  fi
870
881
  fi
871
- # 2. Playwright HTML report zip e2e_report. Operator-
882
+ # 2. The bounded execution metadata makes a timeout distinct
883
+ # from an ordinary failed workflow on the portal.
884
+ if [ -f e2e-artifacts/e2e-regression-metadata.json ]; then
885
+ if bash scripts/upload-evidence.sh \
886
+ {{PROJECT_SLUG}} "$REQ" test_report \
887
+ e2e-artifacts/e2e-regression-metadata.json \
888
+ --category test_report ${FLAGS} --release "${DERIVED_RELEASE}" \
889
+ "${LINEAGE_FLAGS[@]}" \
890
+ --meta-key "tier=${TIER}" --meta-key "execution_outcome=${EXECUTION_OUTCOME:-unknown}"
891
+ then
892
+ :
893
+ else
894
+ echo "::warning::E2E execution metadata upload failed for ${REQ}"
895
+ UPLOAD_FAILURES=$((UPLOAD_FAILURES + 1))
896
+ REQ_FAILURES=$((REQ_FAILURES + 1))
897
+ fi
898
+ fi
899
+ # 3. Playwright HTML report zip → e2e_report. Operator-
872
900
  # facing artefact (the formatted run summary). Skipped if
873
901
  # the report directory is missing (e.g. an upstream
874
902
  # infrastructure failure aborted before reporting).
@@ -908,6 +936,12 @@ jobs:
908
936
  action_required) REQ_OUTCOME=action_required ;;
909
937
  *) REQ_OUTCOME=unknown ;;
910
938
  esac
939
+ if [ "$EXECUTION_OUTCOME" = "timed_out" ]; then
940
+ REQ_OUTCOME=timed_out
941
+ OUTCOME_REASON="bounded Playwright execution timed out (metadata); workflow_run conclusion: ${{ github.event.workflow_run.conclusion }}"
942
+ else
943
+ OUTCOME_REASON="authoritative workflow_run conclusion: ${{ github.event.workflow_run.conclusion }}"
944
+ fi
911
945
  bash scripts/report-test-cycle.sh complete \
912
946
  --project-slug {{PROJECT_SLUG}} \
913
947
  --release "${DERIVED_RELEASE}" \
@@ -924,7 +958,7 @@ jobs:
924
958
  --workflow-url "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.event.workflow_run.id }}" \
925
959
  --started-at "${{ github.event.workflow_run.run_started_at }}" \
926
960
  --completed-at "${{ github.event.workflow_run.updated_at }}" \
927
- --outcome-reason "authoritative workflow_run conclusion: ${{ github.event.workflow_run.conclusion }}" \
961
+ --outcome-reason "$OUTCOME_REASON" \
928
962
  --outcome "${REQ_OUTCOME}"
929
963
  case "$REQ_OUTCOME" in
930
964
  passed) CHECK_STATUS=successful ;;
@@ -932,8 +966,9 @@ jobs:
932
966
  esac
933
967
  CHECK_DETAILS=$(jq -cn \
934
968
  --arg conclusion "${{ github.event.workflow_run.conclusion }}" \
969
+ --arg executionOutcome "${EXECUTION_OUTCOME:-unknown}" \
935
970
  --argjson artifactUploadFailures "$REQ_FAILURES" \
936
- '{executionSource:"workflow_run.conclusion",conclusion:$conclusion,artifactUploadFailures:$artifactUploadFailures}')
971
+ '{executionSource:"workflow_run.conclusion",conclusion:$conclusion,executionOutcome:$executionOutcome,artifactUploadFailures:$artifactUploadFailures}')
937
972
  bash scripts/report-release-check.sh \
938
973
  --project-slug {{PROJECT_SLUG}} --release "${DERIVED_RELEASE}" \
939
974
  --check-key "e2e-regression:${{ github.event.workflow_run.id }}:${{ github.event.workflow_run.run_attempt }}:${REQ}" \
@@ -3,7 +3,9 @@
3
3
  # Generated by `devaudit install` / `devaudit update` from sdlc-config.json.
4
4
  # Do not edit manually — re-run the CLI (`devaudit update`) to regenerate.
5
5
  #
6
- # Production verification is READ-ONLY.
6
+ # Production verification is READ-ONLY and starts only after a successful
7
+ # production deployment status. It must not join the main-push check suite a
8
+ # host evaluates before deciding whether to deploy.
7
9
  # No E2E tests, no database operations, no API mutations.
8
10
  #
9
11
  # Promotes EVERY in-scope release (each requirement with a pending release
@@ -27,12 +29,16 @@ on:
27
29
  release:
28
30
  description: 'Optional REQ-XXX / version to promote (blank = all in-scope from pending release tickets).'
29
31
  required: false
30
- push:
31
- branches: [main]
32
+ deployment_status:
33
+ types: [created]
32
34
 
33
35
  jobs:
34
36
  production-evidence:
35
37
  name: Production Evidence
38
+ if: >-
39
+ github.event_name == 'workflow_dispatch' ||
40
+ (github.event.deployment_status.state == 'success' &&
41
+ (github.event.deployment.environment == 'production' || github.event.deployment.environment == 'prod'))
36
42
  runs-on: {{RUNNER}}
37
43
  permissions:
38
44
  contents: read
@@ -43,7 +49,7 @@ jobs:
43
49
  DEVAUDIT_API_KEY: ${{ secrets.DEVAUDIT_API_KEY }}
44
50
  PROD_URL: ${{ secrets.{{PRODUCTION_URL_SECRET}} }}
45
51
  PROJECT_SLUG: {{PROJECT_SLUG}}
46
- GIT_SHA: ${{ github.sha }}
52
+ GIT_SHA: ${{ github.event.deployment.sha || github.sha }}
47
53
  CI_RUN: ${{ github.run_id }}
48
54
  RELEASE_INPUT: ${{ github.event.inputs.release }}
49
55
 
@@ -157,45 +163,68 @@ jobs:
157
163
  --workflow-url "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
158
164
  done
159
165
 
160
- - name: Wait for production deployment
161
- id: wait_deployment
166
+ - name: Probe production health independently
167
+ id: production_probe
162
168
  run: |
169
+ set -euo pipefail
163
170
  DEPLOY_READY=false
171
+ LAST_HTTP_CODE=000
164
172
  for i in $(seq 1 30); do
165
173
  HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" "${PROD_URL}/" || echo "000")
174
+ LAST_HTTP_CODE="$HTTP_CODE"
166
175
  if [ "$HTTP_CODE" -ge 200 ] && [ "$HTTP_CODE" -lt 400 ]; then
167
- echo "Production is up (HTTP ${HTTP_CODE})"
176
+ echo "::notice::production health probe attempt=${i}/30 url=${PROD_URL}/ http_status=${HTTP_CODE}"
168
177
  DEPLOY_READY=true
169
178
  break
170
179
  fi
171
- echo "Attempt ${i}/30: HTTP ${HTTP_CODE} — waiting 10s..."
180
+ echo "::notice::production health probe attempt=${i}/30 url=${PROD_URL}/ http_status=${HTTP_CODE}"
172
181
  sleep 10
173
182
  done
174
- if [ "$DEPLOY_READY" != "true" ]; then
175
- echo "::error::Production did not become ready within the deployment window"
176
- exit 1
183
+ if [ "$DEPLOY_READY" = "true" ]; then
184
+ echo "outcome=success" >> "$GITHUB_OUTPUT"
185
+ echo "verification=production_health_success" >> "$GITHUB_OUTPUT"
186
+ else
187
+ echo "outcome=failure" >> "$GITHUB_OUTPUT"
188
+ echo "verification=production_health_timeout" >> "$GITHUB_OUTPUT"
189
+ echo "::error::production_health_timeout: ${PROD_URL}/ did not return a successful response after 30 attempts (last HTTP ${LAST_HTTP_CODE})."
177
190
  fi
191
+ echo "http_code=${LAST_HTTP_CODE}" >> "$GITHUB_OUTPUT"
178
192
 
179
193
  - name: Confirm terminal host deployment success
180
194
  id: host_deployment
181
- if: steps.wait_deployment.outcome == 'success'
182
195
  env:
183
196
  GH_TOKEN: ${{ github.token }}
184
197
  run: |
198
+ set +e
185
199
  chmod +x scripts/check-host-deployment.sh 2>/dev/null || true
186
200
  bash scripts/check-host-deployment.sh \
187
201
  --repo="${{ github.repository }}" \
188
202
  --sha="${GIT_SHA}" \
189
203
  --max-attempts=30 \
190
- --poll-seconds=10
204
+ --poll-seconds=10 \
205
+ --output-file=host-deployment-result.env
206
+ SCRIPT_EXIT=$?
207
+ set -e
208
+ if [ -f host-deployment-result.env ]; then
209
+ for KEY in verification deployment_id deployment_state target_url environment elapsed_seconds; do
210
+ VALUE=$(sed -n "s/^${KEY}=//p" host-deployment-result.env | head -n 1)
211
+ echo "${KEY}=${VALUE}" >> "$GITHUB_OUTPUT"
212
+ done
213
+ fi
214
+ exit "$SCRIPT_EXIT"
191
215
 
192
216
  - name: Complete production deployment cycles
193
217
  if: always() && env.BASE != ''
194
218
  run: |
195
- if [ "${{ steps.wait_deployment.outcome }}" = "success" ] && [ "${{ steps.host_deployment.outcome }}" = "success" ]; then
219
+ HOST_VERIFICATION="${{ steps.host_deployment.outputs.verification || 'not_run' }}"
220
+ PROBE_VERIFICATION="${{ steps.production_probe.outputs.verification || 'not_run' }}"
221
+ if [ "$HOST_VERIFICATION" = "success" ] && [ "$PROBE_VERIFICATION" = "production_health_success" ]; then
196
222
  OUTCOME=passed
197
223
  CHECK_STATUS=successful
198
- elif [ "${{ steps.wait_deployment.outcome }}" = "cancelled" ] || [ "${{ steps.host_deployment.outcome }}" = "cancelled" ]; then
224
+ elif [ "$HOST_VERIFICATION" = "deployment_status_timeout" ]; then
225
+ OUTCOME=timed_out
226
+ CHECK_STATUS=failed
227
+ elif [ "${{ steps.host_deployment.outcome }}" = "cancelled" ]; then
199
228
  OUTCOME=cancelled
200
229
  CHECK_STATUS=cancelled
201
230
  else
@@ -217,7 +246,7 @@ jobs:
217
246
  --commit-sha "$GIT_SHA" --branch main \
218
247
  --workflow-name "Post-Deploy Production" \
219
248
  --workflow-url "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" \
220
- --outcome "$OUTCOME" --outcome-reason "host deployment: ${{ steps.host_deployment.outcome }}"
249
+ --outcome "$OUTCOME" --outcome-reason "host verification=${HOST_VERIFICATION}; production probe=${PROBE_VERIFICATION}; last HTTP=${{ steps.production_probe.outputs.http_code || 'unknown' }}"
221
250
  bash scripts/report-release-check.sh \
222
251
  --project-slug "$PROJECT_SLUG" --release "$VERSION" \
223
252
  --check-key "production-deployment:${CI_RUN}:${{ github.run_attempt }}" \
@@ -225,11 +254,23 @@ jobs:
225
254
  --external-run-id "$CI_RUN" \
226
255
  --external-url "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" \
227
256
  --commit-sha "$GIT_SHA" --branch main \
228
- --details-json '{"waitOutcome":"${{ steps.wait_deployment.outcome }}","hostOutcome":"${{ steps.host_deployment.outcome }}"}'
257
+ --details-json "$(jq -cn \
258
+ --arg hostVerification "$HOST_VERIFICATION" \
259
+ --arg hostOutcome "${{ steps.host_deployment.outcome }}" \
260
+ --arg deploymentId "${{ steps.host_deployment.outputs.deployment_id || '' }}" \
261
+ --arg deploymentState "${{ steps.host_deployment.outputs.deployment_state || '' }}" \
262
+ --arg targetUrl "${{ steps.host_deployment.outputs.target_url || '' }}" \
263
+ --arg probeVerification "$PROBE_VERIFICATION" \
264
+ --arg probeHttpCode "${{ steps.production_probe.outputs.http_code || '' }}" \
265
+ '{hostVerification:$hostVerification,hostOutcome:$hostOutcome,deploymentId:$deploymentId,deploymentState:$deploymentState,targetUrl:$targetUrl,probeVerification:$probeVerification,probeHttpCode:$probeHttpCode}')"
229
266
  done
267
+ if [ "$CHECK_STATUS" != "successful" ]; then
268
+ echo "::error::Production deployment verification is not successful: host=${HOST_VERIFICATION}, probe=${PROBE_VERIFICATION}. Investigate the hosting provider before retrying."
269
+ exit 1
270
+ fi
230
271
 
231
272
  - name: Start production smoke cycles
232
- if: steps.wait_deployment.outcome == 'success' && steps.host_deployment.outcome == 'success'
273
+ if: steps.production_probe.outputs.verification == 'production_health_success' && steps.host_deployment.outputs.verification == 'success'
233
274
  run: |
234
275
  chmod +x scripts/report-test-cycle.sh 2>/dev/null || true
235
276
  for PREFIX in ${REQS}; do
@@ -250,7 +291,7 @@ jobs:
250
291
 
251
292
  - name: Production smoke tests (read-only)
252
293
  id: production_smoke
253
- if: steps.wait_deployment.outcome == 'success' && steps.host_deployment.outcome == 'success'
294
+ if: steps.production_probe.outputs.verification == 'production_health_success' && steps.host_deployment.outputs.verification == 'success'
254
295
  run: |
255
296
  echo "=== Production Smoke Tests ==="
256
297
  HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" "${PROD_URL}/")
@@ -0,0 +1,59 @@
1
+ name: Reconcile Verified Host Deployment
2
+
3
+ on:
4
+ workflow_dispatch:
5
+ inputs:
6
+ release:
7
+ description: 'REQ-XXX release receiving recovery evidence'
8
+ required: true
9
+ expected_sha:
10
+ description: 'Exact main SHA verified at the provider'
11
+ required: true
12
+ github_deployment_id:
13
+ description: 'Existing GitHub deployment ID to reconcile'
14
+ required: true
15
+ railway_deployment_id:
16
+ description: 'Terminal-successful Railway deployment ID'
17
+ required: true
18
+ railway_project:
19
+ description: 'Railway project ID'
20
+ required: true
21
+ railway_service:
22
+ description: 'Railway service ID or name'
23
+ required: true
24
+ health_url:
25
+ description: 'Configured production health URL'
26
+ required: true
27
+
28
+ jobs:
29
+ reconcile:
30
+ name: Verify and reconcile deployment
31
+ runs-on: {{RUNNER}}
32
+ permissions:
33
+ contents: read
34
+ deployments: write
35
+ env:
36
+ DEVAUDIT_API_KEY: ${{ secrets.DEVAUDIT_API_KEY }}
37
+ DEVAUDIT_BASE_URL: ${{ vars.DEVAUDIT_BASE_URL }}
38
+ steps:
39
+ - uses: actions/checkout@v6
40
+ with:
41
+ ref: ${{ inputs.expected_sha }}
42
+ - name: Verify provider deployment before reconciliation
43
+ env:
44
+ GH_TOKEN: ${{ github.token }}
45
+ RAILWAY_TOKEN: ${{ secrets.RAILWAY_TOKEN }}
46
+ run: |
47
+ chmod +x scripts/reconcile-railway-deployment.sh
48
+ bash scripts/reconcile-railway-deployment.sh \
49
+ --repo="${{ github.repository }}" --sha="${{ inputs.expected_sha }}" \
50
+ --github-deployment-id="${{ inputs.github_deployment_id }}" \
51
+ --railway-deployment-id="${{ inputs.railway_deployment_id }}" \
52
+ --railway-project="${{ inputs.railway_project }}" --railway-service="${{ inputs.railway_service }}" \
53
+ --health-url="${{ inputs.health_url }}"
54
+ - name: Upload manual reconciliation evidence
55
+ run: |
56
+ bash scripts/upload-evidence.sh {{PROJECT_SLUG}} "${{ inputs.release }}" test_report deployment-reconciliation.json \
57
+ --category release_artifact --release "${{ inputs.release }}" --environment production \
58
+ --git-sha "${{ inputs.expected_sha }}" --branch main --sdlc-stage 5 \
59
+ --meta-key 'provenance=manual_reconciliation' --meta-key 'provider=railway'
package/sdlc/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@metasession.co/devaudit-sdlc",
3
- "version": "0.3.17",
3
+ "version": "0.3.18",
4
4
  "description": "DevAudit SDLC CLI Engine — cross-agent terminal-driven SDLC orchestrator.",
5
5
  "bin": {
6
6
  "devaudit-sdlc": "./src/bin/devaudit-sdlc.js"