# Build the native punktfunk Linux CLIENT as a single-file Flatpak bundle and publish it to
# Gitea's GENERIC package registry, so the Steam Deck (and any flatpak distro) installs it
# the SteamOS-native, update-survivable way: `flatpak install --user <downloaded>.flatpak`.
# (The HOST stays an RPM/deb — it needs unsandboxed /dev/uinput + zero-copy NVENC; only the
# CLIENT is sandbox-friendly. See packaging/README.md and packaging/flatpak/README.md.)
#
# Gitea has NO flatpak/ostree registry, so the bundle lives in the generic registry:
#   PUT  https://git.unom.io/api/packages/unom/generic/punktfunk-client-flatpak/<version>/<file>
#   GET  https://git.unom.io/api/packages/unom/generic/punktfunk-client-flatpak/<version>/<file>
# On tags the bundle is ALSO attached to the Gitea release (mirrors release.yml's DMG).
#
# PRIVILEGED-BUILD CONSTRAINT: flatpak-builder runs bubblewrap, which needs user namespaces.
# In a Gitea/act_runner Docker executor that means the job container must be --privileged
# (the same runner already runs `docker build` in docker.yml, so its Docker daemon allows it).
# If your runner CANNOT grant --privileged, this job will fail at `flatpak-builder` with
# "Creating new namespace failed: Operation not permitted" — see the fallback in
# packaging/flatpak/README.md (build on the Deck via org.flatpak.Builder, or on a Linux box,
# then upload with the curl line below).
#
# REGISTRY_TOKEN: repo Actions secret, a PAT with write:package scope (shared with deb/rpm/docker).
name: flatpak

on:
  push:
    branches: [main]
    # The flatpak is the CLIENT — only rebuild when the client/core/manifest change, not on every
    # design/host push (this is a heavy flatpak-builder run). Tags (v*, the client release) build too.
    # The bundle ships BOTH client binaries (shell + Vulkan session), so every crate in either
    # binary's dependency closure must be listed here.
    paths:
      - 'clients/linux/**'
      - 'clients/session/**'
      - 'crates/punktfunk-core/**'
      - 'crates/pf-client-core/**'
      - 'crates/pf-presenter/**'
      - 'crates/pf-console-ui/**'
      - 'packaging/flatpak/**'
      - 'Cargo.lock'
      - '.gitea/workflows/flatpak.yml'
    tags: ['v*']
  workflow_dispatch:

env:
  REGISTRY: git.unom.io
  OWNER: unom
  APP_ID: io.unom.Punktfunk
  MANIFEST: packaging/flatpak/io.unom.Punktfunk.yml
  PACKAGE: punktfunk-client-flatpak     # generic-registry package name
  REPO_URL: https://flatpak.unom.io   # shared unom OSTree repo (reusable across unom apps)
  DEPLOY_DIR: unom-flatpak             # ~/<dir> on unom-1 (compose + ./site tree)

jobs:
  build-publish:
    runs-on: ubuntu-24.04
    timeout-minutes: 120
    container:
      # Fedora ships a recent flatpak + flatpak-builder + the kernel userns support.
      # --privileged is required for bubblewrap inside the Docker executor (see header).
      image: fedora:43
      options: --privileged
    steps:
      # DNS fix — MUST run before any network step. fedora:43's nsswitch.conf is
      # `hosts: files myhostname resolve [!UNAVAIL=return] dns`: the `resolve`
      # module is nss-resolve (systemd-resolved), which isn't running in a CI
      # container. glibc getaddrinfo (git, curl, dnf) mostly falls through to the
      # `dns` module -> Docker's embedded 127.0.0.11 -> works. flatpak/ostree's
      # resolver does NOT fall through: the absent-daemon socket connect trips
      # `[!UNAVAIL=return]` and it reports "[6] Could not resolve hostname". This
      # was masked as an intermittent "busy runner drops DNS" and papered over
      # with retry.sh — but it's deterministic on a runner where the resolve
      # module tips that way (surfaced when jobs began landing on home-runner-2).
      # Drop the `resolve` entry so host lookups use plain `dns`. NOTE: this alone is not
      # sufficient — the Tooling step's dnf install pulls a systemd package upgrade whose RPM
      # trigger re-runs authselect and regenerates this file, undoing the fix. It's reapplied
      # there, right before the first `flatpak` network call.
      - name: Fix container DNS (drop nss-resolve)
        run: |
          sed -i 's/resolve \[!UNAVAIL=return\] //' /etc/nsswitch.conf
          # History: this step used to ALSO force glibc onto TCP DNS (`options use-vc`) because
          # the runner fleet's Docker embedded resolver dropped UDP lookups under parallel-job
          # load (investigated 2026-07-11; v0.15.0/v0.16.0 each burned retry.sh's whole budget).
          # That root cause is now fixed at the infra level (2026-07-22): the runner host runs a
          # local dnsmasq cache on the docker bridge and daemon.json points every job container
          # at it, so lookups terminate on-box instead of crossing the saturated uplink — the
          # UDP path is reliable again. The TCP path through the same chain proved FLAKY under
          # fleet concurrency (flatpak remote-add failed 10/10 with instant NXDOMAIN while dnf
          # in the same container resolved fine), so `use-vc` flipped from mitigation to sole
          # cause of this leg's failures — removed. retry.sh (10×) stays as the backstop for
          # genuine upstream blips.
          cat /etc/resolv.conf || true

      # fedora:43 has no node, but actions/checkout (a JS action) needs it. A plain `run:` step
      # executes via the container shell (no node needed), so install node BEFORE checkout.
      - name: node for the JS actions
        run: dnf -y install nodejs

      - uses: actions/checkout@v4

      - name: Tooling
        run: |
          # flatpak-cargo-generator.py (master) needs aiohttp + tomlkit (NOT the old `toml`).
          # gnupg2/rsync/openssh-clients: sign the OSTree repo + rsync it to unom-1 (see the deploy step).
          dnf -y install flatpak flatpak-builder git python3 python3-aiohttp python3-tomlkit curl jq \
            gnupg2 rsync openssh-clients
          # Belt-and-suspenders: keep nsswitch on plain `dns` even if this dnf transaction pulled
          # in a fresh systemd-resolved (it does — flatpak recommends xdg-desktop-portal ->
          # pipewire/wireplumber -> systemd-networkd/-resolved). Verified on the real runner
          # (2026-07-11) this dnf install does NOT actually rewrite /etc/nsswitch.conf — no
          # authselect trigger fires — so this line alone was never the fix for the failures
          # below. See the retry.sh bump for the real cause.
          sed -i 's/resolve \[!UNAVAIL=return\] //' /etc/nsswitch.conf
          # Flathub provides the GNOME runtime/SDK + the rust-stable + ffmpeg-full extensions.
          #
          # ROOT CAUSE (confirmed 2026-07-11 by watching a live run on home-runner-1): this is
          # NOT a deterministic nsswitch/DNS-config bug. gitea-runner-fleet on home-runner-1 is
          # a SHARED, resource-capped fleet (3 replicas, --cpus 5 --memory 7g each, on a 16c/24G
          # box) serving punktfunk AND several other orgs' repos (chatwoot, website, cms, data,
          # infra, tempblade, played...). A push to punktfunk's main fans out ~8 workflows at
          # once, and this runner box's Docker embedded DNS resolver (127.0.0.11) genuinely
          # drops UDP lookups under that concurrent load — exactly what retry.sh's own header
          # comment already documented. Reproduced: manually dispatching this job when the box
          # was idle (1 container running) succeeded immediately, with or without the sed above.
          # 5 attempts (~100s total) isn't always enough to outlast a synchronized multi-org
          # burst, so bump the bootstrap fetch's budget — never fail the job on a single-shot
          # fetch. Same treatment for every network command below.
          bash scripts/ci/retry.sh 10 flatpak remote-add --user --if-not-exists flathub \
            https://dl.flathub.org/repo/flathub.flatpakrepo
          git config --global --add safe.directory "$PWD"

      - name: Version + channel
        # Tag vX.Y.Z -> X.Y.Z on the OSTree `stable` branch (a real release); a main push ->
        # <next-minor>-ciN.g<sha> on the `canary` branch (base one minor ahead of the latest stable
        # tag via scripts/ci/pf-version.sh). The two branches live side-by-side in one repo
        # (rsync runs without --delete), each tracked by its own .flatpakref, so `flatpak update`
        # on a stable box never jumps to a canary build. The generic-registry version string allows
        # letters/dots/hyphens.
        run: |
          eval "$(bash scripts/ci/pf-version.sh)"   # -> PF_BASE (one minor ahead of the latest stable tag)
          SHORT=$(echo "$GITHUB_SHA" | cut -c1-8)
          case "$GITHUB_REF" in
            refs/tags/v*) V="${GITHUB_REF_NAME#v}"; BRANCH=stable; ALIAS=latest ;;
            *)            V="${PF_BASE}-ci${GITHUB_RUN_NUMBER}.g${SHORT}"; BRANCH=canary; ALIAS=canary ;;
          esac
          echo "VERSION=$V" >> "$GITHUB_ENV"
          echo "BUNDLE=punktfunk-client-${V}.flatpak" >> "$GITHUB_ENV"
          echo "FLATPAK_BRANCH=$BRANCH" >> "$GITHUB_ENV"
          echo "ALIAS=$ALIAS" >> "$GITHUB_ENV"
          echo "flatpak version $V -> branch '$BRANCH' alias '$ALIAS'"

      - name: Generate offline cargo sources
        # flatpak builds with no network; vendor every crate from Cargo.lock into
        # cargo-sources.json next to the manifest (referenced by the manifest's
        # punktfunk-client module).
        #
        # Prune the microsoft/windows-rs git crates first: they belong to
        # punktfunk-client-windows, which the flatpak never builds, and leaving them in makes
        # flatpak-builder full-clone that multi-GB repo at build time → "No space left on
        # device" (see packaging/flatpak/prune-windows-lock.py). The committed Cargo.lock is
        # untouched; cargo --offline only needs sources for the crates it compiles.
        run: |
          curl -fsSL --retry 5 --retry-all-errors --retry-delay 5 -o /tmp/flatpak-cargo-generator.py \
            https://raw.githubusercontent.com/flatpak/flatpak-builder-tools/master/cargo/flatpak-cargo-generator.py
          python3 packaging/flatpak/prune-windows-lock.py Cargo.lock /tmp/Cargo.flatpak.lock
          python3 /tmp/flatpak-cargo-generator.py /tmp/Cargo.flatpak.lock \
            -o packaging/flatpak/cargo-sources.json

      - name: Seed the local OSTree repo from the live server (keep BOTH channels in the summary)
        # Each CI run builds only ONE branch (canary on main, stable on a tag). The deploy step's
        # `flatpak build-update-repo` regenerates the repo SUMMARY from whatever refs are in the
        # LOCAL repo, and the rsync publishes it (without --delete). A fresh single-branch local
        # repo therefore produces a single-branch summary that CLOBBERS the other channel on the
        # server — the exact bug that made `app/io.unom.Punktfunk/x86_64/stable` unresolvable
        # ("No such ref") after a canary main-push overwrote the post-release summary, even though
        # the stable commit's objects were still on disk. Fix: mirror the published repo DOWN first,
        # so the local repo carries every existing branch; the build below then only ADDS this run's
        # commit and the regenerated+signed summary keeps both channels. No-op on a fresh repo (first
        # publish) or when the deploy secrets aren't set (the build still produces a valid bundle).
        env:
          DEPLOY_HOST: ${{ secrets.DEPLOY_HOST }}
          DEPLOY_USER: ${{ secrets.DEPLOY_USER }}
          DEPLOY_PORT: ${{ secrets.DEPLOY_PORT }}
          DEPLOY_SSH_KEY: ${{ secrets.DEPLOY_SSH_KEY }}
        run: |
          set -euo pipefail
          if [ -z "${DEPLOY_HOST:-}" ] || [ -z "${DEPLOY_SSH_KEY:-}" ]; then
            echo "::warning::DEPLOY_* not set — no seed; building a fresh single-branch repo."
            exit 0
          fi
          install -d -m700 ~/.ssh
          printf '%s\n' "$DEPLOY_SSH_KEY" > ~/.ssh/deploy; chmod 600 ~/.ssh/deploy
          SSH="ssh -i $HOME/.ssh/deploy -p ${DEPLOY_PORT:-22} -o StrictHostKeyChecking=accept-new"
          DEST="${DEPLOY_USER}@${DEPLOY_HOST}"
          mkdir -p "$PWD/repo"
          # Pull the currently-published repo (all channels' objects + refs) into the repo the build
          # will extend. No --delete: the local repo starts empty, so this only ADDS.
          # Probe first (retried) whether a published repo exists at all: ONLY that case may
          # continue with a fresh repo. A transient network failure must FAIL the job instead —
          # a blanket `rsync || continue` here is exactly how a flaky link produces the
          # single-branch summary that clobbers the other channel (the bug described above).
          PRESENT=$(bash scripts/ci/retry.sh 5 $SSH "$DEST" \
            "[ -d $DEPLOY_DIR/site/repo/refs ] && echo present || echo absent")
          if [ "$PRESENT" = present ]; then
            bash scripts/ci/retry.sh 5 rsync -az --info=stats1 -e "$SSH" \
              "$DEST:$DEPLOY_DIR/site/repo/" "$PWD/repo/"
          else
            echo "::warning::no published repo on the server (first publish) — continuing fresh"
          fi
          echo "seeded refs:"; ls "$PWD/repo/refs/heads/app/$APP_ID/x86_64/" 2>/dev/null || echo "  (none)"

      - name: Prefetch deps + sources (retried — the network phase, split off the build)
        run: |
          # All of the job's heavy network I/O happens HERE, retried, so a dropped DNS lookup
          # or TCP dial costs a backoff-retry instead of the whole (long) compile:
          #   1) --install-deps-only pulls everything the manifest declares from Flathub: the
          #      GNOME 50 runtime/SDK + the rust-stable (//25.08, rustc 1.96) and llvm20 SDK
          #      extensions, plus the runtime's auto codecs-extra (HEVC libavcodec).
          #   2) --download-only fetches every source (all crates in cargo-sources.json) into
          #      the .flatpak-builder state dir. Both are resumable/idempotent, so re-running
          #      after a partial failure is safe and cheap.
          # --disable-rofiles-fuse is the container-safe path (no FUSE).
          # 10 attempts (~9min budget), matching the remote-add bootstrap above — same shared,
          # load-sensitive runner, same flathub.org resolution path.
          bash scripts/ci/retry.sh 10 flatpak-builder --user --force-clean --disable-rofiles-fuse \
            --install-deps-from=flathub --install-deps-only \
            "$PWD/build-dir" "$MANIFEST"
          bash scripts/ci/retry.sh 10 flatpak-builder --user --force-clean --disable-rofiles-fuse \
            --download-only \
            "$PWD/build-dir" "$MANIFEST"

      - name: Build the flatpak (offline — deps + sources prefetched above)
        run: |
          # Everything is already local (state dir warmed by the prefetch step), so this long
          # step needs no network; --install-deps-from stays as a no-op safety net.
          # --default-branch=$FLATPAK_BRANCH pins the ref to app/io.unom.Punktfunk/x86_64/<branch>
          # (canary or stable) so the matching hosted .flatpakref resolves deterministically
          # (manifest sets no branch).
          flatpak-builder --user --force-clean --disable-rofiles-fuse \
            --default-branch="$FLATPAK_BRANCH" \
            --install-deps-from=flathub \
            --repo="$PWD/repo" \
            "$PWD/build-dir" "$MANIFEST"

      - name: Export single-file bundle
        run: |
          # Branch must be passed explicitly (matches --default-branch above); build-bundle
          # otherwise defaults to `master` and errors "Refspec … not found".
          flatpak build-bundle "$PWD/repo" "$BUNDLE" "$APP_ID" "$FLATPAK_BRANCH"
          ls -lh "$BUNDLE"

      - name: Publish to the Gitea generic registry
        env:
          TOKEN: ${{ secrets.REGISTRY_TOKEN }}
        run: |
          BASE="https://$REGISTRY/api/packages/$OWNER/generic/$PACKAGE"
          # 1) Versioned URL. A re-tagged release re-fires this workflow and the registry 409s on
          #    duplicate uploads — delete any prior copy first (404 on the first publish is fine).
          curl -fsS -o /dev/null --user "enricobuehler:$TOKEN" -X DELETE \
            "$BASE/$VERSION/$BUNDLE" || true
          curl -fsS --user "enricobuehler:$TOKEN" --upload-file "$BUNDLE" \
            "$BASE/$VERSION/$BUNDLE"
          echo "published $BASE/$VERSION/$BUNDLE"
          # 2) Channel alias (stable release -> latest/, canary main build -> canary/) for the
          #    Decky fallback + scripts. The generic registry rejects re-uploading an existing
          #    version/file (409), so delete the prior alias file first (ignore 404 on run #1).
          curl -fsS -o /dev/null --user "enricobuehler:$TOKEN" -X DELETE \
            "$BASE/$ALIAS/punktfunk-client.flatpak" || true
          curl -fsS --user "enricobuehler:$TOKEN" --upload-file "$BUNDLE" \
            "$BASE/$ALIAS/punktfunk-client.flatpak"
          echo "published $BASE/$ALIAS/punktfunk-client.flatpak"

      # Sign the OSTree repo flatpak-builder already produced and publish it to flatpak.unom.io on
      # unom-1, so users get `flatpak update` (the single-file bundle above has no remote). Mirrors
      # docker.yml's deploy-docs (DEPLOY_* = the unom-ci-deploy key). No-ops cleanly until the GPG
      # secret + DEPLOY_* exist, so the bundle build stays green during setup.
      - name: Sign + deploy the OSTree repo to unom-1 (flatpak.unom.io)
        env:
          FLATPAK_GPG_PRIVATE_KEY: ${{ secrets.FLATPAK_GPG_PRIVATE_KEY }}
          DEPLOY_HOST: ${{ secrets.DEPLOY_HOST }}
          DEPLOY_USER: ${{ secrets.DEPLOY_USER }}
          DEPLOY_PORT: ${{ secrets.DEPLOY_PORT }}
          DEPLOY_SSH_KEY: ${{ secrets.DEPLOY_SSH_KEY }}
        run: |
          set -euo pipefail
          if [ -z "${FLATPAK_GPG_PRIVATE_KEY:-}" ] || [ -z "${DEPLOY_HOST:-}" ]; then
            echo "::warning::FLATPAK_GPG_PRIVATE_KEY/DEPLOY_* not set — skipping repo deploy (bundle still published)."
            exit 0
          fi
          # 1) Import the signing key into a throwaway keyring; sign the repo.
          export GNUPGHOME="$(mktemp -d)"; chmod 700 "$GNUPGHOME"
          echo "$FLATPAK_GPG_PRIVATE_KEY" | base64 -d | gpg --batch --import
          KEYID="$(gpg --list-keys --with-colons | awk -F: '/^fpr:/{print $10; exit}')"
          # build-sign signs the COMMIT objects; build-update-repo signs the SUMMARY. Both are
          # required — clients with gpg-verify=true verify the commit, so summary-only signing
          # fails the pull with "GPG verification enabled, but no signatures found".
          flatpak build-sign "$PWD/repo" "$APP_ID" "$FLATPAK_BRANCH" \
            --gpg-sign="$KEYID" --gpg-homedir="$GNUPGHOME"
          flatpak build-update-repo --generate-static-deltas \
            --gpg-sign="$KEYID" --gpg-homedir="$GNUPGHOME" "$PWD/repo"
          # The regenerated summary advertises exactly these refs — must include EVERY channel that
          # has ever published (the seed step ensures the other channel's commit is present). If this
          # ever shows only one branch on a repo that had two, the seed didn't run — investigate.
          echo "published summary advertises:"; ls "$PWD/repo/refs/heads/app/$APP_ID/x86_64/" 2>/dev/null || echo "  (none)"
          # 2) Build the install descriptors (GPGKey = the committed public key, base64).
          GPGKEY="$(base64 -w0 packaging/flatpak/unom-flatpak.gpg)"
          rm -rf site && mkdir -p site
          cat > site/unom.flatpakrepo <<EOF
          [Flatpak Repo]
          Title=unom
          Url=$REPO_URL/repo/
          Homepage=https://punktfunk.unom.io
          Comment=unom Flatpak applications
          GPGKey=$GPGKEY
          EOF
          # Two refs, one per channel. Both descriptor files are regenerated every run and rsync'd
          # without --delete; the repo SUMMARY carries both branches because the build was seeded
          # from the live repo above (so build-update-repo below re-signs a summary listing every
          # published channel, not just this run's). The stable ref resolves for good once any
          # release has built the `stable` branch. A box installs ONE; `flatpak update` then tracks
          # that channel's branch.
          write_ref() {  # <filename> <branch> <title>
            cat > "site/$1" <<EOF
          [Flatpak Ref]
          Name=$APP_ID
          Branch=$2
          Url=$REPO_URL/repo/
          Title=$3
          Homepage=https://punktfunk.unom.io
          IsRuntime=false
          GPGKey=$GPGKEY
          RuntimeRepo=https://dl.flathub.org/repo/flathub.flatpakrepo
          EOF
          }
          write_ref "${APP_ID}.flatpakref"        stable "Punktfunk"
          write_ref "${APP_ID}.Canary.flatpakref" canary "Punktfunk (Canary)"
          cat > site/index.html <<EOF
          <!doctype html><meta charset=utf-8><title>unom flatpak repo</title>
          <h1>unom Flatpak repository</h1>
          <p>Install the Punktfunk Linux client (auto-adds Flathub for the GNOME runtime, then tracks updates).</p>
          <p><b>Stable</b> (recommended — only moves on releases):</p>
          <pre>flatpak install --user $REPO_URL/${APP_ID}.flatpakref
          flatpak run $APP_ID</pre>
          <p><b>Canary</b> (latest main build, unstable):</p>
          <pre>flatpak install --user $REPO_URL/${APP_ID}.Canary.flatpakref</pre>
          <p>Or add the whole remote: <code>flatpak remote-add --user --if-not-exists unom $REPO_URL/unom.flatpakrepo</code></p>
          EOF
          # 3) Ship to unom-1 and (re)start the static server. rsync WITHOUT --delete keeps old
          #    objects so clients mid-update aren't broken; the fresh signed summary advertises latest.
          install -d -m700 ~/.ssh
          printf '%s\n' "$DEPLOY_SSH_KEY" > ~/.ssh/deploy; chmod 600 ~/.ssh/deploy
          SSH="ssh -i $HOME/.ssh/deploy -p ${DEPLOY_PORT:-22} -o StrictHostKeyChecking=accept-new"
          DEST="${DEPLOY_USER}@${DEPLOY_HOST}"
          # All idempotent — retried because the runner's link to unom-1 drops TCP dials under
          # load (the same flake that hits docker.yml's deploy-docs with "dial tcp: i/o timeout").
          bash scripts/ci/retry.sh 5 $SSH "$DEST" "mkdir -p ~/$DEPLOY_DIR/site/repo"
          bash scripts/ci/retry.sh 5 rsync -az --info=stats1 -e "$SSH" repo/ "$DEST:$DEPLOY_DIR/site/repo/"
          bash scripts/ci/retry.sh 5 rsync -az -e "$SSH" site/unom.flatpakrepo "site/${APP_ID}.flatpakref" "site/${APP_ID}.Canary.flatpakref" site/index.html "$DEST:$DEPLOY_DIR/site/"
          bash scripts/ci/retry.sh 5 rsync -az -e "$SSH" packaging/flatpak/server/compose.production.yml packaging/flatpak/server/Caddyfile "$DEST:$DEPLOY_DIR/"
          bash scripts/ci/retry.sh 5 $SSH "$DEST" "cd ~/$DEPLOY_DIR && docker compose -f compose.production.yml up -d"
          echo "deployed → $REPO_URL/${APP_ID}.flatpakref"

      - name: Attach bundle to the Gitea release (stable tags only)
        if: startsWith(gitea.ref, 'refs/tags/v')
        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" "$BUNDLE"
