Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 70 additions & 2 deletions .github/workflows/dependency-watch.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,15 @@ permissions:
contents: read
issues: write

concurrency:
group: dependency-watch-${{ github.repository }}
cancel-in-progress: false

jobs:
dependency-watch:
name: Weekly Dependency and Advisory Check
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0

Expand Down Expand Up @@ -108,6 +113,7 @@ jobs:
} >> "$GITHUB_STEP_SUMMARY"

- name: Create or update dependency alert issue
id: alert_issue
if: steps.pnpm_audit.outputs.exit_code != '0' || steps.cargo_summary.outputs.warning_count != '0' || steps.cargo_summary.outputs.vuln_count != '0'
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3
env:
Expand Down Expand Up @@ -149,22 +155,35 @@ jobs:

const existing = issues.find(issue => issue.title === title);

let mutation;
let issueAction;
if (existing) {
await github.rest.issues.update({
mutation = await github.rest.issues.update({
owner,
repo,
issue_number: existing.number,
body
});
issueAction = 'updated';
} else {
await github.rest.issues.create({
mutation = await github.rest.issues.create({
owner,
repo,
title,
body,
labels: ['dependencies', 'security']
});
issueAction = 'created';
}

const issueNumber = mutation.data.number;
const readback = await github.rest.issues.get({ owner, repo, issue_number: issueNumber });
if (readback.data.title !== title || readback.data.body !== body) {
throw new Error(`Dependency alert issue readback mismatch for #${issueNumber}`);
}
core.setOutput('issue_action', issueAction);
Comment on lines +181 to +184

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Record mutation outputs before readback can fail

When the create/update call succeeds but issues.get fails or returns mismatched content, this throws before issue_action and issue_number are written. The always-run completion step then sees an attempted issue step with empty action/number and reports mutation_count:0 and destination_id:null, losing the partial mutation accounting the new terminal state is meant to provide. Set those outputs as soon as mutation.data.number is known, before the readback failure path.

Useful? React with 👍 / 👎.

core.setOutput('issue_number', String(issueNumber));
core.setOutput('destination_readback_verified', 'true');

- name: Upload dependency reports
if: always()
Expand All @@ -185,3 +204,52 @@ jobs:
run: |
echo "Dependency watch detected actionable vulnerabilities."
exit 1

- name: Emit machine-readable completion state
if: always()
env:
JOB_STATUS: ${{ job.status }}
ISSUE_OUTCOME: ${{ steps.alert_issue.outcome }}
ISSUE_ACTION: ${{ steps.alert_issue.outputs.issue_action }}
ISSUE_NUMBER: ${{ steps.alert_issue.outputs.issue_number }}
ISSUE_READBACK: ${{ steps.alert_issue.outputs.destination_readback_verified }}
run: |
python3 <<'PY'
import json
import os
from datetime import datetime, timezone

job_ok = os.environ.get("JOB_STATUS") == "success"
action = os.environ.get("ISSUE_ACTION", "")
issue_number = os.environ.get("ISSUE_NUMBER", "")
issue_outcome = os.environ.get("ISSUE_OUTCOME", "")
issue_attempted = issue_outcome not in ("", "skipped")
readback_required = issue_attempted
readback_verified = os.environ.get("ISSUE_READBACK") == "true" if readback_required else True
skipped = job_ok and not action
partial = issue_attempted and not readback_verified
payload = {
"schema": "AutomationTerminalStateV1",
"automation_id": "github:AssistSupport/dependency-watch",
"state": "succeeded" if job_ok else "failed",
"completed": job_ok,
"partial": partial,
"skipped": skipped,
"mutation_count": 1 if action else 0,
"destination_readback": {
"required": readback_required,
"verified": readback_verified,
"destination_id": f"issue:{issue_number}" if issue_number else None,
"evidence": {"issue_action": action or "unknown", "issue_step_outcome": issue_outcome},
},
"operator_action_required": not job_ok,
"can_auto_archive": job_ok,
"observed_at": datetime.now(timezone.utc).isoformat().replace("+00:00", "Z"),
}
line = "automation_completion: " + json.dumps(payload, separators=(",", ":"))
print(line)
with open(os.environ["GITHUB_STEP_SUMMARY"], "a", encoding="utf-8") as summary:
summary.write("\n" + line + "\n")
if readback_required and not readback_verified:
raise SystemExit("dependency issue destination readback was not verified")
PY
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
"pr:notes": "bash scripts/git/generate-pr-notes.sh",
"check:workstation-preflight": "node scripts/ci/check-workstation-preflight.mjs",
"check:workflow-drift": "node scripts/ci/check-workflow-command-drift.mjs",
"check:dependency-watch-contract": "node scripts/ci/check-dependency-watch-contract.mjs",
"check:version-parity": "node scripts/ci/check-version-parity.mjs",
"check:monorepo-readiness": "pnpm health:repo",
"test": "vitest run",
Expand Down Expand Up @@ -69,7 +70,7 @@
"ui:gate:regression": "pnpm test:e2e:smoke && pnpm ui:test:visual && pnpm ui:test:a11y && pnpm ui:test:responsive",
"ui:gate": "pnpm ui:gate:static && pnpm ui:gate:regression",
"ui:test:visual:update": "bash scripts/ui/run-playwright-tag.sh @visual --update-snapshots",
"health:repo:checks": "pnpm git:guard:branch && pnpm check:workstation-preflight && pnpm check:workflow-drift && pnpm check:version-parity",
"health:repo:checks": "pnpm git:guard:branch && pnpm check:workstation-preflight && pnpm check:workflow-drift && pnpm check:dependency-watch-contract && pnpm check:version-parity",
"health:repo:static": "pnpm ui:gate:static",
"health:repo:frontend": "pnpm test",
"health:repo:search": "pnpm search-api:test",
Expand Down
22 changes: 22 additions & 0 deletions scripts/ci/check-dependency-watch-contract.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
#!/usr/bin/env node

import fs from "node:fs";

const workflow = fs.readFileSync(new URL("../../.github/workflows/dependency-watch.yml", import.meta.url), "utf8");
const requirements = [
["workflow concurrency", /^concurrency:\n {2}group:/m],
["job timeout", /^ {4}timeout-minutes: \d+/m],
["mutation action output", /core\.setOutput\('issue_action'/],
["destination id output", /core\.setOutput\('issue_number'/],
["destination readback", /github\.rest\.issues\.get\(/],
["readback mismatch failure", /throw new Error\(`Dependency alert issue readback mismatch/],
["machine completion state", /AutomationTerminalStateV1/],
["partial mutation accounting", /partial = issue_attempted and not readback_verified/],
];

const missing = requirements.filter(([, pattern]) => !pattern.test(workflow)).map(([name]) => name);
if (missing.length) {
console.error(`dependency-watch contract missing: ${missing.join(", ")}`);
process.exit(1);
}
console.log(JSON.stringify({ schema: "DependencyWatchContractTestV1", ok: true, tests: requirements.length }));
Loading