Repository navigation
feat(monitor): name why a daily batch is incomplete #23
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: CI | |
| # Runs the test suite on Linux and macOS, the Windows platform contract, the Linux lane sandbox | |
| # tests, a parse check of every module, a boot smoke test, the Linux installer and the | |
| # documentation freshness gate. | |
| # | |
| # WHAT MAKES THIS RUNNABLE AT ALL. Tests that read the LIVE FLEET off the machine they run on (the | |
| # registry's declared repository paths, ~/.cloudflared, and a checkout declared in | |
| # monitor/projects.json) SKIP WITH A STATED REASON when the fleet is absent, because a runner has | |
| # one checkout at a path that changes per run and no fleet. They run in full on a fleet | |
| # machine. A red test nobody can turn green is how a check gets switched off. Nothing is silently | |
| # excluded: `npm test` runs every file, and the reasons appear in this job's log. | |
| # | |
| # Deliberately NOT here: a real sweep. A sweep over real repositories can block for hours on one | |
| # of them, and a job that gets cancelled teaches nobody anything. | |
| on: | |
| # release.yml runs this file as its gate on a v* tag. | |
| workflow_call: | |
| secrets: | |
| CW_RELEASE_SCOPE: | |
| required: false | |
| push: | |
| # Branches only: a tag is tested by release.yml's call above, and a second run of the same tag | |
| # here would take the same concurrency group and cancel it. | |
| branches: ['**'] | |
| # Skip prose-only pushes: most pushes go to main, and a docs edit should not cost a full CI run. | |
| # Test fixtures are markdown too, so they are re-included last (later patterns win). | |
| paths: | |
| - '**' | |
| - '!**/*.md' | |
| - '!docs/**' | |
| - '**/test/fixtures/**' | |
| # Every pull request runs in full, prose-only ones included: the docs job lives in this file, and a | |
| # required check that never reports blocks the merge. | |
| pull_request: | |
| # A newer push to the same ref cancels the run it supersedes; only the latest commit's result is read, | |
| # and every superseded minute is billed on a private repository. | |
| concurrency: | |
| group: ci-${{ github.event_name }}-${{ github.ref }} | |
| cancel-in-progress: true | |
| # Every job reads the tree and publishes nothing; a token with write scopes here is the credential a | |
| # poisoned dependency or a compromised action would move on. | |
| permissions: | |
| contents: read | |
| jobs: | |
| tests: | |
| name: Test suite (${{ matrix.os }}, Node ${{ matrix.node }}) | |
| # Supported majors: 22 is the floor package.json engines.node declares (maintenance LTS until | |
| # 2027-04-30) and 26 is the version CI, releases and the Action run. bin/test/node-floor.test.mjs holds the lowest version here, | |
| # the engines floor and the CLI's refusal to one declaration, and requires every OS to run it. | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| os: [ubuntu-latest, macos-latest] | |
| node: ['22', '26'] | |
| runs-on: ${{ matrix.os }} | |
| timeout-minutes: 30 | |
| steps: | |
| # Actions pinned to commit SHAs, not tags: a tag can be silently repointed by the action | |
| # owner (the trivy-action / tj-actions compromise class). The trailing comment names the | |
| # release each SHA was resolved from via `git ls-remote --tags`. | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| persist-credentials: false | |
| # the history guards read real history; a depth-1 clone makes them skip with the reason | |
| fetch-depth: 0 | |
| - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 | |
| with: | |
| # node:sqlite, zlib zstd and node --test's glob form are all used. The floor is real, not habitual. | |
| node-version: ${{ matrix.node }} | |
| # The release-name gates need the private redaction maps, which never enter the tree. They come | |
| # from one secret, a base64 gzipped tar of the three maps, set from the sidecar with: | |
| # tar -czf - -C <sidecar> monitor/release-redactions.json monitor/publish-redactions.json \ | |
| # identity/repo-identities.json | base64 | gh secret set CW_RELEASE_SCOPE | |
| # This log is public. Every report those gates print masks the name it found, which | |
| # bin/test/release-names-output.test.mjs holds. Pushes only: a pull request's code never runs | |
| # with the maps. | |
| - name: Load the release-name maps | |
| # Linux only: the maps are about tree content, not platform, so one OS is enough to hold | |
| # the secret; elsewhere the release-name gates skip and say so. | |
| if: github.event_name == 'push' && runner.os == 'Linux' | |
| env: | |
| RELEASE_SCOPE: ${{ secrets.CW_RELEASE_SCOPE }} | |
| run: | | |
| if [ -z "$RELEASE_SCOPE" ]; then | |
| echo "::notice::CW_RELEASE_SCOPE is not set, so the release-name gates skip and say so" | |
| exit 0 | |
| fi | |
| dir="$RUNNER_TEMP/release-scope" | |
| mkdir -p "$dir" | |
| printf '%s' "$RELEASE_SCOPE" | base64 -d | tar -xzf - -C "$dir" | |
| { | |
| echo "CW_RELEASE_REDACTIONS=$dir/monitor/release-redactions.json" | |
| echo "CW_PUBLISH_REDACTIONS=$dir/monitor/publish-redactions.json" | |
| echo "CW_REPO_IDENTITIES=$dir/identity/repo-identities.json" | |
| } >> "$GITHUB_ENV" | |
| # No `npm ci`. commitwork declares zero runtime dependencies, and this job is part of what | |
| # keeps it that way — an install step would quietly absorb the day that changes. | |
| - name: Run the suite | |
| run: npm test | |
| # The Linux lane sandbox, measured. The suite above runs without bwrap, so its sandbox tests skip | |
| # with a reason; this job installs bubblewrap and passt and runs them for real, so open lanes | |
| # reaching host loopback, the host's addresses or /run sockets is a red build here rather than | |
| # a skip. A skip of a Linux confinement test in this job is a failure. | |
| linux-sandbox: | |
| name: Linux lane sandbox (bwrap + pasta) | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| persist-credentials: false | |
| - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 | |
| with: | |
| node-version: '26' | |
| - name: Install bubblewrap and passt | |
| run: | | |
| sudo apt-get update -qq | |
| sudo apt-get install -y --no-install-recommends bubblewrap passt | |
| # Ubuntu 24.04 can restrict unprivileged user namespaces through AppArmor. This job measures | |
| # the sandbox, not the host policy, so it lifts the restriction and says so. | |
| if [ "$(sysctl -n kernel.apparmor_restrict_unprivileged_userns 2>/dev/null)" = "1" ]; then | |
| echo "::notice::lifting kernel.apparmor_restrict_unprivileged_userns for this job" | |
| sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0 | |
| fi | |
| bwrap --ro-bind / / --unshare-net --die-with-parent -- /bin/true | |
| pasta --version | |
| - name: Sandbox tests, with no Linux skip allowed | |
| run: | | |
| set -o pipefail | |
| node --test bin/test/sandbox-netns-live.test.mjs bin/test/sandbox-net.test.mjs \ | |
| bin/test/sandbox-host.test.mjs bin/test/lane-env-confinement.test.mjs \ | |
| bin/test/sandbox-linux-devices.test.mjs 2>&1 | tee "$RUNNER_TEMP/sandbox.log" | |
| if grep -E 'bwrap and pasta confinement is NOT verified|bwrap cannot create a namespace|pasta \(package passt\) is absent' "$RUNNER_TEMP/sandbox.log"; then | |
| echo "::error::a Linux confinement test skipped in the job that exists to run it" | |
| exit 1 | |
| fi | |
| syntax: | |
| name: Every module parses (Node ${{ matrix.node }}) | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| node: ['22', '26'] | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| persist-credentials: false | |
| - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 | |
| with: | |
| node-version: ${{ matrix.node }} | |
| # Cheap, and it catches the class of breakage that makes the suite above fail to even start, | |
| # with a clearer message than a test runner gives for the same fault. | |
| # | |
| # workflows/ IS EXCLUDED, AND THE REASON IS THAT THE CHECK IS WRONG THERE, not the code. | |
| # Those files are Workflow scripts, not Node modules: the runtime wraps the body in an async | |
| # function, so `export const meta = {...}` followed by a top-level `return` or `await` is | |
| # legal and `node --check` rejects it. Running it anyway would make this job red over a | |
| # correct file, and a check that cries wolf gets switched off, which is worse than not having | |
| # it. Excluded by path, declared here, and the count is printed so the exclusion cannot grow | |
| # unnoticed. | |
| - name: node --check every module | |
| run: | | |
| fail=0 checked=0 skipped=0 | |
| # An assignment, not a pipe in the for list: bash -e fails the step when git does, where a | |
| # failing git inside `$(git … | grep …)` yielded zero files and a green job. | |
| modules=$(git ls-files -- '*.mjs' ':(exclude,glob)**/node_modules/**') | |
| for f in $modules; do | |
| case "$f" in | |
| workflows/*) skipped=$((skipped+1)); continue ;; | |
| esac | |
| checked=$((checked+1)) | |
| node --check "$f" || { echo "::error file=$f::does not parse"; fail=1; } | |
| done | |
| echo "checked $checked module(s); skipped $skipped Workflow script(s) under workflows/ — see the note in this file" | |
| [ "$checked" -gt 0 ] || { echo "::error::no module was checked"; exit 1; } | |
| exit $fail | |
| windows: | |
| name: Windows portability (Node ${{ matrix.node }}) | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| node: ['22', '26'] | |
| runs-on: windows-latest | |
| # 30, not the 20 the Linux jobs use: a Windows runner's process spawn is markedly slower and | |
| # this job runs the suite twice — once serially for verdicts, once for side effects. | |
| timeout-minutes: 30 | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| persist-credentials: false | |
| - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 | |
| with: | |
| node-version: ${{ matrix.node }} | |
| # WHY THIS JOB EXISTS. Windows defects are invisible on Linux and rarely findable by | |
| # reading: separator mismatches that make the structural gates flag everything, CRLF that | |
| # silently disables line-based parsers, `import(absolutePath)` that stops a suite from | |
| # running at all, and `npm` (which is `npm.cmd` here) being unspawnable. Code that is never | |
| # EXECUTED on its second platform collects them. | |
| # | |
| # WHY IT IS NOT `npm test`, YET. The full suite is not reproducible run to run: test files | |
| # that read live ambient state (`reports/`, the ledgers, the lock files) instead of fixtures | |
| # move the failure count between runs. Gating on that count would be red forever, and a | |
| # check that cries wolf gets switched off. So this job gates on what IS deterministic, and | |
| # the full Windows suite joins it once those tests run on fixtures. | |
| # 1. Every module parses. The Linux job does this with a shell loop; PowerShell needs its own, | |
| # and having it here is not duplication — it is the check catching Windows-only syntax and | |
| # path faults at the cheapest possible point. | |
| - name: node --check every module | |
| shell: pwsh | |
| run: | | |
| $fail = 0; $checked = 0; $skipped = 0 | |
| foreach ($f in (git ls-files '*.mjs')) { | |
| if ($f -like 'workflows/*') { $skipped++; continue } # Workflow scripts, not modules — see the Linux job | |
| $checked++ | |
| node --check $f | |
| if ($LASTEXITCODE -ne 0) { Write-Output "::error file=$f::does not parse"; $fail = 1 } | |
| } | |
| Write-Output "checked $checked module(s); skipped $skipped Workflow script(s)" | |
| if ($checked -eq 0) { Write-Output "::error::no module was checked"; exit 1 } | |
| exit $fail | |
| # 2. The platform contract itself. These suites ARE the Windows contract — shell resolution, | |
| # the Node repo walk, the process-tree kill, batch-shim spawning, the path hazards, the | |
| # listener lens, the installer's manager shapes, and the two structural gates. All are | |
| # fixture-driven and deterministic, so this list must be GREEN. | |
| - name: The Windows contract holds | |
| shell: pwsh | |
| run: | | |
| node --test --test-concurrency=1 ` | |
| lib/test/posix-shell.test.mjs ` | |
| lib/test/proc-tree.test.mjs ` | |
| lib/test/repo-walk.test.mjs ` | |
| lib/test/win-spawn.test.mjs ` | |
| lib/test/win-path-safety.test.mjs ` | |
| lib/test/untrusted-text.test.mjs ` | |
| monitor/test/port-bind-windows.test.mjs ` | |
| monitor/test/port-bind.test.mjs ` | |
| bin/test/install-catalog-managers.test.mjs ` | |
| bin/test/install-catalog.test.mjs ` | |
| bin/test/esm-specifier-shape.test.mjs ` | |
| bin/test/tracked-imports.test.mjs ` | |
| bin/test/docs-doctor.test.mjs ` | |
| bin/test/measured.test.mjs ` | |
| bin/test/test-run.test.mjs ` | |
| bin/test/scan-exclusions-regex.test.mjs | |
| # 3. THE SUITE MUST NOT MODIFY THE WORKING TREE. Cheap and deterministic. Two Windows-only | |
| # ways a test dirties the tree: | |
| # - a Windows path interpolated unquoted into a POSIX shell loses its backslashes, | |
| # becomes RELATIVE, and the redirect writes into the repository root; | |
| # - cmd.exe reads the `>` of an arrow function `()=>{}` on a command line as an output | |
| # redirection and creates a file literally named `{}` in the repository root. | |
| # A test that dirties the tree is a defect regardless of whether it passes. | |
| # | |
| # NOTE WHAT THIS STEP CANNOT SEE: `reports/` and `.claude/` are BOTH GITIGNORED, so | |
| # `git status` is structurally incapable of reporting pollution there, such as production | |
| # code spawned by tests appending to the live `reports/perf-feedback.jsonl`. The ignored | |
| # paths are covered instead by bin/test-run.mjs, which fingerprints them around the run | |
| # and fails if one moved; the two checks together cover tracked AND ignored, and neither | |
| # alone does. | |
| - name: The suite leaves the tree clean | |
| shell: pwsh | |
| run: | | |
| # Default concurrency, not the runner's serial mode: this step reads SIDE EFFECTS, never | |
| # assertions, so results are discarded and there is nothing for parallelism to | |
| # destabilise. The serial run belongs in the step above, where the verdicts matter. | |
| node --test "bin/**/*.test.mjs" "lib/**/*.test.mjs" 2>&1 | Out-Null | |
| $dirty = (git status --porcelain) | |
| if ($dirty) { | |
| Write-Output "::error::the test suite modified the working tree:" | |
| $dirty | ForEach-Object { Write-Output " $_" } | |
| exit 1 | |
| } | |
| Write-Output "working tree clean after the suite" | |
| # 4. The install path a Windows operator actually walks. `setup` with no TTY and no --yes is | |
| # report-only by contract, so this asserts the REPORT rather than installing anything: it | |
| # must run, and it must resolve a POSIX shell from the Git that the runner already has. | |
| # `doctor` is where a missing shell becomes visible, before it shows up as unexplained | |
| # check failures. | |
| - name: The install path reports honestly | |
| shell: pwsh | |
| run: | | |
| node bin/commitwork.mjs setup | |
| $out = node bin/commitwork.mjs doctor --manifest security-baseline 2>&1 | Out-String | |
| Write-Output $out | |
| if ($out -notmatch 'POSIX shell') { Write-Output '::error::doctor does not report the POSIX shell prerequisite'; exit 1 } | |
| if ($out -match '(?m)^\s*✗ POSIX shell') { | |
| Write-Output '::error::no POSIX shell resolved on a runner that has Git for Windows — the git-adjacent lookup regressed' | |
| exit 1 | |
| } | |
| smoke: | |
| name: It starts | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| persist-credentials: false | |
| - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 | |
| with: | |
| node-version: '26' | |
| # The suite above is entirely unit-level: it proves functions behave, never that the CLI | |
| # runs or the panel serves. commitwork has no compile step — zero runtime dependencies is | |
| # the point — so "does it build" has no meaning here and "does it boot" is the equivalent | |
| # question. | |
| # | |
| # bin/smoke.mjs asserts the panel's AUTH BOUNDARY, not merely that something responded: | |
| # the operator port must serve a real HTML document and the published port must refuse an | |
| # unauthenticated request. The second is the one worth having — a smoke test that only | |
| # checks for a response passes just as happily when auth has been switched off, on the port | |
| # a tunnel publishes. The smoke pins an absent auth store, so the panel it boots is | |
| # unbootstrapped on every runner and the refusal is the 503 bootstrap answer; a box with an | |
| # operator account answers 401. | |
| - name: CLI resolves and the panel boots | |
| run: node bin/smoke.mjs | |
| installer: | |
| name: Installer on Linux | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| persist-credentials: false | |
| - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 | |
| with: | |
| node-version: '26' | |
| # On Linux `commitwork setup` installs through Homebrew when brew is on PATH, else pipx, npm, | |
| # cargo, gem, composer and go, with apt or dnf after them. This job exercises the pipx route | |
| # end to end: a report-only run must exit 0 without a prompt, and a real install must leave | |
| # a tool that runs and that doctor then sees. | |
| - name: Report-only setup never prompts | |
| run: mkdir -p "$HOME" && node bin/commitwork.mjs setup < /dev/null | |
| env: | |
| HOME: ${{ runner.temp }}/home | |
| - name: Install one scanner through pipx | |
| run: | | |
| node bin/commitwork.mjs setup --yes --only flawfinder | |
| echo "$HOME/.local/bin" >> "$GITHUB_PATH" | |
| - name: The installed scanner runs and doctor sees it | |
| run: | | |
| flawfinder --version | |
| report=$(node bin/commitwork.mjs doctor --manifest security-baseline) | |
| printf '%s\n' "$report" | grep -E '✓ flawfinder' | |
| docs: | |
| name: Documentation freshness | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| persist-credentials: false | |
| # docs-doctor compares each durable doc's `verified-against` stamp against the commits | |
| # that have touched it since. With the default shallow clone it cannot see them and | |
| # reports drift that does not exist. | |
| fetch-depth: 0 | |
| - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 | |
| with: | |
| node-version: '26' | |
| # THIS JOB GATES ON GREY, WARNS ON ORANGE, AND THE ASYMMETRY IS DELIBERATE. | |
| # | |
| # docs-doctor exits 1 for orange (a stamped doc whose stamp is behind) and 2 for grey-only | |
| # (a durable doc with NO stamp at all). | |
| # | |
| # On a tree where several writers commit concurrently, a stamp falls behind its doc | |
| # routinely and transiently. Gating on orange would put CI red on a condition that resolves | |
| # itself and train everyone to ignore it: a check that cries wolf gets switched off, which is | |
| # worse than not having it. Grey is gated because grey is the worse class: an unstamped | |
| # durable doc has never been verified AT ALL. Orange ratchets to a gate when orange stops | |
| # being the normal condition of a shared tree; the gate exists and is runnable before it is | |
| # enforced, exactly as quality-gates does for qg-boot-test-pass. | |
| # | |
| # `rc=0` and `|| rc=$?` ARE LOAD-BEARING. GitHub runs `run:` steps as `bash -e {0}`, so a | |
| # bare invocation aborts the step the instant docs-doctor exits non-zero and the case | |
| # statement below is never reached — the job would fail on orange and reintroduce exactly | |
| # the problem this shape exists to avoid. `|| ...` suppresses -e; initialising rc keeps a | |
| # green run (which never assigns it) from falling through to the catch-all. | |
| - name: docs-doctor — grey blocks, orange warns | |
| run: | | |
| rc=0 | |
| node bin/docs-doctor.mjs || rc=$? | |
| case "$rc" in | |
| 0) echo "docs: all durable docs stamped and fresh" ;; | |
| 1) echo "::warning title=Documentation drift::orange — one or more stamps are behind their doc. Re-verify and refresh the stamp. Not blocking yet." ;; | |
| 2) echo "::error title=Unstamped durable doc::grey — a durable doc carries no verified-against stamp, so it has never been verified. Blocking."; exit 1 ;; | |
| *) echo "::error title=docs-doctor failed::exited $rc, which is neither 0, 1 nor 2 — treating an unrecognised outcome as failure rather than assuming it passed."; exit 1 ;; | |
| esac | |
| dependencies: | |
| name: Dependency advisories | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| persist-credentials: false | |
| - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 | |
| with: | |
| node-version: '26' | |
| # Reads the committed lockfile and the registry; installs nothing. Exit 21 is a shipped | |
| # advisory at high or above that an upgrade or override can clear; 22 is a result nobody could | |
| # measure, which fails the job rather than reading as clear. Advisories in devDependencies are | |
| # printed and do not gate: they do not ship. | |
| - name: Audit shipped dependencies | |
| run: node bin/commitwork.mjs deps . --audit-gate high --no-record | |
| # A Dependabot pull request merges itself only when every job above passed and | |
| # .github/dependency-updates.json permits every dependency it moves. Anything else stays open for | |
| # review. The decision is bin/dependency-policy-check.mjs, the same policy `commitwork deps` reads. | |
| # The author check reads the pull request's own author, not github.actor: whoever re-runs a job | |
| # becomes the actor, and an actor test is spoofable that way (zizmor bot-conditions). | |
| dependabot-merge: | |
| name: Merge a Dependabot update the policy allows | |
| needs: [tests, linux-sandbox, syntax, windows, smoke, installer, docs, dependencies] | |
| if: ${{ !failure() && !cancelled() && github.event_name == 'pull_request' && github.event.pull_request.user.login == 'dependabot[bot]' }} | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 5 | |
| permissions: | |
| contents: write | |
| pull-requests: write | |
| steps: | |
| - id: meta | |
| uses: dependabot/fetch-metadata@25dd0e34f4fe68f24cc83900b1fe3fe149efef98 # v3.1.0 | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| persist-credentials: false | |
| - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 | |
| with: | |
| node-version: '26' | |
| - id: policy | |
| name: Ask the dependency policy | |
| env: | |
| DEP_UPDATES_JSON: ${{ steps.meta.outputs.updated-dependencies-json }} | |
| DEP_GHSA_ID: ${{ steps.meta.outputs.ghsa-id }} | |
| run: node bin/dependency-policy-check.mjs --github-output | |
| - name: Merge | |
| if: steps.policy.outputs.allowed == 'true' | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| PR_URL: ${{ github.event.pull_request.html_url }} | |
| run: gh pr merge --squash "$PR_URL" |