@metasession.co/devaudit-cli 0.1.1 → 0.1.3

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 (69) hide show
  1. package/README.md +13 -10
  2. package/dist/index.js +17 -5
  3. package/dist/index.js.map +1 -1
  4. package/package.json +9 -5
  5. package/scripts/upload-evidence.sh +225 -0
  6. package/sdlc/CLAUDE.md +73 -0
  7. package/sdlc/HOST_ADAPTER.md +127 -0
  8. package/sdlc/SKILLS.md +137 -0
  9. package/sdlc/STACK_ADAPTER.md +130 -0
  10. package/sdlc/ai-rules/INSTRUCTIONS-SDLC.md +172 -0
  11. package/sdlc/ai-rules/README.md +103 -0
  12. package/sdlc/ai-rules/SDLC_RULES.md +584 -0
  13. package/sdlc/ai-rules/claude/CLAUDE.md +192 -0
  14. package/sdlc/ai-rules/cursor/.cursorrules +167 -0
  15. package/sdlc/ai-rules/windsurf/.windsurfrules +167 -0
  16. package/sdlc/article.md +219 -0
  17. package/sdlc/files/_common/0-project-setup.md +410 -0
  18. package/sdlc/files/_common/1-plan-requirement.md +381 -0
  19. package/sdlc/files/_common/2-implement-and-test.md +276 -0
  20. package/sdlc/files/_common/3-compile-evidence.md +603 -0
  21. package/sdlc/files/_common/4-submit-for-review.md +362 -0
  22. package/sdlc/files/_common/5-deploy-main.md +251 -0
  23. package/sdlc/files/_common/Periodic_Security_Review_Schedule.md +169 -0
  24. package/sdlc/files/_common/README_TEMPLATE.md +441 -0
  25. package/sdlc/files/_common/Test_Architecture.md +461 -0
  26. package/sdlc/files/_common/Test_Plan_TEMPLATE.md +311 -0
  27. package/sdlc/files/_common/Test_Policy.md +277 -0
  28. package/sdlc/files/_common/Test_Strategy.md +359 -0
  29. package/sdlc/files/_common/github/ISSUE_TEMPLATE/bug.yml +75 -0
  30. package/sdlc/files/_common/github/ISSUE_TEMPLATE/config.yml +11 -0
  31. package/sdlc/files/_common/github/ISSUE_TEMPLATE/requirement.yml +75 -0
  32. package/sdlc/files/_common/github/ISSUE_TEMPLATE/task.yml +48 -0
  33. package/sdlc/files/_common/github/pull_request_template.md +69 -0
  34. package/sdlc/files/_common/implementing-an-sdlc-issue.md +413 -0
  35. package/sdlc/files/_common/scripts/derive-release-version.sh +40 -0
  36. package/sdlc/files/_common/scripts/derive-release-version.test.sh +98 -0
  37. package/sdlc/files/_common/scripts/submit-for-uat-review.sh +162 -0
  38. package/sdlc/files/_common/scripts/validate-commits.sh +83 -0
  39. package/sdlc/files/_common/scripts/validate-compliance-artifacts.sh +202 -0
  40. package/sdlc/files/_common/scripts/validate-compliance-artifacts.test.sh +202 -0
  41. package/sdlc/files/_common/skills/_schema/skill.schema.json +36 -0
  42. package/sdlc/files/_common/skills/e2e-test-engineer/SKILL.md +254 -0
  43. package/sdlc/files/_common/skills/e2e-test-engineer/references/bootstrap.md +244 -0
  44. package/sdlc/files/_common/skills/e2e-test-engineer/references/evidence.ts +40 -0
  45. package/sdlc/files/_common/skills/sdlc-implementer/SKILL.md +189 -0
  46. package/sdlc/files/_common/skills/sdlc-implementer/references/call-graph.md +64 -0
  47. package/sdlc/files/_common/skills/sdlc-implementer/references/change-request-loop.md +192 -0
  48. package/sdlc/files/_common/skills/sdlc-implementer/references/compliance-constraints.md +81 -0
  49. package/sdlc/files/ci/check-release-approval.yml.template +201 -0
  50. package/sdlc/files/ci/ci-status-fallback.yml.template +41 -0
  51. package/sdlc/files/ci/ci.yml.template +390 -0
  52. package/sdlc/files/ci/compliance-evidence.yml.template +161 -0
  53. package/sdlc/files/ci/compliance-validation.yml.template +34 -0
  54. package/sdlc/files/ci/post-deploy-prod.yml.template +159 -0
  55. package/sdlc/files/ci/python/ci.yml.template +335 -0
  56. package/sdlc/files/hosts/_schema/adapter.schema.json +103 -0
  57. package/sdlc/files/hosts/railway/adapter.json +32 -0
  58. package/sdlc/files/sdlc-config.example.json +74 -0
  59. package/sdlc/files/stacks/_schema/adapter.schema.json +151 -0
  60. package/sdlc/files/stacks/node/adapter.json +54 -0
  61. package/sdlc/files/stacks/node/hooks/.prettierrc.json +9 -0
  62. package/sdlc/files/stacks/node/hooks/commit-msg +7 -0
  63. package/sdlc/files/stacks/node/hooks/commitlint.config.mjs +64 -0
  64. package/sdlc/files/stacks/node/hooks/lint-staged.config.mjs +16 -0
  65. package/sdlc/files/stacks/node/hooks/pre-commit +13 -0
  66. package/sdlc/files/stacks/node/hooks/pre-push +15 -0
  67. package/sdlc/files/stacks/node/scripts/check-requirement-jsdoc.sh +54 -0
  68. package/sdlc/files/stacks/python/adapter.json +36 -0
  69. package/sdlc/files/stacks/python/hooks/.pre-commit-config.yaml +51 -0
@@ -0,0 +1,201 @@
1
+ # Release Approval Gate — blocks PR merge until release is approved in DevAudit.
2
+ #
3
+ # Four-eyes release-approval gate (always required). When UAT-environment
4
+ # verification is configured for the project (sdlc-config.json `uat.enabled`),
5
+ # the human approver factors that into their review, but the gate itself is
6
+ # about the approval click — not the UAT-env exercise. Renamed from
7
+ # "UAT Approval Gate" in sdlc-v1.22.0; backend release status enum
8
+ # `uat_approved` is preserved for backwards-compat (renamed in v1.23.0).
9
+ #
10
+ # Generated by `devaudit install` / `devaudit update` from sdlc-config.json.
11
+ # Do not edit manually — re-run the CLI (`devaudit update`) to regenerate.
12
+ #
13
+ # Auth: project-scoped API key. Issue from DevAudit → Project
14
+ # Settings → API Keys (uploader role). Set as repo Secret
15
+ # DEVAUDIT_API_KEY. Base URL is read from sdlc-config.json
16
+ # devaudit.base_url; falls back to repo Variable DEVAUDIT_BASE_URL
17
+ # (deprecated, removed in v1.23.0).
18
+
19
+ name: Release Approval Gate
20
+
21
+ on:
22
+ pull_request:
23
+ branches: [main]
24
+ workflow_dispatch:
25
+
26
+ jobs:
27
+ check-approval:
28
+ name: DevAudit Release Approval
29
+ runs-on: {{RUNNER}}
30
+ env:
31
+ DEVAUDIT_BASE_URL_VAR: ${{ vars.DEVAUDIT_BASE_URL }}
32
+ DEVAUDIT_API_KEY: ${{ secrets.DEVAUDIT_API_KEY }}
33
+ PROJECT_SLUG: {{PROJECT_SLUG}}
34
+
35
+ steps:
36
+ - uses: actions/checkout@v4
37
+
38
+ - name: Resolve DevAudit base URL
39
+ run: |
40
+ # Prefer sdlc-config.json (visible in PR review) over repo Variable.
41
+ CONFIG_URL=""
42
+ if [ -f sdlc-config.json ]; then
43
+ CONFIG_URL=$(jq -r '.devaudit.base_url // empty' sdlc-config.json 2>/dev/null || true)
44
+ fi
45
+ if [ -n "$CONFIG_URL" ]; then
46
+ BASE="$CONFIG_URL"
47
+ echo "Using devaudit.base_url from sdlc-config.json: $BASE"
48
+ elif [ -n "$DEVAUDIT_BASE_URL_VAR" ]; then
49
+ BASE="$DEVAUDIT_BASE_URL_VAR"
50
+ echo "::warning::Using repo Variable DEVAUDIT_BASE_URL (deprecated in v1.23.0). Move base_url to sdlc-config.json devaudit.base_url for PR-visible config."
51
+ else
52
+ echo "::error::No DevAudit base URL configured. Set devaudit.base_url in sdlc-config.json."
53
+ exit 1
54
+ fi
55
+ # Bootstrap mode (#301): the first PR introducing the SDLC framework
56
+ # can't pass the gate that the framework introduces. If the API key
57
+ # secret isn't set yet, fall through with a notice — gate passes,
58
+ # enforcement kicks in once the secret is configured AND the project
59
+ # is auto-created by the first compliance-evidence.yml run.
60
+ if [ -z "${DEVAUDIT_API_KEY}" ]; then
61
+ echo "::notice::DEVAUDIT_API_KEY not set — bootstrap mode. Gate passes. Configure the secret after the first compliance-evidence.yml run initialises ${PROJECT_SLUG} in DevAudit; enforcement starts on the next PR after that."
62
+ echo "BOOTSTRAP_MODE=true" >> "$GITHUB_ENV"
63
+ exit 0
64
+ fi
65
+ # Pre-flight: confirm destination is reachable. Catches dead aliases / wrong hosts early.
66
+ CODE=$(curl -s -o /dev/null -w "%{http_code}" -m 10 -I "${BASE%/}/" || echo "000")
67
+ case "$CODE" in
68
+ 2*|3*) echo "DevAudit reachable at ${BASE} (HTTP ${CODE})" ;;
69
+ *) echo "::error::DevAudit base URL ${BASE} returned HTTP ${CODE}. Likely a stale alias or wrong host. Check sdlc-config.json devaudit.base_url."; exit 1 ;;
70
+ esac
71
+ # Bootstrap probe (#301): project may not exist in DevAudit yet —
72
+ # the first compliance-evidence.yml run auto-creates it. A 404 here
73
+ # means we're on the introducing PR; pass with a notice. 401/403
74
+ # means the API key is invalid → fail (not bootstrap).
75
+ PROJ_CODE=$(curl -s -o /dev/null -w "%{http_code}" -m 10 \
76
+ -H "Authorization: Bearer ${DEVAUDIT_API_KEY}" \
77
+ "${BASE%/}/api/ci/projects/${PROJECT_SLUG}" || echo "000")
78
+ case "$PROJ_CODE" in
79
+ 2*)
80
+ echo "DevAudit project '${PROJECT_SLUG}' confirmed (HTTP ${PROJ_CODE})"
81
+ ;;
82
+ 404)
83
+ echo "::notice::DevAudit project '${PROJECT_SLUG}' does not exist yet (HTTP 404) — bootstrap mode. Gate passes. The project will be auto-created by the first compliance-evidence.yml run; enforcement kicks in on the next PR after that."
84
+ echo "BOOTSTRAP_MODE=true" >> "$GITHUB_ENV"
85
+ exit 0
86
+ ;;
87
+ 401|403)
88
+ echo "::error::DevAudit returned HTTP ${PROJ_CODE} for project '${PROJECT_SLUG}' — API key is invalid or lacks access. Verify DEVAUDIT_API_KEY belongs to the right project."
89
+ exit 1
90
+ ;;
91
+ *)
92
+ echo "::error::Unexpected HTTP ${PROJ_CODE} when probing DevAudit project '${PROJECT_SLUG}'. Investigate before retrying."
93
+ exit 1
94
+ ;;
95
+ esac
96
+ echo "BOOTSTRAP_MODE=false" >> "$GITHUB_ENV"
97
+ # Strip trailing slash so we don't double-up later.
98
+ echo "BASE=${BASE%/}" >> "$GITHUB_ENV"
99
+ # Export for upload-evidence.sh / any other tool that reads $DEVAUDIT_BASE_URL directly.
100
+ echo "DEVAUDIT_BASE_URL=${BASE%/}" >> "$GITHUB_ENV"
101
+
102
+ - name: Resolve current release
103
+ id: release
104
+ if: env.BOOTSTRAP_MODE != 'true'
105
+ run: |
106
+ DATE_PREFIX="v$(date +%Y.%m.%d)"
107
+ RESOLVE_URL="${BASE}/api/ci/releases/resolve?projectSlug=${PROJECT_SLUG}&versionPrefix=${DATE_PREFIX}"
108
+ # Retry: register-release in ci.yml may still be racing with us.
109
+ MAX_ATTEMPTS=6
110
+ WAIT_SECONDS=10
111
+ RESP=""
112
+ for ATTEMPT in $(seq 1 $MAX_ATTEMPTS); do
113
+ RESP=$(curl -s -H "Authorization: Bearer ${DEVAUDIT_API_KEY}" "${RESOLVE_URL}")
114
+ VERSION=$(echo "$RESP" | jq -r '.latest.version // empty')
115
+ if [ -n "$VERSION" ]; then
116
+ break
117
+ fi
118
+ if [ "$ATTEMPT" -lt "$MAX_ATTEMPTS" ]; then
119
+ echo "Attempt ${ATTEMPT}/${MAX_ATTEMPTS}: no release for ${DATE_PREFIX} yet — waiting ${WAIT_SECONDS}s..."
120
+ sleep $WAIT_SECONDS
121
+ fi
122
+ done
123
+ VERSION=$(echo "$RESP" | jq -r '.latest.version // empty')
124
+ if [ -z "$VERSION" ]; then
125
+ echo "::error::No release found for ${DATE_PREFIX} after ${MAX_ATTEMPTS} attempts. Push to develop first to create the release."
126
+ exit 1
127
+ fi
128
+ RELEASE_ID=$(echo "$RESP" | jq -r '.latest.id')
129
+ STATUS=$(echo "$RESP" | jq -r '.latest.status')
130
+ APPROVED_SHA=$(echo "$RESP" | jq -r '.latestApprovedSha // empty')
131
+ echo "release_id=${RELEASE_ID}" >> "$GITHUB_OUTPUT"
132
+ echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
133
+ echo "status=${STATUS}" >> "$GITHUB_OUTPUT"
134
+ echo "approved_sha=${APPROVED_SHA}" >> "$GITHUB_OUTPUT"
135
+ echo "Release ${VERSION} status: ${STATUS}"
136
+ case "$STATUS" in
137
+ uat_approved|prod_review|prod_approved|released)
138
+ echo "Release approval confirmed (status: ${STATUS})"
139
+ ;;
140
+ *)
141
+ echo "::error::Release ${VERSION} is '${STATUS}' — release approval required (have an authorised reviewer approve in DevAudit; in v1.22.x the backend status enum remains 'uat_approved')."
142
+ exit 1
143
+ ;;
144
+ esac
145
+
146
+ - name: Link PR to release
147
+ if: env.BOOTSTRAP_MODE != 'true' && github.event_name == 'pull_request'
148
+ run: |
149
+ RELEASE_ID="${{ steps.release.outputs.release_id }}"
150
+ PR_URL="${{ github.event.pull_request.html_url }}"
151
+ PR_NUMBER="${{ github.event.pull_request.number }}"
152
+ curl -s -o /dev/null -w "PR link: HTTP %{http_code}\n" \
153
+ -X PATCH "${BASE}/api/ci/releases/${RELEASE_ID}" \
154
+ -H "Authorization: Bearer ${DEVAUDIT_API_KEY}" \
155
+ -H "Content-Type: application/json" \
156
+ -d "{\"prInfo\":{\"url\":\"${PR_URL}\",\"number\":${PR_NUMBER}}}"
157
+ echo "Linked PR #${PR_NUMBER} to release ${RELEASE_ID}"
158
+
159
+ - name: Post release link on PR
160
+ if: env.BOOTSTRAP_MODE != 'true' && github.event_name == 'pull_request'
161
+ env:
162
+ GH_TOKEN: ${{ github.token }}
163
+ run: |
164
+ VERSION="${{ steps.release.outputs.version }}"
165
+ RELEASE_ID="${{ steps.release.outputs.release_id }}"
166
+ COMPLY_URL="${BASE}/projects/${PROJECT_SLUG}/releases/${RELEASE_ID}"
167
+ BODY="**DevAudit Release:** [${VERSION}](${COMPLY_URL})"
168
+ EXISTING=$(gh pr view ${{ github.event.pull_request.number }} --json comments --jq '.comments[] | select(.body | startswith("**DevAudit Release:**")) | .id' | head -1)
169
+ if [ -n "$EXISTING" ]; then
170
+ gh api repos/${{ github.repository }}/issues/comments/${EXISTING} -X PATCH -f body="${BODY}" || true
171
+ else
172
+ gh pr comment ${{ github.event.pull_request.number }} --body "${BODY}" || true
173
+ fi
174
+
175
+ - name: SHA comparison
176
+ if: env.BOOTSTRAP_MODE != 'true' && github.event_name == 'pull_request'
177
+ run: |
178
+ PR_HEAD_SHA="${{ github.event.pull_request.head.sha }}"
179
+ APPROVED_SHA="${{ steps.release.outputs.approved_sha }}"
180
+ if [ -n "$APPROVED_SHA" ] && [ "$APPROVED_SHA" != "$PR_HEAD_SHA" ]; then
181
+ echo "::warning::Approved SHA (${APPROVED_SHA:0:8}) differs from PR HEAD (${PR_HEAD_SHA:0:8}). Code may have changed after approval."
182
+ fi
183
+
184
+ - name: Update PR status (workflow_dispatch re-trigger)
185
+ if: env.BOOTSTRAP_MODE != 'true' && github.event_name == 'workflow_dispatch'
186
+ env:
187
+ GH_TOKEN: ${{ github.token }}
188
+ run: |
189
+ PR_DATA=$(gh pr list --base main --head develop --json headRefOid --limit 1 2>/dev/null || echo '[]')
190
+ PR_SHA=$(echo "$PR_DATA" | jq -r '.[0].headRefOid // empty' 2>/dev/null || true)
191
+ if [ -n "$PR_SHA" ]; then
192
+ gh api "repos/${{ github.repository }}/statuses/${PR_SHA}" \
193
+ -X POST \
194
+ -f state=success \
195
+ -f context="Release Approval Gate" \
196
+ -f description="DevAudit release approval confirmed" \
197
+ -f target_url="${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" \
198
+ || echo "Warning: Failed to post commit status"
199
+ else
200
+ echo "No open PR from develop to main found — skipping status update"
201
+ fi
@@ -0,0 +1,41 @@
1
+ # CI Status Fallback — emits the 'Quality Gates' status on docs/compliance-only commits
2
+ #
3
+ # Generated by `devaudit install` / `devaudit update` from sdlc-config.json.
4
+ # Do not edit manually — re-run the CLI (`devaudit update`) to regenerate.
5
+ #
6
+ # Why this exists:
7
+ # ci.yml uses `paths-ignore` to skip the heavy quality-gates job on docs/compliance-only
8
+ # commits. When such a commit is the HEAD of a develop→main PR, branch protection on
9
+ # main still requires the `Quality Gates` status check, so the PR sits in
10
+ # "Expected — Waiting for status to be reported" indefinitely.
11
+ #
12
+ # This workflow fires on EXACTLY the paths ci.yml ignores, and emits a passing
13
+ # `Quality Gates` status. Branch protection is satisfied without running the heavy gates
14
+ # on a commit that has no code in it.
15
+ #
16
+ # Mixed commits (some docs + some code) trigger BOTH workflows. Same status name, both
17
+ # green = branch protection happy.
18
+
19
+ name: CI Status Fallback
20
+
21
+ on:
22
+ workflow_dispatch:
23
+ push:
24
+ branches: [develop]
25
+ paths:
26
+ {{PATHS_IGNORE}} - 'sdlc-config.json'
27
+
28
+ concurrency:
29
+ group: ${{ github.workflow }}-${{ github.ref }}
30
+ cancel-in-progress: true
31
+
32
+ jobs:
33
+ quality-gates:
34
+ name: Quality Gates
35
+ runs-on: ubuntu-latest
36
+ steps:
37
+ - name: Acknowledge docs/compliance-only commit
38
+ run: |
39
+ echo "Commit ${{ github.sha }} only touches docs/compliance paths."
40
+ echo "Heavy quality-gates workflow (ci.yml) was correctly skipped."
41
+ echo "This fallback emits the 'Quality Gates' status so branch protection is satisfied."
@@ -0,0 +1,390 @@
1
+ # CI Pipeline — all gates on every code push to develop
2
+ #
3
+ # Generated by `devaudit install` / `devaudit update` from sdlc-config.json.
4
+ # Do not edit manually — re-run the CLI (`devaudit update`) to regenerate.
5
+ #
6
+ # Single consolidated job — on a self-hosted runner, parallel jobs run
7
+ # sequentially anyway. One checkout + one cached npm ci = fast.
8
+ #
9
+ # PRs to main inherit commit status via branch protection.
10
+ # Compliance validation runs separately on PRs (compliance-validation.yml).
11
+
12
+ name: CI Pipeline
13
+
14
+ on:
15
+ workflow_dispatch:
16
+ push:
17
+ branches: [develop]
18
+ paths-ignore:
19
+ {{PATHS_IGNORE}} - 'sdlc-config.json'
20
+
21
+ concurrency:
22
+ group: ${{ github.workflow }}-${{ github.ref }}
23
+ cancel-in-progress: true
24
+
25
+ jobs:
26
+ # ──────────────────────────────────────────────
27
+ # All quality gates in a single job — one checkout, cached installs
28
+ # ──────────────────────────────────────────────
29
+ quality-gates:
30
+ name: Quality Gates
31
+ runs-on: {{RUNNER}}
32
+
33
+ services:
34
+ {{DATABASE_SERVICE}}:
35
+ image: {{DATABASE_IMAGE}}
36
+ ports:
37
+ - {{DATABASE_PORT}}
38
+
39
+ env:
40
+ {{DATABASE_ENV}}
41
+ {{APP_ENV}}
42
+
43
+ steps:
44
+ - uses: actions/checkout@v4
45
+
46
+ # ── Cached installs (skip if already present on self-hosted runner) ──
47
+
48
+ - uses: actions/setup-node@v4
49
+ with:
50
+ node-version: {{NODE_VERSION}}
51
+
52
+ - name: Install dependencies (skip if lockfile unchanged)
53
+ run: |
54
+ LOCK_HASH=$(sha256sum package-lock.json | cut -d' ' -f1)
55
+ if [ -f node_modules/.lock-hash ] && [ "$(cat node_modules/.lock-hash)" = "$LOCK_HASH" ]; then
56
+ echo "node_modules up to date — skipping npm ci"
57
+ else
58
+ npm ci
59
+ echo "$LOCK_HASH" > node_modules/.lock-hash
60
+ fi
61
+
62
+ - name: Install Semgrep (skip if already installed)
63
+ run: |
64
+ VENV="$HOME/.semgrep-venv"
65
+ if [ -x "$VENV/bin/semgrep" ]; then
66
+ echo "Semgrep already installed"
67
+ else
68
+ python3 -m venv "$VENV"
69
+ "$VENV/bin/pip" install semgrep
70
+ fi
71
+ echo "$VENV/bin" >> "$GITHUB_PATH"
72
+
73
+ - name: Install Playwright (skip if already cached)
74
+ run: npx playwright install --with-deps chromium 2>/dev/null || npx playwright install chromium
75
+
76
+ # ── Gate 1: TypeScript ──
77
+
78
+ - name: TypeScript Check
79
+ run: npx tsc --noEmit
80
+
81
+ # ── Gate 2: SAST (Semgrep) ──
82
+
83
+ - name: SAST Scan
84
+ run: |
85
+ semgrep scan --config auto {{SOURCE_DIRS}} \
86
+ --severity ERROR --severity WARNING \
87
+ --json > sast-results.json 2>&1 || true
88
+ FINDINGS=$(python3 -c "
89
+ import json
90
+ with open('sast-results.json') as f:
91
+ data = json.load(f)
92
+ results = data.get('results', [])
93
+ print(len(results))
94
+ " 2>/dev/null || echo "0")
95
+ echo "SAST findings: $FINDINGS"
96
+ BASELINE={{SAST_BASELINE}}
97
+ if [ "$FINDINGS" -gt "$BASELINE" ]; then
98
+ echo "::error::New SAST findings ($FINDINGS > baseline $BASELINE)."
99
+ exit 1
100
+ fi
101
+
102
+ # ── Gate 3: Dependency Audit ──
103
+
104
+ - name: Dependency Audit
105
+ run: |
106
+ npm audit --json > dependency-audit.json 2>&1 || true
107
+ ACCEPTED="{{ACCEPTED_DEP_RISKS}}"
108
+ UNACCEPTED=$(python3 -c "
109
+ import json
110
+ with open('dependency-audit.json') as f:
111
+ data = json.load(f)
112
+ accepted = set('${ACCEPTED}'.split()) if '${ACCEPTED}' else set()
113
+ vulns = data.get('vulnerabilities', {})
114
+ issues = [name for name, v in vulns.items()
115
+ if v.get('severity') in ('high', 'critical')
116
+ and name not in accepted]
117
+ print(len(issues))
118
+ " 2>/dev/null || echo "unknown")
119
+ echo "Unaccepted high/critical: $UNACCEPTED"
120
+ if [ "$UNACCEPTED" != "0" ] && [ "$UNACCEPTED" != "unknown" ]; then
121
+ echo "::error::$UNACCEPTED unaccepted high/critical vulnerability(ies)."
122
+ exit 1
123
+ fi
124
+
125
+ # ── Gate 4: E2E Tests (Playwright) ──
126
+
127
+ {{DATABASE_URI_STEP}}
128
+
129
+ - name: Kill stale dev server
130
+ run: lsof -ti:3000 | xargs kill -9 2>/dev/null || true
131
+
132
+ - name: Start dev server
133
+ run: {{E2E_START_COMMAND}} &
134
+
135
+ - name: Wait for dev server
136
+ run: npx wait-on http://localhost:3000 --timeout 120000
137
+
138
+ - name: E2E Tests
139
+ run: |
140
+ PLAYWRIGHT_HTML_REPORTER_OPEN=never npx playwright test --project={{E2E_PROJECT}} --reporter=json,html > e2e-results.json 2>/dev/null \
141
+ || PLAYWRIGHT_HTML_REPORTER_OPEN=never npx playwright test --project={{E2E_PROJECT}} --reporter=html
142
+
143
+ # ── Gate 5: Build ──
144
+
145
+ - name: Build Check
146
+ run: npm run build
147
+ env:
148
+ {{BUILD_ENV}}
149
+
150
+ # ── Upload artifacts ──
151
+
152
+ - uses: actions/upload-artifact@v4
153
+ if: always()
154
+ continue-on-error: true
155
+ with:
156
+ name: ci-results
157
+ path: |
158
+ sast-results.json
159
+ dependency-audit.json
160
+ e2e-results.json
161
+ playwright-report/
162
+ coverage/coverage-summary.json
163
+ retention-days: 90
164
+
165
+ # ──────────────────────────────────────────────
166
+ # Register release early (parallel with gates) so UAT gate can find it
167
+ # ──────────────────────────────────────────────
168
+ register-release:
169
+ name: Register Release
170
+ runs-on: {{RUNNER}}
171
+ if: ${{ vars.DEVAUDIT_BASE_URL != '' }}
172
+ outputs:
173
+ version: ${{ steps.version.outputs.version }}
174
+ env:
175
+ DEVAUDIT_BASE_URL: ${{ vars.DEVAUDIT_BASE_URL }}
176
+ DEVAUDIT_API_KEY: ${{ secrets.DEVAUDIT_API_KEY }}
177
+ steps:
178
+ - uses: actions/checkout@v4
179
+
180
+ - name: Validate DevAudit env
181
+ run: |
182
+ if [ -z "${DEVAUDIT_BASE_URL}" ] || [ -z "${DEVAUDIT_API_KEY}" ]; then
183
+ echo "::error::DEVAUDIT_BASE_URL (variable) and DEVAUDIT_API_KEY (secret) must both be set."
184
+ exit 1
185
+ fi
186
+ echo "BASE=${DEVAUDIT_BASE_URL%/}" >> "$GITHUB_ENV"
187
+
188
+ - name: Determine release version
189
+ id: version
190
+ run: |
191
+ # Release identity is the REQ tag on the latest commit
192
+ # ([REQ-XXX] in subject, Ref: REQ-XXX in body), with a bare-date
193
+ # fallback for housekeeping commits. Both this workflow and
194
+ # compliance-evidence.yml call the same helper so a single
195
+ # feature converges on one release record. See DevAudit #310.
196
+ chmod +x scripts/derive-release-version.sh 2>/dev/null || true
197
+ VERSION=$(./scripts/derive-release-version.sh)
198
+ echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
199
+ echo "Release version: ${VERSION}"
200
+
201
+ - name: Ensure release exists
202
+ run: |
203
+ chmod +x scripts/upload-evidence.sh 2>/dev/null || true
204
+ # Create the release in DevAudit (no evidence yet — just registration)
205
+ bash scripts/upload-evidence.sh \
206
+ {{PROJECT_SLUG}} _compliance-docs compliance_document README.md \
207
+ --release ${{ steps.version.outputs.version }} --create-release-if-missing \
208
+ --environment uat --category planning \
209
+ --git-sha ${{ github.sha }} --branch ${{ github.ref_name }} || true
210
+
211
+ - name: Sync known requirements from RTM
212
+ run: |
213
+ if [ -f "compliance/RTM.md" ]; then
214
+ REQS=$(grep -oP 'REQ-\d+' compliance/RTM.md | sort -t- -k2 -n -u)
215
+ if [ -n "$REQS" ]; then
216
+ JSON_ARRAY=$(echo "$REQS" | jq -R -s -c 'split("\n") | map(select(length > 0))')
217
+ HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" \
218
+ -X PATCH "${BASE}/api/ci/projects/{{PROJECT_SLUG}}/known-requirements" \
219
+ -H "Authorization: Bearer ${DEVAUDIT_API_KEY}" \
220
+ -H "Content-Type: application/json" \
221
+ -d "{\"requirements\": ${JSON_ARRAY}}")
222
+ echo "known_requirements sync: HTTP ${HTTP_CODE}"
223
+ echo "Synced $(echo "$REQS" | wc -w) requirements from RTM.md"
224
+ fi
225
+ fi
226
+
227
+ # ──────────────────────────────────────────────
228
+ # Upload Evidence to DevAudit (after gates pass)
229
+ # ──────────────────────────────────────────────
230
+ upload-evidence:
231
+ name: Upload Evidence
232
+ runs-on: {{RUNNER}}
233
+ needs: [quality-gates, register-release]
234
+ if: ${{ !failure() && !cancelled() && vars.DEVAUDIT_BASE_URL != '' }}
235
+ env:
236
+ DEVAUDIT_BASE_URL: ${{ vars.DEVAUDIT_BASE_URL }}
237
+ DEVAUDIT_API_KEY: ${{ secrets.DEVAUDIT_API_KEY }}
238
+ steps:
239
+ - uses: actions/checkout@v4
240
+
241
+ - name: Download CI gate artifacts
242
+ uses: actions/download-artifact@v4
243
+ continue-on-error: true
244
+ with:
245
+ name: ci-results
246
+ path: ci-evidence/
247
+
248
+ - name: Generate and upload gate evidence
249
+ if: env.DEVAUDIT_BASE_URL != ''
250
+ run: |
251
+ chmod +x scripts/upload-evidence.sh 2>/dev/null || true
252
+ FLAGS="--git-sha ${{ github.sha }} --ci-run-id ${{ github.run_id }} --branch ${{ github.ref_name }}"
253
+ FLAGS="${FLAGS} --release ${{ needs.register-release.outputs.version }} --create-release-if-missing"
254
+ FLAGS="${FLAGS} --environment uat"
255
+
256
+ # Track failures across all uploads so we can fail the job at the
257
+ # end with the full picture. Previously each upload used
258
+ # `|| echo "Warning: ..."` which masked every error as a benign
259
+ # warning, letting broken releases ship without gate evidence
260
+ # (DevAudit #132).
261
+ UPLOAD_FAILURES=0
262
+ upload() {
263
+ local label="$1"; shift
264
+ echo "Uploading: ${label}"
265
+ if ! bash scripts/upload-evidence.sh "$@"; then
266
+ echo "::error::Failed to upload ${label}"
267
+ UPLOAD_FAILURES=$((UPLOAD_FAILURES + 1))
268
+ fi
269
+ }
270
+
271
+ # Re-generate gate evidence as fallback if artifact download failed
272
+ mkdir -p ci-evidence
273
+ if [ ! -f ci-evidence/sast-results.json ]; then
274
+ VENV="$HOME/.semgrep-venv"
275
+ if [ -x "$VENV/bin/semgrep" ]; then
276
+ "$VENV/bin/semgrep" scan --config auto {{SOURCE_DIRS}} --json > ci-evidence/sast-results.json 2>/dev/null || echo '{"results":[]}' > ci-evidence/sast-results.json
277
+ else
278
+ echo '{"results":[]}' > ci-evidence/sast-results.json
279
+ fi
280
+ fi
281
+ if [ ! -f ci-evidence/dependency-audit.json ]; then
282
+ npm audit --json > ci-evidence/dependency-audit.json 2>/dev/null || echo '{"vulnerabilities":{}}' > ci-evidence/dependency-audit.json
283
+ fi
284
+
285
+ # Upload SAST results (security_scan category)
286
+ if [ -f ci-evidence/sast-results.json ]; then
287
+ upload sast-results.json \
288
+ {{PROJECT_SLUG}} _compliance-docs audit_log ci-evidence/sast-results.json \
289
+ --category security_scan ${FLAGS}
290
+ fi
291
+
292
+ # Upload dependency audit (security_scan category)
293
+ if [ -f ci-evidence/dependency-audit.json ]; then
294
+ upload dependency-audit.json \
295
+ {{PROJECT_SLUG}} _compliance-docs audit_log ci-evidence/dependency-audit.json \
296
+ --category security_scan ${FLAGS}
297
+ fi
298
+
299
+ # Upload E2E test results (ci_pipeline category)
300
+ if [ -f ci-evidence/e2e-results.json ]; then
301
+ upload e2e-results.json \
302
+ {{PROJECT_SLUG}} _compliance-docs e2e_result ci-evidence/e2e-results.json \
303
+ --category ci_pipeline ${FLAGS}
304
+ fi
305
+
306
+ # Upload Playwright HTML report (test_report category)
307
+ if [ -d ci-evidence/playwright-report ]; then
308
+ (cd ci-evidence && zip -qr playwright-report.zip playwright-report/) 2>/dev/null || true
309
+ if [ -f ci-evidence/playwright-report.zip ]; then
310
+ upload playwright-report.zip \
311
+ {{PROJECT_SLUG}} _compliance-docs test_report ci-evidence/playwright-report.zip \
312
+ --category test_report ${FLAGS}
313
+ fi
314
+ fi
315
+
316
+ # Upload Jest coverage summary (test_report category)
317
+ if [ -f ci-evidence/coverage/coverage-summary.json ]; then
318
+ upload coverage-summary.json \
319
+ {{PROJECT_SLUG}} _compliance-docs test_report ci-evidence/coverage/coverage-summary.json \
320
+ --category test_report ${FLAGS}
321
+ fi
322
+
323
+ # Upload test summary report (test_report category)
324
+ if [ -f "compliance/test-summary-report.md" ]; then
325
+ upload test-summary-report.md \
326
+ {{PROJECT_SLUG}} _compliance-docs compliance_document compliance/test-summary-report.md \
327
+ --category test_report ${FLAGS}
328
+ fi
329
+
330
+ # Upload compliance docs (planning category)
331
+ for DOC in compliance/RTM.md compliance/test-plan.md compliance/test-cases.md; do
332
+ if [ -f "$DOC" ]; then
333
+ upload "$(basename "$DOC")" \
334
+ {{PROJECT_SLUG}} _compliance-docs compliance_document "$DOC" \
335
+ --category planning ${FLAGS}
336
+ fi
337
+ done
338
+
339
+ # Upload release tickets (pending only — approved releases are historical)
340
+ for DIR in compliance/pending-releases; do
341
+ if [ -d "$DIR" ]; then
342
+ for TICKET in "$DIR"/*.md; do
343
+ [ -f "$TICKET" ] || continue
344
+ upload "$(basename "$TICKET")" \
345
+ {{PROJECT_SLUG}} _compliance-docs compliance_document "$TICKET" \
346
+ --category release_artifact ${FLAGS}
347
+ done
348
+ fi
349
+ done
350
+
351
+ # Upload per-requirement evidence — scoped to requirements with a
352
+ # pending release ticket. Without this scoping every historical
353
+ # compliance/evidence/REQ-*/ folder would be re-uploaded on every
354
+ # run, re-populating the release-requirement matrix with the full
355
+ # project catalogue (DevAudit #133).
356
+ IN_SCOPE_REQS=()
357
+ if [ -d compliance/pending-releases ]; then
358
+ for TICKET in compliance/pending-releases/RELEASE-TICKET-REQ-*.md; do
359
+ [ -f "$TICKET" ] || continue
360
+ REQ_ID=$(basename "$TICKET" .md | sed 's/^RELEASE-TICKET-//')
361
+ IN_SCOPE_REQS+=("$REQ_ID")
362
+ done
363
+ fi
364
+
365
+ if [ ${#IN_SCOPE_REQS[@]} -eq 0 ]; then
366
+ echo "No pending release tickets found — skipping per-requirement evidence upload"
367
+ else
368
+ echo "In-scope requirements for this release: ${IN_SCOPE_REQS[*]}"
369
+ for REQ_ID in "${IN_SCOPE_REQS[@]}"; do
370
+ REQ_DIR="compliance/evidence/${REQ_ID}/"
371
+ if [ ! -d "$REQ_DIR" ]; then
372
+ echo "Warning: pending ticket for ${REQ_ID} but no ${REQ_DIR} on disk"
373
+ continue
374
+ fi
375
+ for ARTIFACT in "$REQ_DIR"*.md; do
376
+ [ -f "$ARTIFACT" ] || continue
377
+ upload "${REQ_ID}/$(basename "$ARTIFACT")" \
378
+ {{PROJECT_SLUG}} "${REQ_ID}" compliance_document "$ARTIFACT" \
379
+ --category planning ${FLAGS}
380
+ done
381
+ done
382
+ fi
383
+
384
+ if [ "$UPLOAD_FAILURES" -gt 0 ]; then
385
+ echo "::error::${UPLOAD_FAILURES} evidence upload(s) failed — release is missing gate evidence and cannot pass UAT review"
386
+ exit 1
387
+ fi
388
+
389
+ - name: Summary
390
+ run: echo "Evidence uploaded for ${{ needs.register-release.outputs.version }} (UAT)"