feedBack/.github/workflows/ci.yml
Matthew Harris Glover 270cb39f41
Some checks are pending
ship-ci / ci (push) Waiting to run
Enable Linux nightly AppImage auto-update in the System settings UI (#999)
* fix(settings): enable Linux nightly AppImage auto-update in System settings

Fixes the Settings → System "App updates" panel so it actually works on
Linux, and adds Nightly as a selectable channel — previously missing
entirely, so Linux self-update couldn't be reached from this UI at all.

- The channel dropdown no longer gets permanently disabled the moment
  the desktop bridge reports 'unsupported', which is the normal state
  whenever the channel isn't Nightly on Linux. It stays enabled so the
  user can switch to Nightly, the only way out of that state.
- Shows live download progress ("Downloading update… N%") and an
  explicit button state machine (Check → grayed out while busy →
  Restart now once staged), instead of a frozen "Checking…" during the
  ~1.5GB background download.
- Renders every status update from the triggering action's own return
  value (checkNow()/setChannel()'s result) rather than a separate
  follow-up getStatus() call, which can race against other state
  changes and show a stale result even after a real success.
- setupAppUpdates() no longer re-syncs the channel to the backend on
  every Settings-panel re-render — only once per page load — so a
  redundant sync can no longer stomp an in-flight download's state.
- Routes update-flow events into the existing diagnostics.js
  console-capture + contribute() snapshot API, so the user's existing
  "Export Diagnostics" button now captures the full update decision
  trace end to end — no new UI or log file. This diagnostic tracing is
  what actually root-caused the bugs above, from real on-device
  captures rather than guesswork.

Companion PR in feedBack-desktop (the underlying update engine).

Verified end-to-end on a Steam Deck: channel switch → check → live
download progress → restart button → relaunch onto the new build,
confirmed via a real Export Diagnostics capture showing a clean,
fully-accounted-for trace.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* refactor(settings): extract + unit-test the app-update status view; dedupe diag log

- Extract the status→UI state machine from renderFrom into a pure, exported
  _appUpdateStatusView() (DOM-free) and cover it with tests/js — settings.js's
  large module graph made importing it for a full harness impractical, so the
  pure function is the testable seam. Behavior-preserving; renderFrom applies
  the returned shape to the DOM exactly as before.
- Dedupe the [update-diag] renderFrom console line so the ~1.5s download poll
  no longer floods the diagnostics ring buffer with byte-identical entries;
  every real state/percent change still logs, and the structured contribute()
  snapshot stays unconditional.

Left the 'audio_engine' diagnostics key as-is: the server export filters
client contributions to loaded plugin ids (diagnostics_bundle.py path-traversal
guard), so a dedicated key would be silently dropped from the bundle.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Byron Gamatos <xasiklas@gmail.com>

---------

Signed-off-by: Byron Gamatos <xasiklas@gmail.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Byron Gamatos <xasiklas@gmail.com>
2026-07-19 12:16:02 +02:00

246 lines
10 KiB
YAML

name: ci
# Runs only as a reusable workflow invoked by ship-ci.yml (for PRs into main
# and release/**). It deliberately has no standalone pull_request trigger: a
# direct run would publish unprefixed "<job>" checks, but the org rulesets
# require the "ci / <job>" names produced when ship-ci.yml calls this workflow.
on:
workflow_call:
permissions:
contents: read
pull-requests: read
checks: read
jobs:
test:
name: test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.12'
cache: 'pip'
- name: Guard against print() / traceback.print_exc() in server.py, lib/, and bundled plugin routes
run: |
# git grep: tracked files only — no .pyc / __pycache__ noise from
# later pytest runs. Covers both audited patterns from #155 / #242.
# `(^|[^A-Za-z0-9_])` anchor avoids matching suffixes like `myprint(`;
# POSIX leaves `\b` undefined, so we use an explicit character class.
hits=$(git grep -nE '(^|[^A-Za-z0-9_])(print|traceback\.print_exc)[[:space:]]*\(' \
-- server.py lib/ \
$(git ls-files 'plugins/*/routes.py') || true)
if [ -n "$hits" ]; then
echo "$hits"
first=$(printf '%s\n' "$hits" | head -n1)
file=$(printf '%s' "$first" | cut -d: -f1)
line=$(printf '%s' "$first" | cut -d: -f2)
echo "::error file=${file},line=${line}::print() or traceback.print_exc() found in server.py, lib/, or a bundled plugin routes.py. Use the feedBack logger (lib/logging_setup.py) — see issues #155 / #242."
exit 1
fi
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt -r requirements-test.txt
- name: Run pytest
run: pytest
- name: Run JS plugin-API tests
run: node --test tests/js/*.test.js 'tests/plugins/*/js/*.test.js' 'plugins/*/tests/*.test.js'
tailwind-fresh:
# Guard that the committed static/tailwind.min.css is in sync with source.
# The Play CDN's runtime JIT was removed (feedBack-desktop#110); a prebuilt
# stylesheet only contains classes the scanner saw at build time, so stale
# CSS silently ships unstyled elements. Rebuild and fail on any diff.
name: tailwind-fresh
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
# npm ci runs third-party postinstall scripts; don't leave the token in
# git config for them (this job never pushes).
with:
persist-credentials: false
- uses: actions/setup-node@v4
with:
node-version: '20'
- name: Install dependencies
run: npm ci
- name: Rebuild Tailwind CSS
run: bash scripts/build-tailwind.sh
- name: Verify committed static/tailwind.min.css is fresh
run: |
# Hard-fail (matches the print() guard convention) — do NOT auto-commit.
if ! git diff --quiet -- static/tailwind.min.css; then
echo "::error file=static/tailwind.min.css::static/tailwind.min.css is stale. Run 'bash scripts/build-tailwind.sh' and commit the regenerated file."
git diff -- static/tailwind.min.css
exit 1
fi
manifest-validation:
name: manifest-validation
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.12'
- name: Validate plugin manifests
run: |
python - <<'EOF'
import json, sys
from pathlib import Path
errors = []
manifests = sorted(Path("plugins").glob("*/plugin.json"))
for manifest in manifests:
try:
data = json.loads(manifest.read_text())
except json.JSONDecodeError as e:
errors.append(f"{manifest}: invalid JSON — {e}")
continue
for field in ("id", "name"):
if field not in data:
errors.append(f"{manifest}: missing required field '{field}'")
pid = data.get("id", "")
if pid and pid != pid.lower():
errors.append(f"{manifest}: 'id' must be lowercase, got '{pid}'")
# The plugin loader treats each plugins/<dir> as a Python module,
# so the manifest 'id' must match its directory name.
dirname = manifest.parent.name
if pid and pid != dirname:
errors.append(f"{manifest}: 'id' ({pid!r}) must match directory name ({dirname!r})")
if errors:
for e in errors:
print(f"::error::{e}")
sys.exit(1)
print(f"Validated {len(manifests)} manifest(s) — OK")
EOF
feedpak-spec:
# Guard that core stays faithful to the feedpak format spec, which lives in
# its own repo (got-feedback/feedpak-spec) and is the contract third-party
# packers and players build against. Four surface checks: core reads/writes
# only manifest keys the spec declares (and the scanned-module list can't
# fall behind); the exception allowlist never grows, so the FEP process is
# the only way a new key lands; core ingests the spec's example packs; packs
# committed here pass the spec's reference validator. Motivated by
# #933, where a manifest key (`original_audio`) shipped in core without ever
# reaching the spec.
#
# The gate checks against the spec repo's HEAD, deliberately: the app must
# conform to the LIVING spec, always. The dev flow is self-serve — a gated
# PR opens a FEP, the spec PR merges, re-running this job goes green; no
# pin file to bump, nothing to maintain. Accepted trade-off: a BREAKING
# spec change (rare, deliberate, MAJOR per the spec's compatibility policy)
# reddens every PR here until core conforms — which is the correct
# org-wide signal that the app is out of conformance. The normal FEP is
# additive and can never redden this job.
name: feedpak-spec
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
# This job runs repository code (tools/check_spec_conformance.py) and
# never pushes; don't leave the token in git config for it.
# fetch-depth: 0 so the base branch is available — the gate must prove
# the exception allowlist didn't grow in this PR.
with:
persist-credentials: false
fetch-depth: 0
- uses: actions/setup-python@v5
with:
python-version: '3.12'
cache: 'pip'
- name: Check out feedpak-spec at HEAD
uses: actions/checkout@v4
with:
repository: got-feedback/feedpak-spec
ref: main
path: .feedpak-spec
persist-credentials: false
- name: Record the spec commit this run verified against
# HEAD-tracking means CI results can differ across time on the same
# commit. Log the exact spec SHA so a red run is reproducible.
run: git -C .feedpak-spec rev-parse HEAD
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
# CI-only: the spec's reference validator needs jsonschema. Not a
# runtime dependency — this gate never runs on the serve/Docker path
# (constitution Principle I). Pinned for the same reason the spec SHA
# is: an upstream release must not turn this job red on a PR that
# changed neither this repo nor the spec.
pip install 'jsonschema==4.26.0'
- name: Fetch the base branch's exception allowlist
id: baseline
run: |
# The allowlist is closed: it grandfathers keys that predate this gate
# and may only shrink. Prove that by diffing against the base branch —
# without this, anyone could append an entry and route around the FEP
# process from inside this repo.
#
# Resolve the base rather than hardcoding `main`: ship-ci.yml also runs
# this workflow for PRs into release/** and for pushes to release/**,
# where a main baseline would diff against the wrong branch.
# PR -> the branch it merges into
# push -> the branch itself (its tip already contains the change, so
# this is a no-op; enforcement happens at PR time)
BASE="${{ github.event.pull_request.base.ref || github.ref_name }}"
echo "diffing the allowlist against origin/$BASE"
git fetch --no-tags --depth=1 origin "$BASE"
if git cat-file -e FETCH_HEAD:feedpak-spec-exceptions.yml 2>/dev/null; then
git show FETCH_HEAD:feedpak-spec-exceptions.yml > "$RUNNER_TEMP/baseline-exceptions.yml"
echo "args=--baseline-exceptions $RUNNER_TEMP/baseline-exceptions.yml" >> "$GITHUB_OUTPUT"
else
# Only true until the PR that introduces this gate lands.
echo "args=--bootstrap-allowlist" >> "$GITHUB_OUTPUT"
fi
- name: Check feedpak spec conformance
run: python tools/check_spec_conformance.py --spec .feedpak-spec ${{ steps.baseline.outputs.args }}
lint:
# Maintainer/CI-only size + module-hygiene gate (constitution Principle I:
# dev tooling, never on the serve/Docker path — same category as
# scripts/build-tailwind.sh). max-lines WARNS (the 1,500-line size ratchet;
# non-blocking), while import-x/no-unresolved + no-cycle HARD-ERROR on the
# ES-module graphs the refactor produces. Exemptions: docs/size-exemptions.md.
name: lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
# npm ci runs third-party postinstall scripts; don't leave the token in
# git config for them (this job never pushes).
with:
persist-credentials: false
- uses: actions/setup-node@v4
with:
node-version: '20'
- name: Install dependencies
run: npm ci
- name: ESLint (size norm + module hygiene)
run: npm run lint