Files
punktfunk/.gitea/workflows/decky.yml
T
enricobuehler 22bc81238d fix(decky): Decky's plugin list says "Punktfunk", not "punktfunk"
The label Decky shows for an installed plugin is plugin.json "name", which
we had set to the lowercase directory name — so the one place every user
sees the plugin listed was the one place it was off-brand, while the panel
header (titleView) already read "Punktfunk".

The two were conflated because the name looked load-bearing: the zip's
top-level dir becomes ~/homebrew/plugins/<dir>, and the scripts derived
that dir FROM plugin.json "name". They are in fact independent — Decky
extracts the zip as-is and locates an installed plugin by MATCHING
plugin.json "name", never by folder name (that is how a plugin can live in
DeckWebBrowser/ and list itself as "Web Browser").

So brand-case the label and pin the on-disk dir to the literal `punktfunk`
in package.sh/deploy.sh/CI instead of deriving it. Pinning is the part that
matters: had the dir followed the label, this rename would have installed a
second `Punktfunk/` folder beside the existing `punktfunk/` and the plugin
would have shown up twice.

The self-update call passes the name Decky uninstalls before extracting, so
it moves to "Punktfunk" with it. The upgrade INTO this build still passes
"punktfunk" (the installed build's own value), which matches that build's
plugin.json — so the old folder is removed and the new zip lands in the
same lowercase dir either way. Decky's per-plugin settings dir is unused
(all state lives in ~/.config/punktfunk), so nothing is stranded.
2026-08-05 23:20:28 +02:00

176 lines
10 KiB
YAML

# Build the punktfunk Decky Loader plugin (Gaming-Mode QAM launcher) into a distribution zip
# and publish it to Gitea's GENERIC package registry, giving Decky's "install from URL" a
# stable link. On tags the zip is ALSO attached to the Gitea release.
#
# PUT/GET https://git.unom.io/api/packages/unom/generic/punktfunk-decky/<version>/punktfunk.zip
#
# The plugin backend is PURE PYTHON (clients/decky/main.py — no compiled binary), so we do NOT
# need the Decky CLI (which requires Docker + rust-nightly only to compile native backends).
# We build the frontend with pnpm and stage the store-layout tree with the SAME script local
# builds use (clients/decky/scripts/package.sh) — the plugin's file list lives in exactly ONE
# place, so a file added there (bin/, assets/, controller_config/, …) can never be silently
# missing from the published build. (Hand-assembling the zip here is how the shipped plugin
# lost the shortcut artwork + Steam Input layout for a while.) CI only adds `update.json` on
# top: the {channel, manifest} pointer the plugin's self-update check polls.
#
# SELF-UPDATE (no Decky store): alongside the zip we also publish a tiny per-channel
# `manifest.json` ({version, artifact=<immutable per-version zip URL>, sha256}). The installed
# plugin polls it (main.py check_update), and the frontend drives Decky's own install RPC to
# apply a newer build. See clients/decky/README.md "Updating".
#
# REGISTRY_TOKEN: repo Actions secret, a PAT with write:package scope (shared with deb/rpm/docker).
name: decky
# One pending run per workflow+ref: a newer push supersedes the queued/running one and cancels
# it (a canary only needs the latest commit; each release tag is its own ref so tag runs never
# cancel each other). Keeps a busy push cadence from piling ~10 queued runs per commit onto the
# runner fleet. Gitea honors this for push triggers (PR triggers: see gitea#35933).
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
on:
push:
branches: [main]
# Scope canary builds to what this artifact is built FROM — a docs-only or
# web-only push should not light up the whole fleet. Applies to branch pushes;
# tag runs are matched by `tags:` (proven by flatpak/windows-msix releases).
paths:
- 'clients/decky/**'
- 'scripts/ci/**'
- '.gitea/workflows/decky.yml'
tags: ['v*']
workflow_dispatch:
env:
REGISTRY: git.unom.io
OWNER: unom
PACKAGE: punktfunk-decky # generic-registry package name
# The plugin's ON-DISK dir == the zip's top-level dir. Deliberately NOT plugin.json "name"
# (that is the brand-cased label Decky lists, and it locates a plugin by matching it, not by
# the folder) — see clients/decky/scripts/package.sh.
PLUGIN: punktfunk
jobs:
build-publish:
runs-on: ubuntu-24.04
timeout-minutes: 30
container:
image: node:22-bookworm # node + corepack(pnpm); matches the @decky toolchain
defaults:
run:
working-directory: clients/decky
steps:
- uses: actions/checkout@v4
- name: pnpm
run: |
corepack enable
# The repo's pnpm-lock.yaml + package.json devDeps target pnpm 9 (the version the
# @decky toolchain and the local build use). Pin it so --frozen-lockfile holds.
corepack prepare pnpm@9 --activate
- name: Build frontend
run: |
pnpm install --frozen-lockfile
pnpm run build # rollup -> clients/decky/dist/index.js
- name: Version + channel + stamp
# Tag vX.Y.Z -> X.Y.Z (stable `latest/` alias + Gitea Release); main push -> <next-minor>.<run>
# (base one minor ahead of the latest stable tag via scripts/ci/pf-version.sh)
# (`canary/` alias). Decky reads a plugin's INSTALLED version from package.json (NOT
# plugin.json), and the plugin's own update check (clients/decky/main.py check_update)
# compares against it — so the build version is STAMPED into package.json here (mirrored
# into plugin.json for store parity). Canary is a PLAIN numeric semver, never a
# `-ci<N>` prerelease: compare-versions orders prerelease identifiers lexically
# (ci10 < ci9), which would break update detection; the run number is monotonic.
working-directory: ${{ gitea.workspace }}
run: |
eval "$(bash scripts/ci/pf-version.sh)" # -> PF_MAJOR/PF_MINOR (base one minor ahead of latest stable)
case "$GITHUB_REF" in
refs/tags/v*) V="${GITHUB_REF_NAME#v}"; ALIAS=latest ;;
# Canary MUST be a plain monotonic numeric semver (see the note above): <major>.<minor>.<run>,
# where major.minor track one minor ahead of the latest stable and the run number climbs.
*) V="${PF_MAJOR}.${PF_MINOR}.${GITHUB_RUN_NUMBER}"; ALIAS=canary ;;
esac
BASE="https://$REGISTRY/api/packages/$OWNER/generic/$PACKAGE"
echo "VERSION=$V" >> "$GITHUB_ENV"
echo "ALIAS=$ALIAS" >> "$GITHUB_ENV"
echo "BASE=$BASE" >> "$GITHUB_ENV"
echo "decky version $V -> alias '$ALIAS'"
VERSION="$V" node -e 'const fs=require("fs");for(const f of ["clients/decky/package.json","clients/decky/plugin.json"]){const j=JSON.parse(fs.readFileSync(f,"utf8"));j.version=process.env.VERSION;fs.writeFileSync(f,JSON.stringify(j,null,2)+"\n");}'
- name: Assemble store-layout zip
working-directory: ${{ gitea.workspace }}
run: |
# node:22-bookworm ships python3 (a package.sh dep) but not zip; install both anyway
# so an image change can't silently break the build.
apt-get update && apt-get install -y --no-install-recommends zip python3 >/dev/null
# Stage the canonical plugin tree (dist/, main.py, bin/, assets/, controller_config/,
# LICENSE, …) with the same script local/sideload builds use — see the header comment.
# Runs AFTER the version stamp, so the staged package.json carries $VERSION.
bash clients/decky/scripts/package.sh
DEST="clients/decky/out/$PLUGIN"
# CI-only addition: the self-update channel pointer the backend reads (main.py
# check_update). It points at THIS channel's manifest.json (published below); that
# manifest in turn points at the immutable per-version zip, so its sha256 stays valid
# across future alias re-uploads.
printf '{"channel":"%s","manifest":"%s/%s/manifest.json"}\n' "$ALIAS" "$BASE" "$ALIAS" > "$DEST/update.json"
( cd clients/decky/out && zip -r "$RUNNER_TEMP/punktfunk.zip" "$PLUGIN" )
ls -lh "$RUNNER_TEMP/punktfunk.zip"
unzip -l "$RUNNER_TEMP/punktfunk.zip"
# Backstop against packaging drift: the runtime-loaded pieces MUST be in the zip.
for f in main.py dist/index.js bin/punktfunkrun.sh assets/grid.png \
controller_config/punktfunk.vdf update.json; do
unzip -l "$RUNNER_TEMP/punktfunk.zip" "$PLUGIN/$f" >/dev/null || { echo "MISSING $f" >&2; exit 1; }
done
# The update manifest the plugin polls: the immutable per-version artifact + its
# sha256 (Decky's installer verifies the download against this hash, aborting on
# mismatch — so it MUST be the per-version URL, never the mutable alias).
SHA=$(sha256sum "$RUNNER_TEMP/punktfunk.zip" | cut -d' ' -f1)
printf '{"version":"%s","artifact":"%s/%s/punktfunk.zip","sha256":"%s"}\n' \
"$VERSION" "$BASE" "$VERSION" "$SHA" > "$RUNNER_TEMP/manifest.json"
cat "$RUNNER_TEMP/manifest.json"
- name: Publish to the Gitea generic registry
working-directory: ${{ gitea.workspace }}
env:
TOKEN: ${{ secrets.REGISTRY_TOKEN }}
run: |
BASE="https://$REGISTRY/api/packages/$OWNER/generic/$PACKAGE"
# 1) Versioned URL + its update manifest (the manifest's `artifact` points here, so the
# published sha256 keeps matching what Decky later downloads). A re-tagged release
# re-fires this workflow and the registry 409s on duplicate uploads — delete any
# prior copy of this version first (404 on the first publish is fine).
for f in punktfunk.zip manifest.json; do
curl -fsS -o /dev/null --user "enricobuehler:$TOKEN" -X DELETE "$BASE/$VERSION/$f" || true
done
curl -fsS --user "enricobuehler:$TOKEN" --upload-file "$RUNNER_TEMP/punktfunk.zip" \
"$BASE/$VERSION/punktfunk.zip"
curl -fsS --user "enricobuehler:$TOKEN" --upload-file "$RUNNER_TEMP/manifest.json" \
"$BASE/$VERSION/manifest.json"
echo "published $BASE/$VERSION/punktfunk.zip"
# 2) Channel alias (stable release -> latest/, canary main build -> canary/) — the
# zip is the "install from URL" link; manifest.json is what the installed plugin
# polls for updates. The generic registry rejects re-uploading an existing
# version/file (409), so delete the prior alias copies first (ignore 404 on run #1).
for f in punktfunk.zip manifest.json; do
curl -fsS -o /dev/null --user "enricobuehler:$TOKEN" -X DELETE "$BASE/$ALIAS/$f" || true
done
curl -fsS --user "enricobuehler:$TOKEN" --upload-file "$RUNNER_TEMP/punktfunk.zip" \
"$BASE/$ALIAS/punktfunk.zip"
curl -fsS --user "enricobuehler:$TOKEN" --upload-file "$RUNNER_TEMP/manifest.json" \
"$BASE/$ALIAS/manifest.json"
echo "install-from-URL link: $BASE/$ALIAS/punktfunk.zip"
echo "update manifest: $BASE/$ALIAS/manifest.json"
- name: Attach zip to the Gitea release (stable tags only)
if: startsWith(gitea.ref, 'refs/tags/v')
working-directory: ${{ gitea.workspace }}
env:
GITEA_TOKEN: ${{ secrets.REGISTRY_TOKEN }}
run: |
. scripts/ci/gitea-release.sh
RID=$(ensure_release "$GITHUB_REF_NAME" "$GITHUB_REF_NAME" auto)
upsert_asset "$RID" "$RUNNER_TEMP/punktfunk.zip" "punktfunk-${VERSION}.zip"