Skip to content

build(deps): bump the wasm-cargo-dependencies group across 1 directory with 3 updates #976

build(deps): bump the wasm-cargo-dependencies group across 1 directory with 3 updates

build(deps): bump the wasm-cargo-dependencies group across 1 directory with 3 updates #976

Workflow file for this run

name: CI
# The PR/master gate, organized as ONE plan-gated orchestrator (the pattern
# uv, cargo, zed and wgpu converged on independently): a ~10 s `plan` job
# classifies the event's diff into AREAS, every gate job runs only when its
# area fired, and a final `ci-ok` fan-in job is the single branch-protection
# context.
#
# The gate jobs themselves live in four workflow_call reusables, one caller job
# each: core.yml (the root workspace), repo.yml (the whole-tree scanners),
# gui.yml and wasm.yml (the sibling crates). A caller fires when ANY of the
# areas its inner jobs consume fired, and passes the plan's outputs through as
# INPUTS so each inner job keeps its own per-area `if:`; the check runs render
# prefixed (`core / build + test (…)`, `repo / lint YAML (action-validator +
# ryl)`). The three mutation jobs stay HERE rather than inside a reusable: a
# failure inside a called workflow reddens its caller and so reaches ci-ok,
# which would silently promote an advisory job to a gating one. Their shared
# body is the .github/actions/mutate-crate COMPOSITE action — a composite's
# failure surfaces on the calling job itself, preserving that posture.
#
# A job fires if and only if the diff could change that job's verdict, so the
# classifier asks what KIND of input a file is before it asks which directory
# holds it — see .github/scripts/classify-diff.ps1, which owns the rules, the
# mechanism behind each one, and a self-test that refuses any tracked path no
# rule claims. That self-test is its own job here, running beside the gate
# jobs: it makes an unclassified file a failure rather than a silent skip, and
# reaching ci-ok is what gives it teeth. So gating rests on the rules
# themselves; sweep.yml
# re-runs this orchestrator UNGATED on master daily so that environment drift
# (runner images, toolchain point releases, link rot) is caught as well.
#
# WHY plan + `if:`, never `on.paths:`, on anything required: a job skipped via
# `if:` still reports a check-run with conclusion `skipped`, which satisfies a
# required status check — but a workflow filtered out by `on.paths:` never
# reports at all and leaves the PR's required check "Expected" forever. One
# nuance (observed live): a skipped MATRIX job reports once, UNexpanded — its
# per-leg names never appear — so per-leg contexts cannot survive gating;
# only the job-level result feeds ci-ok, which is what branch protection
# requires. For the same reason, never rename or delete a job whose `name:`
# string is a live required context (contexts match on that string).
#
# A master push first asks whether its run is needed at all. master takes
# only squash merges of branches required to be up to date, so the pushed
# commit's TREE is byte-identical to the PR head tree a successful
# pull_request run already verified — and no required context reads a push
# run's verdict. The plan job compares the pushed tree id against the head
# trees of recent successful pull_request runs and on a match emits
# should_run=false, which every downstream job's `if:` consumes, ending the
# run with plan alone. Tree identity also pins the workflow definition
# itself (the tree contains .github), so the matched run executed exactly
# this gate. Any API failure fails open to should_run=true, and sweep.yml
# still runs everything on master daily, so environment drift stays caught.
on:
push:
branches: [master]
pull_request:
# sweep.yml invokes the whole orchestrator daily (and on demand) with every
# area forced — the completeness backstop that makes the PR-path gating
# provably non-weakening.
workflow_call:
permissions:
contents: read
env:
CARGO_TERM_COLOR: always
# Two knobs that exist to keep the SAVED target/ small, set here rather than
# in a tracked Cargo profile so local dev loops keep both. A hosted job builds
# from a restored cache, near enough from scratch that incremental
# compilation buys no rebuild it can amortise — it only writes
# dependency-tracking metadata into the tree rust-cache then archives.
# Debuginfo is the other bulk contributor; `line-tables-only` rather than `0`
# so a failing test still prints file:line backtraces.
#
# Why size is the thing being optimised: a repository's Actions cache store
# is capped (10 GB at the time of writing) and evicted least-recently-used
# across ALL entries, so oversized Rust archives push out the few-MB
# compiled-tool caches, whose miss costs minutes of `cargo install` on the
# pull-request path. Smaller archives also restore faster, on every job.
#
# Jobs running cargo-mutants override CARGO_INCREMENTAL back to 1 — the
# mutate-crate composite action does it for all of them.
CARGO_INCREMENTAL: "0"
CARGO_PROFILE_DEV_DEBUG: line-tables-only
concurrency:
# Namespaced by event so that, should a workflow_call invocation from
# sweep.yml ever be subject to this group (the caller's concurrency is the
# one that governs a called run), a sweep could still never queue against —
# and supersede — a master-push run on the same ref.
group: ci-${{ github.event_name }}-${{ github.ref }}
# PR-only: rapid sequential squash-merges must each keep their master run.
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
jobs:
# Classify the event's changed files into areas. The rules, the mechanism
# behind each one and the self-test that holds them honest all live in
# .github/scripts/classify-diff.ps1; this job only decides WHICH files to
# hand it.
#
# Fail-safes: if the diff cannot be computed (missing before-SHA,
# force-push, an API error, a files list at an endpoint's cap) every area
# fires; so does the `ci-full` PR label. Labels are read from the event
# payload, so apply the label BEFORE a push or a close/reopen — a bare
# re-run replays the old payload without it.
plan:
name: plan (classify the diff into areas)
runs-on: ubuntu-latest
# The default token carries contents: read only; the tree-identity skip
# lists workflow runs (actions) and the classifier reads the PR's files
# (pull-requests).
#
# COUPLED to sweep.yml's `orchestrator` job, which calls this workflow: a
# called job may only request permissions its caller holds, so widening
# this set without widening that one fails the whole sweep run at startup.
permissions:
actions: read
contents: read
pull-requests: read
outputs:
# 'false' only when the tree-identity step proved this pushed tree
# already passed a pull_request run; defaults to 'true' on every other
# event, where the step is skipped and its output empty.
should_run: ${{ steps.skip.outputs.should_run || 'true' }}
core: ${{ steps.classify.outputs.core }}
gui: ${{ steps.classify.outputs.gui }}
gui-full: ${{ steps.classify.outputs.gui-full }}
wasm: ${{ steps.classify.outputs.wasm }}
fuzz: ${{ steps.classify.outputs.fuzz }}
deps: ${{ steps.classify.outputs.deps }}
yaml: ${{ steps.classify.outputs.yaml }}
workflows: ${{ steps.classify.outputs.workflows }}
canary: ${{ steps.classify.outputs.canary }}
dist: ${{ steps.classify.outputs.dist }}
pkg: ${{ steps.classify.outputs.pkg }}
toml: ${{ steps.classify.outputs.toml }}
links: ${{ steps.classify.outputs.links }}
typos: ${{ steps.classify.outputs.typos }}
test: ${{ steps.classify.outputs.test }}
test-os: ${{ steps.classify.outputs.test-os }}
test-os-pr: ${{ steps.classify.outputs.test-os-pr }}
steps:
# Depth 1, .github only: the diff comes from the API, so the checkout
# exists to run the classifier. The fixture discs never materialize
# here.
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
sparse-checkout: .github
# Tree identity, not commit identity (see the header): a pull_request
# run's head_sha is the merged branch's tip — still API-reachable after
# the merge — and only the TREE survives the squash, so each
# candidate's tree id comes from its commit object. Bounded at the last
# 20 successful runs; no match, an empty list, or any API failure falls
# open to should_run=true.
- name: Skip a push whose tree a pull_request run already verified
id: skip
if: github.event_name == 'push'
shell: pwsh
env:
GH_TOKEN: ${{ github.token }}
REPO: ${{ github.repository }}
PUSHED: ${{ github.sha }}
run: |
$ErrorActionPreference = 'Stop'
$should = 'true'
$matched = ''
$pushedTree = gh api "repos/$env:REPO/commits/$env:PUSHED" --jq '.commit.tree.sha'
if ($LASTEXITCODE -eq 0 -and $pushedTree) {
$heads = @(gh api "repos/$env:REPO/actions/workflows/ci.yml/runs?event=pull_request&status=success&per_page=20" --jq '.workflow_runs[].head_sha')
if ($LASTEXITCODE -ne 0) { $heads = @() }
foreach ($sha in $heads) {
$tree = gh api "repos/$env:REPO/commits/$sha" --jq '.commit.tree.sha'
if ($LASTEXITCODE -eq 0 -and $tree -eq $pushedTree) {
$should = 'false'
$matched = $sha
break
}
}
}
"should_run=$should" | Out-File -FilePath $env:GITHUB_OUTPUT -Append -Encoding utf8
if ($should -eq 'false') {
Write-Host "tree $pushedTree already verified by the pull_request run on $matched — skipping"
}
else {
Write-Host 'no successful pull_request run with an identical tree — running everything'
}
# A gh failure handled fail-open above leaves a non-zero exit code
# that the pwsh step wrapper would otherwise propagate as a step
# failure.
exit 0
- name: Classify the changed files into areas
id: classify
shell: pwsh
env:
GH_TOKEN: ${{ github.token }}
REPO: ${{ github.repository }}
EVENT: ${{ github.event_name }}
PR: ${{ github.event.pull_request.number }}
BEFORE: ${{ github.event.before }}
AFTER: ${{ github.event.after }}
CI_FULL: ${{ contains(github.event.pull_request.labels.*.name, 'ci-full') }}
run: |
$ErrorActionPreference = 'Stop'
$mode = 'diff'
$reason = ''
$changed = @()
# Each TSV line is one changed file: new path, TAB, previous path
# (empty unless renamed). Both endpoints report a rename as one
# entry carrying previous_filename, so feeding both sides keeps a
# pure move-out-of-an-area firing the old area's gates — fail-open,
# it can only classify MORE areas, never fewer.
$fileTsv = '[.filename, .previous_filename // ""] | @tsv'
function ConvertTo-Paths {
param([string[]] $TsvLines)
return @(foreach ($line in $TsvLines) {
$new, $old = $line -split "`t", 2
if ($new) { $new }
if ($old) { $old }
})
}
if ($env:CI_FULL -eq 'true') {
$mode = 'all'; $reason = 'ci-full label'
}
elseif ($env:EVENT -eq 'pull_request') {
# The PR files endpoint diffs head against the merge base — the
# changes the PR itself introduces, which is what the merge
# commit this run tests adds to master. The endpoint stops at
# 3000 files, so a count that reaches that cap may be truncated
# and fails safe.
$lines = @(gh api "repos/$env:REPO/pulls/$env:PR/files?per_page=100" --paginate --jq ".[] | $fileTsv")
if ($LASTEXITCODE -ne 0) { $mode = 'all'; $reason = 'PR files API failed' }
elseif ($lines.Count -ge 3000) { $mode = 'all'; $reason = 'at the PR files cap (3000 files), possibly truncated' }
else { $changed = ConvertTo-Paths $lines }
}
elseif ($env:EVENT -eq 'push') {
# before...after is exactly the squash-merge's diff. A zero or
# missing before (branch creation, history rewrite) fails safe.
if (-not $env:BEFORE -or $env:BEFORE -eq ('0' * 40)) {
$mode = 'all'; $reason = 'no usable before SHA'
}
else {
# The compare endpoint pages its files list at up to 300 per
# page; --paginate follows the pages, and the same 3000-file
# guard as the PR path bounds the walk.
$lines = @(gh api "repos/$env:REPO/compare/$($env:BEFORE)...$($env:AFTER)?per_page=250" --paginate --jq ".files[]? | $fileTsv")
if ($LASTEXITCODE -ne 0) { $mode = 'all'; $reason = 'compare API failed' }
elseif ($lines.Count -ge 3000) { $mode = 'all'; $reason = 'at the compare files cap (3000 files), possibly truncated' }
else { $changed = ConvertTo-Paths $lines }
}
}
else {
# workflow_call from sweep.yml (schedule / workflow_dispatch).
$mode = 'all'; $reason = "$($env:EVENT) event — the sweep runs everything"
}
$areas = if ($mode -eq 'all') {
./.github/scripts/classify-diff.ps1 -All
}
else {
$changed | ./.github/scripts/classify-diff.ps1
}
$areas | Out-File -FilePath $env:GITHUB_OUTPUT -Append -Encoding utf8
Write-Host "mode: $mode$(if ($reason) { " ($reason)" })"
if ($mode -eq 'diff') {
Write-Host 'changed files:'
$changed | ForEach-Object { Write-Host " $_" }
}
Write-Host 'areas:'
$areas | ForEach-Object { Write-Host " $_" }
# A gh failure handled fail-open above leaves a non-zero exit code
# that the pwsh step wrapper would otherwise propagate as a step
# failure.
exit 0
# Incremental mutation testing on the PR diff: every line a pull request
# changes must be killed by a test that FAILS when the line is mutated. The
# full-tree sweep (~hours) lives in sweep.yml (sharded, daily + release
# tags); this PR-scoped slice (cargo mutants --in-diff, via the mutate-crate
# composite) is fast. It reads the tracked .cargo/mutants.toml policy and
# the `mutants` nextest profile (.config/nextest.toml) — both published, so
# CI mutates exactly as the local gate does.
#
# ADVISORY, by design: this job is intentionally NOT in branch protection's
# required status checks AND NOT in ci-ok's `needs`. cargo-mutants --in-diff
# can false-TIMEOUT on the 4-core hosted runners (a hang-prone loop-guard
# mutant runs in ceil(n/cores) nextest-terminate waves), so gating merges on
# it would let an environment artifact block an unrelated PR. The
# AUTHORITATIVE "0 missed mutants" gate is sweep.yml's sharded full sweep
# (daily when the workspace changed + release tags); this surfaces
# regressions early but does not block. Keep it out of the required set,
# out of ci-ok, and out of core.yml — a failure inside a called workflow
# reddens its caller, which is in ci-ok's `needs`.
mutants:
name: mutation testing (PR diff)
needs: plan
if: github.event_name == 'pull_request' && needs.plan.outputs.should_run != 'false' && needs.plan.outputs.core == 'true'
# x64: taiki-e/install-action's cargo-mutants manifest carries no aarch64
# Linux asset (x86_64-unknown-linux-gnu, x86_64 macOS and x86_64 Windows
# only), and `fallback: none` turns a missing prebuilt into a hard failure.
# Same for gui-mutants and wasm-mutants below.
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0 # need the base branch to diff the PR against
persist-credentials: false
# No marker inputs: the root workspace has no PR-side short-circuit, so
# every run diffs and mutates.
- uses: ./.github/actions/mutate-crate
with:
mode: pr-diff
# Advisory in-diff mutation over the GUI crate on the PR path: mirrors the
# local gui gate's default mt scope. When the `gui` area fired via the core
# edge only, the crate's own diff is empty and the composite exits with
# nothing to mutate. Same ADVISORY posture and false-TIMEOUT rationale as
# `mutants` above: not required, not in ci-ok's needs, not inside a
# reusable. The authoritative full-crate 0-missed bar lives in sweep.yml
# (daily when the crate changed + release tags).
#
# The sweep's tree-hash marker short-circuits this job too, RESTORE-ONLY: a
# marker hit means this byte-identical crate tree (+ toolchain) already
# passed a green FULL sweep, which subsumes any diff slice of it — mutating
# the slice again would re-verify unchanged inputs. Only a green full sweep
# ever writes the marker; this job must never save one (a diff slice proves
# nothing about the rest of the crate), so it leaves `save-marker` off. The
# marker prefix + paths are byte-identical to sweep.yml's gui-mutants call
# ON PURPOSE — the composite builds the key in one place, and matching
# inputs are what let a green sweep short-circuit this job. The guard is
# what keeps a crate-sized PR diff (e.g. the branch that introduces a
# crate) from re-paying the ~50-min sweep on every push that leaves the
# crate untouched.
gui-mutants:
name: gui mutation testing (PR diff)
needs: plan
if: github.event_name == 'pull_request' && needs.plan.outputs.should_run != 'false' && needs.plan.outputs.gui == 'true'
runs-on: ubuntu-latest # no aarch64 cargo-mutants prebuilt — see `mutants`
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0 # need the base branch to diff the PR against
persist-credentials: false
- uses: ./.github/actions/mutate-crate
with:
mode: pr-diff
working-directory: crates/bdinfo-rs-gui
marker-prefix: gui-mutants-ok
marker-paths: crates/bdinfo-rs-gui rust-toolchain.toml
# --in-place: the ../bdinfo-rs-core path dependency does not survive
# cargo-mutants' temp copy; --workspace: a diff touching the
# assets-gen member is mutated too (cargo-mutants otherwise scopes
# to the root package).
mutants-args: --in-place --workspace
# The suite the mutants face includes the iced_test snapshot ties;
# the deterministic software backend is what makes them run on a
# GPU-less runner at all (same as gui.yml).
extra-env: ICED_TEST_BACKEND=tiny-skia
cache-workspaces: crates/bdinfo-rs-gui
# The wasm twin of gui-mutants: advisory in-diff mutation over the wasm
# crate's own PR diff (--in-place for the same path-dep reason; the
# crate-local .cargo/mutants.toml carries the cfg(wasm32) exclusions), with
# the same restore-only full-sweep marker short-circuit and the same
# byte-identical marker inputs as sweep.yml's wasm-mutants call. Not
# required, not in ci-ok's needs, not inside a reusable; the full-crate bar
# lives in sweep.yml.
wasm-mutants:
name: wasm mutation testing (PR diff)
needs: plan
if: github.event_name == 'pull_request' && needs.plan.outputs.should_run != 'false' && needs.plan.outputs.wasm == 'true'
runs-on: ubuntu-latest # no aarch64 cargo-mutants prebuilt — see `mutants`
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0 # need the base branch to diff the PR against
persist-credentials: false
- uses: ./.github/actions/mutate-crate
with:
mode: pr-diff
working-directory: crates/bdinfo-rs-wasm
marker-prefix: wasm-mutants-ok
marker-paths: crates/bdinfo-rs-wasm rust-toolchain.toml
mutants-args: --in-place
cache-workspaces: crates/bdinfo-rs-wasm
# Prove the classifier still holds its own rules: every golden case, and no
# tracked path that matches no rule (which is what stops a newly added file
# from silently gating nothing).
#
# It runs BESIDE the gate jobs rather than inside `plan` because it does not
# have to run before the classification to protect it — it only has to fail
# the RUN, and ci-ok below turns this job's failure into a red verdict that
# blocks the merge whatever the areas said. Inside `plan` it was pure
# latency: `plan` gates every other job, so its ~90 s (2026-08-15, before
# the exhaustiveness pass early-exited) was paid by every pull request
# before any gate job could start. Here it hides entirely behind gate jobs
# that run for minutes.
classifier:
name: classifier self-test
needs: plan
if: needs.plan.outputs.should_run != 'false'
runs-on: ubuntu-latest
steps:
# Sparse like `plan`'s: `git ls-files` reads the index, which a sparse
# checkout does not filter, so the exhaustiveness pass still sees every
# tracked path without materializing the fixture discs.
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
sparse-checkout: .github
- name: Self-test the classifier
shell: pwsh
run: ./.github/scripts/classify-diff.ps1 -SelfTest
# The root-workspace gate: build + test, the quality gate, coverage, the
# install + scan and FROM-scratch scans, the .deb/.rpm smoke, the two
# dependency audits, the release-workflow drift guard and the corpus replay.
# The `if:` is the UNION of the areas those jobs consume — a caller that
# never starts cannot skip its inner jobs individually — and every area it
# unions is passed through, so each inner job keeps its own per-area `if:`.
core:
name: core
needs: plan
if: >-
needs.plan.outputs.should_run != 'false' &&
(needs.plan.outputs.core == 'true' ||
needs.plan.outputs.deps == 'true' ||
needs.plan.outputs.dist == 'true' ||
needs.plan.outputs.fuzz == 'true' ||
needs.plan.outputs.pkg == 'true' ||
needs.plan.outputs.test == 'true')
uses: ./.github/workflows/core.yml
with:
core: ${{ needs.plan.outputs.core }}
deps: ${{ needs.plan.outputs.deps }}
dist: ${{ needs.plan.outputs.dist }}
fuzz: ${{ needs.plan.outputs.fuzz }}
pkg: ${{ needs.plan.outputs.pkg }}
test: ${{ needs.plan.outputs.test }}
# Pull requests take the classifier's shrunk matrix — no aarch64
# Linux/Windows legs; those run on master pushes and the daily sweep,
# which pass the full list here. A matrix shrunk by expression still
# reports each remaining leg normally; nothing here is a required
# context, so the absent legs hang no PR.
test-os: ${{ github.event_name == 'pull_request' && needs.plan.outputs.test-os-pr || needs.plan.outputs.test-os }}
# The whole-tree scanners: YAML, TOML, workflow security, spelling, links.
# Same caller shape as `core` above.
repo:
name: repo
needs: plan
if: >-
needs.plan.outputs.should_run != 'false' &&
(needs.plan.outputs.yaml == 'true' ||
needs.plan.outputs.toml == 'true' ||
needs.plan.outputs.workflows == 'true' ||
needs.plan.outputs.typos == 'true' ||
needs.plan.outputs.links == 'true')
uses: ./.github/workflows/repo.yml
with:
yaml: ${{ needs.plan.outputs.yaml }}
toml: ${{ needs.plan.outputs.toml }}
workflows: ${{ needs.plan.outputs.workflows }}
typos: ${{ needs.plan.outputs.typos }}
links: ${{ needs.plan.outputs.links }}
# The sibling-crate gates, invoked as workflow_call reusables so their job
# content stays with their crates while the plan gates them here. Their
# check-runs render prefixed (`gui / gui build + test (…)`,
# `wasm / wasm gate (…)`); a red anywhere inside reddens the caller job and
# therefore ci-ok. When every consumed area is skipped, the single caller
# job reports `skipped` — the inner jobs never appear, which is fine because
# only the caller jobs feed ci-ok and no inner name is a required context.
# Same caller shape as `core` above: the `if:` is the UNION of the areas the
# inner jobs consume, each passed through as an input. `gui` alone (a
# bdinfo-rs-core edit reaching the crate through the path-dependency edge)
# runs the gui build + test and checks jobs; `gui-full` (a gui-path or
# toolchain change) adds the drive, packaging and mosaic jobs; `deps` runs
# both crates' msrv jobs.
gui:
name: gui
needs: plan
if: >-
needs.plan.outputs.should_run != 'false' &&
(needs.plan.outputs.gui == 'true' ||
needs.plan.outputs.gui-full == 'true' ||
needs.plan.outputs.deps == 'true')
uses: ./.github/workflows/gui.yml
with:
gui: ${{ needs.plan.outputs.gui }}
full: ${{ needs.plan.outputs.gui-full }}
deps: ${{ needs.plan.outputs.deps }}
wasm:
name: wasm
needs: plan
if: >-
needs.plan.outputs.should_run != 'false' &&
(needs.plan.outputs.wasm == 'true' ||
needs.plan.outputs.deps == 'true')
uses: ./.github/workflows/wasm.yml
with:
wasm: ${{ needs.plan.outputs.wasm }}
deps: ${{ needs.plan.outputs.deps }}
# The fan-in: the ONE result branch protection requires (plus the three
# standalone checks outside this workflow). Green = every gate job either
# succeeded or was skipped by the plan; red = anything failed, was
# cancelled, or never ran because a `needs` upstream failed.
#
# A skipped job is considered a success by GitHub, so overwrite `if:`.
# ALL THE PREVIOUS JOBS NEED TO BE ADDED TO THE `needs` SECTION OF THIS JOB!
# (cargo's warning, kept verbatim: a job missing from `needs` silently stops
# gating merges.) With the gate jobs inside the four called workflows, that
# means every CALLER job — a red inside a reusable reddens its caller. The
# deliberate exceptions are the ADVISORY mutation jobs (`mutants`,
# `gui-mutants`, `wasm-mutants` — see their headers: false-TIMEOUTs on
# 4-core runners must not block unrelated PRs), which stay visible on the PR
# but do not gate. `!cancelled()` rather than `always()`: a manually-
# cancelled run must not end green.
#
# `should_run != 'false'` rather than `== 'true'`, here and on every job
# that consumes it: when plan itself fails its outputs are EMPTY, and an
# equality test would skip ci-ok — a skipped required check satisfies
# branch protection, so a plan failure would open the merge gate instead
# of blocking it. The inequality skips only on the one value the
# tree-identity step deliberately emits.
ci-ok:
name: ci-ok
needs: [plan, classifier, core, repo, gui, wasm]
if: ${{ !cancelled() && needs.plan.outputs.should_run != 'false' }}
runs-on: ubuntu-latest
steps:
- name: Fail unless every gate job succeeded or was skipped
env:
NEEDS_JSON: ${{ toJSON(needs) }}
run: |
echo "$NEEDS_JSON" | jq .
failing=$(echo "$NEEDS_JSON" | jq -r 'to_entries[] | select(.value.result != "success" and .value.result != "skipped") | "\(.key): \(.value.result)"')
if [ -n "$failing" ]; then
echo "not green:"
echo "$failing"
exit 1
fi
echo "ci-ok: every gate job succeeded or was skipped"