@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.
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/package.json +2 -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.test.sh +7 -6
- 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/skills/e2e-test-engineer/references/e2e-regression-3-tier.yml +59 -10
- package/sdlc/files/_common/skills/sdlc-implementer/SKILL.md +3 -2
- package/sdlc/files/ci/ci.yml.template +38 -8
- package/sdlc/files/ci/compliance-evidence.yml.template +38 -3
- package/sdlc/files/ci/post-deploy-prod.yml.template +60 -19
- package/sdlc/files/ci/reconcile-deployment.yml.template +59 -0
- package/sdlc/package.json +1 -1
|
@@ -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
|
|
6
|
-
#
|
|
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
|
-
|
|
27
|
-
|
|
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
|
-
|
|
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 -
|
|
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
|
-
|
|
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-
|
|
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 == '
|
|
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
|
|
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
|
|
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
|
-
#
|
|
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.
|
|
656
|
-
#
|
|
657
|
-
#
|
|
658
|
-
#
|
|
659
|
-
#
|
|
660
|
-
|
|
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.
|
|
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 "
|
|
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
|
-
|
|
31
|
-
|
|
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:
|
|
161
|
-
id:
|
|
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 "
|
|
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 "
|
|
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"
|
|
175
|
-
echo "
|
|
176
|
-
|
|
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
|
-
|
|
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 [ "$
|
|
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
|
|
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
|
|
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.
|
|
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.
|
|
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