ci: publish the devices that did build when the matrix goes red (#125)

* ci: publish the devices that did build when the matrix goes red

This morning's nightly built 97 of 98 devices. ssc333_lite_meari-speed-6s
failed, and that one device cost every device its release: there is no
nightly-20260824-* release, and `nightly` and `latest` still carry the
23rd's images. 2026-08-21 went the same way on two failures out of 98 --
no nightly-20260821-* either.

The publish job is documented as best-effort -- "publish whatever the
matrix produced even if some devices failed" -- and its condition already
lists 'failure' as an accepted matrix result. It never got that far. A job
whose `needs` failed is skipped by default, and an `if:` overrides that
only when it CONTAINS a status function; reading needs.buildroot.result
inside contains() does not count. So the job was skipped outright, never
evaluated, and reported "skipped" rather than anything red.

This has been true since #121 consolidated the release writes into a
single writer to stop the concurrent-upload rate limits. That change was
right -- before it each device uploaded from its own matrix job, so a
failing device took down only itself. Both nightlies that have failed
since then lost their whole release this way.

`!cancelled()` rather than `always()`: the contains() already drops a
cancelled or skipped matrix, but a run cancelled after a green matrix
would still start uploading under always(), and a half-written nightly is
what the single-writer design exists to avoid.

The gate stays red on a partial matrix; that is unchanged. What changes is
that a red run now still delivers the images it built. Devices that failed
keep their previous image on nightly/latest, which the ci-gate already
assumed; the new warning in Collect assets says so on the run rather than
leaving it to be inferred.

Same fix as OpenIPC/firmware#2303 -- the two publish jobs share a common
ancestor and carried the identical gate.

Verification: master.yml parses; lint-workflow-shell.py checks 23 run
blocks clean (and its own self-test passes); ci-matrix.py --self-test ok.
Publish condition audited against every path -- PR, no-op schedule, clone
guard, cancelled run, failed select and all-devices-failed all still skip;
only "matrix failed, some devices built" changes from skip to publish.

* ci: finish the sentence in the partial-publish warning

The line ended on "for every device that did not", leaving the reader to
supply "build" from the clause before it. It is an operator-facing log
line on the one run where somebody is trying to work out what shipped, so
say it in full and point at where the failed devices are listed. builder
has no Build summary job, so the run's own job list is that place.

Raised by Qodo on #125.
This commit is contained in:
OpenIPC AI team
2026-08-24 14:13:56 +03:00
committed by GitHub
parent fc476c895d
commit 6fcd657a00
+24
View File
@@ -327,7 +327,24 @@ jobs:
publish:
name: Publish releases
needs: [preflight, select, buildroot]
# The leading status function is load-bearing. A job whose `needs` failed is
# skipped by default, and an `if:` lifts that only when it CONTAINS a status
# function -- reading `needs.buildroot.result` inside the `contains()` below
# does not count. So since #121 gave this job the matrix as a dependency,
# any single failing device skipped it outright and the whole release went
# unwritten: 2026-08-24 lost every device's nightly to one failure out of 98
# (ssc333_lite_meari-speed-6s), and 2026-08-21 lost it to two out of 98.
# There is no nightly-20260821-* or nightly-20260824-* release as a result.
# Before #121 each device uploaded from its own matrix job, so a bad device
# took down only itself; this restores that without giving up the
# single-writer fix that stopped the concurrent-upload rate limits.
#
# `!cancelled()` rather than `always()`: the `contains()` already drops a
# cancelled or skipped matrix, but a run cancelled AFTER a green matrix
# would still start uploading under `always()`, and a half-written nightly
# is the thing this job exists to avoid.
if: >-
!cancelled() &&
github.event_name != 'pull_request' &&
needs.preflight.outputs.should_build == 'true' &&
contains(fromJSON('["success", "failure"]'), needs.buildroot.result)
@@ -376,6 +393,13 @@ jobs:
fi
elif [ "${count}" -eq 0 ]; then
echo "::notice::matrix failed and produced nothing; no release to write"
else
# The partial case this job was always meant to serve, unreachable
# until the `!cancelled()` above. Say so out loud: the devices that
# did not build keep their PREVIOUS image on nightly/latest, which
# is the right outcome for a flasher but is invisible next to the
# freshly-written ones.
echo "::warning::partial publish: matrix failed, writing the ${count} asset(s) that did build; nightly and latest keep their previous image for every device that did not build — see the failed jobs in this run for which"
fi
# Drive the release writes with gh rather than softprops/action-gh-release,