@metasession.co/devaudit-cli 0.3.11 → 0.3.13
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 +94 -6
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/scripts/upload-evidence.sh +55 -1
- package/sdlc/ai-rules/SDLC_RULES.md +27 -7
- package/sdlc/files/_common/0-project-setup.md +13 -7
- 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 +65 -0
- package/sdlc/files/_common/scripts/extract-release-metadata.test.sh +38 -0
- package/sdlc/files/_common/scripts/generate-bundled-changes.sh +307 -58
- package/sdlc/files/_common/scripts/generate-bundled-changes.test.sh +128 -78
- 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-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 +55 -1
- package/sdlc/files/_common/scripts/upload-evidence.test.sh +119 -0
- package/sdlc/files/_common/scripts/validate-compliance-artifacts.sh +169 -2
- package/sdlc/files/_common/scripts/validate-compliance-artifacts.test.sh +169 -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 +57 -3
- 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/check-release-approval.yml.template +18 -28
- package/sdlc/files/ci/ci.yml.template +95 -35
- package/sdlc/files/ci/close-out-release.yml.template +7 -4
- package/sdlc/files/ci/compliance-evidence.yml.template +234 -49
- package/sdlc/files/ci/feature-e2e.yml.template +84 -1
- package/sdlc/files/ci/post-deploy-prod.yml.template +59 -4
- 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 +102 -20
- package/sdlc/src/blueprints/4-submit-for-review.raw.md +3 -3
- package/sdlc/src/blueprints/5-deploy-main.raw.md +2 -2
|
@@ -49,13 +49,16 @@ jobs:
|
|
|
49
49
|
# double-upload every compliance doc.
|
|
50
50
|
if: github.event_name != 'workflow_run'
|
|
51
51
|
runs-on: {{RUNNER}}
|
|
52
|
-
# Permissions are needed because the
|
|
53
|
-
#
|
|
54
|
-
#
|
|
55
|
-
#
|
|
56
|
-
#
|
|
57
|
-
#
|
|
58
|
-
#
|
|
52
|
+
# Permissions are needed because the housekeeping path now does two
|
|
53
|
+
# GitHub mutations:
|
|
54
|
+
# 1. pushes a stub branch + opens a PR via `gh pr create`
|
|
55
|
+
# 2. dispatches `ci.yml` on develop after that stub PR merges, so
|
|
56
|
+
# compliance-only housekeeping releases still get the gate evidence
|
|
57
|
+
# the portal approval path currently requires (#361)
|
|
58
|
+
#
|
|
59
|
+
# Without these the github-actions[bot] token gets HTTP 403 on the push,
|
|
60
|
+
# PR create, or workflow dispatch and the housekeeping path leaves a red
|
|
61
|
+
# badge or an unapprovable release behind.
|
|
59
62
|
#
|
|
60
63
|
# `DEVAUDIT_USER_TOKEN` is no longer consulted by this workflow (the
|
|
61
64
|
# auto-stub step now uses `github.token` directly — see the env block
|
|
@@ -63,6 +66,7 @@ jobs:
|
|
|
63
66
|
# needs; preferring a (potentially stale) PAT was the cause of the
|
|
64
67
|
# 2026-06-07 401 chain on this issue.
|
|
65
68
|
permissions:
|
|
69
|
+
actions: write # gh workflow run ci.yml --ref develop
|
|
66
70
|
contents: write # push the auto-PR branch
|
|
67
71
|
pull-requests: write # gh pr create
|
|
68
72
|
env:
|
|
@@ -237,6 +241,43 @@ jobs:
|
|
|
237
241
|
echo "PR #${EXISTING} already open for ${VERSION} — branch updated in place."
|
|
238
242
|
fi
|
|
239
243
|
|
|
244
|
+
- name: Dispatch ci.yml for merged housekeeping stub
|
|
245
|
+
if: github.event_name == 'push' && steps.resolve.outputs.skip != 'true' && steps.version.outputs.version != 'skip'
|
|
246
|
+
env:
|
|
247
|
+
GH_TOKEN: ${{ github.token }}
|
|
248
|
+
VERSION: ${{ steps.version.outputs.version }}
|
|
249
|
+
run: |
|
|
250
|
+
# devaudit-installer#361 — compliance-only housekeeping pushes
|
|
251
|
+
# intentionally skip the heavy quality-gates workflow, but the
|
|
252
|
+
# portal approval path still requires gate evidence before a
|
|
253
|
+
# housekeeping release can be approved. When the current develop
|
|
254
|
+
# push actually carries the housekeeping release-ticket / security-
|
|
255
|
+
# summary stubs, dispatch ci.yml once so the gates run on that
|
|
256
|
+
# exact HEAD and upload the missing evidence automatically.
|
|
257
|
+
if ! [[ "$VERSION" =~ ^v[0-9]{4}\.[0-9]{2}\.[0-9]{2}(\.[0-9]+)?$ ]]; then
|
|
258
|
+
echo "Version ${VERSION} is not housekeeping; skipping ci.yml dispatch."
|
|
259
|
+
exit 0
|
|
260
|
+
fi
|
|
261
|
+
|
|
262
|
+
TICKET_PATH="compliance/pending-releases/RELEASE-TICKET-${VERSION}.md"
|
|
263
|
+
SECSUM_PATH="compliance/security-summary-${VERSION}.md"
|
|
264
|
+
CHANGED_FILES=$(git diff-tree --no-commit-id --name-only -r "${{ github.sha }}" || true)
|
|
265
|
+
|
|
266
|
+
if ! echo "$CHANGED_FILES" | grep -Fxq "$TICKET_PATH" \
|
|
267
|
+
&& ! echo "$CHANGED_FILES" | grep -Fxq "$SECSUM_PATH"; then
|
|
268
|
+
echo "Current push did not modify housekeeping stubs for ${VERSION}; skipping ci.yml dispatch."
|
|
269
|
+
exit 0
|
|
270
|
+
fi
|
|
271
|
+
|
|
272
|
+
if [ ! -f "$TICKET_PATH" ] || [ ! -f "$SECSUM_PATH" ]; then
|
|
273
|
+
echo "Housekeeping stubs for ${VERSION} are not both present on develop; skipping ci.yml dispatch."
|
|
274
|
+
exit 0
|
|
275
|
+
fi
|
|
276
|
+
|
|
277
|
+
echo "Dispatching ci.yml on develop so housekeeping release ${VERSION} gets gate evidence."
|
|
278
|
+
gh workflow run ci.yml --ref develop
|
|
279
|
+
echo "Triggered ci.yml workflow_dispatch for housekeeping release ${VERSION}."
|
|
280
|
+
|
|
240
281
|
- name: Upload compliance documents
|
|
241
282
|
if: steps.resolve.outputs.skip != 'true' && steps.version.outputs.version != 'skip'
|
|
242
283
|
run: |
|
|
@@ -250,7 +291,6 @@ jobs:
|
|
|
250
291
|
# release. Note: upload-evidence.sh keeps the LAST --release seen.
|
|
251
292
|
FLAGS="--git-sha ${{ github.sha }} --ci-run-id ${{ github.run_id }} --branch ${{ github.ref_name }}"
|
|
252
293
|
FLAGS="${FLAGS} --create-release-if-missing --environment uat --sdlc-stage 3"
|
|
253
|
-
FLAGS="${FLAGS} --test-cycle ${{ github.run_id }}"
|
|
254
294
|
DERIVED_RELEASE="${{ steps.version.outputs.version }}"
|
|
255
295
|
|
|
256
296
|
# Derive change_type for the bare-date (housekeeping) DERIVED_RELEASE:
|
|
@@ -463,6 +503,30 @@ jobs:
|
|
|
463
503
|
if [ -n "$CT" ]; then printf -- '--change-type %q ' "$CT"; fi
|
|
464
504
|
}
|
|
465
505
|
|
|
506
|
+
# Upload root-level housekeeping security summaries:
|
|
507
|
+
# compliance/security-summary-vYYYY.MM.DD.md
|
|
508
|
+
# compliance/security-summary-vYYYY.MM.DD.N.md
|
|
509
|
+
#
|
|
510
|
+
# These are generated by the housekeeping stub path and belong to
|
|
511
|
+
# the bare-date housekeeping release encoded in the filename, not
|
|
512
|
+
# to whichever release the current commit happened to derive.
|
|
513
|
+
shopt -s nullglob
|
|
514
|
+
for SECSUM in compliance/security-summary-*.md; do
|
|
515
|
+
[ -f "$SECSUM" ] || continue
|
|
516
|
+
SECSUM_VER=$(basename "$SECSUM" .md | sed 's/^security-summary-//')
|
|
517
|
+
if ! echo "$SECSUM_VER" | grep -qE '^v[0-9]{4}\.[0-9]{2}\.[0-9]{2}([.][0-9]+)?$'; then
|
|
518
|
+
echo "::warning::Skipping $(basename "$SECSUM"): filename does not encode a housekeeping release version."
|
|
519
|
+
continue
|
|
520
|
+
fi
|
|
521
|
+
echo "Uploading housekeeping security summary: $(basename "$SECSUM") -> release ${SECSUM_VER}"
|
|
522
|
+
bash scripts/upload-evidence.sh \
|
|
523
|
+
{{PROJECT_SLUG}} _compliance-docs security_summary "$SECSUM" \
|
|
524
|
+
--category security ${FLAGS} --release "${SECSUM_VER}" \
|
|
525
|
+
"${DERIVED_META[@]}" \
|
|
526
|
+
|| echo "Warning: Failed to upload $(basename "$SECSUM")"
|
|
527
|
+
done
|
|
528
|
+
shopt -u nullglob
|
|
529
|
+
|
|
466
530
|
# Upload release tickets (pending only)
|
|
467
531
|
if [ -d "compliance/pending-releases" ]; then
|
|
468
532
|
for TICKET in compliance/pending-releases/*.md; do
|
|
@@ -724,7 +788,6 @@ jobs:
|
|
|
724
788
|
--ci-run-id ${{ github.event.workflow_run.id }} \
|
|
725
789
|
--branch ${PRIOR_BRANCH}"
|
|
726
790
|
[ -n "$STAGE" ] && FLAGS="${FLAGS} --sdlc-stage ${STAGE}"
|
|
727
|
-
FLAGS="${FLAGS} --test-cycle ${{ github.event.workflow_run.id }}"
|
|
728
791
|
|
|
729
792
|
# In-scope REQs from pending release tickets at the triggering
|
|
730
793
|
# SHA. If the ticket was created after the triggering E2E run,
|
|
@@ -746,38 +809,39 @@ jobs:
|
|
|
746
809
|
fi
|
|
747
810
|
|
|
748
811
|
if [ "${#REQS[@]}" -eq 0 ] && [ -f e2e-artifacts/e2e-regression-results.json ]; then
|
|
812
|
+
{
|
|
813
|
+
printf '%s\n' 'import json'
|
|
814
|
+
printf '%s\n' 'import re'
|
|
815
|
+
printf '%s\n' 'import sys'
|
|
816
|
+
printf '%s\n' ''
|
|
817
|
+
printf '%s\n' 'try:'
|
|
818
|
+
printf '%s\n' " with open('e2e-artifacts/e2e-regression-results.json', 'r', encoding='utf-8') as handle:"
|
|
819
|
+
printf '%s\n' ' data = json.load(handle)'
|
|
820
|
+
printf '%s\n' 'except Exception:'
|
|
821
|
+
printf '%s\n' ' sys.exit(0)'
|
|
822
|
+
printf '%s\n' ''
|
|
823
|
+
printf '%s\n' 'seen = set()'
|
|
824
|
+
printf '%s\n' ''
|
|
825
|
+
printf '%s\n' 'def walk(value):'
|
|
826
|
+
printf '%s\n' ' if isinstance(value, str):'
|
|
827
|
+
printf '%s\n' " for req in re.findall(r'REQ-[0-9]+', value):"
|
|
828
|
+
printf '%s\n' ' seen.add(req)'
|
|
829
|
+
printf '%s\n' ' elif isinstance(value, dict):'
|
|
830
|
+
printf '%s\n' ' for item in value.values():'
|
|
831
|
+
printf '%s\n' ' walk(item)'
|
|
832
|
+
printf '%s\n' ' elif isinstance(value, list):'
|
|
833
|
+
printf '%s\n' ' for item in value:'
|
|
834
|
+
printf '%s\n' ' walk(item)'
|
|
835
|
+
printf '%s\n' ''
|
|
836
|
+
printf '%s\n' 'walk(data)'
|
|
837
|
+
printf '%s\n' 'for req in sorted(seen):'
|
|
838
|
+
printf '%s\n' ' print(req)'
|
|
839
|
+
} > /tmp/devaudit-extract-e2e-reqs.py
|
|
749
840
|
while IFS= read -r REQ_ID; do
|
|
750
841
|
[ -n "$REQ_ID" ] || continue
|
|
751
842
|
REQS+=("$REQ_ID")
|
|
752
|
-
done < <(python3 -
|
|
753
|
-
|
|
754
|
-
import re
|
|
755
|
-
import sys
|
|
756
|
-
|
|
757
|
-
try:
|
|
758
|
-
with open('e2e-artifacts/e2e-regression-results.json', 'r', encoding='utf-8') as handle:
|
|
759
|
-
data = json.load(handle)
|
|
760
|
-
except Exception:
|
|
761
|
-
sys.exit(0)
|
|
762
|
-
|
|
763
|
-
seen = set()
|
|
764
|
-
|
|
765
|
-
def walk(value):
|
|
766
|
-
if isinstance(value, str):
|
|
767
|
-
for req in re.findall(r'REQ-[0-9]+', value):
|
|
768
|
-
seen.add(req)
|
|
769
|
-
elif isinstance(value, dict):
|
|
770
|
-
for item in value.values():
|
|
771
|
-
walk(item)
|
|
772
|
-
elif isinstance(value, list):
|
|
773
|
-
for item in value:
|
|
774
|
-
walk(item)
|
|
775
|
-
|
|
776
|
-
walk(data)
|
|
777
|
-
for req in sorted(seen):
|
|
778
|
-
print(req)
|
|
779
|
-
PY
|
|
780
|
-
)
|
|
843
|
+
done < <(python3 /tmp/devaudit-extract-e2e-reqs.py)
|
|
844
|
+
rm -f /tmp/devaudit-extract-e2e-reqs.py
|
|
781
845
|
fi
|
|
782
846
|
|
|
783
847
|
if [ "${#REQS[@]}" -eq 1 ] && echo "$DERIVED_RELEASE" | grep -qE '^v[0-9]{4}\.[0-9]{2}\.[0-9]{2}([.][0-9]+)?$'; then
|
|
@@ -792,6 +856,29 @@ jobs:
|
|
|
792
856
|
|
|
793
857
|
UPLOAD_FAILURES=0
|
|
794
858
|
for REQ in "${REQS[@]}"; do
|
|
859
|
+
REQ_FAILURES=0
|
|
860
|
+
LINEAGE_FLAGS=(--test-cycle "${{ github.event.workflow_run.id }}")
|
|
861
|
+
CYCLE_OUT="$(mktemp)"
|
|
862
|
+
bash scripts/report-test-cycle.sh start \
|
|
863
|
+
--project-slug {{PROJECT_SLUG}} \
|
|
864
|
+
--release "${DERIVED_RELEASE}" \
|
|
865
|
+
--sdlc-stage "${STAGE:-2}" \
|
|
866
|
+
--environment uat \
|
|
867
|
+
--cycle-kind e2e \
|
|
868
|
+
--provider github_actions \
|
|
869
|
+
--external-run-id "${{ github.event.workflow_run.id }}" \
|
|
870
|
+
--external-run-attempt "${{ github.event.workflow_run.run_attempt }}" \
|
|
871
|
+
--idempotency-key "github:${{ github.repository }}:e2e-regression.${REQ}.e2e:${{ github.event.workflow_run.id }}:${{ github.event.workflow_run.run_attempt }}:${STAGE:-2}:${DERIVED_RELEASE}" \
|
|
872
|
+
--commit-sha "${{ github.event.workflow_run.head_sha }}" \
|
|
873
|
+
--branch "${PRIOR_BRANCH}" \
|
|
874
|
+
--workflow-name "E2E Regression" \
|
|
875
|
+
--workflow-url "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.event.workflow_run.id }}" \
|
|
876
|
+
--output-file "$CYCLE_OUT"
|
|
877
|
+
. "$CYCLE_OUT"
|
|
878
|
+
rm -f "$CYCLE_OUT"
|
|
879
|
+
if [ "${cycle_supported:-false}" = "true" ] && [ -n "${cycle_record_id:-}" ]; then
|
|
880
|
+
LINEAGE_FLAGS+=(--evidence-scope cycle --test-cycle-record-id "${cycle_record_id}")
|
|
881
|
+
fi
|
|
795
882
|
# 1. JSON reporter output → e2e_result. The single canonical
|
|
796
883
|
# machine-readable artefact for the run.
|
|
797
884
|
if [ -f e2e-artifacts/e2e-regression-results.json ]; then
|
|
@@ -799,12 +886,14 @@ jobs:
|
|
|
799
886
|
{{PROJECT_SLUG}} "$REQ" e2e_result \
|
|
800
887
|
e2e-artifacts/e2e-regression-results.json \
|
|
801
888
|
--category e2e_result ${FLAGS} --release "${DERIVED_RELEASE}" \
|
|
889
|
+
"${LINEAGE_FLAGS[@]}" \
|
|
802
890
|
--meta-key "tier=${TIER}"
|
|
803
891
|
then
|
|
804
892
|
:
|
|
805
893
|
else
|
|
806
894
|
echo "::warning::e2e_result upload failed for ${REQ}"
|
|
807
895
|
UPLOAD_FAILURES=$((UPLOAD_FAILURES + 1))
|
|
896
|
+
REQ_FAILURES=$((REQ_FAILURES + 1))
|
|
808
897
|
fi
|
|
809
898
|
fi
|
|
810
899
|
# 2. Playwright HTML report zip → e2e_report. Operator-
|
|
@@ -829,13 +918,36 @@ jobs:
|
|
|
829
918
|
{{PROJECT_SLUG}} "$REQ" e2e_report \
|
|
830
919
|
e2e-artifacts/playwright-report.zip \
|
|
831
920
|
--category e2e_report ${FLAGS} --release "${DERIVED_RELEASE}" \
|
|
921
|
+
"${LINEAGE_FLAGS[@]}" \
|
|
832
922
|
--meta-key "tier=${TIER}"
|
|
833
923
|
then
|
|
834
924
|
:
|
|
835
925
|
else
|
|
836
926
|
echo "::warning::playwright HTML report upload failed for ${REQ} (non-blocking — JSON results are canonical evidence)"
|
|
927
|
+
REQ_FAILURES=$((REQ_FAILURES + 1))
|
|
837
928
|
fi
|
|
838
929
|
fi
|
|
930
|
+
if [ "$REQ_FAILURES" -gt 0 ]; then
|
|
931
|
+
REQ_OUTCOME=failed
|
|
932
|
+
else
|
|
933
|
+
REQ_OUTCOME=passed
|
|
934
|
+
fi
|
|
935
|
+
bash scripts/report-test-cycle.sh complete \
|
|
936
|
+
--project-slug {{PROJECT_SLUG}} \
|
|
937
|
+
--release "${DERIVED_RELEASE}" \
|
|
938
|
+
--sdlc-stage "${STAGE:-2}" \
|
|
939
|
+
--environment uat \
|
|
940
|
+
--cycle-kind e2e \
|
|
941
|
+
--provider github_actions \
|
|
942
|
+
--external-run-id "${{ github.event.workflow_run.id }}" \
|
|
943
|
+
--external-run-attempt "${{ github.event.workflow_run.run_attempt }}" \
|
|
944
|
+
--idempotency-key "github:${{ github.repository }}:e2e-regression.${REQ}.e2e:${{ github.event.workflow_run.id }}:${{ github.event.workflow_run.run_attempt }}:${STAGE:-2}:${DERIVED_RELEASE}" \
|
|
945
|
+
--commit-sha "${{ github.event.workflow_run.head_sha }}" \
|
|
946
|
+
--branch "${PRIOR_BRANCH}" \
|
|
947
|
+
--workflow-name "E2E Regression" \
|
|
948
|
+
--workflow-url "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.event.workflow_run.id }}" \
|
|
949
|
+
--started-at "${cycle_started_at:-}" \
|
|
950
|
+
--outcome "${REQ_OUTCOME}"
|
|
839
951
|
done
|
|
840
952
|
|
|
841
953
|
# Per-spec failure screenshots from test-results/ are NOT
|
|
@@ -893,19 +1005,88 @@ jobs:
|
|
|
893
1005
|
APP_DEFECT_COUNT=0
|
|
894
1006
|
|
|
895
1007
|
# Parse the Playwright JSON reporter output and classify each failing test.
|
|
896
|
-
#
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
1008
|
+
# Walk suites/specs/tests/results recursively so nested suite structure,
|
|
1009
|
+
# multi-test specs, and richer error payloads are handled reliably.
|
|
1010
|
+
FAILING_TESTS=$(python3 - <<'PY'
|
|
1011
|
+
import json
|
|
1012
|
+
from pathlib import Path
|
|
1013
|
+
|
|
1014
|
+
path = Path("e2e-artifacts/e2e-regression-results.json")
|
|
1015
|
+
if not path.exists():
|
|
1016
|
+
raise SystemExit(0)
|
|
1017
|
+
|
|
1018
|
+
try:
|
|
1019
|
+
payload = json.loads(path.read_text(encoding="utf-8"))
|
|
1020
|
+
except Exception:
|
|
1021
|
+
raise SystemExit(0)
|
|
1022
|
+
|
|
1023
|
+
failing_states = {"failed", "timedOut", "interrupted", "flaky"}
|
|
1024
|
+
rows = []
|
|
1025
|
+
|
|
1026
|
+
def first_error_message(result):
|
|
1027
|
+
error = result.get("error") or {}
|
|
1028
|
+
if isinstance(error, dict):
|
|
1029
|
+
message = str(error.get("message") or "").strip()
|
|
1030
|
+
if message:
|
|
1031
|
+
return message
|
|
1032
|
+
stack = str(error.get("stack") or "").strip()
|
|
1033
|
+
if stack:
|
|
1034
|
+
for line in stack.splitlines():
|
|
1035
|
+
line = line.strip()
|
|
1036
|
+
if line:
|
|
1037
|
+
return line
|
|
1038
|
+
return "no error message"
|
|
1039
|
+
|
|
1040
|
+
def visit_suite(suite, parent_titles):
|
|
1041
|
+
local_title = str(suite.get("title") or "").strip()
|
|
1042
|
+
titles = parent_titles + ([local_title] if local_title else [])
|
|
1043
|
+
for child in suite.get("suites") or []:
|
|
1044
|
+
if isinstance(child, dict):
|
|
1045
|
+
visit_suite(child, titles)
|
|
1046
|
+
for spec in suite.get("specs") or []:
|
|
1047
|
+
if not isinstance(spec, dict):
|
|
1048
|
+
continue
|
|
1049
|
+
spec_file = (
|
|
1050
|
+
str((spec.get("location") or {}).get("file") or "").strip()
|
|
1051
|
+
or str(spec.get("file") or "").strip()
|
|
1052
|
+
or "(unknown spec file)"
|
|
1053
|
+
)
|
|
1054
|
+
spec_title = str(spec.get("title") or "").strip()
|
|
1055
|
+
title_parts = titles + ([spec_title] if spec_title else [])
|
|
1056
|
+
for test in spec.get("tests") or []:
|
|
1057
|
+
if not isinstance(test, dict):
|
|
1058
|
+
continue
|
|
1059
|
+
test_title = str(test.get("title") or "").strip()
|
|
1060
|
+
full_title = " › ".join(part for part in (title_parts + ([test_title] if test_title else [])) if part)
|
|
1061
|
+
results = [r for r in (test.get("results") or []) if isinstance(r, dict)]
|
|
1062
|
+
final_result = next((r for r in reversed(results) if (r.get("status") or "") in failing_states), None)
|
|
1063
|
+
status = str((final_result or {}).get("status") or test.get("status") or spec.get("status") or "").strip()
|
|
1064
|
+
if status not in failing_states:
|
|
1065
|
+
continue
|
|
1066
|
+
rows.append((spec_file, full_title or spec_title or test_title or "(untitled test)", status, first_error_message(final_result or {})))
|
|
1067
|
+
|
|
1068
|
+
for root_suite in payload.get("suites") or []:
|
|
1069
|
+
if isinstance(root_suite, dict):
|
|
1070
|
+
visit_suite(root_suite, [])
|
|
1071
|
+
|
|
1072
|
+
seen = set()
|
|
1073
|
+
for spec_file, title, status, message in rows:
|
|
1074
|
+
key = (spec_file, title, status, message)
|
|
1075
|
+
if key in seen:
|
|
1076
|
+
continue
|
|
1077
|
+
seen.add(key)
|
|
1078
|
+
print("\t".join([spec_file, title.replace("\t", " "), status, message.replace("\t", " ")]))
|
|
1079
|
+
if len(seen) >= 20:
|
|
1080
|
+
break
|
|
1081
|
+
PY
|
|
1082
|
+
)
|
|
902
1083
|
|
|
903
1084
|
if [ -z "$FAILING_TESTS" ]; then
|
|
904
1085
|
echo "No failing tests found in JSON report — nothing to triage."
|
|
905
1086
|
exit 0
|
|
906
1087
|
fi
|
|
907
1088
|
|
|
908
|
-
while IFS=$'\t' read -r TEST_NAME TEST_STATUS ERROR_MSG; do
|
|
1089
|
+
while IFS=$'\t' read -r SPEC_FILE TEST_NAME TEST_STATUS ERROR_MSG; do
|
|
909
1090
|
[ -z "$TEST_NAME" ] && continue
|
|
910
1091
|
|
|
911
1092
|
# --- Classification ---
|
|
@@ -961,10 +1142,10 @@ jobs:
|
|
|
961
1142
|
# --- File issue ---
|
|
962
1143
|
if [ "$FILE_LABEL" = "incident" ]; then
|
|
963
1144
|
# Dedup: check for existing open incident issue for the same test name.
|
|
964
|
-
EXISTING=$(gh issue list --label incident --state open --search "$TEST_NAME" --json number --jq '.[0].number' || true)
|
|
1145
|
+
EXISTING=$(gh issue list --label incident --state open --search "$SPEC_FILE $TEST_NAME" --json number --jq '.[0].number' || true)
|
|
965
1146
|
if [ -n "$EXISTING" ]; then
|
|
966
1147
|
echo " [DEDUP] $TEST_NAME — existing incident #$EXISTING already open. Posting comment."
|
|
967
|
-
gh issue comment "$EXISTING" --body "Additional failure detected in workflow run ${WORKFLOW_URL} (SHA ${GIT_SHA}). Classification: ${CLASSIFICATION}. Rationale: ${RATIONALE}"
|
|
1148
|
+
gh issue comment "$EXISTING" --body "Additional failure detected in workflow run ${WORKFLOW_URL} (SHA ${GIT_SHA}). Spec: ${SPEC_FILE}. Classification: ${CLASSIFICATION}. Rationale: ${RATIONALE}"
|
|
968
1149
|
continue
|
|
969
1150
|
fi
|
|
970
1151
|
|
|
@@ -977,7 +1158,9 @@ jobs:
|
|
|
977
1158
|
ISSUE_BODY=$(cat <<EOF
|
|
978
1159
|
## E2E Regression Failure — ${CLASSIFICATION}
|
|
979
1160
|
|
|
1161
|
+
**Spec file:** ${SPEC_FILE}
|
|
980
1162
|
**Test name:** ${TEST_NAME}
|
|
1163
|
+
**Final status:** ${TEST_STATUS}
|
|
981
1164
|
**Error message:**
|
|
982
1165
|
\`\`\`
|
|
983
1166
|
${ERROR_MSG}
|
|
@@ -1009,7 +1192,7 @@ jobs:
|
|
|
1009
1192
|
ISSUE_BODY=$(echo "$ISSUE_BODY" | sed 's/^ //')
|
|
1010
1193
|
|
|
1011
1194
|
gh issue create \
|
|
1012
|
-
--title "[REGRESSION] ${TEST_NAME}" \
|
|
1195
|
+
--title "[REGRESSION] ${SPEC_FILE} :: ${TEST_NAME}" \
|
|
1013
1196
|
--label "incident,${CLASSIFICATION}" \
|
|
1014
1197
|
--body "$ISSUE_BODY"
|
|
1015
1198
|
|
|
@@ -1020,7 +1203,9 @@ jobs:
|
|
|
1020
1203
|
ISSUE_BODY=$(cat <<EOF
|
|
1021
1204
|
## E2E Regression Failure — ${CLASSIFICATION}
|
|
1022
1205
|
|
|
1206
|
+
**Spec file:** ${SPEC_FILE}
|
|
1023
1207
|
**Test name:** ${TEST_NAME}
|
|
1208
|
+
**Final status:** ${TEST_STATUS}
|
|
1024
1209
|
**Error message:**
|
|
1025
1210
|
\`\`\`
|
|
1026
1211
|
${ERROR_MSG}
|
|
@@ -1039,7 +1224,7 @@ jobs:
|
|
|
1039
1224
|
ISSUE_BODY=$(echo "$ISSUE_BODY" | sed 's/^ //')
|
|
1040
1225
|
|
|
1041
1226
|
gh issue create \
|
|
1042
|
-
--title "[TEST-BUG] ${TEST_NAME}" \
|
|
1227
|
+
--title "[TEST-BUG] ${SPEC_FILE} :: ${TEST_NAME}" \
|
|
1043
1228
|
--label "test-bug" \
|
|
1044
1229
|
--body "$ISSUE_BODY"
|
|
1045
1230
|
fi
|
|
@@ -15,6 +15,7 @@ on:
|
|
|
15
15
|
jobs:
|
|
16
16
|
detect-req:
|
|
17
17
|
name: Detect REQ from branch
|
|
18
|
+
if: ${{ !startsWith(github.head_ref, 'chore/close-out-') }}
|
|
18
19
|
runs-on: {{RUNNER}}
|
|
19
20
|
outputs:
|
|
20
21
|
req_id: ${{ steps.detect.outputs.req_id }}
|
|
@@ -108,6 +109,46 @@ jobs:
|
|
|
108
109
|
echo "No new e2e specs detected — all captures will tag origin=regression."
|
|
109
110
|
fi
|
|
110
111
|
|
|
112
|
+
- name: Register feature release and start feature E2E cycle
|
|
113
|
+
id: cycle
|
|
114
|
+
if: always()
|
|
115
|
+
run: |
|
|
116
|
+
REQ_ID="${{ needs.detect-req.outputs.req_id }}"
|
|
117
|
+
: > "$GITHUB_OUTPUT"
|
|
118
|
+
CONFIG_URL=""
|
|
119
|
+
if [ -f sdlc-config.json ]; then
|
|
120
|
+
CONFIG_URL=$(jq -r '.devaudit.base_url // empty' sdlc-config.json 2>/dev/null || true)
|
|
121
|
+
fi
|
|
122
|
+
BASE="${CONFIG_URL:-$DEVAUDIT_BASE_URL_VAR}"
|
|
123
|
+
if [ -z "$BASE" ] || [ -z "$DEVAUDIT_API_KEY" ]; then
|
|
124
|
+
echo "::warning::DevAudit not configured — skipping feature E2E cycle lifecycle."
|
|
125
|
+
echo "cycle_supported=false" >> "$GITHUB_OUTPUT"
|
|
126
|
+
exit 0
|
|
127
|
+
fi
|
|
128
|
+
export DEVAUDIT_BASE_URL="${BASE%/}"
|
|
129
|
+
chmod +x scripts/upload-evidence.sh scripts/report-test-cycle.sh 2>/dev/null || true
|
|
130
|
+
bash scripts/upload-evidence.sh \
|
|
131
|
+
{{PROJECT_SLUG}} _compliance-docs release_registration README.md \
|
|
132
|
+
--release "$REQ_ID" --create-release-if-missing \
|
|
133
|
+
--environment uat --category planning \
|
|
134
|
+
--git-sha "${{ github.event.pull_request.head.sha }}" \
|
|
135
|
+
--branch "${{ github.head_ref }}" || true
|
|
136
|
+
bash scripts/report-test-cycle.sh start \
|
|
137
|
+
--project-slug {{PROJECT_SLUG}} \
|
|
138
|
+
--release "$REQ_ID" \
|
|
139
|
+
--sdlc-stage 2 \
|
|
140
|
+
--environment uat \
|
|
141
|
+
--cycle-kind e2e \
|
|
142
|
+
--provider github_actions \
|
|
143
|
+
--external-run-id "${{ github.run_id }}" \
|
|
144
|
+
--external-run-attempt "${{ github.run_attempt }}" \
|
|
145
|
+
--idempotency-key "github:${{ github.repository }}:feature-in-scope-e2e.e2e:${{ github.run_id }}:${{ github.run_attempt }}:2:${REQ_ID}" \
|
|
146
|
+
--commit-sha "${{ github.event.pull_request.head.sha }}" \
|
|
147
|
+
--branch "${{ github.head_ref }}" \
|
|
148
|
+
--workflow-name "Feature In-Scope E2E" \
|
|
149
|
+
--workflow-url "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" \
|
|
150
|
+
--output-file "$GITHUB_OUTPUT"
|
|
151
|
+
|
|
111
152
|
{{E2E_FEATURE_TEST_STEP}}
|
|
112
153
|
- name: Upload feature E2E evidence (stage 2, origin=feature)
|
|
113
154
|
if: always()
|
|
@@ -127,13 +168,55 @@ jobs:
|
|
|
127
168
|
zip -qr playwright-report.zip playwright-report/ 2>/dev/null || true
|
|
128
169
|
fi
|
|
129
170
|
if [ -f playwright-report.zip ]; then
|
|
171
|
+
LINEAGE_FLAGS=(--test-cycle "${{ github.run_id }}")
|
|
172
|
+
if [ "${{ steps.cycle.outputs.cycle_supported }}" = "true" ] && [ -n "${{ steps.cycle.outputs.cycle_record_id }}" ]; then
|
|
173
|
+
LINEAGE_FLAGS+=(--evidence-scope cycle --test-cycle-record-id "${{ steps.cycle.outputs.cycle_record_id }}")
|
|
174
|
+
fi
|
|
130
175
|
bash scripts/upload-evidence.sh \
|
|
131
176
|
{{PROJECT_SLUG}} "$REQ_ID" feature_e2e_report playwright-report.zip \
|
|
132
177
|
--category e2e_report --release "$REQ_ID" --create-release-if-missing \
|
|
133
178
|
--environment uat --sdlc-stage 2 \
|
|
134
179
|
--git-sha "${{ github.event.pull_request.head.sha }}" \
|
|
135
180
|
--ci-run-id "${{ github.run_id }}" --branch "${{ github.head_ref }}" \
|
|
136
|
-
|
|
181
|
+
"${LINEAGE_FLAGS[@]}" \
|
|
137
182
|
--meta-key "origin=feature" \
|
|
138
183
|
|| echo "::warning::feature E2E report upload failed"
|
|
139
184
|
fi
|
|
185
|
+
|
|
186
|
+
- name: Complete feature E2E cycle
|
|
187
|
+
if: always()
|
|
188
|
+
run: |
|
|
189
|
+
if [ "${{ steps.cycle.outputs.cycle_supported }}" != "true" ] || [ -z "${{ steps.cycle.outputs.cycle_record_id }}" ]; then
|
|
190
|
+
exit 0
|
|
191
|
+
fi
|
|
192
|
+
CONFIG_URL=""
|
|
193
|
+
if [ -f sdlc-config.json ]; then
|
|
194
|
+
CONFIG_URL=$(jq -r '.devaudit.base_url // empty' sdlc-config.json 2>/dev/null || true)
|
|
195
|
+
fi
|
|
196
|
+
BASE="${CONFIG_URL:-$DEVAUDIT_BASE_URL_VAR}"
|
|
197
|
+
if [ -z "$BASE" ] || [ -z "$DEVAUDIT_API_KEY" ]; then
|
|
198
|
+
exit 0
|
|
199
|
+
fi
|
|
200
|
+
case "${{ job.status }}" in
|
|
201
|
+
success) OUTCOME=passed ;;
|
|
202
|
+
cancelled) OUTCOME=cancelled ;;
|
|
203
|
+
*) OUTCOME=failed ;;
|
|
204
|
+
esac
|
|
205
|
+
export DEVAUDIT_BASE_URL="${BASE%/}"
|
|
206
|
+
chmod +x scripts/report-test-cycle.sh 2>/dev/null || true
|
|
207
|
+
bash scripts/report-test-cycle.sh complete \
|
|
208
|
+
--project-slug {{PROJECT_SLUG}} \
|
|
209
|
+
--release "${{ needs.detect-req.outputs.req_id }}" \
|
|
210
|
+
--sdlc-stage 2 \
|
|
211
|
+
--environment uat \
|
|
212
|
+
--cycle-kind e2e \
|
|
213
|
+
--provider github_actions \
|
|
214
|
+
--external-run-id "${{ github.run_id }}" \
|
|
215
|
+
--external-run-attempt "${{ github.run_attempt }}" \
|
|
216
|
+
--idempotency-key "github:${{ github.repository }}:feature-in-scope-e2e.e2e:${{ github.run_id }}:${{ github.run_attempt }}:2:${{ needs.detect-req.outputs.req_id }}" \
|
|
217
|
+
--commit-sha "${{ github.event.pull_request.head.sha }}" \
|
|
218
|
+
--branch "${{ github.head_ref }}" \
|
|
219
|
+
--workflow-name "Feature In-Scope E2E" \
|
|
220
|
+
--workflow-url "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" \
|
|
221
|
+
--started-at "${{ steps.cycle.outputs.cycle_started_at }}" \
|
|
222
|
+
--outcome "$OUTCOME"
|
|
@@ -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 }}
|
|
@@ -128,6 +129,17 @@ jobs:
|
|
|
128
129
|
sleep 10
|
|
129
130
|
done
|
|
130
131
|
|
|
132
|
+
- name: Confirm terminal host deployment success
|
|
133
|
+
env:
|
|
134
|
+
GH_TOKEN: ${{ github.token }}
|
|
135
|
+
run: |
|
|
136
|
+
chmod +x scripts/check-host-deployment.sh 2>/dev/null || true
|
|
137
|
+
bash scripts/check-host-deployment.sh \
|
|
138
|
+
--repo="${{ github.repository }}" \
|
|
139
|
+
--sha="${GIT_SHA}" \
|
|
140
|
+
--max-attempts=30 \
|
|
141
|
+
--poll-seconds=10
|
|
142
|
+
|
|
131
143
|
- name: Production smoke tests (read-only)
|
|
132
144
|
run: |
|
|
133
145
|
echo "=== Production Smoke Tests ==="
|
|
@@ -230,7 +242,7 @@ jobs:
|
|
|
230
242
|
|
|
231
243
|
- name: Promote in-scope releases (evidence + status)
|
|
232
244
|
run: |
|
|
233
|
-
chmod +x scripts/upload-evidence.sh 2>/dev/null || true
|
|
245
|
+
chmod +x scripts/upload-evidence.sh scripts/report-test-cycle.sh 2>/dev/null || true
|
|
234
246
|
PROMOTED=0
|
|
235
247
|
for PREFIX in ${REQS}; do
|
|
236
248
|
echo "=== Promoting ${PREFIX} ==="
|
|
@@ -239,14 +251,37 @@ jobs:
|
|
|
239
251
|
VERSION=$(echo "$RESP" | jq -r '.latest.version // empty')
|
|
240
252
|
[ -z "$VERSION" ] && VERSION="${PREFIX}"
|
|
241
253
|
RELEASE_ID=$(echo "$RESP" | jq -r '.latest.id // empty')
|
|
254
|
+
REQ_FAILURES=0
|
|
255
|
+
LINEAGE_FLAGS=(--test-cycle "${CI_RUN}")
|
|
256
|
+
CYCLE_OUT="$(mktemp)"
|
|
257
|
+
bash scripts/report-test-cycle.sh start \
|
|
258
|
+
--project-slug "${PROJECT_SLUG}" \
|
|
259
|
+
--release "${VERSION}" \
|
|
260
|
+
--sdlc-stage 5 \
|
|
261
|
+
--environment production \
|
|
262
|
+
--cycle-kind smoke \
|
|
263
|
+
--provider github_actions \
|
|
264
|
+
--external-run-id "${CI_RUN}" \
|
|
265
|
+
--external-run-attempt "${{ github.run_attempt }}" \
|
|
266
|
+
--idempotency-key "github:${{ github.repository }}:post-deploy-prod.smoke:${CI_RUN}:${{ github.run_attempt }}:5:${VERSION}" \
|
|
267
|
+
--commit-sha "${GIT_SHA}" \
|
|
268
|
+
--branch main \
|
|
269
|
+
--workflow-name "Post-Deploy Production" \
|
|
270
|
+
--workflow-url "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" \
|
|
271
|
+
--output-file "$CYCLE_OUT"
|
|
272
|
+
. "$CYCLE_OUT"
|
|
273
|
+
rm -f "$CYCLE_OUT"
|
|
274
|
+
if [ "${cycle_supported:-false}" = "true" ] && [ -n "${cycle_record_id:-}" ]; then
|
|
275
|
+
LINEAGE_FLAGS+=(--evidence-scope cycle --test-cycle-record-id "${cycle_record_id}")
|
|
276
|
+
fi
|
|
242
277
|
# Production smoke evidence (whole-app health) attached to this release.
|
|
243
278
|
if [ -f prod-smoke-results.json ]; then
|
|
244
279
|
bash scripts/upload-evidence.sh \
|
|
245
280
|
"${PROJECT_SLUG}" "${VERSION}" smoke_test prod-smoke-results.json \
|
|
246
281
|
--release "${VERSION}" --create-release-if-missing --environment production \
|
|
247
282
|
--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}"
|
|
283
|
+
"${LINEAGE_FLAGS[@]}" \
|
|
284
|
+
|| { echo "Warning: smoke upload failed for ${VERSION}"; REQ_FAILURES=$((REQ_FAILURES + 1)); }
|
|
250
285
|
fi
|
|
251
286
|
# Carry the release ticket into the production environment so the
|
|
252
287
|
# prod-review gate is self-contained.
|
|
@@ -261,11 +296,31 @@ jobs:
|
|
|
261
296
|
"${PROJECT_SLUG}" "${VERSION}" release_ticket "$TICKET" \
|
|
262
297
|
--release "${VERSION}" --create-release-if-missing --environment production \
|
|
263
298
|
--category release_artifact --sdlc-stage 5 --git-sha "${GIT_SHA}" --ci-run-id "${CI_RUN}" --branch main \
|
|
264
|
-
--test-cycle "${CI_RUN}" \
|
|
265
299
|
|| echo "Warning: ticket upload failed for ${VERSION}"
|
|
266
300
|
else
|
|
267
301
|
echo "No RELEASE-TICKET-${VERSION}.md found — skipping ticket (date-versioned or archived)."
|
|
268
302
|
fi
|
|
303
|
+
if [ "$REQ_FAILURES" -gt 0 ]; then
|
|
304
|
+
REQ_OUTCOME=failed
|
|
305
|
+
else
|
|
306
|
+
REQ_OUTCOME=passed
|
|
307
|
+
fi
|
|
308
|
+
bash scripts/report-test-cycle.sh complete \
|
|
309
|
+
--project-slug "${PROJECT_SLUG}" \
|
|
310
|
+
--release "${VERSION}" \
|
|
311
|
+
--sdlc-stage 5 \
|
|
312
|
+
--environment production \
|
|
313
|
+
--cycle-kind smoke \
|
|
314
|
+
--provider github_actions \
|
|
315
|
+
--external-run-id "${CI_RUN}" \
|
|
316
|
+
--external-run-attempt "${{ github.run_attempt }}" \
|
|
317
|
+
--idempotency-key "github:${{ github.repository }}:post-deploy-prod.smoke:${CI_RUN}:${{ github.run_attempt }}:5:${VERSION}" \
|
|
318
|
+
--commit-sha "${GIT_SHA}" \
|
|
319
|
+
--branch main \
|
|
320
|
+
--workflow-name "Post-Deploy Production" \
|
|
321
|
+
--workflow-url "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" \
|
|
322
|
+
--started-at "${cycle_started_at:-}" \
|
|
323
|
+
--outcome "${REQ_OUTCOME}"
|
|
269
324
|
# Advance status (idempotent — re-PATCHing an already-promoted release is a no-op).
|
|
270
325
|
if [ -n "$RELEASE_ID" ]; then
|
|
271
326
|
curl -s -o /dev/null -w " ${VERSION} status patch: HTTP %{http_code}\n" \
|