Files
yellowjacket/.gitea/workflows/ci.yml
logan 37e3373db9
CI / check (push) Skipped
CI / e2e (push) Skipped
CI / check (pull_request) Failing after 1m35s
CI / e2e (pull_request) Skipped
docs: correct the workflow counts these comments name
Adding release.yml and desktop-assets.yml made 'the three workflows a
tag fires' wrong in three files that each said it slightly differently.
2026-08-17 19:47:55 -04:00

405 lines
17 KiB
YAML

name: CI
# The other five workflows package, publish or release; none of them test
# anything, so a green tick on this repo used to mean "the Arch package
# built", which is not the question anyone was asking. This is the
# workflow that gates.
#
# Both jobs were prototyped end to end in a bare ubuntu:24.04 container
# before being written here, so every step below is a transcription of
# something observed working rather than something expected to.
# **A branch push and its PR are the same commit, and testing it twice
# costs the only runner there is.** `branches: ['**']` here meant every
# PR booked four runs — `check` and `e2e` for the branch push, then both
# again for `refs/pull/N/head` — on a host with capacity 1, where the
# queue is shared with an index build that can hold it for three hours.
#
# `pull_request` covers feature branches, and `main` is kept because a
# post-merge run is the record of the trunk's health. Since main now
# refuses direct pushes, that run happens exactly once per merge.
#
# The trade is explicit: a branch pushed with **no** PR open gets no CI.
# That is consistent with the workflow this repo committed to — every
# change goes through a PR — and the signal returns the moment one is
# opened, on the same commit.
on:
push:
branches: [main]
pull_request:
workflow_dispatch:
# A newer push supersedes an older one on the same ref. Job 2 binds
# :34115, so overlapping runs on one runner would fight over the port.
concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true
env:
GO_VERSION: '1.25.0'
# Shared by all three Playwright consumers (@playwright/cli, e2e/'s
# @playwright/test, frontend/'s Vitest provider). See the browsers
# step in job 2 for why that is not the whole story.
PLAYWRIGHT_BROWSERS_PATH: /cache/ms-playwright
# Best-effort pnpm store reuse; pnpm reads npm_config_* for its own
# config keys. If it ever stops honouring this we lose cache warmth
# and nothing else.
npm_config_store_dir: /cache/pnpm-store
jobs:
# ---------------------------------------------------------------- #
# Job 1: everything that does not need a display. #
# ---------------------------------------------------------------- #
check:
runs-on: ubuntu-latest
container:
# Not golang:1.25 — this job runs `make ui-test`, which is Vitest
# *browser* mode and needs a Chromium and its system libraries
# anyway, so the "fast job needs no browser" split does not hold.
# Not the Playwright image either: e2e/ pins @playwright/test
# ^1.56 and frontend/ pins playwright ^1.62, so a prebuilt browser
# set matches at most one of them. Ubuntu 24.04 is also what
# Playwright's WebKit build links against, which job 2 needs.
image: ubuntu:24.04
# GOMODCACHE / GOCACHE / GOLANGCI_LINT_CACHE are already mounted
# and exported for every job by the runner's container.options, so
# only the Node-side caches are listed here. The runner's
# valid_volumes allows anything under the cache root.
volumes:
- /home/logan/docker/gitea/data/runner/cache/tool:/cache/tool
- /home/logan/docker/gitea/data/runner/cache/ms-playwright:/cache/ms-playwright
- /home/logan/docker/gitea/data/runner/cache/pnpm-store:/cache/pnpm-store
env:
PACKAGE_TOKEN: ${{ secrets.PACKAGE_TOKEN }}
SERVER_URL: ${{ github.server_url }}
REPO: ${{ github.repository }}
SHA: ${{ github.sha }}
DEBIAN_FRONTEND: noninteractive
steps:
- name: System packages
run: |
set -eu
apt-get update -qq
# libwebkitgtk-6.0-dev and libasound2-dev are not optional:
# the app is cgo, and without alsa.pc oto/v3 fails at
# `pkg-config --cflags -- alsa` before anything is compiled.
# ubuntu:24.04 ships webkitgtk-6.0, which is what wails v3
# builds against by default.
apt-get install -y -qq --no-install-recommends \
ca-certificates curl git jq build-essential pkg-config \
libwebkitgtk-6.0-dev libgtk-4-dev libasound2-dev ffmpeg
# Cloned by hand rather than with actions/checkout: that is a JS
# action and needs node inside the job container before any step
# has had a chance to install it. Same approach as the other
# other workflows in this directory.
- name: Clone repo at this commit
run: |
set -eu
git clone --quiet \
"https://x-access-token:${PACKAGE_TOKEN}@${SERVER_URL#https://}/${REPO}.git" /src
git -C /src checkout --quiet --detach "$SHA"
git -C /src log --oneline -1
# make bindings-check compares against the work tree, so git
# has to be willing to operate on a directory it does not own.
git config --global --add safe.directory /src
# Conventional Commits. `.releaserc.yml` has always derived the
# version from the commit type; until now nothing checked that the
# type was one it recognises, so a malformed subject silently meant
# "no release". BEFORE is the push's previous tip and is absent or
# all-zeros for a new branch, in which case only the tip is linted.
- name: Commit messages
working-directory: /src
env:
BEFORE: ${{ github.event.before }}
run: |
set -eu
if [ -n "${BEFORE:-}" ] && [ "${BEFORE#0000000}" = "$BEFORE" ] \
&& git cat-file -e "$BEFORE^{commit}" 2>/dev/null; then
make commit-check RANGE="$BEFORE..$SHA"
else
make commit-check
fi
- name: Go toolchain
run: |
set -eu
if [ ! -x /cache/tool/go/bin/go ] || ! /cache/tool/go/bin/go version | grep -q "$GO_VERSION"; then
mkdir -p /cache/tool && rm -rf /cache/tool/go
curl -fsSL "https://go.dev/dl/go${GO_VERSION}.linux-amd64.tar.gz" | tar -C /cache/tool -xz
fi
echo "/cache/tool/go/bin" >> "$GITHUB_PATH"
/cache/tool/go/bin/go version
- name: Node toolchain
run: |
set -eu
curl -fsSL https://deb.nodesource.com/setup_22.x | bash -
apt-get install -y -qq --no-install-recommends nodejs
corepack enable
node --version
- name: Vitest provider browser
working-directory: /src/frontend
run: |
set -eu
pnpm install --frozen-lockfile
npx playwright install --with-deps chromium
# main.go embeds the built frontend (`//go:embed all:frontend/dist`),
# so *every* Go typecheck needs it to exist first — lint, test and
# bindings-check all fail with "pattern all:frontend/dist: no
# matching files found" on a fresh clone. This never bites locally
# because anyone who has run the app once has a dist/ lying around,
# which is exactly why CI has to do it explicitly.
- name: Build the frontend
working-directory: /src/frontend
run: pnpm build
# `make lint` and `make test` each run all three build
# configurations (app / indexbuild / dev) with matching tag sets.
- name: Lint
working-directory: /src
run: make lint
- name: Test
working-directory: /src
run: make test
- name: Typecheck the frontend
working-directory: /src/frontend
run: npx tsc --noEmit
# A backtick inside a comment in a css`` literal ends the literal.
# tsc above does fail on it, with a message about CSSResult
# pointing at a line of prose; this one names the cause. It runs
# after tsc for exactly that reason — whichever fails, the log has
# the sentence in it.
- name: CSS template literals are intact
if: ${{ !cancelled() }}
working-directory: /src
run: make css-check
- name: Component and store suite
working-directory: /src
run: make ui-test
# frontend/bindings is generated by `wails3`, not by `go generate`,
# so the codegen pre-commit hook does not cover it.
- name: Bindings are current
working-directory: /src
run: make bindings-check
# Every `make <target>` named under .pi/**/*.md must exist, so an
# agent is never sent at a command that was renamed away.
- name: Documented make targets exist
working-directory: /src
run: make skill-check
# ---------------------------------------------------------------- #
# Job 2: the real app, headless. v3's `-tags server` needs no #
# display, so the Xvfb this job used to wrap everything in is gone. #
# `dbus-run-session` stays, for MPRIS. #
# ---------------------------------------------------------------- #
e2e:
runs-on: ubuntu-latest
needs: check
container:
image: ubuntu:24.04
volumes:
- /home/logan/docker/gitea/data/runner/cache/tool:/cache/tool
- /home/logan/docker/gitea/data/runner/cache/ms-playwright:/cache/ms-playwright
- /home/logan/docker/gitea/data/runner/cache/pnpm-store:/cache/pnpm-store
env:
PACKAGE_TOKEN: ${{ secrets.PACKAGE_TOKEN }}
SERVER_URL: ${{ github.server_url }}
REPO: ${{ github.repository }}
SHA: ${{ github.sha }}
DEBIAN_FRONTEND: noninteractive
# The explore artifact is stubbed with a dead address, exactly as
# scripts/seed-sandbox.sh does it. Serving a real cut-down
# artifact would mean building one under the indexbuild tag from
# dump state this runner does not have, and no spec asserts on
# explore content, so it would buy nothing. Note that
# dev-headless.sh does *not* set this itself — only seed-sandbox
# does — so the run would otherwise fetch the real artifact over
# the network. It is also worth ~8x on suite wall clock: the
# testctl DB restore spec copies every table, and the real
# artifact makes that table set enormous.
YJ_CORE_INDEX_URL: 'http://127.0.0.1:1/none.tar.zst'
steps:
- name: System packages
run: |
set -eu
apt-get update -qq
apt-get install -y -qq --no-install-recommends \
ca-certificates curl git jq build-essential pkg-config \
libwebkitgtk-6.0-dev libgtk-4-dev libasound2-dev \
dbus dbus-x11 ffmpeg libasound2t64 \
alsa-utils libasound2-plugins pulseaudio pulseaudio-utils
- name: Clone repo at this commit
run: |
set -eu
git clone --quiet \
"https://x-access-token:${PACKAGE_TOKEN}@${SERVER_URL#https://}/${REPO}.git" /src
git -C /src checkout --quiet --detach "$SHA"
git config --global --add safe.directory /src
- name: Go toolchain
run: |
set -eu
if [ ! -x /cache/tool/go/bin/go ] || ! /cache/tool/go/bin/go version | grep -q "$GO_VERSION"; then
mkdir -p /cache/tool && rm -rf /cache/tool/go
curl -fsSL "https://go.dev/dl/go${GO_VERSION}.linux-amd64.tar.gz" | tar -C /cache/tool -xz
fi
echo "/cache/tool/go/bin" >> "$GITHUB_PATH"
- name: Node toolchain
run: |
set -eu
curl -fsSL https://deb.nodesource.com/setup_22.x | bash -
apt-get install -y -qq --no-install-recommends nodejs
corepack enable
# @playwright/cli is gone with v2. seed-sandbox.sh drove the real
# AddLibrary binding through a browser because `window.go` was the
# only way in; v3 answers the same call over HTTP, so the seed is
# curl now and needs no CLI, no second Chromium and no shared
# PLAYWRIGHT_BROWSERS_PATH revision dance.
- name: Browsers
working-directory: /src/e2e
run: |
set -eu
pnpm install --frozen-lockfile
npx playwright install --with-deps chromium webkit
# oto/v3 talks to libasound directly, and a container has no
# PulseAudio socket to fall back on — so it needs a default device
# that not only accepts audio but **paces** it, because the player's
# position is derived from what has been consumed.
#
# ALSA's `null` plugin does not pace. It was used here on the
# belief that it advances its pointer on a timer. Measured in
# this exact image, through beep and oto with the same
# `speaker.Init` arguments `player.InitSpeaker` uses:
#
# type null 3000 ms of audio consumed in 2.96 ms
# pulse sink 3000 ms of audio consumed in 3762 ms
#
# A thousand times too fast. Every track finished instantly, the
# position reset to zero, and three specs failed on a clock that
# never moved — which is the whole of the e2e job's red history,
# and it looked like a flake because `InitSpeaker` succeeds either
# way (in ~3 ms, also either way).
#
# PulseAudio's null sink is timer-scheduled and does pace — the
# 0.76 s over is the buffer draining, not a rate error; 12 s of
# audio takes 13.5 s. Verified under the private session bus
# dev-headless.sh runs the app in. It needs no system D-Bus and
# no kernel module, which is why it is reachable from a container
# at all.
- name: Real-time audio sink
run: |
set -eu
# --system because the job runs as root and PulseAudio refuses
# to start as root any other way.
adduser root pulse-access
pulseaudio --system --daemonize --disallow-exit \
--exit-idle-time=-1 \
--load="module-null-sink sink_name=yellowjacket"
printf 'pcm.!default { type pulse }\nctl.!default { type pulse }\n' \
> /etc/asound.conf
pactl list short sinks
# The sink is a dependency with a *rate*, so it is checked like
# one. Without this the failure surfaces three steps later as
# "the elapsed clock is 19 s adrift", which reads as an app bug
# and cost two sessions of exactly that suspicion.
- name: The sink plays at real time
run: |
set -eu
ffmpeg -loglevel quiet -f lavfi -i "sine=frequency=440:duration=3" \
-ar 44100 /tmp/probe.wav
# aplay rather than the app: this is a check on the *device*,
# and it has to be able to fail before the app is built.
start=$(date +%s%N)
aplay -q /tmp/probe.wav
ms=$(( ($(date +%s%N) - start) / 1000000 ))
echo "3000 ms of audio took ${ms} ms"
if [ "$ms" -lt 2000 ]; then
echo "The default ALSA device is discarding audio rather than" \
"playing it. Every track will finish instantly and the" \
"player's position will never advance." >&2
exit 1
fi
- name: Fixtures and seed
working-directory: /src
run: |
set -eu
make testdata
# A seed is built by *running the app* and driving the real
# AddLibrary binding — never by writing config.toml and DB
# rows, which would be a second description of a valid YJ_HOME.
make sandbox-seed NAME=default
# dev-headless daemonises (writes .dev/app.pid and returns), which
# is why Playwright's webServer cannot supervise it and why this is
# a step of its own. e2e/'s globalSetup checks /__test/health.
- name: Start the app headless
working-directory: /src
run: make dev-headless SEED=default
- name: E2E — chromium
working-directory: /src
run: make e2e
# Playwright's Linux WebKit links Ubuntu 24.04 libraries that Arch
# does not provide, so this cannot run on a dev machine at all: CI
# is the only place we get any signal about the WebKit2GTK renderer
# we actually ship. Required rather than advisory because it was
# measured green (19/19) in this exact container before being
# enabled, and because nothing in e2e/ compares pixels — every
# assertion is an event payload, a testid, an attribute or backend
# state, so a WebKit failure here is an engine bug, not baseline
# noise. It costs ~11 s.
#
# `if: !cancelled()` because without it a chromium failure skips
# this step, and chromium has been failing on the container's
# audio clock — so the run that was the *only* source of WebKit
# signal quietly stopped producing any, and the plan spent a pass
# treating "CI also runs WebKit" as true when the job log said
# `conclusion: skipped`.
- name: E2E — webkit
if: ${{ !cancelled() }}
working-directory: /src
env:
YJ_E2E_WEBKIT: '1'
run: make e2e E2E_ARGS="--project=webkit"
# The app log is the only place a hung binding call explains
# itself, so put it in the job log where `gitea_ci job_logs` can
# reach it without downloading an artifact.
- name: App log on failure
if: failure()
working-directory: /src
run: tail -n 200 .dev/app.log || true
- name: Upload traces and screenshots
if: failure()
continue-on-error: true
uses: actions/upload-artifact@v4
with:
name: e2e-report-${{ github.run_id }}
path: |
/src/e2e/playwright-report/
/src/.dev/app.log
retention-days: 7
- name: Stop the app
if: always()
working-directory: /src
run: make dev-stop || true