@metasession.co/devaudit-cli 0.3.12 → 0.3.14
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 +53 -4
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/scripts/upload-evidence.sh +55 -1
- package/sdlc/files/_common/0-project-setup.md +10 -4
- package/sdlc/files/_common/Periodic_Security_Review_Schedule.md +5 -5
- package/sdlc/files/_common/README_TEMPLATE.md +8 -8
- package/sdlc/files/_common/Test_Architecture.md +15 -15
- package/sdlc/files/_common/Test_Plan_TEMPLATE.md +13 -13
- package/sdlc/files/_common/Test_Policy.md +2 -2
- package/sdlc/files/_common/Test_Strategy.md +10 -10
- package/sdlc/files/_common/governance/ai-disclosure.md.template +3 -3
- package/sdlc/files/_common/governance/dpia.md.template +3 -3
- package/sdlc/files/_common/governance/incident-report.md.template +3 -3
- package/sdlc/files/_common/governance/nil-incident-report.md.template +1 -1
- package/sdlc/files/_common/governance/periodic-review.md.template +3 -3
- package/sdlc/files/_common/governance/risk-register.md.template +1 -1
- package/sdlc/files/_common/governance/ropa.md.template +2 -2
- package/sdlc/files/_common/joining-an-existing-project.md +4 -4
- package/sdlc/files/_common/scripts/check-host-deployment.sh +107 -0
- package/sdlc/files/_common/scripts/check-host-deployment.test.sh +119 -0
- package/sdlc/files/_common/scripts/check-release-pr-scope.sh +95 -0
- package/sdlc/files/_common/scripts/check-release-pr-scope.test.sh +137 -0
- package/sdlc/files/_common/scripts/close-out-release.sh +175 -54
- package/sdlc/files/_common/scripts/close-out-release.test.sh +90 -0
- package/sdlc/files/_common/scripts/extract-release-metadata.sh +27 -0
- package/sdlc/files/_common/scripts/extract-release-metadata.test.sh +13 -1
- package/sdlc/files/_common/scripts/generate-bundled-changes.sh +311 -64
- package/sdlc/files/_common/scripts/generate-bundled-changes.test.sh +133 -82
- package/sdlc/files/_common/scripts/generate-housekeeping-release-ticket.sh +18 -9
- package/sdlc/files/_common/scripts/generate-housekeeping-release-ticket.test.sh +66 -0
- package/sdlc/files/_common/scripts/generate-security-summary.sh +14 -5
- package/sdlc/files/_common/scripts/generate-security-summary.test.sh +57 -0
- package/sdlc/files/_common/scripts/prepare-release-pr.sh +44 -4
- package/sdlc/files/_common/scripts/render-test-cycles.sh +227 -0
- package/sdlc/files/_common/scripts/render-test-cycles.test.sh +107 -0
- package/sdlc/files/_common/scripts/report-release-check.sh +86 -0
- package/sdlc/files/_common/scripts/report-release-check.test.sh +97 -0
- package/sdlc/files/_common/scripts/report-test-cycle.sh +344 -0
- package/sdlc/files/_common/scripts/submit-bundle-manifest.sh +100 -0
- package/sdlc/files/_common/scripts/submit-bundle-manifest.test.sh +138 -0
- package/sdlc/files/_common/scripts/upload-evidence.sh +24 -0
- package/sdlc/files/_common/scripts/validate-compliance-artifacts.sh +16 -0
- package/sdlc/files/_common/scripts/validate-compliance-artifacts.test.sh +14 -0
- package/sdlc/files/_common/scripts/validate-test-summary.test.sh +4 -4
- package/sdlc/files/_common/skills/adr-author/SKILL.md +2 -2
- package/sdlc/files/_common/skills/e2e-test-engineer/SKILL.md +2 -2
- package/sdlc/files/_common/skills/e2e-test-engineer/references/bootstrap.md +5 -5
- package/sdlc/files/_common/skills/governance-doc-author/references/incident-classification.md +1 -1
- package/sdlc/files/_common/skills/requirements-aligner/SKILL.md +4 -4
- package/sdlc/files/_common/skills/sdlc-implementer/SKILL.md +21 -2
- package/sdlc/files/_common/skills/sdlc-implementer/references/change-request-loop.md +1 -1
- package/sdlc/files/_common/skills/sdlc-implementer/references/compliance-constraints.md +7 -14
- package/sdlc/files/ci/ci.yml.template +119 -28
- package/sdlc/files/ci/close-out-release.yml.template +7 -4
- package/sdlc/files/ci/compliance-evidence.yml.template +230 -20
- package/sdlc/files/ci/feature-e2e.yml.template +84 -1
- package/sdlc/files/ci/post-deploy-prod.yml.template +189 -6
- package/sdlc/files/ci/python/ci.yml.template +60 -12
- package/sdlc/files/ci/quality-gates-provenance.yml.template +123 -28
- package/sdlc/package.json +1 -1
- package/sdlc/src/bin/devaudit-sdlc.js +44 -2
- package/sdlc/src/blueprints/1-plan-requirement.raw.md +6 -1
- package/sdlc/src/blueprints/2-implement-and-test.raw.md +1 -1
- package/sdlc/src/blueprints/3-compile-evidence.raw.md +34 -20
- package/sdlc/src/blueprints/4-submit-for-review.raw.md +2 -2
- package/sdlc/src/blueprints/5-deploy-main.raw.md +2 -2
|
@@ -36,6 +36,7 @@ jobs:
|
|
|
36
36
|
runs-on: {{RUNNER}}
|
|
37
37
|
permissions:
|
|
38
38
|
contents: read
|
|
39
|
+
deployments: read
|
|
39
40
|
issues: write
|
|
40
41
|
env:
|
|
41
42
|
DEVAUDIT_BASE_URL_VAR: ${{ vars.DEVAUDIT_BASE_URL }}
|
|
@@ -116,19 +117,119 @@ jobs:
|
|
|
116
117
|
echo "In-scope releases to promote: ${REQS}"
|
|
117
118
|
echo "REQS=${REQS}" >> "$GITHUB_ENV"
|
|
118
119
|
|
|
120
|
+
- name: Start production deployment cycles
|
|
121
|
+
run: |
|
|
122
|
+
chmod +x scripts/report-test-cycle.sh 2>/dev/null || true
|
|
123
|
+
for PREFIX in ${REQS}; do
|
|
124
|
+
RESP=$(curl -fsS -H "Authorization: Bearer ${DEVAUDIT_API_KEY}" \
|
|
125
|
+
"${BASE}/api/ci/releases/resolve?projectSlug=${PROJECT_SLUG}&versionPrefix=${PREFIX}")
|
|
126
|
+
VERSION=$(echo "$RESP" | jq -r '.latest.version // empty')
|
|
127
|
+
[ -n "$VERSION" ] || VERSION="$PREFIX"
|
|
128
|
+
bash scripts/report-test-cycle.sh start \
|
|
129
|
+
--project-slug "$PROJECT_SLUG" --release "$VERSION" \
|
|
130
|
+
--sdlc-stage 5 --environment production --cycle-kind deployment \
|
|
131
|
+
--provider github_actions --external-run-id "$CI_RUN" \
|
|
132
|
+
--external-run-attempt "${{ github.run_attempt }}" --external-job-id "host-deployment" \
|
|
133
|
+
--idempotency-key "github:${{ github.repository }}:post-deploy-prod.deployment:${CI_RUN}:${{ github.run_attempt }}:5:${VERSION}" \
|
|
134
|
+
--commit-sha "$GIT_SHA" --branch main \
|
|
135
|
+
--workflow-name "Post-Deploy Production" \
|
|
136
|
+
--workflow-url "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
|
|
137
|
+
done
|
|
138
|
+
|
|
119
139
|
- name: Wait for production deployment
|
|
140
|
+
id: wait_deployment
|
|
120
141
|
run: |
|
|
142
|
+
DEPLOY_READY=false
|
|
121
143
|
for i in $(seq 1 30); do
|
|
122
144
|
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" "${PROD_URL}/" || echo "000")
|
|
123
145
|
if [ "$HTTP_CODE" -ge 200 ] && [ "$HTTP_CODE" -lt 400 ]; then
|
|
124
146
|
echo "Production is up (HTTP ${HTTP_CODE})"
|
|
147
|
+
DEPLOY_READY=true
|
|
125
148
|
break
|
|
126
149
|
fi
|
|
127
150
|
echo "Attempt ${i}/30: HTTP ${HTTP_CODE} — waiting 10s..."
|
|
128
151
|
sleep 10
|
|
129
152
|
done
|
|
153
|
+
if [ "$DEPLOY_READY" != "true" ]; then
|
|
154
|
+
echo "::error::Production did not become ready within the deployment window"
|
|
155
|
+
exit 1
|
|
156
|
+
fi
|
|
157
|
+
|
|
158
|
+
- name: Confirm terminal host deployment success
|
|
159
|
+
id: host_deployment
|
|
160
|
+
if: steps.wait_deployment.outcome == 'success'
|
|
161
|
+
env:
|
|
162
|
+
GH_TOKEN: ${{ github.token }}
|
|
163
|
+
run: |
|
|
164
|
+
chmod +x scripts/check-host-deployment.sh 2>/dev/null || true
|
|
165
|
+
bash scripts/check-host-deployment.sh \
|
|
166
|
+
--repo="${{ github.repository }}" \
|
|
167
|
+
--sha="${GIT_SHA}" \
|
|
168
|
+
--max-attempts=30 \
|
|
169
|
+
--poll-seconds=10
|
|
170
|
+
|
|
171
|
+
- name: Complete production deployment cycles
|
|
172
|
+
if: always() && env.BASE != ''
|
|
173
|
+
run: |
|
|
174
|
+
if [ "${{ steps.wait_deployment.outcome }}" = "success" ] && [ "${{ steps.host_deployment.outcome }}" = "success" ]; then
|
|
175
|
+
OUTCOME=passed
|
|
176
|
+
CHECK_STATUS=successful
|
|
177
|
+
elif [ "${{ steps.wait_deployment.outcome }}" = "cancelled" ] || [ "${{ steps.host_deployment.outcome }}" = "cancelled" ]; then
|
|
178
|
+
OUTCOME=cancelled
|
|
179
|
+
CHECK_STATUS=cancelled
|
|
180
|
+
else
|
|
181
|
+
OUTCOME=failed
|
|
182
|
+
CHECK_STATUS=failed
|
|
183
|
+
fi
|
|
184
|
+
chmod +x scripts/report-test-cycle.sh scripts/report-release-check.sh 2>/dev/null || true
|
|
185
|
+
for PREFIX in ${REQS}; do
|
|
186
|
+
RESP=$(curl -fsS -H "Authorization: Bearer ${DEVAUDIT_API_KEY}" \
|
|
187
|
+
"${BASE}/api/ci/releases/resolve?projectSlug=${PROJECT_SLUG}&versionPrefix=${PREFIX}")
|
|
188
|
+
VERSION=$(echo "$RESP" | jq -r '.latest.version // empty')
|
|
189
|
+
[ -n "$VERSION" ] || VERSION="$PREFIX"
|
|
190
|
+
bash scripts/report-test-cycle.sh complete \
|
|
191
|
+
--project-slug "$PROJECT_SLUG" --release "$VERSION" \
|
|
192
|
+
--sdlc-stage 5 --environment production --cycle-kind deployment \
|
|
193
|
+
--provider github_actions --external-run-id "$CI_RUN" \
|
|
194
|
+
--external-run-attempt "${{ github.run_attempt }}" --external-job-id "host-deployment" \
|
|
195
|
+
--idempotency-key "github:${{ github.repository }}:post-deploy-prod.deployment:${CI_RUN}:${{ github.run_attempt }}:5:${VERSION}" \
|
|
196
|
+
--commit-sha "$GIT_SHA" --branch main \
|
|
197
|
+
--workflow-name "Post-Deploy Production" \
|
|
198
|
+
--workflow-url "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" \
|
|
199
|
+
--outcome "$OUTCOME" --outcome-reason "host deployment: ${{ steps.host_deployment.outcome }}"
|
|
200
|
+
bash scripts/report-release-check.sh \
|
|
201
|
+
--project-slug "$PROJECT_SLUG" --release "$VERSION" \
|
|
202
|
+
--check-key "production-deployment:${CI_RUN}:${{ github.run_attempt }}" \
|
|
203
|
+
--label "Production Deployment" --provider github_actions --status "$CHECK_STATUS" \
|
|
204
|
+
--external-run-id "$CI_RUN" \
|
|
205
|
+
--external-url "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" \
|
|
206
|
+
--commit-sha "$GIT_SHA" --branch main \
|
|
207
|
+
--details-json '{"waitOutcome":"${{ steps.wait_deployment.outcome }}","hostOutcome":"${{ steps.host_deployment.outcome }}"}'
|
|
208
|
+
done
|
|
209
|
+
|
|
210
|
+
- name: Start production smoke cycles
|
|
211
|
+
if: steps.wait_deployment.outcome == 'success' && steps.host_deployment.outcome == 'success'
|
|
212
|
+
run: |
|
|
213
|
+
chmod +x scripts/report-test-cycle.sh 2>/dev/null || true
|
|
214
|
+
for PREFIX in ${REQS}; do
|
|
215
|
+
RESP=$(curl -fsS -H "Authorization: Bearer ${DEVAUDIT_API_KEY}" \
|
|
216
|
+
"${BASE}/api/ci/releases/resolve?projectSlug=${PROJECT_SLUG}&versionPrefix=${PREFIX}")
|
|
217
|
+
VERSION=$(echo "$RESP" | jq -r '.latest.version // empty')
|
|
218
|
+
[ -n "$VERSION" ] || VERSION="$PREFIX"
|
|
219
|
+
bash scripts/report-test-cycle.sh start \
|
|
220
|
+
--project-slug "$PROJECT_SLUG" --release "$VERSION" \
|
|
221
|
+
--sdlc-stage 5 --environment production --cycle-kind smoke \
|
|
222
|
+
--provider github_actions --external-run-id "$CI_RUN" \
|
|
223
|
+
--external-run-attempt "${{ github.run_attempt }}" --external-job-id "production-smoke" \
|
|
224
|
+
--idempotency-key "github:${{ github.repository }}:post-deploy-prod.smoke:${CI_RUN}:${{ github.run_attempt }}:5:${VERSION}" \
|
|
225
|
+
--commit-sha "$GIT_SHA" --branch main \
|
|
226
|
+
--workflow-name "Post-Deploy Production" \
|
|
227
|
+
--workflow-url "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
|
|
228
|
+
done
|
|
130
229
|
|
|
131
230
|
- name: Production smoke tests (read-only)
|
|
231
|
+
id: production_smoke
|
|
232
|
+
if: steps.wait_deployment.outcome == 'success' && steps.host_deployment.outcome == 'success'
|
|
132
233
|
run: |
|
|
133
234
|
echo "=== Production Smoke Tests ==="
|
|
134
235
|
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" "${PROD_URL}/")
|
|
@@ -151,8 +252,43 @@ jobs:
|
|
|
151
252
|
}
|
|
152
253
|
RESULTS_EOF
|
|
153
254
|
|
|
255
|
+
- name: Complete production smoke cycles
|
|
256
|
+
if: always() && env.BASE != '' && steps.host_deployment.outcome == 'success'
|
|
257
|
+
run: |
|
|
258
|
+
case "${{ steps.production_smoke.outcome }}" in
|
|
259
|
+
success) OUTCOME=passed; CHECK_STATUS=successful ;;
|
|
260
|
+
cancelled) OUTCOME=cancelled; CHECK_STATUS=cancelled ;;
|
|
261
|
+
skipped) OUTCOME=skipped; CHECK_STATUS=skipped ;;
|
|
262
|
+
*) OUTCOME=failed; CHECK_STATUS=failed ;;
|
|
263
|
+
esac
|
|
264
|
+
chmod +x scripts/report-test-cycle.sh scripts/report-release-check.sh 2>/dev/null || true
|
|
265
|
+
for PREFIX in ${REQS}; do
|
|
266
|
+
RESP=$(curl -fsS -H "Authorization: Bearer ${DEVAUDIT_API_KEY}" \
|
|
267
|
+
"${BASE}/api/ci/releases/resolve?projectSlug=${PROJECT_SLUG}&versionPrefix=${PREFIX}")
|
|
268
|
+
VERSION=$(echo "$RESP" | jq -r '.latest.version // empty')
|
|
269
|
+
[ -n "$VERSION" ] || VERSION="$PREFIX"
|
|
270
|
+
bash scripts/report-test-cycle.sh complete \
|
|
271
|
+
--project-slug "$PROJECT_SLUG" --release "$VERSION" \
|
|
272
|
+
--sdlc-stage 5 --environment production --cycle-kind smoke \
|
|
273
|
+
--provider github_actions --external-run-id "$CI_RUN" \
|
|
274
|
+
--external-run-attempt "${{ github.run_attempt }}" --external-job-id "production-smoke" \
|
|
275
|
+
--idempotency-key "github:${{ github.repository }}:post-deploy-prod.smoke:${CI_RUN}:${{ github.run_attempt }}:5:${VERSION}" \
|
|
276
|
+
--commit-sha "$GIT_SHA" --branch main \
|
|
277
|
+
--workflow-name "Post-Deploy Production" \
|
|
278
|
+
--workflow-url "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" \
|
|
279
|
+
--outcome "$OUTCOME" --outcome-reason "production smoke: ${{ steps.production_smoke.outcome }}"
|
|
280
|
+
bash scripts/report-release-check.sh \
|
|
281
|
+
--project-slug "$PROJECT_SLUG" --release "$VERSION" \
|
|
282
|
+
--check-key "production-smoke:${CI_RUN}:${{ github.run_attempt }}" \
|
|
283
|
+
--label "Production Smoke" --provider github_actions --status "$CHECK_STATUS" \
|
|
284
|
+
--external-run-id "$CI_RUN" \
|
|
285
|
+
--external-url "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" \
|
|
286
|
+
--commit-sha "$GIT_SHA" --branch main \
|
|
287
|
+
--details-json '{"executionSource":"steps.production_smoke.outcome"}'
|
|
288
|
+
done
|
|
289
|
+
|
|
154
290
|
- name: File incident on smoke failure
|
|
155
|
-
if: failure
|
|
291
|
+
if: steps.production_smoke.outcome == 'failure'
|
|
156
292
|
env:
|
|
157
293
|
# GitHub issue/label mutations use the workflow token. DevAudit
|
|
158
294
|
# user tokens are portal PATs, not guaranteed GitHub PATs (#305).
|
|
@@ -229,9 +365,11 @@ jobs:
|
|
|
229
365
|
fi
|
|
230
366
|
|
|
231
367
|
- name: Promote in-scope releases (evidence + status)
|
|
368
|
+
if: steps.production_smoke.outcome == 'success'
|
|
232
369
|
run: |
|
|
233
|
-
chmod +x scripts/upload-evidence.sh 2>/dev/null || true
|
|
370
|
+
chmod +x scripts/upload-evidence.sh scripts/report-test-cycle.sh scripts/report-release-check.sh 2>/dev/null || true
|
|
234
371
|
PROMOTED=0
|
|
372
|
+
EVIDENCE_FAILURES=0
|
|
235
373
|
for PREFIX in ${REQS}; do
|
|
236
374
|
echo "=== Promoting ${PREFIX} ==="
|
|
237
375
|
RESP=$(curl -s -H "Authorization: Bearer ${DEVAUDIT_API_KEY}" \
|
|
@@ -239,14 +377,38 @@ jobs:
|
|
|
239
377
|
VERSION=$(echo "$RESP" | jq -r '.latest.version // empty')
|
|
240
378
|
[ -z "$VERSION" ] && VERSION="${PREFIX}"
|
|
241
379
|
RELEASE_ID=$(echo "$RESP" | jq -r '.latest.id // empty')
|
|
380
|
+
REQ_FAILURES=0
|
|
381
|
+
LINEAGE_FLAGS=(--test-cycle "${CI_RUN}")
|
|
382
|
+
CYCLE_OUT="$(mktemp)"
|
|
383
|
+
bash scripts/report-test-cycle.sh start \
|
|
384
|
+
--project-slug "${PROJECT_SLUG}" \
|
|
385
|
+
--release "${VERSION}" \
|
|
386
|
+
--sdlc-stage 5 \
|
|
387
|
+
--environment production \
|
|
388
|
+
--cycle-kind smoke \
|
|
389
|
+
--provider github_actions \
|
|
390
|
+
--external-run-id "${CI_RUN}" \
|
|
391
|
+
--external-run-attempt "${{ github.run_attempt }}" \
|
|
392
|
+
--external-job-id "production-smoke" \
|
|
393
|
+
--idempotency-key "github:${{ github.repository }}:post-deploy-prod.smoke:${CI_RUN}:${{ github.run_attempt }}:5:${VERSION}" \
|
|
394
|
+
--commit-sha "${GIT_SHA}" \
|
|
395
|
+
--branch main \
|
|
396
|
+
--workflow-name "Post-Deploy Production" \
|
|
397
|
+
--workflow-url "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" \
|
|
398
|
+
--output-file "$CYCLE_OUT"
|
|
399
|
+
. "$CYCLE_OUT"
|
|
400
|
+
rm -f "$CYCLE_OUT"
|
|
401
|
+
if [ "${cycle_supported:-false}" = "true" ] && [ -n "${cycle_record_id:-}" ]; then
|
|
402
|
+
LINEAGE_FLAGS+=(--evidence-scope cycle --test-cycle-record-id "${cycle_record_id}")
|
|
403
|
+
fi
|
|
242
404
|
# Production smoke evidence (whole-app health) attached to this release.
|
|
243
405
|
if [ -f prod-smoke-results.json ]; then
|
|
244
406
|
bash scripts/upload-evidence.sh \
|
|
245
407
|
"${PROJECT_SLUG}" "${VERSION}" smoke_test prod-smoke-results.json \
|
|
246
408
|
--release "${VERSION}" --create-release-if-missing --environment production \
|
|
247
409
|
--category smoke_test --sdlc-stage 5 --git-sha "${GIT_SHA}" --ci-run-id "${CI_RUN}" --branch main \
|
|
248
|
-
|
|
249
|
-
|| echo "Warning: smoke upload failed for ${VERSION}"
|
|
410
|
+
"${LINEAGE_FLAGS[@]}" \
|
|
411
|
+
|| { echo "Warning: smoke upload failed for ${VERSION}"; REQ_FAILURES=$((REQ_FAILURES + 1)); }
|
|
250
412
|
fi
|
|
251
413
|
# Carry the release ticket into the production environment so the
|
|
252
414
|
# prod-review gate is self-contained.
|
|
@@ -261,11 +423,28 @@ jobs:
|
|
|
261
423
|
"${PROJECT_SLUG}" "${VERSION}" release_ticket "$TICKET" \
|
|
262
424
|
--release "${VERSION}" --create-release-if-missing --environment production \
|
|
263
425
|
--category release_artifact --sdlc-stage 5 --git-sha "${GIT_SHA}" --ci-run-id "${CI_RUN}" --branch main \
|
|
264
|
-
|
|
265
|
-
|| echo "Warning: ticket upload failed for ${VERSION}"
|
|
426
|
+
|| { echo "Warning: ticket upload failed for ${VERSION}"; REQ_FAILURES=$((REQ_FAILURES + 1)); }
|
|
266
427
|
else
|
|
267
428
|
echo "No RELEASE-TICKET-${VERSION}.md found — skipping ticket (date-versioned or archived)."
|
|
268
429
|
fi
|
|
430
|
+
if [ "$REQ_FAILURES" -gt 0 ]; then
|
|
431
|
+
EVIDENCE_STATUS=failed
|
|
432
|
+
EVIDENCE_FAILURES=$((EVIDENCE_FAILURES + 1))
|
|
433
|
+
else
|
|
434
|
+
EVIDENCE_STATUS=successful
|
|
435
|
+
fi
|
|
436
|
+
bash scripts/report-release-check.sh \
|
|
437
|
+
--project-slug "$PROJECT_SLUG" --release "$VERSION" \
|
|
438
|
+
--check-key "production-evidence:${CI_RUN}:${{ github.run_attempt }}" \
|
|
439
|
+
--label "Production Evidence Completeness" --provider devaudit_installer \
|
|
440
|
+
--status "$EVIDENCE_STATUS" --external-run-id "$CI_RUN" \
|
|
441
|
+
--external-url "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" \
|
|
442
|
+
--commit-sha "$GIT_SHA" --branch main \
|
|
443
|
+
--details-json "{\"uploadFailures\":${REQ_FAILURES}}"
|
|
444
|
+
if [ "$REQ_FAILURES" -gt 0 ]; then
|
|
445
|
+
echo "::error::Skipping status promotion for ${VERSION}: production evidence is incomplete"
|
|
446
|
+
continue
|
|
447
|
+
fi
|
|
269
448
|
# Advance status (idempotent — re-PATCHing an already-promoted release is a no-op).
|
|
270
449
|
if [ -n "$RELEASE_ID" ]; then
|
|
271
450
|
curl -s -o /dev/null -w " ${VERSION} status patch: HTTP %{http_code}\n" \
|
|
@@ -279,6 +458,10 @@ jobs:
|
|
|
279
458
|
echo "::warning::No release_id resolved for ${PREFIX} — skipping status patch"
|
|
280
459
|
fi
|
|
281
460
|
done
|
|
461
|
+
if [ "$EVIDENCE_FAILURES" -gt 0 ]; then
|
|
462
|
+
echo "::error::${EVIDENCE_FAILURES} release(s) have incomplete production evidence"
|
|
463
|
+
exit 1
|
|
464
|
+
fi
|
|
282
465
|
echo "Promoted ${PROMOTED} release(s) to ${TERMINAL_STATUS}."
|
|
283
466
|
if [ "${TERMINAL_STATUS}" = "prod_review" ]; then
|
|
284
467
|
echo "Next: a human in the portal clicks 'Approve Production' then 'Mark as Released' for each."
|
|
@@ -5,8 +5,9 @@
|
|
|
5
5
|
#
|
|
6
6
|
# Single consolidated job. Order: install → ruff → mypy → semgrep → pip-audit → pytest → build.
|
|
7
7
|
#
|
|
8
|
-
# PRs to
|
|
9
|
-
#
|
|
8
|
+
# PRs to {{INTEGRATION_BRANCH}} run Quality Gates before integration. PRs to
|
|
9
|
+
# {{RELEASE_BRANCH}} are handled by quality-gates-provenance.yml: provenance-only
|
|
10
|
+
# for release PRs, full CI rerouted for hotfix/* PRs.
|
|
10
11
|
# Compliance validation runs separately on PRs (compliance-validation.yml).
|
|
11
12
|
|
|
12
13
|
name: CI Pipeline
|
|
@@ -14,9 +15,9 @@ name: CI Pipeline
|
|
|
14
15
|
on:
|
|
15
16
|
workflow_dispatch:
|
|
16
17
|
pull_request:
|
|
17
|
-
branches: [
|
|
18
|
+
branches: [{{INTEGRATION_BRANCH}}]
|
|
18
19
|
push:
|
|
19
|
-
branches: [
|
|
20
|
+
branches: [{{INTEGRATION_BRANCH}}]
|
|
20
21
|
paths-ignore:
|
|
21
22
|
{{PATHS_IGNORE}} - 'sdlc-config.json'
|
|
22
23
|
|
|
@@ -190,7 +191,7 @@ jobs:
|
|
|
190
191
|
# PRs to develop should report Quality Gates without mutating DevAudit
|
|
191
192
|
# releases or evidence. Release registration remains a develop-push /
|
|
192
193
|
# manual-dispatch side effect.
|
|
193
|
-
if: ${{ github.event_name != 'pull_request' }}
|
|
194
|
+
if: ${{ github.event_name != 'pull_request' && github.ref_name == '{{INTEGRATION_BRANCH}}' }}
|
|
194
195
|
outputs:
|
|
195
196
|
version: ${{ steps.version.outputs.version }}
|
|
196
197
|
env:
|
|
@@ -314,7 +315,7 @@ jobs:
|
|
|
314
315
|
# evidence — `status=failed` is itself the audit trail. `!cancelled()`
|
|
315
316
|
# still guards against partial state on operator-cancel.
|
|
316
317
|
# DevAudit-Installer#96.
|
|
317
|
-
if: ${{ always() && !cancelled() && needs.register-release.result == 'success' }}
|
|
318
|
+
if: ${{ always() && !cancelled() && github.ref_name == '{{INTEGRATION_BRANCH}}' && needs.register-release.result == 'success' }}
|
|
318
319
|
env:
|
|
319
320
|
DEVAUDIT_BASE_URL_VAR: ${{ vars.DEVAUDIT_BASE_URL }}
|
|
320
321
|
DEVAUDIT_API_KEY: ${{ secrets.DEVAUDIT_API_KEY }}
|
|
@@ -356,11 +357,33 @@ jobs:
|
|
|
356
357
|
- name: Generate and upload gate evidence
|
|
357
358
|
if: env.DEVAUDIT_BASE_URL != ''
|
|
358
359
|
run: |
|
|
359
|
-
chmod +x scripts/upload-evidence.sh 2>/dev/null || true
|
|
360
|
+
chmod +x scripts/upload-evidence.sh scripts/report-test-cycle.sh 2>/dev/null || true
|
|
360
361
|
FLAGS="--git-sha ${{ github.sha }} --ci-run-id ${{ github.run_id }} --branch ${{ github.ref_name }}"
|
|
361
362
|
FLAGS="${FLAGS} --release ${{ needs.register-release.outputs.version }} --create-release-if-missing"
|
|
362
363
|
FLAGS="${FLAGS} --environment uat"
|
|
363
|
-
FLAGS="${FLAGS} --
|
|
364
|
+
FLAGS="${FLAGS} --sdlc-stage 2"
|
|
365
|
+
PRIMARY_LINEAGE_FLAGS=(--test-cycle "${{ github.run_id }}")
|
|
366
|
+
CYCLE_OUT="$(mktemp)"
|
|
367
|
+
bash scripts/report-test-cycle.sh start \
|
|
368
|
+
--project-slug {{PROJECT_SLUG}} \
|
|
369
|
+
--release "${{ needs.register-release.outputs.version }}" \
|
|
370
|
+
--sdlc-stage 2 \
|
|
371
|
+
--environment uat \
|
|
372
|
+
--cycle-kind quality_gate \
|
|
373
|
+
--provider github_actions \
|
|
374
|
+
--external-run-id "${{ github.run_id }}" \
|
|
375
|
+
--external-run-attempt "${{ github.run_attempt }}" \
|
|
376
|
+
--idempotency-key "github:${{ github.repository }}:quality-gates-python.quality_gate:${{ github.run_id }}:${{ github.run_attempt }}:2:${{ needs.register-release.outputs.version }}" \
|
|
377
|
+
--commit-sha "${{ github.sha }}" \
|
|
378
|
+
--branch "${{ github.ref_name }}" \
|
|
379
|
+
--workflow-name "Quality Gates" \
|
|
380
|
+
--workflow-url "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" \
|
|
381
|
+
--output-file "$CYCLE_OUT"
|
|
382
|
+
. "$CYCLE_OUT"
|
|
383
|
+
rm -f "$CYCLE_OUT"
|
|
384
|
+
if [ "${cycle_supported:-false}" = "true" ] && [ -n "${cycle_record_id:-}" ]; then
|
|
385
|
+
PRIMARY_LINEAGE_FLAGS+=(--evidence-scope cycle --test-cycle-record-id "${cycle_record_id}")
|
|
386
|
+
fi
|
|
364
387
|
|
|
365
388
|
UPLOAD_FAILURES=0
|
|
366
389
|
upload() {
|
|
@@ -394,7 +417,7 @@ jobs:
|
|
|
394
417
|
STATUS_TESTS=$(gate_status "$(jq -r '.tests // "skipped"' "$OUTCOMES_FILE")")
|
|
395
418
|
upload gate-outcomes.json \
|
|
396
419
|
{{PROJECT_SLUG}} _compliance-docs gate_outcome "$OUTCOMES_FILE" \
|
|
397
|
-
--category ci_pipeline ${FLAGS}
|
|
420
|
+
--category ci_pipeline ${FLAGS} "${PRIMARY_LINEAGE_FLAGS[@]}"
|
|
398
421
|
fi
|
|
399
422
|
|
|
400
423
|
mkdir -p {{WORKING_DIR_PREFIX}}ci-evidence
|
|
@@ -405,14 +428,14 @@ jobs:
|
|
|
405
428
|
if [ -f {{WORKING_DIR_PREFIX}}ci-evidence/sast-results.json ]; then
|
|
406
429
|
upload sast-results.json \
|
|
407
430
|
{{PROJECT_SLUG}} _compliance-docs sast_report {{WORKING_DIR_PREFIX}}ci-evidence/sast-results.json \
|
|
408
|
-
--category security_scan --gate-status "$STATUS_SAST" ${FLAGS}
|
|
431
|
+
--category security_scan --gate-status "$STATUS_SAST" ${FLAGS} "${PRIMARY_LINEAGE_FLAGS[@]}"
|
|
409
432
|
fi
|
|
410
433
|
|
|
411
434
|
# Precise evidence_type=dependency_audit (matches the node template).
|
|
412
435
|
if [ -f {{WORKING_DIR_PREFIX}}ci-evidence/dependency-audit.json ]; then
|
|
413
436
|
upload dependency-audit.json \
|
|
414
437
|
{{PROJECT_SLUG}} _compliance-docs dependency_audit {{WORKING_DIR_PREFIX}}ci-evidence/dependency-audit.json \
|
|
415
|
-
--category security_scan --gate-status "$STATUS_DEPAUDIT" ${FLAGS}
|
|
438
|
+
--category security_scan --gate-status "$STATUS_DEPAUDIT" ${FLAGS} "${PRIMARY_LINEAGE_FLAGS[@]}"
|
|
416
439
|
fi
|
|
417
440
|
|
|
418
441
|
# pytest junit.xml is the Python equivalent of e2e-results.json —
|
|
@@ -421,7 +444,7 @@ jobs:
|
|
|
421
444
|
if [ -f {{WORKING_DIR_PREFIX}}ci-evidence/junit.xml ]; then
|
|
422
445
|
upload junit.xml \
|
|
423
446
|
{{PROJECT_SLUG}} _compliance-docs e2e_result {{WORKING_DIR_PREFIX}}ci-evidence/junit.xml \
|
|
424
|
-
--category e2e_result --gate-status "$STATUS_TESTS" ${FLAGS}
|
|
447
|
+
--category e2e_result --gate-status "$STATUS_TESTS" ${FLAGS} "${PRIMARY_LINEAGE_FLAGS[@]}"
|
|
425
448
|
fi
|
|
426
449
|
|
|
427
450
|
# Project-level Test Summary Report — intentional compliance_document
|
|
@@ -523,5 +546,30 @@ jobs:
|
|
|
523
546
|
exit 1
|
|
524
547
|
fi
|
|
525
548
|
|
|
549
|
+
- name: Complete primary quality-gate cycle
|
|
550
|
+
if: always() && env.DEVAUDIT_BASE_URL != ''
|
|
551
|
+
run: |
|
|
552
|
+
case "${{ job.status }}" in
|
|
553
|
+
success) OUTCOME=passed ;;
|
|
554
|
+
cancelled) OUTCOME=cancelled ;;
|
|
555
|
+
*) OUTCOME=failed ;;
|
|
556
|
+
esac
|
|
557
|
+
chmod +x scripts/report-test-cycle.sh 2>/dev/null || true
|
|
558
|
+
bash scripts/report-test-cycle.sh complete \
|
|
559
|
+
--project-slug {{PROJECT_SLUG}} \
|
|
560
|
+
--release "${{ needs.register-release.outputs.version }}" \
|
|
561
|
+
--sdlc-stage 2 \
|
|
562
|
+
--environment uat \
|
|
563
|
+
--cycle-kind quality_gate \
|
|
564
|
+
--provider github_actions \
|
|
565
|
+
--external-run-id "${{ github.run_id }}" \
|
|
566
|
+
--external-run-attempt "${{ github.run_attempt }}" \
|
|
567
|
+
--idempotency-key "github:${{ github.repository }}:quality-gates-python.quality_gate:${{ github.run_id }}:${{ github.run_attempt }}:2:${{ needs.register-release.outputs.version }}" \
|
|
568
|
+
--commit-sha "${{ github.sha }}" \
|
|
569
|
+
--branch "${{ github.ref_name }}" \
|
|
570
|
+
--workflow-name "Quality Gates" \
|
|
571
|
+
--workflow-url "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" \
|
|
572
|
+
--outcome "$OUTCOME"
|
|
573
|
+
|
|
526
574
|
- name: Summary
|
|
527
575
|
run: echo "Evidence uploaded for ${{ needs.register-release.outputs.version }} (UAT)"
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
# Quality Gates Provenance —
|
|
1
|
+
# Quality Gates Provenance — routes PRs to the release branch into the correct
|
|
2
|
+
# Quality Gates path.
|
|
2
3
|
#
|
|
3
4
|
# Generated by `devaudit install` / `devaudit update` from sdlc-config.json.
|
|
4
5
|
# Do not edit manually — re-run the CLI (`devaudit update`) to regenerate.
|
|
@@ -9,10 +10,11 @@
|
|
|
9
10
|
# Quality Gates result. When that happens, neither existing workflow fires on
|
|
10
11
|
# the PR to main, so the release PR sits with missing/ambiguous protection.
|
|
11
12
|
#
|
|
12
|
-
# This workflow fires on PRs to
|
|
13
|
-
#
|
|
14
|
-
#
|
|
15
|
-
#
|
|
13
|
+
# This workflow fires on PRs to {{RELEASE_BRANCH}} and chooses between two modes:
|
|
14
|
+
# - release PR (`{{INTEGRATION_BRANCH}} -> {{RELEASE_BRANCH}}`): same-SHA provenance
|
|
15
|
+
# check only
|
|
16
|
+
# - hotfix PR (`hotfix/* -> {{RELEASE_BRANCH}}`): dispatch the real ci.yml gates on
|
|
17
|
+
# the hotfix branch and proxy that result back onto the PR
|
|
16
18
|
#
|
|
17
19
|
# The job name is exactly `Quality Gates` to satisfy existing branch-protection
|
|
18
20
|
# rules without introducing a new required-check name.
|
|
@@ -22,14 +24,102 @@ name: CI Pipeline
|
|
|
22
24
|
on:
|
|
23
25
|
workflow_dispatch:
|
|
24
26
|
pull_request:
|
|
25
|
-
branches: [
|
|
27
|
+
branches: [{{RELEASE_BRANCH}}]
|
|
26
28
|
|
|
27
29
|
jobs:
|
|
30
|
+
release-scope-integrity:
|
|
31
|
+
name: Release Scope Integrity
|
|
32
|
+
if: ${{ !startsWith(github.event.pull_request.head.ref, 'hotfix/') && !startsWith(github.event.pull_request.head.ref, 'fix/hotfix-') }}
|
|
33
|
+
runs-on: ubuntu-latest
|
|
34
|
+
steps:
|
|
35
|
+
- uses: actions/checkout@v6
|
|
36
|
+
with:
|
|
37
|
+
ref: ${{ github.event.pull_request.head.sha }}
|
|
38
|
+
fetch-depth: 0
|
|
39
|
+
|
|
40
|
+
- name: Check release PR metadata against derived scope
|
|
41
|
+
env:
|
|
42
|
+
PR_TITLE: ${{ github.event.pull_request.title }}
|
|
43
|
+
PR_BODY: ${{ github.event.pull_request.body }}
|
|
44
|
+
HEAD_REF: ${{ github.event.pull_request.head.ref }}
|
|
45
|
+
run: |
|
|
46
|
+
chmod +x scripts/check-release-pr-scope.sh 2>/dev/null || true
|
|
47
|
+
bash scripts/check-release-pr-scope.sh
|
|
48
|
+
|
|
28
49
|
quality-gates:
|
|
29
50
|
name: Quality Gates
|
|
30
51
|
runs-on: ubuntu-latest
|
|
31
52
|
steps:
|
|
53
|
+
- name: Classify PR path
|
|
54
|
+
id: classify
|
|
55
|
+
env:
|
|
56
|
+
HEAD_REF: ${{ github.event.pull_request.head.ref }}
|
|
57
|
+
BASE_REF: ${{ github.event.pull_request.base.ref }}
|
|
58
|
+
run: |
|
|
59
|
+
case "$HEAD_REF" in
|
|
60
|
+
hotfix/*|fix/hotfix-*)
|
|
61
|
+
echo "kind=hotfix" >> "$GITHUB_OUTPUT"
|
|
62
|
+
;;
|
|
63
|
+
*)
|
|
64
|
+
echo "kind=release" >> "$GITHUB_OUTPUT"
|
|
65
|
+
;;
|
|
66
|
+
esac
|
|
67
|
+
echo "base_ref=${BASE_REF}" >> "$GITHUB_OUTPUT"
|
|
68
|
+
echo "head_ref=${HEAD_REF}" >> "$GITHUB_OUTPUT"
|
|
69
|
+
|
|
70
|
+
- name: Dispatch ci.yml Quality Gates for hotfix PR
|
|
71
|
+
if: steps.classify.outputs.kind == 'hotfix'
|
|
72
|
+
env:
|
|
73
|
+
GH_TOKEN: ${{ github.token }}
|
|
74
|
+
HEAD_REF: ${{ steps.classify.outputs.head_ref }}
|
|
75
|
+
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
|
|
76
|
+
run: |
|
|
77
|
+
BEFORE_JSON=$(gh run list \
|
|
78
|
+
--workflow "CI Pipeline" \
|
|
79
|
+
--branch "$HEAD_REF" \
|
|
80
|
+
--event workflow_dispatch \
|
|
81
|
+
--limit 50 \
|
|
82
|
+
--json databaseId,headSha)
|
|
83
|
+
|
|
84
|
+
gh workflow run ci.yml --ref "$HEAD_REF"
|
|
85
|
+
|
|
86
|
+
RUN_ID=""
|
|
87
|
+
for ATTEMPT in $(seq 1 20); do
|
|
88
|
+
AFTER_JSON=$(gh run list \
|
|
89
|
+
--workflow "CI Pipeline" \
|
|
90
|
+
--branch "$HEAD_REF" \
|
|
91
|
+
--event workflow_dispatch \
|
|
92
|
+
--limit 50 \
|
|
93
|
+
--json databaseId,headSha)
|
|
94
|
+
RUN_ID=$(BEFORE_JSON="$BEFORE_JSON" AFTER_JSON="$AFTER_JSON" HEAD_SHA="$HEAD_SHA" python3 - <<'PY'
|
|
95
|
+
import json
|
|
96
|
+
import os
|
|
97
|
+
|
|
98
|
+
before = {str(item["databaseId"]) for item in json.loads(os.environ["BEFORE_JSON"])}
|
|
99
|
+
head_sha = os.environ["HEAD_SHA"]
|
|
100
|
+
for item in json.loads(os.environ["AFTER_JSON"]):
|
|
101
|
+
if item.get("headSha") == head_sha and str(item["databaseId"]) not in before:
|
|
102
|
+
print(item["databaseId"])
|
|
103
|
+
break
|
|
104
|
+
PY
|
|
105
|
+
)
|
|
106
|
+
if [ -n "$RUN_ID" ]; then
|
|
107
|
+
break
|
|
108
|
+
fi
|
|
109
|
+
echo "Waiting for dispatched ci.yml run to appear for ${HEAD_REF}..."
|
|
110
|
+
sleep 10
|
|
111
|
+
done
|
|
112
|
+
|
|
113
|
+
if [ -z "$RUN_ID" ]; then
|
|
114
|
+
echo "::error::Unable to locate the dispatched ci.yml run for hotfix branch ${HEAD_REF}."
|
|
115
|
+
exit 1
|
|
116
|
+
fi
|
|
117
|
+
|
|
118
|
+
echo "Watching dispatched ci.yml run ${RUN_ID} for hotfix branch ${HEAD_REF}..."
|
|
119
|
+
gh run watch "$RUN_ID" --exit-status
|
|
120
|
+
|
|
32
121
|
- name: Verify prior Quality Gates success on head SHA
|
|
122
|
+
if: steps.classify.outputs.kind != 'hotfix'
|
|
33
123
|
env:
|
|
34
124
|
GH_TOKEN: ${{ github.token }}
|
|
35
125
|
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
|
|
@@ -37,26 +127,31 @@ jobs:
|
|
|
37
127
|
run: |
|
|
38
128
|
echo "Checking for prior successful 'Quality Gates' on SHA: $HEAD_SHA"
|
|
39
129
|
echo "Excluding current workflow run: $CURRENT_RUN_ID"
|
|
40
|
-
|
|
41
|
-
# Query GitHub API for check runs on this SHA with name "Quality Gates"
|
|
42
130
|
CURRENT_RUN_URL="/actions/runs/${CURRENT_RUN_ID}"
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
131
|
+
MAX_ATTEMPTS=10
|
|
132
|
+
WAIT_SECONDS=30
|
|
133
|
+
|
|
134
|
+
for ATTEMPT in $(seq 1 $MAX_ATTEMPTS); do
|
|
135
|
+
RESPONSE=$(gh api \
|
|
136
|
+
"/repos/${{ github.repository }}/commits/${HEAD_SHA}/check-runs?name=Quality%20Gates" \
|
|
137
|
+
--jq ".check_runs[]
|
|
138
|
+
| select(.status == \"completed\" and .conclusion == \"success\")
|
|
139
|
+
| select((.details_url // \"\") | contains(\"${CURRENT_RUN_URL}\") | not)")
|
|
140
|
+
|
|
141
|
+
if [ -n "$RESPONSE" ]; then
|
|
142
|
+
echo "Found prior successful Quality Gates result:"
|
|
143
|
+
echo "$RESPONSE" | head -5
|
|
144
|
+
echo ""
|
|
145
|
+
echo "Provenance verified — this SHA passed develop-side Quality Gates."
|
|
146
|
+
exit 0
|
|
147
|
+
fi
|
|
148
|
+
|
|
149
|
+
if [ "$ATTEMPT" -lt "$MAX_ATTEMPTS" ]; then
|
|
150
|
+
echo "Develop-side Quality Gates for SHA ${HEAD_SHA} not successful yet. Waiting ${WAIT_SECONDS}s (${ATTEMPT}/${MAX_ATTEMPTS})..."
|
|
151
|
+
sleep "$WAIT_SECONDS"
|
|
152
|
+
fi
|
|
153
|
+
done
|
|
154
|
+
|
|
155
|
+
echo "::error::No successful develop-side Quality Gates result found for SHA ${HEAD_SHA} after waiting $((MAX_ATTEMPTS * WAIT_SECONDS / 60)) minutes."
|
|
156
|
+
echo "::error::Push this exact commit through the normal develop flow first, or wait for the in-flight develop pipeline to finish and rerun this check."
|
|
157
|
+
exit 1
|
package/sdlc/package.json
CHANGED