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.
176 lines
10 KiB
YAML
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"
|