@metasession.co/devaudit-cli 0.1.0 → 0.1.2
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/README.md +23 -11
- package/dist/index.js +21 -5
- package/dist/index.js.map +1 -1
- package/package.json +9 -5
- package/scripts/upload-evidence.sh +225 -0
- package/sdlc/.claude/settings.local.json +11 -0
- package/sdlc/CLAUDE.md +73 -0
- package/sdlc/HOST_ADAPTER.md +127 -0
- package/sdlc/SKILLS.md +137 -0
- package/sdlc/STACK_ADAPTER.md +130 -0
- package/sdlc/ai-rules/INSTRUCTIONS-SDLC.md +172 -0
- package/sdlc/ai-rules/README.md +103 -0
- package/sdlc/ai-rules/SDLC_RULES.md +584 -0
- package/sdlc/ai-rules/claude/CLAUDE.md +192 -0
- package/sdlc/ai-rules/cursor/.cursorrules +167 -0
- package/sdlc/ai-rules/windsurf/.windsurfrules +167 -0
- package/sdlc/article.md +219 -0
- package/sdlc/files/_common/0-project-setup.md +410 -0
- package/sdlc/files/_common/1-plan-requirement.md +381 -0
- package/sdlc/files/_common/2-implement-and-test.md +276 -0
- package/sdlc/files/_common/3-compile-evidence.md +603 -0
- package/sdlc/files/_common/4-submit-for-review.md +362 -0
- package/sdlc/files/_common/5-deploy-main.md +251 -0
- package/sdlc/files/_common/Periodic_Security_Review_Schedule.md +169 -0
- package/sdlc/files/_common/README_TEMPLATE.md +441 -0
- package/sdlc/files/_common/Test_Architecture.md +461 -0
- package/sdlc/files/_common/Test_Plan_TEMPLATE.md +311 -0
- package/sdlc/files/_common/Test_Policy.md +277 -0
- package/sdlc/files/_common/Test_Strategy.md +359 -0
- package/sdlc/files/_common/github/ISSUE_TEMPLATE/bug.yml +75 -0
- package/sdlc/files/_common/github/ISSUE_TEMPLATE/config.yml +11 -0
- package/sdlc/files/_common/github/ISSUE_TEMPLATE/requirement.yml +75 -0
- package/sdlc/files/_common/github/ISSUE_TEMPLATE/task.yml +48 -0
- package/sdlc/files/_common/github/pull_request_template.md +69 -0
- package/sdlc/files/_common/implementing-an-sdlc-issue.md +413 -0
- package/sdlc/files/_common/scripts/derive-release-version.sh +40 -0
- package/sdlc/files/_common/scripts/derive-release-version.test.sh +98 -0
- package/sdlc/files/_common/scripts/submit-for-uat-review.sh +162 -0
- package/sdlc/files/_common/scripts/validate-commits.sh +83 -0
- package/sdlc/files/_common/scripts/validate-compliance-artifacts.sh +202 -0
- package/sdlc/files/_common/scripts/validate-compliance-artifacts.test.sh +202 -0
- package/sdlc/files/_common/skills/_schema/skill.schema.json +36 -0
- package/sdlc/files/_common/skills/e2e-test-engineer/SKILL.md +254 -0
- package/sdlc/files/_common/skills/e2e-test-engineer/references/bootstrap.md +244 -0
- package/sdlc/files/_common/skills/e2e-test-engineer/references/evidence.ts +40 -0
- package/sdlc/files/_common/skills/sdlc-implementer/SKILL.md +189 -0
- package/sdlc/files/_common/skills/sdlc-implementer/references/call-graph.md +64 -0
- package/sdlc/files/_common/skills/sdlc-implementer/references/change-request-loop.md +192 -0
- package/sdlc/files/_common/skills/sdlc-implementer/references/compliance-constraints.md +81 -0
- package/sdlc/files/ci/check-release-approval.yml.template +201 -0
- package/sdlc/files/ci/ci-status-fallback.yml.template +41 -0
- package/sdlc/files/ci/ci.yml.template +390 -0
- package/sdlc/files/ci/compliance-evidence.yml.template +161 -0
- package/sdlc/files/ci/compliance-validation.yml.template +34 -0
- package/sdlc/files/ci/post-deploy-prod.yml.template +159 -0
- package/sdlc/files/ci/python/ci.yml.template +335 -0
- package/sdlc/files/hosts/_schema/adapter.schema.json +103 -0
- package/sdlc/files/hosts/railway/adapter.json +32 -0
- package/sdlc/files/sdlc-config.example.json +74 -0
- package/sdlc/files/stacks/_schema/adapter.schema.json +151 -0
- package/sdlc/files/stacks/node/adapter.json +54 -0
- package/sdlc/files/stacks/node/hooks/.prettierrc.json +9 -0
- package/sdlc/files/stacks/node/hooks/commit-msg +7 -0
- package/sdlc/files/stacks/node/hooks/commitlint.config.mjs +64 -0
- package/sdlc/files/stacks/node/hooks/lint-staged.config.mjs +16 -0
- package/sdlc/files/stacks/node/hooks/pre-commit +13 -0
- package/sdlc/files/stacks/node/hooks/pre-push +15 -0
- package/sdlc/files/stacks/node/scripts/check-requirement-jsdoc.sh +54 -0
- package/sdlc/files/stacks/python/adapter.json +36 -0
- 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)"
|