mirror of
https://github.com/caiwx86/small-packages.git
synced 2026-10-28 08:22:51 +08:00
630 lines
32 KiB
YAML
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
|