Skip to content
Draft
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
42 changes: 34 additions & 8 deletions .github/workflows/pr-risk.yml
Original file line number Diff line number Diff line change
Expand Up @@ -260,13 +260,25 @@ on:
required: true
enabled:
description: >-
Whether to grade at all. DEFAULTS TO FALSE: enrolling this workflow and
switching it on are two separate decisions, so a repo can land the
caller, get it reviewed, and start grading later — or stop grading
without reverting anything. When it resolves false the `grade` job does
not run: nothing is read, nothing is labelled, and no existing `risk:*`
label is touched (a disabled run leaves whatever the last enabled one
left, it does not clean up).
Whether to grade AUTOMATICALLY. DEFAULTS TO FALSE: enrolling this
workflow and switching it on are two separate decisions, so a repo can
land the caller, get it reviewed, and start grading later — or stop
grading without reverting anything. When it resolves false a
`pull_request` run does not grade: nothing is read, nothing is
labelled, and no existing `risk:*` label is touched (a disabled run
leaves whatever the last enabled one left, it does not clean up).


A MANUAL `workflow_dispatch` GRADES REGARDLESS. Dispatching requires
write access and is an explicit, attributed act, so there is no
unattended behaviour left for this switch to govern — and without that
exemption the staged rollout this input exists for cannot happen: a
repo enrolled OFF could not grade even one PR to see what the grader
would say, so "try it here before turning it on" would require turning
it on. The cost, stated plainly: false is a MUTE on the PR stream, not
a lockout. A repo needing the harder guarantee should remove the
caller — nothing here can bind a maintainer who can edit the workflow
file anyway.


`vars.RISK_CONFIG` on the CALLING repo OVERRIDES this, in both
Expand Down Expand Up @@ -320,7 +332,21 @@ jobs:

grade:
needs: gate
if: needs.gate.outputs.enabled == 'true'
# `enabled` governs AUTOMATIC grading — the `pull_request` stream. A manual dispatch is
# never blocked by it: running a workflow_dispatch requires write access on the repo and is
# an explicit, attributed act, so there is no unattended behaviour left for the switch to
# govern. Without this clause the staged rollout the switch exists for does not work — a
# repo that enrols OFF cannot grade even a single PR to see what the grader would say, so
# "try it here before turning it on" requires switching the whole repo on first, which is
# the thing being evaluated.
#
# What this costs, stated rather than buried: `{"enabled": false}` stops the PR-stream
# noise, but it does NOT stop a human running the grader by hand. It is a mute, not a
# lockout. If a repo ever needs the harder guarantee, remove the caller — nothing here can
# bind a maintainer who can edit the workflow file anyway.
if: >-
needs.gate.outputs.enabled == 'true' ||
github.event_name == 'workflow_dispatch'

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟠 High — The exemption is keyed on the event alone, but workflow_dispatch is not necessarily a human act: any principal with actions: write can fire it via gh workflow run or the REST endpoint, and since this file is on: workflow_call only, github.event_name reports the CALLER's event — a dispatch aimed at something else still satisfies the clause. That turns {"enabled": false} into a mute rather than a kill switch for the privileged grade job (pull-requests: write + issues: write), which can then relabel up to MAX_TARGETS=50 PRs per run. Gate on an explicit signal instead (e.g. a dedicated manual_grade input the caller must forward, or requiring a non-empty pr_number/pr_numbers). Raised by 4 of 8 reviewers (claude-opus-5-thinking-max adversarial, claude-opus-5-thinking-max edge-case, gpt-5.6-sol-max edge-case, kimi-k2.7-code adversarial).

name: Grade PR risk

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Medium — Nothing in the clause requires a resolvable target. pr_number/pr_numbers are both required: false, so the default Run-workflow form supplies neither, and on a dispatch there is no github.event.pull_request to fall back to — PR_NUMBERS resolves empty and grade-targets.sh hits its die in parse_targets (exit 2). A disabled repo that previously skipped cleanly now gets a red run; conditioning on (inputs.pr_number != '' || inputs.pr_numbers != '') fixes both this and part of the exemption's blast radius. Raised by 2 of 8 reviewers (claude-opus-5-thinking-max edge-case, claude-opus-5-thinking-max adversarial).

runs-on: ubuntu-latest
timeout-minutes: 30
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ This repo is **public** so any repo — public or private, inside or outside the
| [`assign-reviewers.yml`](.github/workflows/assign-reviewers.yml) | Auto-requests expertise-aware, load-balanced PR reviewers with new-folk randomization. Matches changed paths against a caller-repo `.github/reviewers.yml` (path-glob → reviewers, plus a `default_pool`), drops the author + `vars.REVIEWER_EXCLUDE`, ranks candidates by open review load (steering off anyone at/over `vars.REVIEWER_LOAD_CAP`), and may swap a slot for a `vars.REVIEWER_GROWTH_POOL` member. Requests go through the CLOUD_CODE_BOT app token so they work on fork PRs. Requires `vars.APP_ID` + `CLOUD_CODE_BOT_PRIVATE_KEY`. |
| [`assign-prs-to-author.yml`](.github/workflows/assign-prs-to-author.yml) | Housekeeping — assigns every open PR with no assignees to its author (bot-authored PRs skipped by default). Run on a schedule from a thin caller; useful when a team tracks PR ownership via assignees. The calling job needs `pull-requests: write` and `issues: write`. |
| [`pr-size.yml`](.github/workflows/pr-size.yml) | PR-size cap — fails (or, in `mode: warn`, only reports) when a PR's net diff exceeds `max_lines` non-generated changed lines, keeping diffs reviewable. Excludes dependency lockfiles, `linguist-generated` files (read from the base ref, so a PR can't exempt itself), Go generated-code markers, and per-repo `extra_lockfiles` / `extra_generated_globs`. A `bypass_label` (default `oversized-ok`) waves through a legitimately large change; a sticky bot comment explains overages when `bot_app_id` + `BOT_APP_PRIVATE_KEY` are supplied (degrades to status + step summary without them). Counting logic + tests live in [`scripts/check-pr-size/`](scripts/check-pr-size). |
| [`pr-risk.yml`](.github/workflows/pr-risk.yml) | **Advisory PR risk grading (shadow check)** — **off by default** (`enabled: false`); switch it on with `enabled: true` or by setting the caller repo's `RISK_CONFIG` variable to `{"enabled": true}`, which outranks the input in both directions so `{"enabled": false}` is a no-PR kill switch. Grades every PR into a tier `R0` (safest) .. `R3` (riskiest) and syncs one label (`risk:R0`..`risk:R3`, or `risk:ungraded` when an input was unreadable). The label is the entire product: nothing is gated, routed, commented, or merged. Deterministic (`gh` + `jq`, no LLM): `grade = worst(path_floor, provenance, reversibility)` — path-glob map, what-process-produced-the-diff (registered runbooks with identity + diff-shape assertions; forks are R3 with no exceptions), and revertability (persistent-state mutation, deletions under sensitive classes, did green checks cover the lines). Grader + generic defaults live in [`scripts/pr-risk/`](scripts/pr-risk); a consumer sharpens them with `.github/risk.json` / `.github/risk-runbooks.json`, read from the PR's **base ref** so a PR can't edit the rules that judge it. The job excludes its own run from the check rollup and waits (`wait_for_checks_minutes`) for the rest to settle before labeling. Labels ride the plain `GITHUB_TOKEN` (cannot fire `labeled` triggers — no cascade risk); disagreement is recorded with a human-owned `risk-dispute` label. Label text is remappable via `label_map`. `workflows_ref` is **required** — pin it to the same full commit SHA as `uses:`, so the grader cannot be loaded from a floating ref after the caller was reviewed. Enroll it as its own workflow rather than a job inside an existing CI workflow (the rollup exclusion is per-run). The calling job needs `contents: read` + `issues: write` + `pull-requests: write` + `checks: read` + `actions: read` + `statuses: read`; GitHub rejects a shorter grant at startup (a reusable workflow can only narrow the caller's token, never elevate it), so a caller enrolled from an older copy of this row fails before any step runs. Both writes are the ONE label: repo-side label creation on first use maps to `issues`, and labeling a PR maps to `pull-requests` (the labels endpoint is dual-mapped by what the "issue" is, so `issues: write` alone 403s on a PR). `actions: read` is for the rollup's `CheckRun -> checkSuite -> workflowRun` self-exclusion hop. No secrets. |
| [`pr-risk.yml`](.github/workflows/pr-risk.yml) | **Advisory PR risk grading (shadow check)** — **automatic grading off by default** (`enabled: false`; a manual `workflow_dispatch` grades regardless, so a repo can trial it before switching on); switch it on with `enabled: true` or by setting the caller repo's `RISK_CONFIG` variable to `{"enabled": true}`, which outranks the input in both directions so `{"enabled": false}` is a no-PR kill switch. Grades every PR into a tier `R0` (safest) .. `R3` (riskiest) and syncs one label (`risk:R0`..`risk:R3`, or `risk:ungraded` when an input was unreadable). The label is the entire product: nothing is gated, routed, commented, or merged. Deterministic (`gh` + `jq`, no LLM): `grade = worst(path_floor, provenance, reversibility)` — path-glob map, what-process-produced-the-diff (registered runbooks with identity + diff-shape assertions; forks are R3 with no exceptions), and revertability (persistent-state mutation, deletions under sensitive classes, did green checks cover the lines). Grader + generic defaults live in [`scripts/pr-risk/`](scripts/pr-risk); a consumer sharpens them with `.github/risk.json` / `.github/risk-runbooks.json`, read from the PR's **base ref** so a PR can't edit the rules that judge it. The job excludes its own run from the check rollup and waits (`wait_for_checks_minutes`) for the rest to settle before labeling. Labels ride the plain `GITHUB_TOKEN` (cannot fire `labeled` triggers — no cascade risk); disagreement is recorded with a human-owned `risk-dispute` label. Label text is remappable via `label_map`. `workflows_ref` is **required** — pin it to the same full commit SHA as `uses:`, so the grader cannot be loaded from a floating ref after the caller was reviewed. Enroll it as its own workflow rather than a job inside an existing CI workflow (the rollup exclusion is per-run). The calling job needs `contents: read` + `issues: write` + `pull-requests: write` + `checks: read` + `actions: read` + `statuses: read`; GitHub rejects a shorter grant at startup (a reusable workflow can only narrow the caller's token, never elevate it), so a caller enrolled from an older copy of this row fails before any step runs. Both writes are the ONE label: repo-side label creation on first use maps to `issues`, and labeling a PR maps to `pull-requests` (the labels endpoint is dual-mapped by what the "issue" is, so `issues: write` alone 403s on a PR). `actions: read` is for the rollup's `CheckRun -> checkSuite -> workflowRun` self-exclusion hop. No secrets. |

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Medium — The updated row contradicts itself in one sentence: a manual workflow_dispatch "grades regardless", yet {"enabled": false} is still described as "a no-PR kill switch". That is the sentence an enrolling repo reads to decide whether the switch is a hard stop, and an operator reaching for it during an incident gets a guarantee the workflow no longer provides — qualify it as governing the automatic PR stream only. Raised by 2 of 8 reviewers (claude-opus-5-thinking-max adversarial, claude-opus-5-thinking-max edge-case).

| [`stale.yml`](.github/workflows/stale.yml) | Stale-PR sweeper (`actions/stale`) plus a Slack digest of what it touched. PRs inactive for N days are labeled `stale`; still-inactive PRs are closed. The digest header names the source repo so batches from different repos posted to the same channel are unambiguous. Thresholds, messages, exempt labels, and the Slack channel are inputs; the caller owns the schedule + dry-run toggle. The calling job needs `pull-requests: write` and `issues: write`. Optional `SLACK_BOT_TOKEN`. |
| [`groom.yml`](.github/workflows/groom.yml) | Scheduled/dispatch org-wide **code-cleanup sweep** (finds only — no commits, no PRs, never merges). A read-only FINDER agent scans a clean default-branch checkout (whole-repo, not a diff) for high-value refactors; an INDEPENDENT VERIFIER agent (fresh session) re-checks each as CONFIRM/DOWNGRADE/REJECT with a stable dedup signature; survivors are deduped against a durable GitHub-issue-state ledger and filed as `groom`-labeled GitHub issues (security-adjacent ones get `groom-security` — investigate, don't auto-implement). Mirrors the cursor-review topology: briefs + ledger live in [`.github/groom/`](.github/groom) as the single source of truth. The finder/verifier/builder agent jobs invoke the Claude CLI directly and mint no GitHub token, so they need nothing beyond `contents: read`; filing runs in a separate job as the bot you configure via `bot_app_id` (Comfy: cloud-code-bot). `dry_run` reports what it would file without opening issues. Runs on a **daily base cron** with a runtime cadence gate: set repo Actions variable `GROOM_INTERVAL_DAYS` (default 7 = weekly) to retune how often a real run happens — weekly → every-3-days → daily — with no workflow-file edit; a tick within the interval no-ops before the finder (`workflow_dispatch` bypasses the interval gate, but the volume gate — when the caller leaves it on — still applies). The calling job must grant `contents: read` + `issues: write` + `pull-requests: read` + `actions: read` — the first three are declared by the `file` / `build_select` jobs (needed even with `bot_app_id` set), and the interval gate needs `actions: read` (reads run history for the last real run); GitHub rejects a shorter grant at startup. Requires `ANTHROPIC_API_KEY` (+ `BOT_APP_PRIVATE_KEY` when `bot_app_id` is set). **Opt-in auto-builder** (`builder: true`, BE-4003): the top `max_prs` (default 5) CONFIRMED, non-security findings become **review-gated PRs** (full CI + cursor-review, **never auto-merged**) instead of issues; a credential-free `build` job emits only a patch artifact and a separate `build_pr` job opens the PR as the bot, preserving the security boundary. The ledger's PR-state (open/merged/closed) stops a built finding being re-proposed. Requires `bot_app_id`. `max_prs` is typed **`string`**, not `number`, so a caller can forward its own `workflow_dispatch` input straight through (`max_prs: ${{ github.event.inputs.max_prs \|\| '1' }}`) and let an operator raise the ceiling for one manual run — no `fromJSON()` cast in the caller, and the parse/clamp (empty → default, non-numeric → 0 PRs + warning, never a failed run) happens once inside the reusable. |
| [`agents-md-integrity.yml`](.github/workflows/agents-md-integrity.yml) | Enforces the Comfy `AGENTS.md` standard on the caller repo: a top-level `AGENTS.md` must exist and stay under a hard line ceiling (`max_lines`, default 200; warns over `warn_lines`, default 150), a `CLAUDE.md` (if present) must be a thin `@AGENTS.md` shim rather than a divergent copy, no legacy `.cursorrules` (gated `forbid_cursorrules`), every nested monorepo `AGENTS.md` needs a sibling `@AGENTS.md` shim and to be under the ceiling (gated `check_nested`), and `AGENTS.md` should have a CODEOWNERS DRI (`require_codeowners`, warn-only by default). Fails with a non-zero exit + GitHub annotations so it wires in as a required status check. The checker lives in [`.github/agents-md-integrity/`](.github/agents-md-integrity) (pin `workflows_ref` to the same ref as `uses:`); no secrets required. |
Expand Down
46 changes: 35 additions & 11 deletions scripts/pr-risk/resolve-enabled.sh
Original file line number Diff line number Diff line change
Expand Up @@ -61,15 +61,39 @@ fi
echo "pr-risk enabled=$enabled (decided by ${decided_by})"

if [ "$enabled" != true ] && [ -n "${GITHUB_STEP_SUMMARY:-}" ]; then
# shellcheck disable=SC2016 # the body is literal markdown; backticks are code spans
{
echo "## PR risk grading is DISABLED for this repository"
echo
echo "No grade was computed, and no \`risk:*\` label was added, changed, or removed —"
echo "any label already on this PR is left exactly as the last enabled run left it."
echo
echo "Decided by ${decided_by}. To switch grading on, either set the repository variable"
echo '`RISK_CONFIG` to `{"enabled": true}` (takes effect on the next run, no PR needed), or'
echo 'pass `enabled: true` in the caller'"'"'s `with:` block.'
} >> "$GITHUB_STEP_SUMMARY"
# A manual dispatch grades even while disabled (see the `grade` job's `if:`), so the summary
# MUST distinguish the two. Claiming "no grade was computed" on a run that is about to compute
# one is worse than saying nothing: it is the one line a reader would trust to tell them
# whether their label is current.
if [ "${GITHUB_EVENT_NAME:-}" = workflow_dispatch ]; then

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 MediumGITHUB_EVENT_NAME is a new decision input for this script but is not pinned in the resolve step's env: block in pr-risk.yml (which declares only INPUT_ENABLED and RISK_CONFIG), so it relies on the runner default. The ${GITHUB_EVENT_NAME:-} fallback silently selects the "No grade was computed" branch when the value is absent — precisely the misleading text this change exists to suppress — so a missing value degrades to the wrong message rather than an error. Add it to the step's env:. Raised by 1 of 8 reviewers (claude-opus-5-thinking-max adversarial).

# shellcheck disable=SC2016 # the body is literal markdown; backticks are code spans
{
echo "## Automatic PR risk grading is OFF for this repository"
echo
echo "This MANUAL run grades anyway and will sync a \`risk:*\` label — \`enabled\` governs"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Medium — This gate step runs before grade and asserts an outcome it cannot know: a dispatch with no target dies in parse_targets, an unreadable input lands risk:ungraded, and a 403 on the label write applies nothing — in each case the summary has already promised a risk:* sync. That is the same class of false claim this change exists to remove, pointed the other way; word it as intent ("this manual run will attempt a grade") rather than settled fact. Raised by 2 of 8 reviewers (claude-opus-5-thinking-max edge-case, claude-opus-5-thinking-max adversarial).

echo "the \`pull_request\` stream, not a dispatch anyone with write access chose to run."
echo
echo "So the label this run leaves will NOT be kept up to date by pushes: until grading is"
echo "switched on, it reflects this moment only and every later commit will go ungraded."
echo
echo "Decided by ${decided_by}. To grade automatically from here on, set the repository"
echo 'variable `RISK_CONFIG` to `{"enabled": true}` (no PR needed), or pass'
echo '`enabled: true` in the caller'"'"'s `with:` block.'

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟢 Low — "or pass enabled: true in the caller's with: block" is wrong precisely when decided_by is vars.RISK_CONFIG: the variable outranks the input in both directions, so with {"enabled": false} set, that route changes nothing. Suppress the input alternative (or invert it to "clear/flip the variable") when the variable made the decision. Raised by 1 of 8 reviewers (claude-opus-5-thinking-max edge-case).

} >> "$GITHUB_STEP_SUMMARY"
else
# shellcheck disable=SC2016 # the body is literal markdown; backticks are code spans
{
echo "## PR risk grading is DISABLED for this repository"
echo
echo "No grade was computed, and no \`risk:*\` label was added, changed, or removed —"
echo "any label already on this PR is left exactly as the last enabled run left it."
echo
echo "Decided by ${decided_by}. To switch grading on, either set the repository variable"
echo '`RISK_CONFIG` to `{"enabled": true}` (takes effect on the next run, no PR needed), or'
echo 'pass `enabled: true` in the caller'"'"'s `with:` block.'
echo
echo 'To grade a single PR right now WITHOUT switching the repo on, run this workflow from'

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟢 Low — The new escape-hatch advice assumes the consumer's caller declares a workflow_dispatch trigger with a pr_number input and forwards it; the reusable is on: workflow_call only and cannot be run from the Actions tab, so for a pull_request-only caller (a valid enrollment) this points at a button that does not exist. It also omits pr_numbers, the other accepted target input. Raised by 1 of 8 reviewers (claude-opus-5-thinking-max edge-case).

echo 'the Actions tab with `pr_number` set — a manual dispatch is not gated by this switch.'
} >> "$GITHUB_STEP_SUMMARY"
fi
fi
39 changes: 36 additions & 3 deletions scripts/pr-risk/tests/test_resolve_enabled.sh
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,10 @@ ok() { PASS=$((PASS+1)); printf 'ok %s\n' "$1"; }
bad() { FAIL=$((FAIL+1)); printf 'FAIL %s\n got: %s\n' "$1" "${2:-}"; }
eq() { if [ "$2" = "$3" ]; then ok "$1"; else bad "$1 (expected '$2')" "$3"; fi }

# run <input_enabled> <risk_config> -> sets OUT (resolved value), ERR (stderr), SUMMARY (body)
# run <input_enabled> <risk_config> [event] -> sets OUT (resolved), ERR (stderr), SUMMARY (body)
run() {
: > "$SANDBOX/out"; : > "$SANDBOX/summary"
ERR="$(INPUT_ENABLED="$1" RISK_CONFIG="$2" \
ERR="$(INPUT_ENABLED="$1" RISK_CONFIG="$2" GITHUB_EVENT_NAME="${3:-pull_request}" \
GITHUB_OUTPUT="$SANDBOX/out" GITHUB_STEP_SUMMARY="$SANDBOX/summary" \
bash "$SCRIPT" 2>&1 >/dev/null)"
OUT="$(sed -n 's/^enabled=//p' "$SANDBOX/out")"
Expand Down Expand Up @@ -90,7 +90,40 @@ case "$SUMMARY" in *RISK_CONFIG*) ok "and names the switch" ;; *) bad "and names
run true ""
eq "an enabled run writes no summary" "" "$SUMMARY"

echo "— phase 7: it is runnable outside Actions (neither env file set) —"
echo "— phase 7: a DISABLED dispatch must not claim nothing was graded —"
# A manual dispatch grades even while disabled (the `grade` job's `if:` lets it through), so the
# two summaries have to say different things. "No grade was computed" on a run that is about to
# compute one is the single most misleading line this file could emit: it is exactly the
# sentence a reader would trust to decide whether the label on their PR is current.
run false "" workflow_dispatch
case "$SUMMARY" in
*"No grade was computed"*) bad "a disabled dispatch does NOT claim nothing was graded" "$SUMMARY" ;;
*) ok "a disabled dispatch does NOT claim nothing was graded" ;;
esac
case "$SUMMARY" in
*"This MANUAL run grades anyway"*) ok "it says the manual run will grade" ;;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟢 Low — Phase 7 asserts the summary claims a manual run grades, but nothing tests the if: clause in pr-risk.yml that makes the claim true — the two halves sit in different files with no coupling. Dropping || github.event_name == 'workflow_dispatch' from the job condition leaves this suite fully green while every disabled dispatch announces a grade that was never computed. Raised by 1 of 8 reviewers (claude-opus-5-thinking-max edge-case).

*) bad "it says the manual run will grade" "$SUMMARY" ;;
esac
# And it must warn that the label it leaves goes stale, since pushes will not re-grade it.
case "$SUMMARY" in
*"every later commit will go ungraded"*) ok "and that the label will not stay current" ;;
*) bad "and that the label will not stay current" "$SUMMARY" ;;
esac
# The non-dispatch summary keeps its own claim, and points at the dispatch escape hatch.
run false "" pull_request
case "$SUMMARY" in
*"No grade was computed"*) ok "a disabled pull_request still reports nothing graded" ;;
*) bad "a disabled pull_request still reports nothing graded" "$SUMMARY" ;;
esac
case "$SUMMARY" in
*"not gated by this switch"*) ok "and offers the manual route" ;;
*) bad "and offers the manual route" "$SUMMARY" ;;
esac
# An ENABLED dispatch writes no summary at all — the grade job owns the output from there.
run true "" workflow_dispatch
eq "an enabled dispatch writes no summary" "" "$SUMMARY"

echo "— phase 8: it is runnable outside Actions (neither env file set) —"
out="$(INPUT_ENABLED=true RISK_CONFIG='' bash "$SCRIPT" 2>/dev/null)"
case "$out" in *"enabled=true"*) ok "reports to stdout with no GITHUB_OUTPUT" ;; *) bad "reports to stdout with no GITHUB_OUTPUT" "$out" ;; esac

Expand Down
Loading