Files
small-packages/luci-theme-footstrap/.github/workflows/build.yml
T
2026-09-20 01:22:33 +08:00

630 lines
32 KiB
YAML

# luci-theme-footstrap is noarch: one build, two containers (apk for 25.12+, ipk for 24.10), both
# out of owfeed rather than an OpenWrt SDK. docs/conventions.md has why.
#
# The release pipeline itself is owfeed's `package.yml`, shared by every repository in this
# ecosystem. What is here is this theme's own gates: each one line, each a script under tools/ whose
# header says what it is for.
name: Build LuCI theme packages
on:
push:
tags: ['v*']
branches: [main]
pull_request:
workflow_dispatch:
inputs:
# Off by default: the ordinary path is a v* tag through the `pages` job below, and this
# exists to preview a run's OWN recording before one exists. The github-pages environment
# accepts only `main` or a `v*` tag as a deployment branch (checked via `gh api
# repos/…/environments/github-pages`, 2026-09-14: custom_branch_policies [main, v*]), so a
# dispatch on any other ref reaches `pages-manual` but `deploy` inside it still refuses.
publish-pages:
description: >-
Deploy GitHub Pages with THIS run's own playground instead of the latest release's.
type: boolean
default: false
permissions:
contents: read
# EVERY JOB CARRIES A TIMEOUT, because the default is six hours and a hung step is not a failed one:
# an apt mirror and a playwright download stalled for 68 minutes on two jobs of the 0.13.2 tag with
# GitHub reporting everything operational. The values are the green-run times with room for a slow
# runner — 18s, 1m24s, 1m14s, 1m28s, and the live job, which is the only long one — so a stall goes
# red in minutes and a re-run is one click.
jobs:
check:
name: Static checks
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v7
- run: sh tools/check-shell.sh
- run: sh tools/scan-marker.sh
- run: sh tools/check-acl.sh
- run: luci-theme-footstrap/build-css.sh /tmp/cascade.css
- run: python3 tools/audit.py --strict
# gettext for update-po; the ucode gate needs only docker. Asked for only when the image does
# not already carry it, and under tools/ci-retry.sh: apt does not fail when its mirror is
# unwell, it stalls, and a stalled step is nothing's problem until the job timeout.
- run: |
if command -v msgfmt >/dev/null && command -v msgmerge >/dev/null && command -v xgettext >/dev/null; then
echo 'gettext already on this image'
else
sh tools/ci-retry.sh 300 sudo -n apt-get update
sh tools/ci-retry.sh 300 sudo -n apt-get install -y gettext
fi
- run: luci-theme-footstrap/update-po.sh --check
# npm-based gates, and they live here and nowhere else: the OpenWrt buildbot has no node, luci.mk
# never needs one, and nothing in package.json is shipped.
lint:
name: Lint + accessibility
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with:
node-version: '22'
cache: npm
- run: npm ci
# Through package.json, not a second copy of the globs: a glob restated here goes on linting
# the old path after package.json moves.
- run: npm run lint:js
- run: npm run lint:css
# The unit suite: pure logic, no browser, ~100 ms. Before the browser install because it is
# the fastest thing here and the first to fail on a broken module.
- run: npm test
# The browser off Playwright's CDN, then a launch to prove it works; apt only if that
# fails. `--with-deps` went through apt unconditionally, and apt is the half that stalls.
- run: sh tools/ci-playwright.sh
- run: node tools/a11y-gallery.mjs
# What the router actually sends, weighed on the real artefact: build-css plus the token mangle
# for the sheet, terser over a copy of the resources for the JS. And the app icons, which are
# committed rasters — this proves they still match logo.svg.
- run: node tools/size-budget.mjs --show
- run: node tools/build-icons.mjs --check
- run: node tools/export-tier.mjs
- run: node tools/css-metrics.mjs
# The browser floor, derived from the built sheet rather than asserted in a doc: it fails
# when docs/css.md and the stylesheet disagree, when a `:has()` compound shares a selector
# list with one that has none (the list is not forgiving, so the whole rule dies), and when
# a CSS feature nobody has classified appears.
- run: node tools/css-floor.mjs
- run: node tools/fs-orphans.mjs
- run: node tools/css-dup.mjs
- run: node tools/css-i18n.mjs
- run: node tools/mirror.mjs
- run: node tools/axes.mjs
- run: node tools/chrome-fence.mjs
# Two derived contracts that were in `npm run check` and in no workflow. Both fail the same way
# if nobody runs them: the stylesheet's "an unanswered table takes no room" rule stops naming a
# root fs-select scans, or the page-module map names a page that no longer exists — and the
# page then renders with something missing and says nothing about it.
- run: node tools/table-contract.mjs
- run: node tools/page-modules.mjs
# These three were enforced only when a human remembered to run the whole suite locally. Each
# guards a failure nothing observable reports: a conffile the package manager replaces on
# somebody else's upgrade, an `!important` that drifted out of the allowlist, a changelog
# bullet with no bold lead, which release-notes.sh drops from the release page in silence.
- run: node tools/conffiles.mjs
# po/, tools/stage.sh and owfeed.yml each repeat the language list, and a language
# missing from the last one stages into a directory nothing packages: the build stays
# green and that translation simply does not ship.
- run: node tools/i18n-packages.mjs
- run: node tools/bang-ok.mjs
- run: node tools/changelog.mjs
# jsmin-verify finds the shipped JS itself; JSMIN names the binary to test.
- run: echo "JSMIN=$(sh tools/build-jsmin.sh)" >> "$GITHUB_ENV"
- run: node tools/jsmin-verify.mjs
# No key anywhere in this job. `owfeed check` is build, sign, index and doctor on keys owfeed
# generates and throws away, so a pull request exercises the pipeline that publishes rather than a
# second one that drifts from it.
build:
name: Build the packages
timeout-minutes: 25
# `check` alone, not `lint`. Nothing the linter says changes what this job packs, and it sat in
# front of every live gate: 107 seconds of the critical path for an answer none of them reads.
# `release` names `lint` itself, so a red linter still publishes nothing — that guarantee moved
# rather than went.
needs: check
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
with:
fetch-depth: 0
- uses: actions/setup-node@v7
with:
node-version: '22'
cache: npm
# --ignore-scripts: a dev dependency's lifecycle script has no business running in the job that
# produces the released bytes.
- run: npm ci --ignore-scripts
- uses: owfeed/owfeed/setup@v0.5.1
with:
version: v0.5.1
# The commit's timestamp, not the job's: a package's identity is a hash over its payload and
# mkpkg records mtimes, so re-running a release over the same commit must produce the same
# bytes.
- run: echo "SOURCE_DATE_EPOCH=$(git log -1 --format=%ct)" >> "$GITHUB_ENV"
- run: ./tools/stage.sh
- run: owfeed plan
- run: owfeed check
- run: owfeed build
- run: sh tools/check-packages.sh
# dist/root and dist/scripts are staging, not output.
- uses: actions/upload-artifact@v7
with:
name: owfeed-packages
path: |
dist/noarch
dist/all
if-no-files-found: error
# Everything above proves the package was BUILT correctly. None of it proves what a user cares
# about: that installing it produces a router whose pages render. owlab boots a real OpenWrt,
# installs the package and logs into LuCI to fetch the theme's own pages — an unauthenticated
# request is a redirect to the login form, which is why a hand-written curl reports 403 on a
# healthy page.
#
# No key exists anywhere in this job: it asserts that a package works, never that anyone should
# trust it.
verify:
name: The package works on a real router
timeout-minutes: 25
needs: build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/download-artifact@v8
with:
name: owfeed-packages
path: dist/
# ONE RELEASE PER STEP: 25.12 is apk and 24.10 is opkg, and neither installs the other's file.
- name: Install this build on 25.12 and render its pages
uses: owfeed/owlab/action@v0.6.1
with:
version: v0.6.1
releases: "25.12.4"
install: dist/noarch/luci-theme-footstrap-*.apk
# `exec ucode -T -c` is LuCI's own trycompile, run against the templates as INSTALLED
# and with the router's real luci.core and uci, so it needs no stubs. A stray brace here
# is the one way this repository can ship a completely broken UI with everything else
# green: LuCI falls back to another theme and says nothing.
assert: |
package luci-theme-footstrap
file /www/luci-static/footstrap/cascade.css
http 200 /cgi-bin/luci/admin/status/overview
http 200 /cgi-bin/luci/admin/system/system
exec for f in /usr/share/ucode/luci/template/themes/footstrap/*.ut; do ucode -T -c -o /dev/null "$f" || exit 1; done
- name: Install this build on 24.10 and render its pages
uses: owfeed/owlab/action@v0.6.1
with:
version: v0.6.1
releases: "24.10.8"
install: dist/all/luci-theme-footstrap_*.ipk
# The same compile as the 25.12 leg, and not redundant: the templates are identical but
# the interpreters are not, and what 25.12's ucode accepts 24.10's may not.
assert: |
package luci-theme-footstrap
file /www/luci-static/footstrap/cascade.css
http 200 /cgi-bin/luci/admin/status/overview
http 200 /cgi-bin/luci/admin/system/system
exec for f in /usr/share/ucode/luci/template/themes/footstrap/*.ut; do ucode -T -c -o /dev/null "$f" || exit 1; done
# And the published channel, by NAME, out of the signed index — a different claim: that the
# index parses, the URL does not redirect somewhere apk will not follow, and the key on the
# router matches the one that signed it.
#
# It tests what is ALREADY LIVE rather than this build, so it never fails the job: a feed
# mid-DNS-migration would otherwise block a theme release on a channel this repository does
# not own.
- id: feed-key
name: Fetch the published feed's key
if: github.event_name != 'pull_request'
run: sh tools/feed-key.sh
- id: feed-assert
name: Assert the feed serves a working theme
if: github.event_name != 'pull_request' && steps.feed-key.outputs.reachable == 'true'
continue-on-error: true
uses: owfeed/owlab/action@v0.6.1
with:
version: v0.6.1
releases: "25.12.4"
feed: https://repo.owfeed.org/releases/25.12/x86_64/packages.adb
feed-key: ${{ runner.temp }}/owfeed-packages.pem
feed-name: owfeed-packages
install: luci-theme-footstrap
assert: |
package luci-theme-footstrap
http 200 /cgi-bin/luci/admin/status/overview
# The step above is the only one in this file whose `if` can turn it off from OUTSIDE the
# repository — an unreachable feed switches it off, and a skipped step leaves nothing behind
# except a green job that looks exactly like one where the feed was measured and passed. This
# step always runs on a push and writes which of the three it was into the run's annotations
# and job summary, where a green run is actually read. It never fails either: the whole point
# of the pair is that a channel this repository does not own cannot redden a theme build.
- name: Report whether the published feed was measured
if: always() && github.event_name != 'pull_request'
env:
FEED_REACHABLE: ${{ steps.feed-key.outputs.reachable }}
FEED_OUTCOME: ${{ steps.feed-assert.outcome }}
run: sh tools/feed-key.sh report
# Everything above measures a FILE. This one measures a PAGE, which is where every field report has
# come from: a column shredded to one character per line (#11), a clipped submenu title (#22), a
# doubled scrollbar in one engine (#12), a third-party app laid out wrong (#8, #33, #36), a client
# navigation that painted less than a full load (upstream review). None can fail a static gate.
#
# owlab boots the same containers a developer runs locally, so the gate and the local command are
# the same command. Three routers on a push, one on a pull request: the other release lines double
# the wall clock and have never been the one to catch something first.
live:
# THREE SLICES, because this job was the release's critical path once `anchors` stopped being it.
# Each slice boots its own routers — 157s it does not share — so the job's wall clock is its
# longest slice rather than their sum, and fail-fast is off: a parity failure says nothing about
# whether the reader stays put.
#
# Re-measured on the last run where every slice finished (7ff9e56, push, three routers): the
# gates are 485s (spa-parity), 450s (live-audit) and 754s (everything `motion` still runs), on
# top of 157s of checkout/npm/playwright/boot each slice pays for itself. `motion` used to carry
# a fourth number — chromium's own scroll-anchor sweep, 1434s once `--full` crossed the density
# axis (e48434c) — which alone put the slice at 39 of its 45 minutes. That sweep is now a shard
# of `anchors`, where the one-job-per-engine rule already lives and the cap is 75 minutes; the
# cells did not change, only which runner draws them.
#
# These numbers only hold while a page still answers. A page that pins the browser's main thread
# costs a slice its whole budget rather than a page's worth of seconds, because `page.evaluate()`
# has no deadline of its own — task liveslice, `/admin/system/filemanager` under `13e9864`, both
# measuring slices cancelled at 45 minutes for a day. A slice that runs long is read by looking
# at WHICH page each gate last named, not by raising this cap.
strategy:
fail-fast: false
matrix:
slice: [ parity, audit, motion ]
name: The pages behave on a real router (${{ matrix.slice }})
timeout-minutes: 45
needs: build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with:
node-version: '22'
cache: npm
- run: npm ci
# The browser off Playwright's CDN, then a launch to prove it works; apt only if that
# fails. `--with-deps` went through apt unconditionally, and apt is the half that stalls.
- run: sh tools/ci-playwright.sh
- uses: actions/download-artifact@v8
with:
name: owfeed-packages
path: dist/
- uses: owfeed/owlab/setup@v0.6.1
with:
version: v0.6.1
- name: Boot the routers and install this build
run: |
set -eu
# NOT `. "$GITHUB_ENV"` — that file is a KEY=VALUE list, not a script, so a value with a
# space in it ("owrt2512 owrt2410") runs its second word as a command.
# The three OpenWrt lines the theme supports: both package managers plus the snapshot
# box, which tracks luci-base master and so fails on an upstream change before a user
# reports it. ImmortalWrt is deliberately absent — same luci-base, different brand,
# never the leg that caught something first; run it locally with --all when curious.
if [ "${{ github.event_name }}" = "pull_request" ]; then ROUTERS="owrt2512"; else ROUTERS="owrt2512 owrt2410 owrtsnap"; fi
echo "ROUTERS=$ROUTERS" >> "$GITHUB_ENV"
# Where a gate leaves the page shapes it read, so a second one in the same slice does not
# walk the whole menu again — one load and a 1200ms settle per page. Set here rather than
# in the job's `env`, where $RUNNER_TEMP is not available: the `runner` context exists
# inside a step. Under the runner's temp, so it cannot outlive the install it describes
# (tools/lib/page-shapes.mjs).
echo "FS_SHAPES=$RUNNER_TEMP/page-shapes" >> "$GITHUB_ENV"
owlab up $ROUTERS
for r in $ROUTERS; do
case "$r" in
*2512|*snap) owlab install "$r" dist/noarch/luci-theme-footstrap-*.apk ;;
*) owlab install "$r" dist/all/luci-theme-footstrap_*.ipk ;;
esac
done
# Each gate measures one representative per PAGE SHAPE by default (tools/lib/page-shapes.mjs)
# and names the pages it stood in for; a release takes the whole menu on all four routers
# instead (`--all --pages-all`, docs/releasing.md).
#
# `motion` opens with upstream-contract at 7s, so a broken assumption about luci-base is still
# reported before anything spends minutes rendering pages against it — it cannot come before
# the other two slices any more, since they are on runners of their own.
- name: The gates of this slice
run: |
set -eu
O="$(echo $ROUTERS | tr ' ' ,)"
case "${{ matrix.slice }}" in
parity)
node tools/spa-parity.mjs --only "$O"
;;
audit)
node tools/live-audit.mjs --only "$O"
;;
motion)
node tools/upstream-contract.mjs --only "$O"
# Nothing may re-decide or jump while the page moves. Chromium only here: WebKit needs
# system libraries (gtk-4, gstreamer) this job would install on every run, and the
# engine differences it covers are checked locally before a release.
node tools/scroll-jank.mjs --only "$O"
# The two things a poll tick can do to a page nobody is touching: lay out a replaced
# table before anything answered for it, and move the page under the reader.
node tools/table-tick.mjs --only "$O"
# …and the third: hold the page open with a floor that is the wrong height, or that
# nothing ever takes off. Both shipped (0.14.4 and 0.14.6) and neither is visible in a
# file or on a page that is only loaded.
node tools/floor-contract.mjs --only "$O"
# …and the fourth is the chrome doing it to itself: the bar strips its layout classes
# to measure whether the menu fits a row, and is a row shorter while it asks. Reported
# from an iPhone, where no engine anchoring puts the reader back.
node tools/fit-quiet.mjs --only "$O"
# Chromium's own scroll-anchor sweep is NOT here any more: it is a shard of `anchors`
# below, beside firefox and webkit. It sat here while it was the cheap engine — 145s
# against the other two's 52 minutes — and stopped being one when `--full` started
# crossing chromium's DENSITIES axis too (e48434c): 34 runs became 184 and 145s became
# 1434s, which is 65% of everything this slice does and put it at 39 of its 45 minutes.
# Moving it changes no cell and costs the pipeline nothing, because `anchors` already
# runs one job per engine and its other two shards are longer than this one.
# LAST, because it installs the PUBLISHED release over the build everything above
# measured — and that is the claim: what a user runs is install.sh against the feed,
# not `apk add` against a local file. Three field reports (#16, #28, #30) were this
# script and nothing else, all on the re-install-over-an-existing-copy path, which a
# fresh install cannot exercise. It replaces the build, so it may not share a router
# with a slice still measuring one — which is exactly what a slice of its own buys.
sh tools/install-check.sh $ROUTERS
;;
esac
- if: always()
run: owlab down || true
# EVERY ENGINE'S ANCHOR SWEEP, on a clock of its own.
#
# This was one more step inside `live` and it did not fit: three engines across two routers, two
# page shapes, two widths, both layouts and all three densities is ~290 measurements, and the job
# was cancelled at its 45-minute limit. Splitting it runs the whole matrix beside the rest rather
# than inside somebody else's budget.
#
# Why the other engines at all: the fault this gate hunts lives on the path an engine takes when it
# DOES anchor. WebKit shipped anchoring, so chromium-with-anchoring-suppressed no longer stands in
# for Safari, and on that path WebKit overshoots a section swap by 60px per tick. Measured on
# 24.10, both layouts, 390 and 1440.
anchors:
# ONE JOB PER ENGINE, and it is the release's critical path that pays for it: this gate ran
# 52 minutes against live's 17 on the same three routers, so the whole pipeline was as slow as
# its slowest job. The three engines share nothing — separate browsers, separate runs of the
# same sweep — so a job each measures exactly as much at a third of the wall clock.
#
# Chromium joined them in task liveslice. It had stayed in `live`'s `motion` slice as the cheap
# engine (145s), and `--full` crossing its DENSITIES axis (e48434c) took it to 1434s — 65% of
# that slice, which was then at 39 of its 45 minutes. Here it is the SHORTEST of the three and
# the job's wall clock does not move.
strategy:
fail-fast: false
matrix:
engine: [ chromium, firefox, webkit ]
name: The reader stays put (${{ matrix.engine }})
timeout-minutes: 75
needs: build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with:
node-version: '22'
cache: npm
- run: npm ci
- run: sh tools/ci-playwright.sh
# Only the engine this shard drives: installing all three cost ~1 minute of every run for
# browsers it never launches. Skipped for chromium, which the step above has already fetched
# off the CDN and proved launches — `--with-deps` goes through apt, and apt is the half that
# stalls (see ci-retry.sh); the other two genuinely need gtk-4/gstreamer from it.
- if: matrix.engine != 'chromium'
run: npx playwright install --with-deps ${{ matrix.engine }}
- uses: actions/download-artifact@v8
with:
name: owfeed-packages
path: dist/
- uses: owfeed/owlab/setup@v0.6.1
with:
version: v0.6.1
- name: Boot the routers and install this build
run: |
# The three OpenWrt lines the theme supports: both package managers plus the snapshot
# box, which tracks luci-base master and so fails on an upstream change before a user
# reports it. ImmortalWrt is deliberately absent — same luci-base, different brand,
# never the leg that caught something first; run it locally with --all when curious.
if [ "${{ github.event_name }}" = "pull_request" ]; then ROUTERS="owrt2512"; else ROUTERS="owrt2512 owrt2410 owrtsnap"; fi
echo "ROUTERS=$ROUTERS" >> "$GITHUB_ENV"
owlab up $ROUTERS
for r in $ROUTERS; do
case "$r" in
*2512|*snap) owlab install "$r" dist/noarch/luci-theme-footstrap-*.apk ;;
*) owlab install "$r" dist/all/luci-theme-footstrap_*.ipk ;;
esac
done
# A pull request crosses the axes that were measured to change this gate's answer — three
# width/layout cells (1440 side and 390 side, plus 390 top for the one WebKit-only tick
# misfire that measures 0px at 390 side and 41px at 390 top, task wkanchor), both anchoring
# modes, all three page shapes. A push and a tag add back the ones that were measured not to
# (every width against every layout, all three densities), because that measurement is of
# today's theme and a release is where being wrong about it is expensive. 18 cells per router
# against 72.
- name: The sweep
run: |
if [ "${{ github.event_name }}" = "pull_request" ]; then FULL=""; else FULL="--full"; fi
node tools/scroll-anchor.mjs --engines ${{ matrix.engine }} \
--only "$(echo $ROUTERS | tr ' ' ,)" $FULL
- if: always()
run: owlab down || true
# Builds a real router's `fs-*.js` and `luci.js` into a static site (capture → build → verify) and
# proves it in a real Chromium — never the theme's OWN JS against fixtures, since capture.mjs talks
# to nothing but the router. Runs on a pull request too, not only a tag: the earliest point a
# regression against a real ubus answer can be caught is before a tag exists to publish from.
# `--base` matches the path `pages.yml` serves it under, `/<repo>/playground`, so a link the built
# HTML carries resolves the same way here and there.
playground:
name: Record and verify the playground
timeout-minutes: 20
needs: build
if: startsWith(github.ref, 'refs/tags/v') || github.event_name == 'pull_request' || github.event_name == 'workflow_dispatch'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with:
node-version: '22'
cache: npm
- run: npm ci
# The browser off Playwright's CDN, then a launch to prove it works; apt only if that
# fails. `--with-deps` went through apt unconditionally, and apt is the half that stalls.
- run: sh tools/ci-playwright.sh
- uses: actions/download-artifact@v8
with:
name: owfeed-packages
path: dist/
- uses: owfeed/owlab/setup@v0.6.1
with:
version: v0.6.1
- run: owlab up owrt2512
- run: owlab install owrt2512 dist/noarch/luci-theme-footstrap-*.apk
# Under $RUNNER_TEMP, the same reason FS_SHAPES lives there in the `live` job: it cannot
# outlive the install it describes, and it keeps the recording out of the checkout.
- name: Record, build and verify
run: |
set -eu
PG="$RUNNER_TEMP/playground"
node tools/playground/capture.mjs --recording "$PG/recording"
node tools/playground/build.mjs --recording "$PG/recording" --out "$PG/out" \
--base "/${GITHUB_REPOSITORY#*/}/playground"
# 600 KB: the tar.gz measured 479 KB on owrt2512 with all 29 icons, ~25% headroom.
node tools/playground/verify.mjs --out "$PG/out" --base "/${GITHUB_REPOSITORY#*/}/playground" \
--budget-kb 600
- if: always()
run: owlab down || true
# Only the built tarball, not recording/: its raw server documents carry pre-scrub session
# tokens and the stand's hostname, and this artifact is downloadable on a public repo.
- uses: actions/upload-artifact@v7
with:
name: playground
path: ${{ runner.temp }}/playground/playground.tar.gz
if-no-files-found: error
# The only job that holds a key, and its `needs` is what keeps a tag from publishing a package no
# router has installed. `lint` is named here rather than inherited through `build`, which stopped
# waiting for it: the gate has to hold a release, not the packing step. `playground` joined the
# list for the same reason as the others: a recording that fails to build or verify is a real fault
# in the theme's own `fs-*.js` against a real router, caught by capture.mjs talking to nothing else.
release:
name: Sign and publish
needs: [build, verify, live, anchors, lint, playground]
if: startsWith(github.ref, 'refs/tags/v')
permissions:
contents: write
uses: owfeed/owfeed/.github/workflows/package.yml@v0.5.1
secrets:
# The repository already has this key, and owfeed-packages pins its public half.
sign-key: ${{ secrets.OWFEED_AUTHOR_KEY }}
usign-key: ${{ secrets.FOOTSTRAP_USIGN_KEY }}
with:
owfeed-version: v0.5.1
# Writes dist/install.sh and release-notes.md. The installer is an asset because
# raw.githubusercontent is rate-limited for the very user who most needs it (issue #17) and
# because a signed copy is the only one checkable before it runs as root; the notes fill the
# release page and are not an asset, hence a path beside dist/ rather than in it.
#
# A WORKSPACE PATH, not `${{ runner.temp }}`: at job level a reusable workflow's `with:` may
# only read github/needs/vars/inputs, and naming `runner` there is not an error at edit time —
# it fails the whole run at STARTUP, with zero jobs and no log to read (measured: run
# 31903926488, "failure" with an empty job list).
pre-release: sh tools/stage-release.sh
sign-also: install.sh
notes-file: release-notes.md
verify-with: release.pub
# A separate job, not a `package.yml` input: `dist/*` rides the manifest `release` signs, and
# playground.tar.gz is a demo site, never router payload — owfeed's own pipeline has no input for
# a foreign artifact anyway, and putting it in `dist/` would sign it as if it were one. Needs both
# `release` (the tag it attaches to) and `playground` (this run's own recording).
playground-asset:
name: Attach the playground to the release
needs: [release, playground]
if: startsWith(github.ref, 'refs/tags/v')
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/download-artifact@v8
with:
name: playground
path: pg/
- env:
GH_TOKEN: ${{ github.token }}
run: gh release upload "$GITHUB_REF_NAME" pg/playground.tar.gz --clobber --repo "$GITHUB_REPOSITORY"
# Refresh the Pages mirror of the release metadata. Called rather than triggered: `on: release`
# never fires for a release created with GITHUB_TOKEN. Waits on `playground-asset`, not `release`
# directly, so the mirror step pages.yml runs next (releases/latest/download/playground.tar.gz)
# never races the upload that puts it there.
pages:
name: Refresh the Pages mirror
needs: playground-asset
if: startsWith(github.ref, 'refs/tags/v')
uses: ./.github/workflows/pages.yml
permissions:
contents: read
pages: write
id-token: write
# Lets a maintainer see a run's OWN recording on Pages before a tag exists to publish
# `playground-asset` from — `publish-pages` defaults to false, so the tag path above is
# unchanged. `uses: ./…` runs a local reusable workflow's jobs as jobs of THIS SAME run, so
# `playground-source: artifact` can fetch the `playground` artifact `playground` just uploaded
# here — no `actions: read` needed beyond the permissions below: `download-artifact` reads a
# same-run artifact through the runner's own token, not a `permissions:` scope, which is why the
# `build`/`verify`/`live`/`anchors`/`playground` jobs above never declare it either despite each
# downloading `owfeed-packages`.
#
# A LATER PAGES DEPLOY OVERWRITES THIS: pages.yml's own push trigger (a push to `main` touching its
# `paths:`) or the tag `pages` job both run with `playground-source` at its `release` default and
# refresh the site from the latest RELEASE. This job previews a recording; it does not pin one.
pages-manual:
name: Publish Pages from this run
needs: playground
if: github.event_name == 'workflow_dispatch' && inputs.publish-pages
uses: ./.github/workflows/pages.yml
with:
playground-source: artifact
permissions:
contents: read
pages: write
id-token: write