@metasession.co/devaudit-cli 0.1.32 → 0.1.34
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 +1 -1
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/scripts/upload-evidence.sh +37 -18
- package/sdlc/files/_common/skills/e2e-test-engineer/SKILL.md +20 -3
- package/sdlc/files/_common/skills/e2e-test-engineer/references/evidence-shot-core.ts +62 -0
- package/sdlc/files/_common/skills/e2e-test-engineer/references/evidence.ts +73 -19
- package/sdlc/files/ci/ci.yml.template +61 -5
- package/sdlc/files/ci/compliance-evidence.yml.template +32 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@metasession.co/devaudit-cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.34",
|
|
4
4
|
"description": "DevAudit CLI — installs, syncs, and operates the Metasession SDLC across consumer projects.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
35
|
"@clack/prompts": "^0.8.2",
|
|
36
|
-
"@metasession.co/devaudit-plugin-sdk": "^0.1.
|
|
36
|
+
"@metasession.co/devaudit-plugin-sdk": "^0.1.34",
|
|
37
37
|
"commander": "^12.1.0",
|
|
38
38
|
"consola": "^3.2.3",
|
|
39
39
|
"env-paths": "^3.0.0",
|
|
@@ -72,6 +72,11 @@ EVIDENCE_CATEGORY=""
|
|
|
72
72
|
RELEASE_TITLE=""
|
|
73
73
|
CHANGE_TYPE=""
|
|
74
74
|
GATE_STATUS=""
|
|
75
|
+
# Repeatable `--meta-key key=value` accumulator. Each pair gets merged
|
|
76
|
+
# into the metadata JSON sent to the portal. Used by the screenshot
|
|
77
|
+
# upload loop to pass `origin=feature|regression` from the per-PNG
|
|
78
|
+
# sidecar JSON written by the evidenceShot helper.
|
|
79
|
+
META_KEYS=()
|
|
75
80
|
|
|
76
81
|
while [ "$#" -gt 0 ]; do
|
|
77
82
|
case "$1" in
|
|
@@ -91,6 +96,17 @@ while [ "$#" -gt 0 ]; do
|
|
|
91
96
|
# ran-and-failed != never-ran. Unknown values dropped server-side.
|
|
92
97
|
# DevAudit-Installer#96.
|
|
93
98
|
--gate-status) GATE_STATUS="$2"; shift 2 ;;
|
|
99
|
+
# --meta-key key=value (repeatable). Merged into the metadata JSON
|
|
100
|
+
# before posting. Validates the `key=value` shape; rejects bare
|
|
101
|
+
# keys without `=`.
|
|
102
|
+
--meta-key)
|
|
103
|
+
if [[ "$2" != *=* ]]; then
|
|
104
|
+
echo "Error: --meta-key requires key=value (got: $2)"
|
|
105
|
+
exit 1
|
|
106
|
+
fi
|
|
107
|
+
META_KEYS+=("$2")
|
|
108
|
+
shift 2
|
|
109
|
+
;;
|
|
94
110
|
*) echo "Unknown option: $1"; exit 1 ;;
|
|
95
111
|
esac
|
|
96
112
|
done
|
|
@@ -118,24 +134,27 @@ fi
|
|
|
118
134
|
DEVAUDIT_BASE_URL="${DEVAUDIT_BASE_URL%/}"
|
|
119
135
|
|
|
120
136
|
# --- Build metadata JSON ---
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
METADATA="${
|
|
137
|
+
# Assemble entries first; only emit `{ ... }` if at least one field is
|
|
138
|
+
# set. Each entry is a `"key":"value"` JSON pair with the value
|
|
139
|
+
# json-escaped (quotes + backslashes).
|
|
140
|
+
json_escape() {
|
|
141
|
+
printf '%s' "$1" | sed -e 's/\\/\\\\/g' -e 's/"/\\"/g'
|
|
142
|
+
}
|
|
143
|
+
META_ENTRIES=()
|
|
144
|
+
[ -n "$GIT_SHA" ] && META_ENTRIES+=("\"gitSha\":\"$(json_escape "$GIT_SHA")\"")
|
|
145
|
+
[ -n "$CI_RUN_ID" ] && META_ENTRIES+=("\"ciRunId\":\"$(json_escape "$CI_RUN_ID")\"")
|
|
146
|
+
[ -n "$BRANCH" ] && META_ENTRIES+=("\"branch\":\"$(json_escape "$BRANCH")\"")
|
|
147
|
+
for KV in "${META_KEYS[@]}"; do
|
|
148
|
+
KEY="${KV%%=*}"
|
|
149
|
+
VAL="${KV#*=}"
|
|
150
|
+
META_ENTRIES+=("\"$(json_escape "$KEY")\":\"$(json_escape "$VAL")\"")
|
|
151
|
+
done
|
|
152
|
+
if [ "${#META_ENTRIES[@]}" -gt 0 ]; then
|
|
153
|
+
IFS=','
|
|
154
|
+
METADATA="{${META_ENTRIES[*]}}"
|
|
155
|
+
unset IFS
|
|
156
|
+
else
|
|
157
|
+
METADATA="{}"
|
|
139
158
|
fi
|
|
140
159
|
|
|
141
160
|
# --- Collect files ---
|
|
@@ -193,6 +193,20 @@ Then bucket each failure:
|
|
|
193
193
|
|
|
194
194
|
**Then check for missed requirements.** For each numbered acceptance criterion from Phase 2, confirm at least one *passing* test covers it. An AC with no passing test — because no test was written, or because the test fails — is a missed requirement. File it.
|
|
195
195
|
|
|
196
|
+
### Phase 7 — Regression-pack handoff
|
|
197
|
+
|
|
198
|
+
After Phase 6 succeeds — green run, all ACs proved, defects filed for anything missing — the new spec(s) you authored move into the project's regression pack. There is **no separate graduation step**. The pack is defined as:
|
|
199
|
+
|
|
200
|
+
> **Every `*.spec.ts` (and `*.spec.tsx`) under `tests/e2e/` or `e2e/`.**
|
|
201
|
+
|
|
202
|
+
There is no `regression/` sub-directory, no `@regression` tag, no manifest file. Being committed and merged to `develop` *is* the graduation step. Once that happens:
|
|
203
|
+
|
|
204
|
+
1. The next CI run (on this branch's PR, or on `develop` after merge) executes the new spec alongside every existing one.
|
|
205
|
+
2. The evidenceShot helper sees `process.env.E2E_NEW_SPECS` (computed by CI as `git diff --diff-filter=A <merge-base>...HEAD`) and tags this branch's captures as `origin: 'feature'`.
|
|
206
|
+
3. Post-merge, develop runs see an empty `E2E_NEW_SPECS` and tag every capture as `origin: 'regression'`. The original feature-branch captures stay tagged `feature` as the historical proof of original landing; subsequent develop runs accumulate `regression` captures alongside.
|
|
207
|
+
|
|
208
|
+
You don't need to do anything explicit for this step — it's a property of the pipeline, not an action. Surface it in the final report so the reviewer knows the new tests are now load-bearing for every future release.
|
|
209
|
+
|
|
196
210
|
### Filing defects
|
|
197
211
|
|
|
198
212
|
Use whatever tracker integration you found in Phase 1: `gh issue create`, `glab issue create`, a Jira or Linear MCP tool, `az boards work-item create`. If nothing is available, produce a markdown report with each defect formatted ready to paste.
|
|
@@ -234,7 +248,7 @@ import { evidenceShot } from './helpers/evidence';
|
|
|
234
248
|
test('AC1: edit dialog opens with fields pre-filled', async ({ page }) => {
|
|
235
249
|
await openEditDialog(page, item.id);
|
|
236
250
|
await expect(dialog.locator('#name')).toHaveValue(item.name);
|
|
237
|
-
await evidenceShot(page, 'REQ-037', '
|
|
251
|
+
await evidenceShot(page, 'REQ-037', 1, 'edit-dialog-prefilled');
|
|
238
252
|
// ...rest of test
|
|
239
253
|
});
|
|
240
254
|
```
|
|
@@ -242,11 +256,14 @@ test('AC1: edit dialog opens with fields pre-filled', async ({ page }) => {
|
|
|
242
256
|
**Discipline:**
|
|
243
257
|
|
|
244
258
|
- Call `evidenceShot` **immediately after** the AC-proving assertion, before navigating, closing dialogs, or any further interaction.
|
|
245
|
-
-
|
|
259
|
+
- AC number is a separate argument (`ac: number`) — the helper composes the filename `REQ-XXX-AC<n>-<slug>.png`. The slug describes what the screenshot proves (`edit-dialog-prefilled`), NOT the AC number.
|
|
260
|
+
- Slug is kebab-case lowercase (`[a-z0-9-]+`). Capitalised slugs, underscores, or spaces throw.
|
|
246
261
|
- One screenshot per AC, not per test.
|
|
247
262
|
- Failure forensics stays untouched (`screenshot: 'only-on-failure'` + `trace: 'on-first-retry'`).
|
|
248
263
|
|
|
249
|
-
The helper is shipped automatically into `e2e/helpers/evidence.ts` by the SDLC sync (node-stack consumers). Output lands at `compliance/evidence/<REQ-ID>/screenshots
|
|
264
|
+
The helper is shipped automatically into `e2e/helpers/evidence.ts` by the SDLC sync (node-stack consumers). Output lands at `compliance/evidence/<REQ-ID>/screenshots/REQ-XXX-AC<n>-<slug>.png` — commit these PNGs as part of the evidence pack so reviewers can corroborate the test-plan AC mapping.
|
|
265
|
+
|
|
266
|
+
The helper also writes a sidecar `<filename>.meta.json` containing the AC mapping + the screenshot's **origin** — `feature` if the spec was added on the current branch, `regression` if the spec already existed. The consumer's CI passes `origin` through to the DevAudit portal as evidence metadata so the release-detail page can render feature vs regression captures distinctly. Auto-detected from `process.env.E2E_NEW_SPECS` — no manual tagging required.
|
|
250
267
|
|
|
251
268
|
The canonical helper source lives at `references/evidence.ts` in this skill.
|
|
252
269
|
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure helpers backing `evidenceShot`. Kept Playwright-free so they
|
|
3
|
+
* can be unit-tested without pulling `@playwright/test` into the
|
|
4
|
+
* installer repo. The thin wrapper in `evidence.ts` does the Page
|
|
5
|
+
* screenshot + fs writes around these.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export type EvidenceShotOrigin = 'feature' | 'regression';
|
|
9
|
+
|
|
10
|
+
export interface EvidenceShotSidecar {
|
|
11
|
+
readonly origin: EvidenceShotOrigin;
|
|
12
|
+
readonly reqId: string;
|
|
13
|
+
readonly ac: number;
|
|
14
|
+
readonly slug: string;
|
|
15
|
+
readonly specFile: string;
|
|
16
|
+
readonly capturedAt: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const REQ_ID_RE = /^REQ-[A-Z0-9-]+$/;
|
|
20
|
+
const SLUG_RE = /^[a-z0-9-]+$/;
|
|
21
|
+
|
|
22
|
+
export function validateEvidenceShotInputs(reqId: string, ac: number, slug: string): void {
|
|
23
|
+
if (!REQ_ID_RE.test(reqId)) {
|
|
24
|
+
throw new Error(`evidenceShot: invalid reqId "${reqId}" (must match ${REQ_ID_RE})`);
|
|
25
|
+
}
|
|
26
|
+
if (!Number.isInteger(ac) || ac <= 0) {
|
|
27
|
+
throw new Error(`evidenceShot: invalid ac "${ac}" (must be a positive integer)`);
|
|
28
|
+
}
|
|
29
|
+
if (!SLUG_RE.test(slug)) {
|
|
30
|
+
throw new Error(
|
|
31
|
+
`evidenceShot: invalid slug "${slug}" (must match ${SLUG_RE} — kebab-case, no AC prefix)`,
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function composeScreenshotFilename(reqId: string, ac: number, slug: string): string {
|
|
37
|
+
return `${reqId}-AC${ac}-${slug}.png`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Auto-detect origin from `process.env.E2E_NEW_SPECS` — the consumer
|
|
42
|
+
* globalSetup writes the newline-delimited list of spec files added
|
|
43
|
+
* on the current branch (`git diff --diff-filter=A`). If the calling
|
|
44
|
+
* spec appears in that list, it's `feature`; otherwise `regression`.
|
|
45
|
+
*
|
|
46
|
+
* Empty / missing env (typical for post-merge develop runs) → every
|
|
47
|
+
* capture is `regression`, which is the correct semantic outcome.
|
|
48
|
+
*/
|
|
49
|
+
export function autoDetectEvidenceShotOrigin(
|
|
50
|
+
specFile: string,
|
|
51
|
+
newSpecsEnv: string | undefined,
|
|
52
|
+
): EvidenceShotOrigin {
|
|
53
|
+
const list = (newSpecsEnv ?? '').trim();
|
|
54
|
+
if (list.length === 0) return 'regression';
|
|
55
|
+
const newSpecs = new Set(
|
|
56
|
+
list
|
|
57
|
+
.split(/\r?\n/)
|
|
58
|
+
.map((s) => s.trim())
|
|
59
|
+
.filter(Boolean),
|
|
60
|
+
);
|
|
61
|
+
return newSpecs.has(specFile) ? 'feature' : 'regression';
|
|
62
|
+
}
|
|
@@ -1,40 +1,94 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
1
2
|
import path from 'path';
|
|
2
|
-
import { type Page } from '@playwright/test';
|
|
3
|
+
import { test, type Page } from '@playwright/test';
|
|
4
|
+
import {
|
|
5
|
+
autoDetectEvidenceShotOrigin,
|
|
6
|
+
composeScreenshotFilename,
|
|
7
|
+
validateEvidenceShotInputs,
|
|
8
|
+
type EvidenceShotOrigin,
|
|
9
|
+
type EvidenceShotSidecar,
|
|
10
|
+
} from './evidence-shot-core';
|
|
3
11
|
|
|
4
|
-
|
|
12
|
+
export type { EvidenceShotOrigin };
|
|
13
|
+
|
|
14
|
+
export interface EvidenceShotOptions {
|
|
15
|
+
/** Capture the full page rather than the viewport. Default: true. */
|
|
16
|
+
readonly fullPage?: boolean;
|
|
17
|
+
/**
|
|
18
|
+
* Override the auto-detected origin. By default the helper consults
|
|
19
|
+
* `process.env.E2E_NEW_SPECS` — set by the consumer's Playwright
|
|
20
|
+
* `globalSetup` step in CI — and tags the screenshot `feature` if
|
|
21
|
+
* the calling spec's file appears in that list, else `regression`.
|
|
22
|
+
*/
|
|
23
|
+
readonly origin?: EvidenceShotOrigin;
|
|
24
|
+
}
|
|
5
25
|
|
|
6
26
|
/**
|
|
7
|
-
* Write a per-
|
|
27
|
+
* Write a per-AC behavioural-proof screenshot into the requirement's
|
|
28
|
+
* evidence pack.
|
|
8
29
|
*
|
|
9
30
|
* Call this AT the assertion that proves the AC, before any further
|
|
10
31
|
* interaction or navigation. The PNG is committed as part of the
|
|
11
32
|
* evidence pack and used by reviewers to corroborate the test-plan
|
|
12
33
|
* AC mapping.
|
|
13
34
|
*
|
|
14
|
-
*
|
|
35
|
+
* Filename: `REQ-XXX-AC<n>-<slug>.png`
|
|
36
|
+
* Output path: `compliance/evidence/<reqId>/screenshots/<filename>`
|
|
37
|
+
*
|
|
38
|
+
* The helper also writes a sidecar `<filename>.meta.json` containing
|
|
39
|
+
* `{ origin, reqId, ac, slug, specFile, capturedAt }`. The consumer's
|
|
40
|
+
* CI upload step reads the sidecar and passes `origin` as evidence
|
|
41
|
+
* metadata to the portal so the release-detail page can tell feature
|
|
42
|
+
* captures apart from regression-pack reruns.
|
|
15
43
|
*
|
|
16
44
|
* @example
|
|
17
45
|
* await expect(dialog.locator('#name')).toHaveValue(item.name);
|
|
18
|
-
* await evidenceShot(page, 'REQ-037', '
|
|
46
|
+
* await evidenceShot(page, 'REQ-037', 1, 'edit-dialog-prefilled');
|
|
47
|
+
*
|
|
48
|
+
* @param page Playwright Page
|
|
49
|
+
* @param reqId `REQ-` prefixed requirement id (e.g. `REQ-037`)
|
|
50
|
+
* @param ac AC number — mandatory; every screenshot proves one AC
|
|
51
|
+
* @param slug kebab-case descriptive slug (no `AC<n>-` prefix; the
|
|
52
|
+
* helper composes it from `ac`)
|
|
19
53
|
*/
|
|
20
54
|
export async function evidenceShot(
|
|
21
55
|
page: Page,
|
|
22
56
|
reqId: string,
|
|
57
|
+
ac: number,
|
|
23
58
|
slug: string,
|
|
24
|
-
opts:
|
|
59
|
+
opts: EvidenceShotOptions = {},
|
|
25
60
|
): Promise<void> {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
61
|
+
validateEvidenceShotInputs(reqId, ac, slug);
|
|
62
|
+
const fileName = composeScreenshotFilename(reqId, ac, slug);
|
|
63
|
+
const dir = path.join(process.cwd(), 'compliance/evidence', reqId, 'screenshots');
|
|
64
|
+
const pngPath = path.join(dir, fileName);
|
|
65
|
+
const sidecarPath = `${pngPath}.meta.json`;
|
|
66
|
+
|
|
67
|
+
await page.screenshot({ path: pngPath, fullPage: opts.fullPage ?? true });
|
|
68
|
+
|
|
69
|
+
const specFile = resolveSpecFile();
|
|
70
|
+
const origin = opts.origin ?? autoDetectEvidenceShotOrigin(specFile, process.env.E2E_NEW_SPECS);
|
|
71
|
+
const sidecar: EvidenceShotSidecar = {
|
|
72
|
+
origin,
|
|
35
73
|
reqId,
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
74
|
+
ac,
|
|
75
|
+
slug,
|
|
76
|
+
specFile,
|
|
77
|
+
capturedAt: new Date().toISOString(),
|
|
78
|
+
};
|
|
79
|
+
await fs.promises.writeFile(sidecarPath, `${JSON.stringify(sidecar, null, 2)}\n`, 'utf8');
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Test-info gives us the absolute spec path. Make it repo-relative so
|
|
84
|
+
* it survives serialisation into the sidecar JSON + the CI's git diff
|
|
85
|
+
* comparison list.
|
|
86
|
+
*/
|
|
87
|
+
function resolveSpecFile(): string {
|
|
88
|
+
try {
|
|
89
|
+
const info = test.info();
|
|
90
|
+
return path.relative(process.cwd(), info.file);
|
|
91
|
+
} catch {
|
|
92
|
+
return 'unknown';
|
|
93
|
+
}
|
|
40
94
|
}
|
|
@@ -42,6 +42,12 @@ jobs:
|
|
|
42
42
|
|
|
43
43
|
steps:
|
|
44
44
|
- uses: actions/checkout@v4
|
|
45
|
+
with:
|
|
46
|
+
# Full history so the "new specs on this branch" calculation
|
|
47
|
+
# (E2E_NEW_SPECS, below) can do a real diff against the merge
|
|
48
|
+
# base — Playwright's evidenceShot helper reads that env var to
|
|
49
|
+
# tag each captured screenshot as feature vs regression.
|
|
50
|
+
fetch-depth: 0
|
|
45
51
|
|
|
46
52
|
# ── Cached installs (skip if already present on self-hosted runner) ──
|
|
47
53
|
|
|
@@ -142,6 +148,32 @@ jobs:
|
|
|
142
148
|
- name: Wait for dev server
|
|
143
149
|
run: npx wait-on http://localhost:3000 --timeout 120000
|
|
144
150
|
|
|
151
|
+
# Compute the set of e2e spec files added on this branch (relative
|
|
152
|
+
# to the merge base). The evidenceShot helper in the consumer's
|
|
153
|
+
# tests reads E2E_NEW_SPECS at capture time and tags each screenshot
|
|
154
|
+
# `origin=feature` when its spec appears in the list, else
|
|
155
|
+
# `origin=regression`. On main / develop (post-merge) the diff is
|
|
156
|
+
# empty and every capture is `regression`, which is the intended
|
|
157
|
+
# behaviour. Newline-delimited; relative paths.
|
|
158
|
+
- name: Compute new-spec list for E2E origin tagging
|
|
159
|
+
run: |
|
|
160
|
+
BASE_REF="${{ github.base_ref || github.event.repository.default_branch || 'main' }}"
|
|
161
|
+
BASE_SHA=$(git merge-base "origin/${BASE_REF}" HEAD 2>/dev/null || git rev-parse HEAD)
|
|
162
|
+
NEW_SPECS=$(git diff --name-only --diff-filter=A "${BASE_SHA}...HEAD" \
|
|
163
|
+
-- 'tests/e2e/**/*.spec.ts' 'tests/e2e/**/*.spec.tsx' \
|
|
164
|
+
'e2e/**/*.spec.ts' 'e2e/**/*.spec.tsx' 2>/dev/null || true)
|
|
165
|
+
{
|
|
166
|
+
echo "E2E_NEW_SPECS<<EOF"
|
|
167
|
+
echo "$NEW_SPECS"
|
|
168
|
+
echo "EOF"
|
|
169
|
+
} >> "$GITHUB_ENV"
|
|
170
|
+
if [ -n "$NEW_SPECS" ]; then
|
|
171
|
+
echo "New e2e specs on this branch (origin=feature for captures):"
|
|
172
|
+
echo "$NEW_SPECS" | sed 's/^/ /'
|
|
173
|
+
else
|
|
174
|
+
echo "No new e2e specs detected — all captures will tag origin=regression."
|
|
175
|
+
fi
|
|
176
|
+
|
|
145
177
|
{{E2E_TEST_STEP}}
|
|
146
178
|
{{E2E_AUTHENTICATED_STEP}}
|
|
147
179
|
# ── Gate 5: Build ──
|
|
@@ -191,6 +223,7 @@ jobs:
|
|
|
191
223
|
coverage/coverage-summary.json
|
|
192
224
|
gate-outcomes.json
|
|
193
225
|
compliance/evidence/*/screenshots/*.png
|
|
226
|
+
compliance/evidence/*/screenshots/*.png.meta.json
|
|
194
227
|
retention-days: 90
|
|
195
228
|
|
|
196
229
|
# ──────────────────────────────────────────────
|
|
@@ -486,16 +519,39 @@ jobs:
|
|
|
486
519
|
[ -n "$REQ_TITLE" ] && REQ_META+=(--release-title "$REQ_TITLE")
|
|
487
520
|
[ -n "$REQ_CT" ] && REQ_META+=(--change-type "$REQ_CT")
|
|
488
521
|
for PNG in "${SHOTS[@]}"; do
|
|
489
|
-
# The
|
|
490
|
-
#
|
|
491
|
-
#
|
|
492
|
-
|
|
493
|
-
NAMED="${SHOT_TMP}/${
|
|
522
|
+
# The basename is the canonical evidenceShot filename
|
|
523
|
+
# `REQ-XXX-AC<n>-<slug>.png`. The portal validates this
|
|
524
|
+
# shape on upload — anything else is rejected with 400.
|
|
525
|
+
BASE="$(basename "$PNG")"
|
|
526
|
+
NAMED="${SHOT_TMP}/${BASE}"
|
|
494
527
|
cp "$PNG" "$NAMED" 2>/dev/null || continue
|
|
528
|
+
|
|
529
|
+
# Read the sidecar JSON (`<png>.meta.json`) written by the
|
|
530
|
+
# evidenceShot helper. Pull `origin` out so the portal
|
|
531
|
+
# render can tell feature vs regression captures apart.
|
|
532
|
+
# Sidecar missing → upload with no origin metadata; the
|
|
533
|
+
# portal renders those as "unknown" until consumers re-
|
|
534
|
+
# onboard via `devaudit update`.
|
|
535
|
+
ORIGIN_META=()
|
|
536
|
+
if [ -f "${PNG}.meta.json" ]; then
|
|
537
|
+
ORIGIN=$(python3 -c "
|
|
538
|
+
import json, sys
|
|
539
|
+
try:
|
|
540
|
+
with open('${PNG}.meta.json') as f:
|
|
541
|
+
print(json.load(f).get('origin', ''))
|
|
542
|
+
except Exception:
|
|
543
|
+
pass
|
|
544
|
+
" 2>/dev/null || true)
|
|
545
|
+
if [ "$ORIGIN" = "feature" ] || [ "$ORIGIN" = "regression" ]; then
|
|
546
|
+
ORIGIN_META+=(--meta-key "origin=${ORIGIN}")
|
|
547
|
+
fi
|
|
548
|
+
fi
|
|
549
|
+
|
|
495
550
|
bash scripts/upload-evidence.sh \
|
|
496
551
|
{{PROJECT_SLUG}} "$REQ" screenshot "$NAMED" \
|
|
497
552
|
--category test_report ${FLAGS} --release "$REQ" \
|
|
498
553
|
"${REQ_META[@]}" \
|
|
554
|
+
"${ORIGIN_META[@]}" \
|
|
499
555
|
|| echo "::warning::evidence screenshot upload failed: ${PNG} -> ${REQ}"
|
|
500
556
|
done
|
|
501
557
|
done
|
|
@@ -186,6 +186,38 @@ jobs:
|
|
|
186
186
|
upload_governance compliance/incident-report.md incident_report
|
|
187
187
|
upload_governance compliance/governance/incident-report.md incident_report
|
|
188
188
|
|
|
189
|
+
# ── Audit-log export (DevAudit-Installer#98 WS2) ──────────────
|
|
190
|
+
# Snapshot the portal's audit log for the rolling 90-day window
|
|
191
|
+
# and upload as `evidence_type=audit_log`. Closes three
|
|
192
|
+
# framework-coverage clauses on every release:
|
|
193
|
+
# - ISO27001.A.8.16 — Monitoring activities
|
|
194
|
+
# - EUAIA.Art-12 — Record-keeping (automatic logging)
|
|
195
|
+
# - GDPR.Art-32 — Security of processing (audit-log half)
|
|
196
|
+
#
|
|
197
|
+
# The portal endpoint defaults to the last 90 days when no
|
|
198
|
+
# `since`/`until` query params are passed; omit them so the
|
|
199
|
+
# consumer side stays zero-config. Endpoint shipped in
|
|
200
|
+
# META-COMPLY PR #413; project-scoped API key (uploader role)
|
|
201
|
+
# already has read access via `resolveCiUploadAuth`.
|
|
202
|
+
AUDIT_LOG_FILE="$(mktemp -t audit-log-XXXXXX.json)"
|
|
203
|
+
if curl -sSf -H "Authorization: Bearer ${DEVAUDIT_API_KEY}" \
|
|
204
|
+
"${DEVAUDIT_BASE_URL%/}/api/ci/projects/{{PROJECT_SLUG}}/audit-log/export" \
|
|
205
|
+
-o "$AUDIT_LOG_FILE"; then
|
|
206
|
+
echo "Uploading: audit-log.json (audit_log — 90-day window)"
|
|
207
|
+
bash scripts/upload-evidence.sh \
|
|
208
|
+
{{PROJECT_SLUG}} _compliance-docs audit_log "$AUDIT_LOG_FILE" \
|
|
209
|
+
--category compliance_document ${FLAGS} --release "${DERIVED_RELEASE}" \
|
|
210
|
+
"${DERIVED_META[@]}" \
|
|
211
|
+
|| echo "Warning: Failed to upload audit-log.json"
|
|
212
|
+
else
|
|
213
|
+
# Soft-fail: an export hiccup shouldn't break the rest of the
|
|
214
|
+
# evidence pipeline. Surfaces as a warning in the workflow log;
|
|
215
|
+
# the framework-coverage panel will show MISSING for the three
|
|
216
|
+
# clauses above until the next successful upload.
|
|
217
|
+
echo "::warning::Audit-log export failed — endpoint unreachable or 4xx/5xx. Three framework-coverage clauses (ISO27001.A.8.16, EUAIA.Art-12, GDPR.Art-32 audit-log half) will stay MISSING until the next run."
|
|
218
|
+
fi
|
|
219
|
+
rm -f "$AUDIT_LOG_FILE"
|
|
220
|
+
|
|
189
221
|
# Helper: emit a `--release-title …` `--change-type …` pair for a given
|
|
190
222
|
# REQ, derived from its pending release-ticket H1 and the most recent
|
|
191
223
|
# commit attributed to that REQ. Empty pair when neither is available.
|