diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..425e273 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,6 @@ +# Shown as the Sponsor button. Same three destinations the in-app Support +# page lists; keep them in step. + +github: [TinkerNorth] +ko_fi: tinkernorth +buy_me_a_coffee: tinkernorth diff --git a/.github/workflows/_release_harden.yml b/.github/workflows/_release_harden.yml new file mode 100644 index 0000000..f3cb615 --- /dev/null +++ b/.github/workflows/_release_harden.yml @@ -0,0 +1,199 @@ +name: _release_harden (reusable) + +# Reusable release-hardening workflow shared by the TinkerNorth release +# pipelines (satellite, dish-linux, dish-windows). One job: download every +# per-platform build artifact, Grype-scan it, emit SPDX + CycloneDX SBOMs, +# generate SHA256SUMS, cosign-sign every file (keyless), and upload the lot +# as the `release-bundle` artifact the caller's provenance + publish jobs +# consume. +# +# Source of truth: TinkerNorth/satellite/.github/workflows/_release_harden.yml +# Re-sync a copy with: +# cp ../satellite/.github/workflows/_release_harden.yml .github/workflows/_release_harden.yml +# +# Callers invoke this with: +# +# jobs: +# harden: +# needs: [] +# permissions: +# contents: read +# id-token: write # cosign keyless +# security-events: write # Grype SARIF upload to code scanning +# uses: ./.github/workflows/_release_harden.yml +# with: +# artifact-pattern: 'linux-*' # download-artifact glob +# product: 'dish' # SBOM filename prefix +# grype-ignore-name-regex: '' # optional CPE-collision ignore +# +# The caller then feeds `needs.harden.outputs.hashes` to the SLSA generic +# generator and publishes the `release-bundle` artifact. +# +# Pin map (owner/repo @ tag → 40-char SHA), verify on update with +# gh api repos///git/ref/tags/: +# actions/download-artifact @ v8.0.1 → 3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c +# actions/upload-artifact @ v7.0.1 → 043fb46d1a93c77aae656e7c1c64a875d1fc6a0a +# anchore/scan-action @ v7.4.0 → e1165082ffb1fe366ebaf02d8526e7c4989ea9d2 +# anchore/sbom-action @ v0.24.0 → e22c389904149dbc22b58101806040fa8d37a610 +# github/codeql-action @ v4.37.7 → ff2f1c621b7f889edc0d3c761ac2e6a3f8cdb0dd + +on: + workflow_call: + inputs: + artifact-pattern: + description: "download-artifact glob matching the per-platform build artifacts" + required: true + type: string + product: + description: "SBOM filename prefix (e.g. dish, satellite)" + required: true + type: string + grype-ignore-name-regex: + description: "Optional Grype package-name regex ignored as a CPE collision with first-party artifact names" + required: false + default: "" + type: string + outputs: + hashes: + description: "base64-encoded sha256sum block over every non-signature file, for the SLSA generic generator" + value: ${{ jobs.harden.outputs.hashes }} + +jobs: + harden: + name: scan, SBOM, sign + runs-on: ubuntu-24.04 + timeout-minutes: 30 + permissions: + contents: read + id-token: write + security-events: write + outputs: + hashes: ${{ steps.hashes.outputs.hashes }} + steps: + - name: Download platform artifacts + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + pattern: ${{ inputs.artifact-pattern }} + merge-multiple: true + path: release + + - name: Show what arrived + shell: bash + run: find release -type f -printf '%p\n' + + - name: Ignore CPE name collisions against first-party artifact names + if: ${{ inputs.grype-ignore-name-regex != '' }} + shell: bash + env: + IGNORE_REGEX: ${{ inputs.grype-ignore-name-regex }} + run: | + set -euo pipefail + printf 'ignore:\n - package:\n name: "%s"\n' "${IGNORE_REGEX}" > .grype.yaml + cat .grype.yaml + + # Anchore Grype: scan every artifact in release/ for CVEs. + - name: Vulnerability scan (Grype) + uses: anchore/scan-action@e1165082ffb1fe366ebaf02d8526e7c4989ea9d2 # v7.4.0 + id: grype + with: + path: release/ + severity-cutoff: high + fail-build: true + only-fixed: false + add-cpes-if-none: true + output-format: sarif + + - name: Upload Grype SARIF to code scanning + if: ${{ always() && steps.grype.outputs.sarif != '' }} + continue-on-error: true + uses: github/codeql-action/upload-sarif@ff2f1c621b7f889edc0d3c761ac2e6a3f8cdb0dd # v4.37.7 + with: + sarif_file: ${{ steps.grype.outputs.sarif }} + category: release-grype + + # Syft: produce SPDX + CycloneDX SBOMs over the whole bundle. + - name: Generate SBOM (SPDX) + uses: anchore/sbom-action@e22c389904149dbc22b58101806040fa8d37a610 # v0.24.0 + with: + path: release/ + format: spdx-json + output-file: release/${{ inputs.product }}.sbom.spdx.json + upload-artifact: false + upload-release-assets: false + + - name: Generate SBOM (CycloneDX) + uses: anchore/sbom-action@e22c389904149dbc22b58101806040fa8d37a610 # v0.24.0 + with: + path: release/ + format: cyclonedx-json + output-file: release/${{ inputs.product }}.sbom.cdx.json + upload-artifact: false + upload-release-assets: false + + - name: Install cosign (upstream binary) + shell: bash + env: + COSIGN_VERSION: "2.6.5" + # Pin upstream SHA-256 of cosign-linux-amd64. Verify with: + # gh release download v${COSIGN_VERSION} -R sigstore/cosign -p cosign_checksums.txt -O - + COSIGN_SHA256: "c3b4f5410e608af03a5eb0aaac84a4313d8da131248e08ff1759ac70c79d1644" # cosign-linux-amd64 v2.6.5 + run: | + set -euo pipefail + curl -fsSL -o /tmp/cosign \ + "https://github.com/sigstore/cosign/releases/download/v${COSIGN_VERSION}/cosign-linux-amd64" + echo "${COSIGN_SHA256} /tmp/cosign" | sha256sum -c - + chmod +x /tmp/cosign + sudo mv /tmp/cosign /usr/local/bin/cosign + cosign version + + - name: Generate SHA256SUMS + shell: bash + run: | + set -euo pipefail + cd release + # `sha256sum --binary` produces deterministic, byte-mode digests. + # Sort so the file order is stable regardless of how artifacts + # arrived. + find . -maxdepth 1 -type f ! -name 'SHA256SUMS*' -printf '%f\n' \ + | LC_ALL=C sort \ + | xargs -d '\n' sha256sum --binary > SHA256SUMS + cat SHA256SUMS + + - name: Cosign-sign each artifact + SHA256SUMS (keyless) + shell: bash + env: + COSIGN_EXPERIMENTAL: "1" + run: | + set -euo pipefail + cd release + for f in *; do + case "$f" in + *.sig|*.crt|SHA256SUMS.sig|SHA256SUMS.crt) continue ;; + esac + cosign sign-blob --yes \ + --output-signature "${f}.sig" \ + --output-certificate "${f}.crt" \ + "$f" + done + ls -l + + - name: Compute base64 SHA256 hashes for SLSA provenance + id: hashes + shell: bash + run: | + set -euo pipefail + cd release + # SLSA generic generator wants ` ` lines, base64-encoded. + h=$(find . -maxdepth 1 -type f ! -name '*.sig' ! -name '*.crt' \ + -printf '%f\n' \ + | LC_ALL=C sort \ + | xargs -d '\n' sha256sum \ + | base64 -w0) + echo "hashes=${h}" >> "$GITHUB_OUTPUT" + + - name: Upload hardened bundle + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: release-bundle + path: release/ + retention-days: 14 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c7f14b0..69a6722 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -2,7 +2,11 @@ name: Release on: push: - tags: ["v*"] + # Bare MAJOR.MINOR.PATCH tags, no v prefix: org convention for the + # product repos, because dish-android's pipeline requires bare tags. + # (Releases were previously v-prefixed; every version consumer below + # tolerates a legacy v by stripping it.) + tags: ["[0-9]*"] workflow_dispatch: inputs: tag: @@ -12,7 +16,9 @@ on: # Packages a tagged commit four ways — .deb (Debian 13+), .rpm (Fedora/RHEL/ # openSUSE), AppImage (everything else, including an LTS whose Qt is below the # floor) and a Flatpak bundle — emits the latest.json the in-app update check -# reads, and uploads the lot to the GitHub Releases page for the tag. +# reads, and uploads the lot to the GitHub Releases page for the tag. Stable +# tags then regenerate the GPG-signed APT and DNF repositories on gh-pages +# (tinkernorth.github.io/dish-linux) so package managers pull the update. # # The .deb and .rpm jobs install their own package and launch it before # uploading. That is the only gate that can catch a missing runtime dependency: @@ -21,23 +27,31 @@ on: # # Before any artifact is uploaded the `gates` job re-runs every PR-time # check (action-pin lint, allowlist expiry, OSV-Scanner, gitleaks) -# against the tagged commit. The `harden` job then scans, signs (cosign -# keyless), generates SBOMs, and produces SHA256SUMS. +# against the tagged commit. The `harden` job (the shared +# _release_harden.yml) then scans, signs (cosign keyless), generates SBOMs, +# and produces SHA256SUMS. +# +# The release is uploaded as a DRAFT and flipped to published afterwards: +# GitHub never points releases/latest at a draft, so the update permalink +# (latest.json) and the AppImage's zsync pattern both keep resolving to the +# previous release until every asset of the new one is in place. +# +# Prerelease tags (1.2.3-rc.1) publish marked as prerelease, skip +# latest.json, and skip the APT/DNF repos; stable clients and repos never +# see them. # # Linux binaries are not signed with a platform key by default; the # `required-secrets` gate therefore enforces only that the cosign keyless # flow is reachable (id-token: write). # # Pin map (verify with `gh api repos///git/ref/tags/`): -# actions/checkout @ v6.0.2 → de0fac2e4500dabe0009e67214ff5f5447ce83dd +# actions/checkout @ v7.0.0 → 9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # actions/upload-artifact @ v7.0.1 → 043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # actions/download-artifact @ v8.0.1 → 3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c -# anchore/sbom-action @ v0.24.0 → e22c389904149dbc22b58101806040fa8d37a610 -# anchore/scan-action @ v7.4.0 → e1165082ffb1fe366ebaf02d8526e7c4989ea9d2 -# github/codeql-action/upload-sarif @ v4.35.4 → 68bde559dea0fdcac2102bfdf6230c5f70eb485e -# softprops/action-gh-release @ v3.0.0 → b4309332981a82ec1c5618f44dd2e27cc8bfbfda +# softprops/action-gh-release @ v3.0.2 → 3d0d9888cb7fd7b750713d6e236d1fcb99157228 # slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml # @ v2.1.0 → f7dd8c54c2067bafc12ca7a55595d5ee9b75204a +# (scan/SBOM/cosign pins live in _release_harden.yml) # Least privilege at the top; each job widens only what it needs. A workflow # grant is a ceiling for the jobs AND for any reusable workflow they call, so @@ -80,8 +94,8 @@ jobs: shell: bash run: | set -euo pipefail - if [ "${{ github.ref_type }}" != "tag" ] || [ "${{ startsWith(github.ref, 'refs/tags/v') }}" != "true" ]; then - echo "::notice::Non-tag release run — cosign keyless gate is advisory." + if [ "${{ github.ref_type }}" != "tag" ] || ! [[ "$GITHUB_REF_NAME" =~ ^v?[0-9]+\.[0-9]+\.[0-9]+ ]]; then + echo "::notice::Non-release-tag run; cosign keyless gate is advisory." exit 0 fi if [ -z "${ACTIONS_ID_TOKEN_REQUEST_URL:-}" ] && [ -z "${ACTIONS_ID_TOKEN_REQUEST_TOKEN:-}" ]; then @@ -90,6 +104,18 @@ jobs: fi echo "id-token write permission present; cosign keyless flow can authenticate." + - name: Report APT/DNF repo signing readiness (advisory) + env: + GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }} + GPG_KEY_ID: ${{ secrets.GPG_KEY_ID }} + shell: bash + run: | + if [ -z "${GPG_PRIVATE_KEY:-}" ] || [ -z "${GPG_KEY_ID:-}" ]; then + echo "::warning::GPG_PRIVATE_KEY / GPG_KEY_ID not set; the apt-publish and rpm-publish jobs will skip, and package-manager users will not see this release." + else + echo "GPG repo-signing secrets present; APT/DNF repos will be regenerated." + fi + # ========================================================================= # .deb — Debian 13 and derivatives. # @@ -131,7 +157,7 @@ jobs: librsvg2-bin desktop-file-utils appstream lintian - name: Checkout - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: ref: ${{ inputs.tag || github.ref }} @@ -283,7 +309,7 @@ jobs: librsvg2-tools desktop-file-utils libappstream-glib gzip - name: Checkout - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: ref: ${{ inputs.tag || github.ref }} @@ -400,7 +426,7 @@ jobs: timeout-minutes: 45 steps: - name: Checkout - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: ref: ${{ inputs.tag || github.ref }} @@ -425,7 +451,8 @@ jobs: uses: ./.github/actions/setup-qt # Everything fiddly — QML_SOURCES_PATHS, the icon basename, the offscreen - # smoke test — lives in the script so a local build and CI agree. + # smoke test, the zsync update metadata — lives in the script so a local + # build and CI agree. - name: Build AppImage run: scripts/build-appimage.sh @@ -435,12 +462,13 @@ jobs: path: | dist/*.AppImage dist/*.AppImage.sha256 + dist/*.AppImage.zsync if-no-files-found: error # ========================================================================= # Flatpak — the sandboxed build, and the artifact a Flathub PR is validated - # against. flatpak-builder is driven directly so there is one fewer - # third-party action to pin and audit. + # against (docs/FLATHUB.md is the submission runbook). flatpak-builder is + # driven directly so there is one fewer third-party action to pin and audit. # ========================================================================= flatpak: name: Build Flatpak bundle @@ -459,7 +487,7 @@ jobs: org.kde.Platform//6.9 org.kde.Sdk//6.9 - name: Checkout - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: ref: ${{ inputs.tag || github.ref }} @@ -495,6 +523,9 @@ jobs: # not "stable", or whose minimumSupportedVersion is malformed or newer than # its version. This job is the only producer; the asset name never changes. # Without it the in-app update check 404s forever. + # + # Prerelease tags emit nothing: the permalink must keep serving stable + # clients the last stable release's manifest. # ========================================================================= manifest: name: Emit update manifest @@ -503,11 +534,12 @@ jobs: timeout-minutes: 10 steps: - name: Checkout - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: ref: ${{ inputs.tag || github.ref }} - - name: Write latest.json + - name: Write latest.json (stable tags only) + id: emit env: TAG: ${{ inputs.tag || github.ref_name }} REPO: ${{ github.repository }} @@ -515,8 +547,13 @@ jobs: set -euo pipefail version="${TAG#v}" case "${version}" in + *-*) + echo "::notice::Prerelease tag ${TAG}: skipping latest.json so the stable permalink keeps serving the last stable release." + echo "stable=false" >> "$GITHUB_OUTPUT" + exit 0 + ;; [0-9]*.[0-9]*.[0-9]*) ;; - *) echo "::error::tag '${TAG}' is not vMAJOR.MINOR.PATCH"; exit 1 ;; + *) echo "::error::tag '${TAG}' is not MAJOR.MINOR.PATCH[-prerelease]"; exit 1 ;; esac # The drift that breaks every client at once: a manifest naming a @@ -545,136 +582,30 @@ jobs: }, indent=2)) PY cat dist/latest.json + echo "stable=true" >> "$GITHUB_OUTPUT" - - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + - if: ${{ steps.emit.outputs.stable == 'true' }} + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: linux-manifest path: dist/latest.json if-no-files-found: error + # ========================================================================= + # harden: the shared reusable (scan, SBOMs, SHA256SUMS, cosign keyless). + # Canonical copy: TinkerNorth/satellite/.github/workflows/_release_harden.yml + # ========================================================================= harden: - name: Harden artifacts (scan, SBOM, sign) + name: Harden artifacts needs: [deb, rpm, appimage, flatpak, manifest] - runs-on: ubuntu-24.04 - timeout-minutes: 30 permissions: contents: read id-token: write security-events: write - outputs: - hashes: ${{ steps.hashes.outputs.hashes }} - steps: - - name: Download release artifacts - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - with: - pattern: linux-* - merge-multiple: true - path: release - - - name: Vulnerability scan (Grype) - uses: anchore/scan-action@e1165082ffb1fe366ebaf02d8526e7c4989ea9d2 # v7.4.0 - id: grype - with: - path: release/ - severity-cutoff: high - fail-build: true - only-fixed: false - add-cpes-if-none: true - output-format: sarif - - - name: Upload Grype SARIF to code scanning - if: ${{ always() && steps.grype.outputs.sarif != '' }} - continue-on-error: true - uses: github/codeql-action/upload-sarif@68bde559dea0fdcac2102bfdf6230c5f70eb485e # v4.35.4 - with: - sarif_file: ${{ steps.grype.outputs.sarif }} - category: release-grype - - - name: Generate SBOM (SPDX) - uses: anchore/sbom-action@e22c389904149dbc22b58101806040fa8d37a610 # v0.24.0 - with: - path: release/ - format: spdx-json - output-file: release/dish.sbom.spdx.json - upload-artifact: false - upload-release-assets: false - - - name: Generate SBOM (CycloneDX) - uses: anchore/sbom-action@e22c389904149dbc22b58101806040fa8d37a610 # v0.24.0 - with: - path: release/ - format: cyclonedx-json - output-file: release/dish.sbom.cdx.json - upload-artifact: false - upload-release-assets: false - - - name: Install cosign (upstream binary) - shell: bash - env: - COSIGN_VERSION: "2.4.1" - # Pin upstream SHA-256 of cosign-linux-amd64. Verify with: - # gh release view v${COSIGN_VERSION} -R sigstore/cosign --json assets - COSIGN_SHA256: "8b24b946dd5809c6bd93de08033bcf6bc0ed7d336b7785787c080f574b89249b" # cosign-linux-amd64 v2.4.1 - run: | - set -euo pipefail - curl -fsSL -o /tmp/cosign \ - "https://github.com/sigstore/cosign/releases/download/v${COSIGN_VERSION}/cosign-linux-amd64" - if [ "${COSIGN_SHA256}" != "TODO_REPLACE_WITH_UPSTREAM_SHA256" ]; then - echo "${COSIGN_SHA256} /tmp/cosign" | sha256sum -c - - else - echo "::warning::COSIGN_SHA256 is unset — replace with the upstream SHA-256 to enforce binary integrity." - fi - chmod +x /tmp/cosign - sudo mv /tmp/cosign /usr/local/bin/cosign - cosign version - - - name: Generate SHA256SUMS - shell: bash - run: | - set -euo pipefail - cd release - find . -maxdepth 1 -type f ! -name 'SHA256SUMS*' -printf '%f\n' \ - | LC_ALL=C sort \ - | xargs -d '\n' sha256sum --binary > SHA256SUMS - cat SHA256SUMS - - - name: Cosign-sign each artifact + SHA256SUMS (keyless) - shell: bash - env: - COSIGN_EXPERIMENTAL: "1" - run: | - set -euo pipefail - cd release - for f in *; do - case "$f" in - *.sig|*.crt|SHA256SUMS.sig|SHA256SUMS.crt) continue ;; - esac - cosign sign-blob --yes \ - --output-signature "${f}.sig" \ - --output-certificate "${f}.crt" \ - "$f" - done - ls -l - - - name: Compute base64 SHA256 hashes for SLSA provenance - id: hashes - shell: bash - run: | - set -euo pipefail - cd release - h=$(find . -maxdepth 1 -type f ! -name '*.sig' ! -name '*.crt' \ - -printf '%f\n' \ - | LC_ALL=C sort \ - | xargs -d '\n' sha256sum \ - | base64 -w0) - echo "hashes=${h}" >> "$GITHUB_OUTPUT" - - - name: Upload hardened bundle - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 - with: - name: release-bundle - path: release/ - retention-days: 14 + uses: ./.github/workflows/_release_harden.yml + with: + artifact-pattern: linux-* + product: dish provenance: name: SLSA L3 provenance @@ -712,9 +643,365 @@ jobs: - name: Show what we're about to publish run: find release -type f -printf '%p\n' - - name: Upload to GitHub Releases - uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda # v3.0.0 + # draft: true is load-bearing. GitHub never points releases/latest at a + # draft, so the assets upload while the update permalink (latest.json) + # and the AppImage zsync pattern still resolve to the previous release; + # the flip below is what makes publication atomic for every client. + - name: Upload to GitHub Releases (as draft) + uses: softprops/action-gh-release@3d0d9888cb7fd7b750713d6e236d1fcb99157228 # v3.0.2 with: tag_name: ${{ inputs.tag || github.ref_name }} + prerelease: ${{ contains(inputs.tag || github.ref_name, '-') }} generate_release_notes: true + draft: true files: release/**/* + fail_on_unmatched_files: true + + - name: Flip release from draft to published + env: + GH_TOKEN: ${{ github.token }} + GH_REPO: ${{ github.repository }} + TAG: ${{ inputs.tag || github.ref_name }} + run: | + set -euo pipefail + gh release edit "$TAG" --draft=false + draft="$(gh release view "$TAG" --json isDraft --jq .isDraft)" + [ "$draft" = "false" ] || { echo "::error::release is still a draft"; exit 1; } + + # ========================================================================= + # apt-publish: regenerate the APT repository at + # https://tinkernorth.github.io/dish-linux/debian and commit to gh-pages. + # Stable tags only; a prerelease must never reach package managers. + # Skipped entirely if the GPG signing secret isn't set: unsigned repo + # metadata is worse than no repo at all. + # Ported from TinkerNorth/satellite release.yml; keep the two in step. + # ========================================================================= + apt-publish: + name: Publish APT repository (gh-pages) + needs: [publish] + timeout-minutes: 15 + runs-on: ubuntu-24.04 + permissions: + contents: write + if: ${{ github.ref_type == 'tag' && !contains(github.ref_name, '-') }} + steps: + - name: Checkout main (for build scripts) + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + ref: ${{ inputs.tag || github.ref }} + path: main + + - name: Gate on GPG signing secret + id: gate + env: + GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }} + GPG_KEY_ID: ${{ secrets.GPG_KEY_ID }} + shell: bash + run: | + if [ -z "${GPG_PRIVATE_KEY:-}" ] || [ -z "${GPG_KEY_ID:-}" ]; then + echo "::warning::GPG_PRIVATE_KEY / GPG_KEY_ID not set; skipping APT repo publish." + echo "skip=true" >> "$GITHUB_OUTPUT" + else + echo "skip=false" >> "$GITHUB_OUTPUT" + fi + + - name: Install apt-utils + GPG + if: steps.gate.outputs.skip != 'true' + run: sudo apt-get update && sudo apt-get install -y --no-install-recommends apt-utils gnupg + + - name: Import signing key + if: steps.gate.outputs.skip != 'true' + env: + GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }} + shell: bash + run: | + printf '%s' "$GPG_PRIVATE_KEY" | gpg --batch --import + gpg --list-secret-keys --keyid-format=long + + - name: Download release bundle + if: steps.gate.outputs.skip != 'true' + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: release-bundle + path: release + + - name: Check out gh-pages branch + if: steps.gate.outputs.skip != 'true' + shell: bash + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + set -euo pipefail + if git -C main ls-remote --exit-code --heads origin gh-pages >/dev/null 2>&1; then + git clone --branch gh-pages --depth 1 \ + "https://x-access-token:${GH_TOKEN}@github.com/${{ github.repository }}.git" pages + else + # First publish: initialize the orphan gh-pages branch with the + # landing page and gpg.key already in place. + git clone --depth 1 \ + "https://x-access-token:${GH_TOKEN}@github.com/${{ github.repository }}.git" pages + git -C pages checkout --orphan gh-pages + git -C pages rm -rf . || true + cp main/packaging/repo/index.html pages/index.html + fi + # Always refresh the landing page from main so README edits propagate. + cp main/packaging/repo/index.html pages/index.html + + - name: Export public key as gpg.key + if: steps.gate.outputs.skip != 'true' + env: + GPG_KEY_ID: ${{ secrets.GPG_KEY_ID }} + run: | + gpg --batch --yes --armor --export "$GPG_KEY_ID" > pages/gpg.key + + - name: Build APT repo + if: steps.gate.outputs.skip != 'true' + env: + GPG_KEY_ID: ${{ secrets.GPG_KEY_ID }} + run: | + chmod +x main/packaging/repo/build-apt-repo.sh + main/packaging/repo/build-apt-repo.sh "$(pwd)/pages" "$(pwd)/release" + + - name: Commit + push + if: steps.gate.outputs.skip != 'true' + shell: bash + env: + TAG: ${{ inputs.tag || github.ref_name }} + run: | + set -euo pipefail + cd pages + git config user.name 'dish-release-bot' + git config user.email 'releases@tinkernorth.invalid' + git add -A + if git diff --cached --quiet; then + echo "No APT-repo changes to commit" + else + git commit -m "Publish APT repo: ${TAG}" + git push origin gh-pages + fi + + # ========================================================================= + # rpm-publish: same idea as apt-publish, but for the DNF/YUM tree + # under https://tinkernorth.github.io/dish-linux/rpm. + # ========================================================================= + rpm-publish: + name: Publish DNF repository (gh-pages) + needs: [publish, apt-publish] + timeout-minutes: 15 + runs-on: ubuntu-24.04 + permissions: + contents: write + if: ${{ github.ref_type == 'tag' && !contains(github.ref_name, '-') }} + steps: + - name: Checkout main (for build scripts) + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + ref: ${{ inputs.tag || github.ref }} + path: main + + - name: Gate on GPG signing secret + id: gate + env: + GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }} + GPG_KEY_ID: ${{ secrets.GPG_KEY_ID }} + shell: bash + run: | + if [ -z "${GPG_PRIVATE_KEY:-}" ] || [ -z "${GPG_KEY_ID:-}" ]; then + echo "::warning::GPG_PRIVATE_KEY / GPG_KEY_ID not set; skipping DNF repo publish." + echo "skip=true" >> "$GITHUB_OUTPUT" + else + echo "skip=false" >> "$GITHUB_OUTPUT" + fi + + - name: Install createrepo_c + GPG + if: steps.gate.outputs.skip != 'true' + run: sudo apt-get update && sudo apt-get install -y --no-install-recommends createrepo-c gnupg + + - name: Import signing key + if: steps.gate.outputs.skip != 'true' + env: + GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }} + shell: bash + run: | + printf '%s' "$GPG_PRIVATE_KEY" | gpg --batch --import + + - name: Download release bundle + if: steps.gate.outputs.skip != 'true' + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: release-bundle + path: release + + - name: Check out gh-pages branch (apt-publish has already touched it) + if: steps.gate.outputs.skip != 'true' + shell: bash + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + set -euo pipefail + git clone --branch gh-pages --depth 1 \ + "https://x-access-token:${GH_TOKEN}@github.com/${{ github.repository }}.git" pages + + - name: Build DNF repo + if: steps.gate.outputs.skip != 'true' + env: + GPG_KEY_ID: ${{ secrets.GPG_KEY_ID }} + REPO_BASE_URL: "https://${{ github.repository_owner }}.github.io/dish-linux" + run: | + chmod +x main/packaging/repo/build-dnf-repo.sh + main/packaging/repo/build-dnf-repo.sh "$(pwd)/pages" "$(pwd)/release" + + - name: Commit + push + if: steps.gate.outputs.skip != 'true' + shell: bash + env: + TAG: ${{ inputs.tag || github.ref_name }} + run: | + set -euo pipefail + cd pages + git config user.name 'dish-release-bot' + git config user.email 'releases@tinkernorth.invalid' + git add -A + if git diff --cached --quiet; then + echo "No DNF-repo changes to commit" + else + git commit -m "Publish DNF repo: ${TAG}" + git push origin gh-pages + fi + + # ========================================================================= + # arch-publish: build the dish-bin pacman package from the in-tree + # PKGBUILD (against the assets the publish job just made public) and + # regenerate the signed pacman repository under + # https://tinkernorth.github.io/dish-linux/arch/x86_64. + # + # A self-hosted pacman repo, not the AUR: AUR registration is closed, and + # a binary repo is CI-automatable where AUR pushes need a personal SSH + # key. packaging/aur/ stays AUR-ready for if/when registration reopens. + # Chained after rpm-publish because both jobs push gh-pages. + # ========================================================================= + arch-publish: + name: Publish pacman repository (gh-pages) + needs: [publish, rpm-publish] + timeout-minutes: 20 + runs-on: ubuntu-24.04 + container: archlinux:base-devel + permissions: + contents: write + if: ${{ github.ref_type == 'tag' && !contains(github.ref_name, '-') }} + defaults: + run: + shell: bash + steps: + - name: Gate on GPG signing secret + id: gate + env: + GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }} + GPG_KEY_ID: ${{ secrets.GPG_KEY_ID }} + run: | + if [ -z "${GPG_PRIVATE_KEY:-}" ] || [ -z "${GPG_KEY_ID:-}" ]; then + echo "::warning::GPG_PRIVATE_KEY / GPG_KEY_ID not set; skipping pacman repo publish." + echo "skip=true" >> "$GITHUB_OUTPUT" + else + echo "skip=false" >> "$GITHUB_OUTPUT" + fi + + - name: Install tooling + if: steps.gate.outputs.skip != 'true' + run: pacman -Syu --noconfirm git pacman-contrib + + - name: Checkout main (for the PKGBUILD) + if: steps.gate.outputs.skip != 'true' + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + ref: ${{ inputs.tag || github.ref }} + path: main + + - name: Import signing key + if: steps.gate.outputs.skip != 'true' + env: + GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }} + run: | + printf '%s' "$GPG_PRIVATE_KEY" | gpg --batch --import + gpg --list-secret-keys --keyid-format=long + + # makepkg refuses to run as root, and the container runs as root: + # build as a throwaway user. updpkgsums re-downloads the sources from + # the release the publish job just flipped live, so the repo package + # is built from (and checksummed against) the published bytes, not a + # parallel local copy. -d skips dependency checks: a -bin package() + # only copies files, and fuse2/hicolor are runtime deps. + - name: Build dish-bin package + if: steps.gate.outputs.skip != 'true' + env: + TAG: ${{ inputs.tag || github.ref_name }} + run: | + set -euo pipefail + version="${TAG#v}" + useradd -m builder + builddir=/home/builder/dish-bin + mkdir -p "$builddir" + cp main/packaging/aur/PKGBUILD main/packaging/aur/dish-bin.install "$builddir/" + sed -i "s/^pkgver=.*/pkgver=${version}/; s/^pkgrel=.*/pkgrel=1/" "$builddir/PKGBUILD" + chown -R builder:builder "$builddir" + su builder -c "cd '$builddir' && updpkgsums && PACKAGER='dish-release-bot ' makepkg -d --noconfirm" + ls -l "$builddir"/*.pkg.tar.zst + + - name: Sign package + if: steps.gate.outputs.skip != 'true' + env: + GPG_KEY_ID: ${{ secrets.GPG_KEY_ID }} + run: | + set -euo pipefail + for pkg in /home/builder/dish-bin/*.pkg.tar.zst; do + gpg --batch --yes --default-key "$GPG_KEY_ID" --detach-sign "$pkg" + done + + - name: Check out gh-pages branch (rpm-publish has already touched it) + if: steps.gate.outputs.skip != 'true' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + set -euo pipefail + git clone --branch gh-pages --depth 1 \ + "https://x-access-token:${GH_TOKEN}@github.com/${{ github.repository }}.git" pages + + # repo-add emits dish.db / dish.files as symlinks to the .tar.gz + # files; GitHub Pages does not serve symlinks, so replace each with a + # real copy before committing. + - name: Build pacman repo + if: steps.gate.outputs.skip != 'true' + env: + GPG_KEY_ID: ${{ secrets.GPG_KEY_ID }} + run: | + set -euo pipefail + repo_dir=pages/arch/x86_64 + mkdir -p "$repo_dir" + cp /home/builder/dish-bin/*.pkg.tar.zst /home/builder/dish-bin/*.pkg.tar.zst.sig "$repo_dir/" + ( + cd "$repo_dir" + repo-add --sign --key "$GPG_KEY_ID" dish.db.tar.gz ./*.pkg.tar.zst + for link in $(find . -maxdepth 1 -type l); do + target="$(readlink -f "$link")" + rm "$link" + cp "$target" "$link" + done + ) + ls -l "$repo_dir" + + - name: Commit + push + if: steps.gate.outputs.skip != 'true' + env: + TAG: ${{ inputs.tag || github.ref_name }} + run: | + set -euo pipefail + cd pages + git config user.name 'dish-release-bot' + git config user.email 'releases@tinkernorth.invalid' + git add -A + if git diff --cached --quiet; then + echo "No pacman-repo changes to commit" + else + git commit -m "Publish pacman repo: ${TAG}" + git push origin gh-pages + fi diff --git a/README.md b/README.md index f6c67ab..73a3b4b 100644 --- a/README.md +++ b/README.md @@ -41,7 +41,14 @@ Physical controllers only. There is no on-screen touch gamepad; that belongs to ## Install and run You need a 64-bit Linux desktop, a gamepad, and a reachable Satellite server. -Every release publishes four packages; pick the one that matches your distro. + +The comfortable path is a package repository: add it once and your package +manager owns updates from then on. Debian 13+, Fedora-family, and Arch +(pacman repo) instructions all live at +[tinkernorth.github.io/dish-linux](https://tinkernorth.github.io/dish-linux/). + +Alternatively, every release publishes four standalone packages; pick the one +that matches your distro. | You run | Take | Then | |---|---|---| @@ -86,8 +93,10 @@ updater checks and stops: about 15 seconds after launch and every four hours after that it asks GitHub for `latest.json`, and if there is a newer release it shows a pill linking to the release page. Nothing is downloaded and nothing is applied. *Check for updates automatically* in Settings stops every -update-related network request when off. What the check sends is spelled out in -[`PRIVACY.md`](PRIVACY.md). +update-related network request when off; inside a Flatpak it starts off, since +the store that installed Dish also updates it. The AppImage additionally +carries zsync update metadata, so AppImageUpdate or Gear Lever can delta-update +it in place. What the check sends is spelled out in [`PRIVACY.md`](PRIVACY.md). ## Build from source diff --git a/SECURITY.md b/SECURITY.md index 8233a68..462a25a 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -25,7 +25,7 @@ Use one of: 1. **GitHub private vulnerability reporting** — open the repo, click *Security → Report a vulnerability*. This is preferred because it creates a tracked advisory and a private discussion thread. -2. **Email** — `security@tinkernorth.invalid` (PGP key on request). +2. **Email**: `security@tinkernorth.com` (PGP key on request). Include the repo, version (commit SHA or release tag), reproduction steps, and impact. diff --git a/docs/FLATHUB.md b/docs/FLATHUB.md new file mode 100644 index 0000000..e1c2362 --- /dev/null +++ b/docs/FLATHUB.md @@ -0,0 +1,58 @@ +# Flathub submission + +Flathub is the software-centre path (GNOME Software, KDE Discover) on every +distro, the only path on Steam Deck's immutable desktop, and the answer for +an LTS whose Qt is below this project's floor. Once listed, updates are +automatic: the store rebuilds every tagged release. The manifest is ready at +[`packaging/flatpak/flathub/com.tinkernorth.Dish.yml`](../packaging/flatpak/flathub/com.tinkernorth.Dish.yml); +this file is the submission procedure. + +## Before submitting + +1. A published release must exist. The manifest builds from a pinned git + tag; fill its `commit:` placeholder with `git rev-list -n 1 `. + +2. Screenshots are in `docs/screenshots/` and referenced from the + AppStream metainfo, pinned to the release tag. When retaking them, + update the tag in the URLs; a published listing must not have its + images change underneath it. + +3. Run Flathub's own linter. It checks more than `appstreamcli validate`: + + ```sh + flatpak install -y flathub org.flatpak.Builder + flatpak run --command=flatpak-builder-lint org.flatpak.Builder \ + manifest packaging/flatpak/flathub/com.tinkernorth.Dish.yml + flatpak run --command=flatpak-builder-lint org.flatpak.Builder \ + appstream packaging/com.tinkernorth.Dish.metainfo.xml + ``` + +## Submitting + +1. Fork `github.com/flathub/flathub` and branch from `new-pr`. +2. Add the flathub-variant manifest (commit pin filled) as + `com.tinkernorth.Dish.yml`. +3. Open the PR against the `new-pr` branch and fill in their template. + Test instructions: the app runs standalone; pairing needs a Satellite + server on the same network. +4. Expect review pushback on `--device=all`. The position is written into + the manifest comment: there is no hidraw portal, and the USB portal + covers raw USB devices, not hidraw character nodes. If the reviewer + holds the line, take `--device=input`: the SDL path still works, + USB-direct degrades exactly as [PACKAGING.md](PACKAGING.md) documents, + and a user restores it with + `flatpak override --user --device=all com.tinkernorth.Dish`. + A listed app with the narrower grant beats an unlisted one. + +## After acceptance + +- Flathub creates `flathub/com.tinkernorth.Dish`; that copy is the build + source of truth. Mirror any change back into `packaging/flatpak/` here. +- The manifest's `x-checker-data` block lets Flathub's external-data-checker + open the version-bump PR there on every new tag. Verify the first one. +- Point the README and the Pages landing page at Flathub for Ubuntu and + older-LTS users. +- The in-app update check already defaults off inside the sandbox + (`UpdatePreferenceStore`); the store owns delivery. +- Consider dropping the `.flatpak` release asset once the listing is + established: a sideloaded bundle has no update origin. diff --git a/docs/PACKAGING.md b/docs/PACKAGING.md index 2008333..76d9428 100644 --- a/docs/PACKAGING.md +++ b/docs/PACKAGING.md @@ -70,6 +70,32 @@ grant before a fresh install worked at all, which is a poor fit for an app whose headline feature is claiming gamepads, and the four formats above already reach every desktop it would. +aarch64 is deliberately deferred, not refused: public repos get GitHub's +`ubuntu-24.04-arm` runners free, so adding it is a build-matrix change plus a +QA question (nothing in CI can smoke-test a pad on arm hardware). Revisit when +someone actually asks; until then every artifact is x86_64 and the README says +so. + +## Where users get the packages + +Publishing is a channel, not a format. Every release tag feeds: + +| Channel | Who it serves | How it updates | +|---|---|---| +| APT repo (`https://tinkernorth.github.io/dish-linux/debian`) | Debian 13+ | `apt upgrade` | +| DNF repo (`https://tinkernorth.github.io/dish-linux/rpm`) | Fedora/RHEL/openSUSE | `dnf upgrade` | +| pacman repo (`https://tinkernorth.github.io/dish-linux/arch/$arch`) | Arch | `pacman -Syu` (`arch-publish` builds `dish-bin` from [`packaging/aur/`](../packaging/aur/) each release; an AUR listing waits on registration reopening) | +| Flathub | Ubuntu LTS, Steam Deck, software-center users | store-automatic ([submission runbook](FLATHUB.md)) | +| GitHub Releases | everyone else | AppImage self-updates via zsync (AppImageUpdate/Gear Lever); manual otherwise | + +The APT and DNF trees live on the `gh-pages` branch, regenerated and +GPG-signed by the `apt-publish` / `rpm-publish` jobs in `release.yml` using +[`packaging/repo/`](../packaging/repo/) (key fingerprint and rotation story in +its README). The AppImage embeds gh-releases-zsync update information, and +the release uploads the matching `.zsync` beside it; the draft-then-flip publish +keeps `releases/latest` atomic so neither the updater manifest nor the zsync +pattern ever sees a half-uploaded release. + ## The udev rule is not optional `/dev/hidraw*` is root-only by default. Dish's USB-direct path opens the node @@ -115,7 +141,10 @@ reviewed input rather than something the job derives. A packager who does not want the check at all can ship with `updates_check_enabled=false` seeded in the default config; the store reads it -at construction and the checker arms no timer. +at construction and the checker arms no timer. Inside a Flatpak the default is +already `false` (`UpdatePreferenceStore::runningInFlatpak`), because the store +that delivered the sandbox also delivers its updates; the Settings toggle still +overrides. ## Qt version floor diff --git a/docs/screenshots/connections.png b/docs/screenshots/connections.png new file mode 100644 index 0000000..9aac3c9 Binary files /dev/null and b/docs/screenshots/connections.png differ diff --git a/docs/screenshots/controllers.png b/docs/screenshots/controllers.png new file mode 100644 index 0000000..476f9a7 Binary files /dev/null and b/docs/screenshots/controllers.png differ diff --git a/docs/screenshots/home.png b/docs/screenshots/home.png new file mode 100644 index 0000000..01d6b81 Binary files /dev/null and b/docs/screenshots/home.png differ diff --git a/docs/screenshots/settings.png b/docs/screenshots/settings.png new file mode 100644 index 0000000..38d6dfc Binary files /dev/null and b/docs/screenshots/settings.png differ diff --git a/packaging/aur/.SRCINFO b/packaging/aur/.SRCINFO new file mode 100644 index 0000000..9ad35cb --- /dev/null +++ b/packaging/aur/.SRCINFO @@ -0,0 +1,23 @@ +pkgbase = dish-bin + pkgdesc = Stream a gamepad to a Satellite server over your LAN + pkgver = 0.1.0 + pkgrel = 1 + url = https://github.com/TinkerNorth/dish-linux + install = dish-bin.install + arch = x86_64 + license = LGPL-3.0-or-later + depends = fuse2 + depends = hicolor-icon-theme + optdepends = xdg-utils: open the release page from the update pill + source_x86_64 = Dish-0.1.0-x86_64.AppImage::https://github.com/TinkerNorth/dish-linux/releases/download/0.1.0/Dish-0.1.0-x86_64.AppImage + source_x86_64 = com.tinkernorth.Dish.desktop::https://raw.githubusercontent.com/TinkerNorth/dish-linux/0.1.0/packaging/dish.desktop + source_x86_64 = com.tinkernorth.Dish.svg::https://raw.githubusercontent.com/TinkerNorth/dish-linux/0.1.0/packaging/dish.svg + source_x86_64 = com.tinkernorth.Dish.metainfo.xml::https://raw.githubusercontent.com/TinkerNorth/dish-linux/0.1.0/packaging/com.tinkernorth.Dish.metainfo.xml + source_x86_64 = 70-dish-hidraw.rules::https://raw.githubusercontent.com/TinkerNorth/dish-linux/0.1.0/packaging/udev/70-dish-hidraw.rules + sha256sums_x86_64 = 0000000000000000000000000000000000000000000000000000000000000000 + sha256sums_x86_64 = 0000000000000000000000000000000000000000000000000000000000000000 + sha256sums_x86_64 = 0000000000000000000000000000000000000000000000000000000000000000 + sha256sums_x86_64 = 0000000000000000000000000000000000000000000000000000000000000000 + sha256sums_x86_64 = 0000000000000000000000000000000000000000000000000000000000000000 + +pkgname = dish-bin diff --git a/packaging/aur/PKGBUILD b/packaging/aur/PKGBUILD new file mode 100644 index 0000000..6a5a1ff --- /dev/null +++ b/packaging/aur/PKGBUILD @@ -0,0 +1,94 @@ +# Maintainer: TinkerNorth +# +# dish-bin: Arch User Repository package that re-bundles the upstream +# AppImage. Build it locally with `makepkg -si`, or install via an AUR +# helper: +# +# yay -S dish-bin +# paru -S dish-bin +# +# Updating: bump pkgver, refresh `sha256sums` from the release's +# SHA256SUMS asset (`updpkgsums` does it), then regenerate .SRCINFO with +# `makepkg --printsrcinfo > .SRCINFO`. +# +# Why -bin? A from-source Arch package would drag in Qt 6 / SDL2 / +# libsodium headers at build time, and the AppImage is already a bundled +# artifact covered by the release's cosign signature and SLSA provenance. +# Arch users who want from-source builds can use the upstream cmake flow. + +pkgname=dish-bin +pkgver=0.1.0 +pkgrel=1 +pkgdesc="Stream a gamepad to a Satellite server over your LAN" +arch=('x86_64') +install='dish-bin.install' +url="https://github.com/TinkerNorth/dish-linux" +license=('LGPL-3.0-or-later') +# The AppImage bundles Qt, SDL2 and libsodium; what remains is the AppImage +# runtime itself and the desktop plumbing the entry/icon rely on. +depends=( + 'fuse2' + 'hicolor-icon-theme' +) +optdepends=( + 'xdg-utils: open the release page from the update pill' +) +# NOTE: deliberately no provides=/conflicts= on the bare name `dish`. An +# unrelated AUR package already owns it (an interactive shell), so claiming +# the name would misrepresent this package. The /usr/bin/dish launcher shim +# below can still file-conflict with that package; pacman refuses the +# co-install cleanly if anyone ever hits it. + +# Single x86_64 AppImage. Bump the URL and checksums in lockstep with pkgver. +# Release tags are bare MAJOR.MINOR.PATCH (org convention: dish-android's +# pipeline requires bare tags). +source_x86_64=( + "Dish-${pkgver}-x86_64.AppImage::https://github.com/TinkerNorth/dish-linux/releases/download/${pkgver}/Dish-${pkgver}-x86_64.AppImage" + "com.tinkernorth.Dish.desktop::https://raw.githubusercontent.com/TinkerNorth/dish-linux/${pkgver}/packaging/dish.desktop" + "com.tinkernorth.Dish.svg::https://raw.githubusercontent.com/TinkerNorth/dish-linux/${pkgver}/packaging/dish.svg" + "com.tinkernorth.Dish.metainfo.xml::https://raw.githubusercontent.com/TinkerNorth/dish-linux/${pkgver}/packaging/com.tinkernorth.Dish.metainfo.xml" + "70-dish-hidraw.rules::https://raw.githubusercontent.com/TinkerNorth/dish-linux/${pkgver}/packaging/udev/70-dish-hidraw.rules" +) +# PLACEHOLDERS: the 0.1.0 release is not published yet. Before the first +# AUR push, run `updpkgsums` against the real release and cross-check the +# AppImage line against the release's SHA256SUMS asset. SKIP is not +# acceptable here: the whole premise of a -bin package is redistributing an +# upstream binary, and the release ships cosign signatures and SLSA +# provenance; verifying nothing on the way in would throw that away. +sha256sums_x86_64=( + '0000000000000000000000000000000000000000000000000000000000000000' + '0000000000000000000000000000000000000000000000000000000000000000' + '0000000000000000000000000000000000000000000000000000000000000000' + '0000000000000000000000000000000000000000000000000000000000000000' + '0000000000000000000000000000000000000000000000000000000000000000' +) + +package() { + # Drop the AppImage in /opt and shim a launcher into /usr/bin so users + # type `dish` like they would for any other package (and so the desktop + # entry's Exec=dish resolves). + install -Dm755 "${srcdir}/Dish-${pkgver}-x86_64.AppImage" \ + "${pkgdir}/opt/dish/Dish.AppImage" + + install -dm755 "${pkgdir}/usr/bin" + cat > "${pkgdir}/usr/bin/dish" <<'SHIM' +#!/bin/sh +# Shim added by the dish-bin AUR package; forwards to the AppImage bundled +# under /opt/dish. +exec /opt/dish/Dish.AppImage "$@" +SHIM + chmod 755 "${pkgdir}/usr/bin/dish" + + install -Dm644 "${srcdir}/com.tinkernorth.Dish.desktop" \ + "${pkgdir}/usr/share/applications/com.tinkernorth.Dish.desktop" + install -Dm644 "${srcdir}/com.tinkernorth.Dish.svg" \ + "${pkgdir}/usr/share/icons/hicolor/scalable/apps/com.tinkernorth.Dish.svg" + install -Dm644 "${srcdir}/com.tinkernorth.Dish.metainfo.xml" \ + "${pkgdir}/usr/share/metainfo/com.tinkernorth.Dish.metainfo.xml" + # The rule the AppImage cannot install itself; this is the whole reason + # to prefer dish-bin over the bare AppImage on Arch. + install -Dm644 "${srcdir}/70-dish-hidraw.rules" \ + "${pkgdir}/usr/lib/udev/rules.d/70-dish-hidraw.rules" +} + +# vim:set ts=4 sw=4 et: diff --git a/packaging/aur/README.md b/packaging/aur/README.md new file mode 100644 index 0000000..ae47f5f --- /dev/null +++ b/packaging/aur/README.md @@ -0,0 +1,49 @@ +# dish-bin + +`PKGBUILD` re-bundles the release AppImage for Arch users, with the udev +rule, desktop entry, icon and AppStream metadata the bare AppImage cannot +install. Mirrors `TinkerNorth/satellite` `packaging/aur/`. + +> **Status:** AUR account registration is currently closed, so this is not +> on the AUR yet. The live Arch channel is the **self-hosted pacman repo**: +> the `arch-publish` job in `release.yml` builds `dish-bin` from this +> PKGBUILD on every stable release and publishes it (GPG-signed) at +> `https://tinkernorth.github.io/dish-linux/arch/$arch`. The steps below +> are for the eventual AUR listing, if/when registration reopens; local +> `makepkg -si` works today regardless. + +## First publish (one-time) + +The AUR is a separate git remote owned by an AUR account, not something CI +can push to from this repo. After the first release is published: + +```bash +# 1. Fill the real checksums (currently 64-zero placeholders): +cd packaging/aur +updpkgsums # rewrites sha256sums_x86_64 in PKGBUILD +# Cross-check the AppImage line against the release's SHA256SUMS asset. + +# 2. Regenerate .SRCINFO from the PKGBUILD: +makepkg --printsrcinfo > .SRCINFO + +# 3. Build + install locally to verify: +makepkg -si + +# 4. Push to the AUR (needs an AUR account with an SSH key registered): +git clone ssh://aur@aur.archlinux.org/dish-bin.git /tmp/dish-bin-aur +cp PKGBUILD .SRCINFO dish-bin.install /tmp/dish-bin-aur/ +cd /tmp/dish-bin-aur && git add -A && git commit -m "dish-bin 0.1.0-1" && git push +``` + +## Every release after that + +Bump `pkgver`, reset `pkgrel=1`, run `updpkgsums`, regenerate `.SRCINFO`, +copy the three files into the AUR clone, commit, push. (A `release.yml` +step could template this; it is manual for now because the AUR SSH key is +personal, not a repo secret.) + +## Name note + +The bare AUR name `dish` is owned by an unrelated package (an interactive +shell), so this package neither provides nor conflicts with it; see the +comment in `PKGBUILD`. diff --git a/packaging/aur/dish-bin.install b/packaging/aur/dish-bin.install new file mode 100644 index 0000000..1102a51 --- /dev/null +++ b/packaging/aur/dish-bin.install @@ -0,0 +1,41 @@ +# Arch parity with the .deb postinst and the .rpm scriptlets: udev reads a new +# rule file only after a reload, and applies it only to devices that (re)appear +# afterwards — so reload AND retrigger, or a pad plugged in before the install +# keeps its root-only node until the next replug and every USB-direct claim +# fails PermissionDenied. + +_dish_wire_hidraw() { + if command -v udevadm >/dev/null 2>&1; then + udevadm control --reload-rules || true + udevadm trigger --subsystem-match=hidraw || true + fi +} + +post_install() { + _dish_wire_hidraw + cat <<'EOF' + +Dish: the shipped udev rule tags supported pads with `uaccess`, which grants +the active seat user access automatically -- on most desktops nothing further +is needed. Replug the controller once so the rule applies. + +For a headless or non-logind session, the rule's fallback grants the `input` +group instead; add yourself and re-log: + + sudo gpasswd -a "$USER" input + +Without the rule (or before the replug) Dish keeps the pad streaming on the +SDL path -- it still works, just rate-capped. + +EOF +} + +post_upgrade() { + _dish_wire_hidraw +} + +post_remove() { + if command -v udevadm >/dev/null 2>&1; then + udevadm control --reload-rules || true + fi +} diff --git a/packaging/com.tinkernorth.Dish.metainfo.xml b/packaging/com.tinkernorth.Dish.metainfo.xml index 235402c..6a74e2f 100644 --- a/packaging/com.tinkernorth.Dish.metainfo.xml +++ b/packaging/com.tinkernorth.Dish.metainfo.xml @@ -71,6 +71,28 @@ streaming + + + + A connected pad, ready to bind to a satellite + https://raw.githubusercontent.com/TinkerNorth/dish-linux/0.1.0/docs/screenshots/home.png + + + Connected controllers and what each one supports + https://raw.githubusercontent.com/TinkerNorth/dish-linux/0.1.0/docs/screenshots/controllers.png + + + Finding satellites on the LAN + https://raw.githubusercontent.com/TinkerNorth/dish-linux/0.1.0/docs/screenshots/connections.png + + + Theme, forwarded features and controller tuning + https://raw.githubusercontent.com/TinkerNorth/dish-linux/0.1.0/docs/screenshots/settings.png + + + https://github.com/TinkerNorth/dish-linux https://github.com/TinkerNorth/dish-linux/issues https://github.com/TinkerNorth/dish-linux/blob/main/README.md @@ -81,7 +103,7 @@ fails the PR that moves one without the other. --> - https://github.com/TinkerNorth/dish-linux/releases/tag/v0.1.0 + https://github.com/TinkerNorth/dish-linux/releases/tag/0.1.0

First Linux release: Qt Quick UI, LAN and mDNS discovery, TOFU pairing, SDL and USB-direct input paths, and six UI languages.

diff --git a/packaging/flatpak/flathub/com.tinkernorth.Dish.yml b/packaging/flatpak/flathub/com.tinkernorth.Dish.yml new file mode 100644 index 0000000..4a2fa55 --- /dev/null +++ b/packaging/flatpak/flathub/com.tinkernorth.Dish.yml @@ -0,0 +1,114 @@ +# Flathub submission manifest for Dish. +# +# This is the Flathub-facing variant of ../com.tinkernorth.Dish.yml: identical +# build, but the dish module builds from a pinned git tag instead of the +# working tree, because Flathub only accepts reproducible sources. Keep the two +# manifests in step; docs/FLATHUB.md is the submission runbook. +# +# Before submitting: replace the commit placeholder with the tag's commit SHA +# git rev-list -n 1 0.1.0 +app-id: com.tinkernorth.Dish +runtime: org.kde.Platform +runtime-version: '6.9' +sdk: org.kde.Sdk +command: dish + +finish-args: + # --share=ipc is what makes the X11 fallback usable (MIT-SHM) rather than glacial. + - --share=ipc + - --socket=wayland + - --socket=fallback-x11 + - --device=dri + + # LAN discovery (UDP broadcast + mDNS), the pairing REST call and the + # encrypted UDP session all need the host network. + - --share=network + + # SDL input needs /dev/input/event*; USB-direct needs O_RDWR on /dev/hidraw*, + # which no narrower grant provides — there is no hidraw portal, and + # org.freedesktop.portal.Usb covers raw USB devices, not hidraw character + # devices. If a reviewer rejects this, ship --device=input alone: the SDL path + # still works, USB-direct reports PermissionDenied, and the user can restore it + # with `flatpak override --user --device=all com.tinkernorth.Dish`. + - --device=all + + # Util/DisplaySleepInhibitor holds the screen awake through this name. + - --talk-name=org.freedesktop.ScreenSaver + # The tray item registers itself with the shell's watcher. Nothing is needed + # for com.canonical.dbusmenu: that is an interface exported on our own + # connection, which the host reaches through the unique name it already has. + - --talk-name=org.kde.StatusNotifierWatcher + # "Dish is still running" has to reach a user whose window just vanished. + - --talk-name=org.freedesktop.Notifications + # BluetoothRadioProbe needs Adapter1.Powered to tell "no adapter" from "off". + - --system-talk-name=org.bluez + # Both logind locks: DisplaySleepInhibitor's idle block, and SleepMonitor's + # sleep delay plus its PrepareForSleep subscription. Without this the sandbox + # silently loses the suspend handling the host build has. + - --system-talk-name=org.freedesktop.login1 + + # No --filesystem: settings land in ~/.var/app/com.tinkernorth.Dish/config/. + +modules: + # Neither is in org.kde.Platform. Checksums verified against the release + # tarballs; re-verify on every bump with `curl -sL | sha256sum`. + - name: libsodium + buildsystem: autotools + config-opts: + - --disable-static + sources: + - type: archive + url: https://github.com/jedisct1/libsodium/releases/download/1.0.20-RELEASE/libsodium-1.0.20.tar.gz + sha256: ebb65ef6ca439333c2bb41a0c1990587288da07f6c7fd07cb3a18cc18d30ce19 + cleanup: + - /include + - /lib/pkgconfig + - '*.la' + + - name: sdl2 + buildsystem: cmake-ninja + config-opts: + - -DCMAKE_BUILD_TYPE=Release + - -DSDL_STATIC=OFF + - -DSDL_SHARED=ON + - -DSDL_TEST=OFF + # Dish uses SDL for gamepads only; the render and audio backends are dead + # weight in the sandbox. + - -DSDL_RENDER=OFF + - -DSDL_AUDIO=OFF + - -DSDL_HIDAPI=ON + - -DSDL_JOYSTICK=ON + - -DSDL_HAPTIC=ON + sources: + - type: archive + url: https://github.com/libsdl-org/SDL/releases/download/release-2.30.9/SDL2-2.30.9.tar.gz + sha256: 24b574f71c87a763f50704bbb630cbe38298d544a1f890f099a4696b1d6beba4 + cleanup: + - /include + - /lib/pkgconfig + - /lib/cmake + - /bin/sdl2-config + - /share/aclocal + - '*.la' + + - name: dish + buildsystem: cmake-ninja + config-opts: + - -DCMAKE_BUILD_TYPE=Release + - -DDISH_BUILD_TESTS=OFF + # A rule under /app is read by nothing; the app degrades to SDL and says so. + - -DDISH_INSTALL_UDEV_RULES=OFF + sources: + - type: git + url: https://github.com/TinkerNorth/dish-linux.git + tag: '0.1.0' + commit: FILL_WITH_TAG_COMMIT_SHA_BEFORE_SUBMITTING + # Lets Flathub's external-data-checker open the version-bump PR for + # every new tag automatically once the app is in. + x-checker-data: + type: git + tag-pattern: ^([\d.]+)$ + post-install: + # Carried so a user who wants USB-direct can install it on the host. + - install -Dm644 packaging/udev/70-dish-hidraw.rules + /app/share/dish/70-dish-hidraw.rules diff --git a/packaging/repo/README.md b/packaging/repo/README.md new file mode 100644 index 0000000..8ed507a --- /dev/null +++ b/packaging/repo/README.md @@ -0,0 +1,100 @@ +# Linux package repositories on GitHub Pages + +This directory holds everything needed to publish the `dish` `.deb` +and `.rpm` releases as proper APT and DNF/YUM repositories on +`https://tinkernorth.github.io/dish-linux/`. + +Ported from `TinkerNorth/satellite` `packaging/repo/`; the two directories +share their layout and scripts; keep them in step. + +## End-user install (Debian 13+ and derivatives) + +The `.deb` targets distros whose Qt meets the 6.7 floor (Debian 13 ships +6.8). Ubuntu 24.04 LTS ships Qt 6.4.2; its answer is the AppImage or the +Flatpak, not this repo; see `docs/PACKAGING.md`. + +```bash +# Add the signing key (one-time): +curl -fsSL https://tinkernorth.github.io/dish-linux/gpg.key \ + | sudo gpg --dearmor -o /usr/share/keyrings/dish-archive-keyring.gpg + +# Add the repo (one-time): +echo "deb [signed-by=/usr/share/keyrings/dish-archive-keyring.gpg] \ + https://tinkernorth.github.io/dish-linux/debian stable main" \ + | sudo tee /etc/apt/sources.list.d/dish.list + +# Install + future upgrades via apt: +sudo apt update +sudo apt install dish +``` + +## End-user install (Fedora / RHEL / Rocky / Alma / openSUSE) + +```bash +# Drop the .repo file (it already references the gpg key). One-time: +sudo curl -fsSL -o /etc/yum.repos.d/dish.repo \ + https://tinkernorth.github.io/dish-linux/rpm/dish.repo + +# Install + future upgrades via dnf: +sudo dnf install dish +``` + +## End-user install (Arch) + +```bash +# Trust the signing key (one-time): +curl -fsSL https://tinkernorth.github.io/dish-linux/gpg.key | sudo pacman-key --add - +sudo pacman-key --lsign-key 96FFAACB78FE75D18CEEE332C398179512D6BDF3 + +# Add the repo (one-time): +printf '\n[dish]\nSigLevel = Required DatabaseOptional\nServer = https://tinkernorth.github.io/dish-linux/arch/$arch\n' \ + | sudo tee -a /etc/pacman.conf + +# Install + future upgrades via pacman: +sudo pacman -Syu dish-bin +``` + +The `arch-publish` job builds `dish-bin` from `packaging/aur/PKGBUILD` +against the just-published release assets, signs the package and the repo +database with the same key, and pushes `arch/x86_64/` alongside the APT and +DNF trees. (`repo-add`'s `.db`/`.files` symlinks are replaced with copies; +GitHub Pages does not serve symlinks.) + +## Signing key + +The repository signing key is: + +``` +pub ed25519 2026-08-23 + 96FF AACB 78FE 75D1 8CEE E332 C398 1795 12D6 BDF3 +uid Dish Releases +``` + +The private key lives only in the `GPG_PRIVATE_KEY` / `GPG_KEY_ID` +repository secrets (and the maintainer's password manager); the public half +is committed here as `gpg.key` and published at the repo root of the Pages +site. This fingerprint is the pin: a Pages compromise cannot silently swap +the key without this README (in git history) disagreeing. + +Rotation: generate a new key, add its public half here alongside the old +one, sign one release with both (`gpg.key` may contain multiple public +keys), then retire the old key in a follow-up release. Removing a key users +have pinned breaks `apt update` for them; announce first. + +## How publishing works in CI + +The `release.yml` workflow runs an `apt-publish` and `rpm-publish` job +after the main `publish` job succeeds. Both: + +1. Check out the `gh-pages` branch into a working directory (creating an + orphan branch with this directory's `index.html` on first publish). +2. Import the GPG signing key from the `GPG_PRIVATE_KEY` secret and skip + gracefully (with a warning) when it is absent, because unsigned repo + metadata is worse than no repo at all. +3. Run `build-apt-repo.sh` / `build-dnf-repo.sh` from this directory to + stage the new package, regenerate indices, and sign them. +4. Commit and push `gh-pages`. + +One-time repo setup: GitHub Pages must be enabled for the `gh-pages` +branch (Settings → Pages → Deploy from branch → `gh-pages` / root) after +the first release creates it. diff --git a/packaging/repo/build-apt-repo.sh b/packaging/repo/build-apt-repo.sh new file mode 100755 index 0000000..a211c0f --- /dev/null +++ b/packaging/repo/build-apt-repo.sh @@ -0,0 +1,109 @@ +#!/usr/bin/env bash +# build-apt-repo.sh: regenerate the APT repository tree under a given +# pages directory, then GPG-sign the result. +# +# Ported from TinkerNorth/satellite packaging/repo/build-apt-repo.sh; keep the +# two in step when the layout changes. +# +# The repo layout we produce (rooted at $PAGES_DIR): +# +# debian/pool/main/d/dish/dish__amd64.deb +# debian/dists/stable/Release +# debian/dists/stable/Release.gpg +# debian/dists/stable/InRelease +# debian/dists/stable/main/binary-amd64/{Packages,Packages.gz} +# +# Usage (from CI): +# build-apt-repo.sh +# +# The release-staging-dir is the directory the workflow already +# downloaded artifacts into. Any *.deb file matching the dish +# naming pattern is copied into the pool. +# +# The .deb targets Debian 13+ (the distro Qt meets the 6.7 floor there); +# docs/PACKAGING.md is the authority on which distro gets which format. +# +# Requires: apt-utils (apt-ftparchive), gpg (with $GPG_KEY_ID imported). +set -euo pipefail + +PAGES_DIR="${1:-}" +STAGING_DIR="${2:-}" +SUITE="${DISH_APT_SUITE:-stable}" +COMPONENT="main" +ARCH="amd64" +ORIGIN="${DISH_APT_ORIGIN:-TinkerNorth}" +LABEL="${DISH_APT_LABEL:-Dish}" +DESCRIPTION="Dish Linux package repository" + +if [ -z "$PAGES_DIR" ] || [ -z "$STAGING_DIR" ]; then + echo "Usage: $0 " >&2 + exit 2 +fi +if [ -z "${GPG_KEY_ID:-}" ]; then + echo "GPG_KEY_ID must be set (the public key fingerprint or short ID)." >&2 + exit 2 +fi +if ! command -v apt-ftparchive >/dev/null 2>&1; then + echo "apt-ftparchive not found; install apt-utils." >&2 + exit 2 +fi + +# ── 1. Stage the new .deb into the pool ───────────────────────────────────── +pool_dir="$PAGES_DIR/debian/pool/${COMPONENT}/d/dish" +mkdir -p "$pool_dir" +shopt -s nullglob +debs=("$STAGING_DIR"/dish_*_${ARCH}.deb) +if [ "${#debs[@]}" -eq 0 ]; then + echo "::warning::No dish_*_${ARCH}.deb found in $STAGING_DIR; nothing to add" +else + for deb in "${debs[@]}"; do + echo "+ adding $(basename "$deb") to pool" + cp -v "$deb" "$pool_dir/" + done +fi + +# ── 2. Generate the Packages index from everything in the pool ────────────── +arch_dir="$PAGES_DIR/debian/dists/${SUITE}/${COMPONENT}/binary-${ARCH}" +mkdir -p "$arch_dir" +( + cd "$PAGES_DIR/debian" + apt-ftparchive --arch "${ARCH}" packages "pool/${COMPONENT}" \ + > "dists/${SUITE}/${COMPONENT}/binary-${ARCH}/Packages" + gzip -kf "dists/${SUITE}/${COMPONENT}/binary-${ARCH}/Packages" +) + +# ── 3. Generate the per-suite Release manifest ────────────────────────────── +# `apt-ftparchive release` walks the suite dir and emits checksums for every +# Packages / Packages.gz / Contents file under it. The override block below +# fills in the human-meta fields that apt's authenticity check requires. +suite_dir="$PAGES_DIR/debian/dists/${SUITE}" +release_conf="$(mktemp)" +trap 'rm -f "$release_conf"' EXIT +cat > "$release_conf" < Release.new + mv Release.new Release +) + +# ── 4. Sign Release → Release.gpg (detached) + InRelease (clearsigned) ────── +# apt will accept either; modern apt prefers InRelease (one file, one fetch). +( + cd "$suite_dir" + rm -f Release.gpg InRelease + gpg --batch --yes --default-key "$GPG_KEY_ID" --detach-sign --armor \ + --output Release.gpg Release + gpg --batch --yes --default-key "$GPG_KEY_ID" --clearsign \ + --output InRelease Release +) + +echo "APT repo ready at $PAGES_DIR/debian/dists/${SUITE}" diff --git a/packaging/repo/build-dnf-repo.sh b/packaging/repo/build-dnf-repo.sh new file mode 100755 index 0000000..ce69440 --- /dev/null +++ b/packaging/repo/build-dnf-repo.sh @@ -0,0 +1,81 @@ +#!/usr/bin/env bash +# build-dnf-repo.sh: regenerate the DNF / YUM repository tree under a given +# pages directory, then GPG-sign the result. +# +# Ported from TinkerNorth/satellite packaging/repo/build-dnf-repo.sh; keep the +# two in step when the layout changes. +# +# Layout under $PAGES_DIR: +# +# rpm/x86_64/dish--1.x86_64.rpm +# rpm/x86_64/repodata/ +# ├── repomd.xml +# ├── repomd.xml.asc ← detached signature dnf checks +# ├── repomd.xml.key ← public key copied next to repomd for dnf +# ├── primary.xml.gz +# ├── filelists.xml.gz +# └── other.xml.gz +# rpm/dish.repo ← dnf config users curl into /etc/yum.repos.d/ +# +# Usage: +# build-dnf-repo.sh +# +# Requires: createrepo_c, gpg (with $GPG_KEY_ID imported), $REPO_BASE_URL. +set -euo pipefail + +PAGES_DIR="${1:-}" +STAGING_DIR="${2:-}" +ARCH="${DISH_RPM_ARCH:-x86_64}" + +if [ -z "$PAGES_DIR" ] || [ -z "$STAGING_DIR" ]; then + echo "Usage: $0 " >&2 + exit 2 +fi +if [ -z "${GPG_KEY_ID:-}" ] || [ -z "${REPO_BASE_URL:-}" ]; then + echo "GPG_KEY_ID and REPO_BASE_URL must be set." >&2 + exit 2 +fi +if ! command -v createrepo_c >/dev/null 2>&1; then + echo "createrepo_c not found; install it (apt: createrepo-c)." >&2 + exit 2 +fi + +repo_dir="$PAGES_DIR/rpm/${ARCH}" +mkdir -p "$repo_dir" + +# ── 1. Drop new RPMs into the arch tree ───────────────────────────────────── +shopt -s nullglob +rpms=("$STAGING_DIR"/dish-*."${ARCH}".rpm) +if [ "${#rpms[@]}" -eq 0 ]; then + echo "::warning::No dish-*.${ARCH}.rpm found in $STAGING_DIR; nothing to add" +else + for rpm in "${rpms[@]}"; do + echo "+ adding $(basename "$rpm")" + cp -v "$rpm" "$repo_dir/" + done +fi + +# Regenerate metadata from scratch (cheap at our scale). +createrepo_c --update --no-database "$repo_dir" + +# ── 3. Sign repomd.xml ────────────────────────────────────────────────────── +gpg --batch --yes --default-key "$GPG_KEY_ID" --detach-sign --armor \ + --output "$repo_dir/repodata/repomd.xml.asc" \ + "$repo_dir/repodata/repomd.xml" + +# Export the public key next to repomd so dnf can fetch it. +gpg --batch --yes --armor --export "$GPG_KEY_ID" \ + > "$repo_dir/repodata/repomd.xml.key" + +# ── 4. Drop a dish.repo file users can curl into /etc/yum.repos.d/ ────────── +cat > "$PAGES_DIR/rpm/dish.repo" < + + + + + Dish Linux Packages + + + +
+

Dish Linux Packages

+

Official APT and DNF/YUM repositories for the + Dish gamepad streamer.

+ +

Once you've added the repository, the package manager you already use + pulls updates automatically. Dish never installs its own updates; it only + tells you one exists, and apt upgrade and dnf upgrade + do the installing.

+ +

Debian 13+ and derivatives

+

The .deb needs the distro's Qt to be at least 6.7, which Debian 13 + ships. On Ubuntu 24.04 LTS and anything older, skip to the AppImage or + Flatpak below.

+
curl -fsSL https://tinkernorth.github.io/dish-linux/gpg.key \
+  | sudo gpg --dearmor -o /usr/share/keyrings/dish-archive-keyring.gpg
+
+echo "deb [signed-by=/usr/share/keyrings/dish-archive-keyring.gpg] \
+  https://tinkernorth.github.io/dish-linux/debian stable main" \
+  | sudo tee /etc/apt/sources.list.d/dish.list
+
+sudo apt update
+sudo apt install dish
+ +

Fedora / RHEL / Rocky / Alma / openSUSE

+
sudo curl -fsSL -o /etc/yum.repos.d/dish.repo \
+  https://tinkernorth.github.io/dish-linux/rpm/dish.repo
+
+sudo dnf install dish
+ +

Arch Linux

+

Add the signed pacman repository once and pacman -Syu owns + updates from then on; no AUR helper needed:

+
curl -fsSL https://tinkernorth.github.io/dish-linux/gpg.key | sudo pacman-key --add -
+sudo pacman-key --lsign-key 96FFAACB78FE75D18CEEE332C398179512D6BDF3
+
+printf '\n[dish]\nSigLevel = Required DatabaseOptional\nServer = https://tinkernorth.github.io/dish-linux/arch/$arch\n' \
+  | sudo tee -a /etc/pacman.conf
+
+sudo pacman -Syu dish-bin
+

Prefer building it yourself? The + PKGBUILD + is in the repo for makepkg -si. (An AUR listing is planned for + when AUR account registration reopens.)

+ +

Ubuntu LTS, older distros, no package manager

+

Every release ships a + portable AppImage (self-updating via AppImageUpdate/zsync) and a Flatpak + bundle:

+
chmod +x Dish-X.Y.Z-x86_64.AppImage
+./Dish-X.Y.Z-x86_64.AppImage
+

USB-direct claiming needs a udev rule no AppImage or Flatpak can install; + without it Dish keeps the pad streaming on the SDL path, just rate-capped. + The app shows the one-time fix when it applies, and + docs/PACKAGING.md + documents it.

+ +

Verifying the signing key

+

The repository signing key fingerprint is + 96FFAACB78FE75D18CEEE332C398179512D6BDF3, published here: + gpg.key. Diff against the fingerprint pinned in the + repo README + if you want to audit the chain of trust before adding the repo.

+ +

Every published .deb and .rpm is also covered + by the SLSA L3 provenance attestation and cosign signature attached + to the matching GitHub Release. See + SECURITY.md + for the verification recipe.

+ +
+

Source for this site: packaging/repo/. + Generated from release.yml on every tag push.

+
+
+ + diff --git a/scripts/build-appimage.sh b/scripts/build-appimage.sh index 0876019..3c758b1 100755 --- a/scripts/build-appimage.sh +++ b/scripts/build-appimage.sh @@ -91,9 +91,26 @@ install -Dm644 packaging/com.tinkernorth.Dish.metainfo.xml \ "${appdir}/usr/share/metainfo/com.tinkernorth.Dish.metainfo.xml" out="${dist_dir}/Dish-${version}-${arch}.AppImage" -rm -f "${out}" +rm -f "${out}" "${out}.zsync" + +# Embed AppImageUpdate metadata so AppImageUpdate / Gear Lever can delta-update +# straight off the newest GitHub release instead of a full manual re-download. +# appimagetool also emits the matching .zsync index, which release.yml uploads +# beside the AppImage; the draft-then-flip publish keeps `releases/latest` atomic, +# so the pattern never resolves to a half-uploaded release. +export LDAI_UPDATE_INFORMATION="${DISH_APPIMAGE_UPDATE_INFO:-gh-releases-zsync|TinkerNorth|dish-linux|latest|Dish-*-${arch}.AppImage.zsync}" OUTPUT="${out}" "${tools_dir}/linuxdeploy" --appdir "${appdir}" --output appimage +# appimagetool drops the .zsync next to the AppImage or in the CWD depending +# on version; normalise into dist/ and fail soft (the AppImage itself is fine +# without it, the delta channel just stays dark). +if [ ! -f "${out}.zsync" ] && [ -f "$(basename "${out}").zsync" ]; then + mv "$(basename "${out}").zsync" "${out}.zsync" +fi +if [ ! -f "${out}.zsync" ]; then + echo "::warning::appimagetool emitted no .zsync; AppImageUpdate delta updates unavailable for this build" +fi + # A Qt Quick bundle missing one QML module builds perfectly and fails on the # user's machine. Offscreen catches that here instead. echo "==> Smoke test" diff --git a/src/source/store/UpdatePreferenceStore.h b/src/source/store/UpdatePreferenceStore.h index 174e814..4b1768b 100644 --- a/src/source/store/UpdatePreferenceStore.h +++ b/src/source/store/UpdatePreferenceStore.h @@ -13,6 +13,7 @@ #include "architecture/StateSource.h" +#include #include #include @@ -73,9 +74,21 @@ class UpdatePreferenceStore : public arch::StateSource { } private: + // A Flatpak install updates through the store that delivered it (Flathub + // rebuilds every release), so the notify-only pill would announce a version + // the store is about to hand over anyway. Inside the sandbox the check + // therefore defaults OFF; the Settings toggle still works, and a flip + // persists like any other preference (the sandbox has its own config under + // ~/.var/app/, so this default never leaks into a host install). + static bool runningInFlatpak() { + return qEnvironmentVariableIsSet("FLATPAK_ID") || + QFile::exists(QStringLiteral("/.flatpak-info")); + } + static UpdatePreferences readInitial(QSettings& settings) { UpdatePreferences initial; - initial.checksEnabled = settings.value(QLatin1String(kKeyChecksEnabled), true).toBool(); + initial.checksEnabled = + settings.value(QLatin1String(kKeyChecksEnabled), !runningInFlatpak()).toBool(); initial.skippedVersion = settings.value(QLatin1String(kKeySkippedVersion), QString()).toString(); return initial;