name: partner-edge-release

on:
  push:
    tags:
      - 'v*'
  # Manual escape hatch: re-trigger image build/publish for any existing
  # tag without having to delete+re-push it. Why we need this: the
  # release-please action creates the tag via GITHUB_TOKEN which by
  # GitHub anti-recursion design does NOT trigger downstream workflows
  # (push events authored by GITHUB_TOKEN are filtered out). Result: a
  # release-please-merged version-bump leaves a tag with no image
  # built. Until release-please is configured with a Personal Access
  # Token, this workflow_dispatch is the operator-facing escape hatch.
  # Tag form: vX.Y.Z (no component prefix — single package repo).
  workflow_dispatch:
    inputs:
      tag:
        description: 'Existing tag to re-publish (e.g. v0.12.60)'
        required: true
        type: string

permissions:
  contents: write   # upload release assets
  packages: write   # push to GHCR

env:
  REGISTRY: ghcr.io
  OWNER: ${{ github.repository_owner }}

jobs:
  lint-scripts:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          ref: ${{ inputs.tag || github.ref }}
      - name: shellcheck
        run: |
          sudo apt-get update -qq
          sudo apt-get install -y -qq shellcheck
          shopt -s nullglob
          for f in *.sh; do
            echo ">> $f"
            shellcheck -x -S warning "$f"
          done
      - name: "Phase 6: no stray install-firewall.sh sourcing refs in lib/*.sh"
        # Guards the naming reclassification (install-firewall.sh -> firewall-lib.sh):
        # lib/install-firewall.sh itself stays as the frozen back-compat duplicate
        # (excluded below); every OTHER lib/*.sh must source the new firewall-lib.sh
        # name, not the old one. Two patterns: a quoted path ref ("...install-firewall.sh")
        # and a shellcheck source= directive — both are how this repo's other lib
        # files' lazy-source forwarders reference a sibling lib (see
        # lib/reconcile.sh's healthcheck-lib.sh precedent). Plain prose comments that
        # merely mention the old filename (e.g. describing upgrade.sh's own unmodified
        # staging call) do not match either pattern and are not flagged.
        run: |
          set -euo pipefail
          stray=0
          for f in lib/*.sh; do
            [[ "$f" == "lib/install-firewall.sh" ]] && continue
            if grep -nE 'install-firewall\.sh"|shellcheck source=.*install-firewall\.sh$' "$f"; then
              echo "::error file=$f::stray install-firewall.sh source reference — use lib/firewall-lib.sh"
              stray=1
            fi
          done
          exit "$stray"

  extract-version:
    needs: lint-scripts
    runs-on: ubuntu-latest
    outputs:
      version: ${{ steps.ver.outputs.version }}
      tag: ${{ steps.ver.outputs.tag }}
    steps:
      - uses: actions/checkout@v4
        with:
          ref: ${{ inputs.tag || github.ref }}
      - id: ver
        env:
          GITHUB_REF_NAME: ${{ github.ref_name }}
          INPUT_TAG: ${{ inputs.tag }}
        run: |
          # Push event → ref_name is the tag. workflow_dispatch → use input tag.
          tag="${INPUT_TAG:-$GITHUB_REF_NAME}"
          if [[ ! "$tag" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
            echo "::error::tag '$tag' does not match vX.Y.Z"
            exit 1
          fi
          version="${tag#v}"
          echo "version=$version" >> "$GITHUB_OUTPUT"
          echo "tag=$tag"         >> "$GITHUB_OUTPUT"
      - name: Verify VERSION file matches tag
        env:
          EXPECTED_VERSION: ${{ steps.ver.outputs.version }}
        run: |
          # Strip the release-please marker comment — VERSION is
          # "X.Y.Z  # x-release-please-version" so release-please can update it
          # automatically (see release-please-config.json extra-files generic).
          file_version=$(awk '{print $1}' VERSION)
          if [[ "$file_version" != "$EXPECTED_VERSION" ]]; then
            echo "::error::VERSION file ($file_version) != tag version ($EXPECTED_VERSION)"
            exit 1
          fi

  # Per-arch build matrix. ubuntu-24.04-arm runs arm64 NATIVELY (no QEMU),
  # cutting SFU wall time from ~60 min (QEMU emulated) to ~20 min. The
  # two arches run in parallel; total time = max(amd64, arm64) instead
  # of the old sequential amd64 + arm64 inside one buildx call. A
  # merge-manifests job below fuses the per-arch tags into the final
  # multi-arch manifest that clients pull.
  build-and-push:
    needs: extract-version
    strategy:
      fail-fast: true
      matrix:
        component: [caddy, xray, coturn, sfu]
        arch:
          - platform: linux/amd64
            runner: ubuntu-latest
            tag: amd64
          - platform: linux/arm64
            runner: ubuntu-24.04-arm
            tag: arm64
    runs-on: ${{ matrix.arch.runner }}
    steps:
      - uses: actions/checkout@v4
        with:
          ref: ${{ inputs.tag || github.ref }}

      - uses: dtolnay/rust-toolchain@stable
        if: matrix.component == 'sfu'

      - uses: Swatinem/rust-cache@v2
        if: matrix.component == 'sfu'
        with:
          workdir: crates/sfu
          shared-key: "release"  # separate from CI key to avoid cross-contamination

      - uses: docker/setup-buildx-action@v3

      - uses: docker/login-action@v3
        with:
          registry: ${{ env.REGISTRY }}
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Build + push ${{ matrix.component }} (${{ matrix.arch.tag }})
        uses: docker/build-push-action@v6
        with:
          context: ${{ matrix.component == 'sfu' && '.' || 'images' }}
          file: images/Dockerfile.${{ matrix.component }}
          platforms: ${{ matrix.arch.platform }}
          push: true
          build-args: |
            VERSION=${{ needs.extract-version.outputs.version }}
          # Per-arch tag only; merge-manifests produces the final
          # version / latest / sha tags that clients consume.
          tags: |
            ${{ env.REGISTRY }}/${{ env.OWNER }}/partner-edge-${{ matrix.component }}:v${{ needs.extract-version.outputs.version }}-${{ matrix.arch.tag }}
          cache-from: type=gha,scope=partner-edge-${{ matrix.component }}-${{ matrix.arch.tag }}
          cache-to:   type=gha,scope=partner-edge-${{ matrix.component }}-${{ matrix.arch.tag }},mode=max

  # Fuse per-arch tags into a single multi-arch manifest per component.
  # Clients pulling e.g. `partner-edge-sfu:v0.10.0` get the right arch
  # selected automatically (amd64 on relay-x+edge-c, arm64 on hub).
  merge-manifests:
    needs: [extract-version, build-and-push]
    runs-on: ubuntu-latest
    strategy:
      matrix:
        component: [caddy, xray, coturn, sfu]
    steps:
      - uses: docker/login-action@v3
        with:
          registry: ${{ env.REGISTRY }}
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Create multi-arch manifest for ${{ matrix.component }}
        env:
          IMG: ${{ env.REGISTRY }}/${{ env.OWNER }}/partner-edge-${{ matrix.component }}
          V: ${{ needs.extract-version.outputs.version }}
          SHA: ${{ github.sha }}
        run: |
          for tag in "v$V" latest "sha-$SHA"; do
            docker buildx imagetools create -t "$IMG:$tag" \
              "$IMG:v$V-amd64" "$IMG:v$V-arm64"
          done


  # NaiveProxy client image — separate job (not in build-and-push matrix)
  # because Dockerfile.naive lives at repo root, not images/.
  build-and-push-naive:
    needs: extract-version
    strategy:
      fail-fast: true
      matrix:
        arch:
          - platform: linux/amd64
            runner: ubuntu-latest
            tag: amd64
          - platform: linux/arm64
            runner: ubuntu-24.04-arm
            tag: arm64
    runs-on: ${{ matrix.arch.runner }}
    steps:
      - uses: actions/checkout@v4
        with:
          ref: ${{ inputs.tag || github.ref }}

      - uses: docker/setup-buildx-action@v3

      - uses: docker/login-action@v3
        with:
          registry: ${{ env.REGISTRY }}
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Build + push naive (${{ matrix.arch.tag }})
        uses: docker/build-push-action@v6
        with:
          context: .
          file: Dockerfile.naive
          platforms: ${{ matrix.arch.platform }}
          push: true
          tags: |
            ${{ env.REGISTRY }}/${{ env.OWNER }}/partner-edge-naive:v${{ needs.extract-version.outputs.version }}-${{ matrix.arch.tag }}
          cache-from: type=gha,scope=partner-edge-naive-${{ matrix.arch.tag }}
          cache-to:   type=gha,scope=partner-edge-naive-${{ matrix.arch.tag }},mode=max

  merge-manifests-naive:
    needs: [extract-version, build-and-push-naive]
    runs-on: ubuntu-latest
    steps:
      - uses: docker/login-action@v3
        with:
          registry: ${{ env.REGISTRY }}
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Create multi-arch manifest for naive
        env:
          IMG: ${{ env.REGISTRY }}/${{ env.OWNER }}/partner-edge-naive
          V: ${{ needs.extract-version.outputs.version }}
          SHA: ${{ github.sha }}
        run: |
          for tag in "v$V" latest "sha-$SHA"; do
            docker buildx imagetools create -t "$IMG:$tag" \
              "$IMG:v$V-amd64" "$IMG:v$V-arm64"
          done

  build-partner-cli:
    needs: extract-version
    strategy:
      fail-fast: true
      matrix:
        arch:
          - runner: ubuntu-latest
            target: x86_64-unknown-linux-musl
            asset: partner-cli-amd64
          - runner: ubuntu-24.04-arm
            target: aarch64-unknown-linux-musl
            asset: partner-cli-arm64
    runs-on: ${{ matrix.arch.runner }}
    steps:
      - uses: actions/checkout@v4
        with:
          ref: ${{ inputs.tag || github.ref }}

      - uses: dtolnay/rust-toolchain@stable
        with:
          targets: ${{ matrix.arch.target }}

      - uses: Swatinem/rust-cache@v2
        with:
          workdir: crates/partner-cli
          shared-key: "partner-cli-${{ matrix.arch.asset }}"

      - name: Install musl toolchain
        run: |
          sudo apt-get update -qq
          sudo apt-get install -y -qq musl-tools

      - name: Build partner-cli (${{ matrix.arch.asset }})
        run: |
          cargo build -p partner-cli --release \
            --target ${{ matrix.arch.target }}
          cp target/${{ matrix.arch.target }}/release/partner-cli \
            ${{ matrix.arch.asset }}

      - uses: actions/upload-artifact@v4
        with:
          name: ${{ matrix.arch.asset }}
          path: ${{ matrix.arch.asset }}
          retention-days: 1

  build-opec:
    needs: extract-version
    strategy:
      fail-fast: true
      matrix:
        arch:
          - runner: ubuntu-latest
            target: x86_64-unknown-linux-musl
            asset: opec-amd64
          - runner: ubuntu-24.04-arm
            target: aarch64-unknown-linux-musl
            asset: opec-arm64
    runs-on: ${{ matrix.arch.runner }}
    steps:
      - uses: actions/checkout@v4
        with:
          ref: ${{ inputs.tag || github.ref }}

      - uses: dtolnay/rust-toolchain@stable
        with:
          targets: ${{ matrix.arch.target }}

      - uses: Swatinem/rust-cache@v2
        with:
          workdir: crates/opec
          shared-key: "opec-${{ matrix.arch.asset }}"

      - name: Install musl toolchain
        run: |
          sudo apt-get update -qq
          sudo apt-get install -y -qq musl-tools

      - name: Build opec (${{ matrix.arch.asset }})
        run: |
          cargo build -p opec --release \
            --target ${{ matrix.arch.target }}
          cp target/${{ matrix.arch.target }}/release/opec \
            ${{ matrix.arch.asset }}

      - uses: actions/upload-artifact@v4
        with:
          name: ${{ matrix.arch.asset }}
          path: ${{ matrix.arch.asset }}
          retention-days: 1

  build-awg-params-agent:
    needs: extract-version
    strategy:
      fail-fast: true
      matrix:
        arch:
          - runner: ubuntu-latest
            target: x86_64-unknown-linux-musl
            asset: oxpulse-awg-params-agent-amd64
          - runner: ubuntu-24.04-arm
            target: aarch64-unknown-linux-musl
            asset: oxpulse-awg-params-agent-arm64
    runs-on: ${{ matrix.arch.runner }}
    steps:
      - uses: actions/checkout@v4
        with:
          ref: ${{ inputs.tag || github.ref }}

      - uses: dtolnay/rust-toolchain@stable
        with:
          targets: ${{ matrix.arch.target }}

      - uses: Swatinem/rust-cache@v2
        with:
          workdir: crates/awg-params-agent
          shared-key: "awg-params-agent-${{ matrix.arch.asset }}"

      - name: Install musl toolchain
        run: |
          sudo apt-get update -qq
          sudo apt-get install -y -qq musl-tools

      - name: Build awg-params-agent (${{ matrix.arch.asset }})
        run: |
          cargo build -p awg-params-agent --release \
            --target ${{ matrix.arch.target }}
          cp target/${{ matrix.arch.target }}/release/oxpulse-awg-params-agent \
            ${{ matrix.arch.asset }}

      - uses: actions/upload-artifact@v4
        with:
          name: ${{ matrix.arch.asset }}
          path: ${{ matrix.arch.asset }}
          retention-days: 1

  attach-release-assets:
    needs: [extract-version, build-and-push, merge-manifests, build-partner-cli, build-opec, build-awg-params-agent]
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          ref: ${{ inputs.tag || github.ref }}

      - uses: actions/download-artifact@v4
        with:
          name: partner-cli-amd64

      - uses: actions/download-artifact@v4
        with:
          name: partner-cli-arm64

      - uses: actions/download-artifact@v4
        with:
          name: opec-amd64

      - uses: actions/download-artifact@v4
        with:
          name: opec-arm64

      - uses: actions/download-artifact@v4
        with:
          name: oxpulse-awg-params-agent-amd64

      - uses: actions/download-artifact@v4
        with:
          name: oxpulse-awg-params-agent-arm64

      - name: Stage artifacts
        env:
          VERSION: ${{ needs.extract-version.outputs.version }}
          TAG: ${{ needs.extract-version.outputs.tag }}
        run: |
          cp install.sh                partner-edge-installer.sh
          # Bug R: substitute @RELEASE_TAG@ sentinel with the real tag so the
          # released installer pins REPO_RAW to this exact release (not main HEAD).
          # Must happen BEFORE the sha256sum step below so SHA256SUMS reflects
          # the substituted bytes that operators will actually download.
          sed -i "s|@RELEASE_TAG@|${TAG}|g" partner-edge-installer.sh
          cp upgrade.sh                partner-edge-upgrade.sh
          # Bug R (upgrade.sh): substitute @RELEASE_TAG@ in the upgrade script too,
          # so REPO_RAW fetches in sync_host_scripts are pinned to this release tag.
          # Without this, fetched bytes match main HEAD (not the tag) and the SHA256SUMS
          # guard rejects every file that changed since the tag was cut — silently
          # skipping the entire host-script sync on normal `upgrade vX.Y.Z` runs.
          sed -i "s|@RELEASE_TAG@|${TAG}|g" partner-edge-upgrade.sh
          cp healthcheck.sh            partner-edge-healthcheck.sh
          # Phase 4.1 + 4.2: install.sh sources lib/install-*.sh modules. Ship
          # them as individual release assets so install.sh's
          # _install_lib_source helper can fetch them from REPO_RAW (its
          # tier-4 fallback) or from operator-staged /usr/local/lib/partner-edge.
          cp lib/install-preflight.sh     install-preflight.sh
          cp lib/install-deps.sh          install-deps.sh
          cp lib/install-network.sh       install-network.sh
          cp lib/install-healthcheck.sh   install-healthcheck.sh
          cp lib/install-systemd.sh       install-systemd.sh
          cp lib/install-args.sh          install-args.sh
          cp lib/install-awg.sh           install-awg.sh
          cp lib/install-awg-params-agent.sh install-awg-params-agent.sh
          # Bug #5 fix: ship install-firewall.sh so _install_lib_source can
          # verify its sha256 against lib-checksums.txt on tier-4 installs.
          # Phase 6: install.sh/upgrade.sh still fetch this OLD literal name
          # this release (unmodified, out of scope) — lib/install-firewall.sh
          # is now a frozen back-compat duplicate of the renamed
          # lib/firewall-lib.sh (see that file's header); this cp continues
          # to ship real byte content, not a symlink, so raw.githubusercontent.com
          # tier-3/4 fetches keep working (a git symlink would serve the
          # target-path TEXT over raw HTTP, not the script — die-on-fetch).
          cp lib/install-firewall.sh          install-firewall.sh
          # Phase 5.5 MAJOR 1: fail-soft render helpers (sourced by install/hydrate/update/refresh).
          cp lib/render-channel-lib.sh    render-channel-lib.sh
          # Host-script sync assets: stage every file in _HOST_SCRIPT_SBIN_FILES so
          # SHA256SUMS covers the full set and sync_host_scripts can verify each one.
          # Previously only partner-edge-upgrade.sh / render-channel-lib.sh /
          # config-defaults.conf were staged; the other 7 sbin files (including the
          # headline oxpulse-channels-health-report.sh) were fetched UNVERIFIED.
          # These 7 already live in the repo root (= staging cwd) with the same
          # name — no cp needed; they are picked up directly by the sha256sum +
          # upload steps below. (A `cp X X` here fails "same file" and aborts the
          # whole Stage step → 0 assets. Only subdir/renamed files need cp.)
          cp lib/telegram-alert-lib.sh         telegram-alert-lib.sh
          # SSRF / internal-IP guard (P1 of the 2026-07-08 health-report-lib-
          # extraction plan): same _HOST_SCRIPT_SBIN_FILES / sync_host_scripts
          # delivery shape as telegram-alert-lib.sh directly above — staged so
          # SHA256SUMS covers it and a tier-4 curl|bash fresh install (no local
          # clone, install-systemd.sh's curl fallback) can fetch it too.
          cp lib/peer-ip-guard-lib.sh          peer-ip-guard-lib.sh
          # P2 strangler extraction (2026-07-08 plan): channel probes +
          # report/verdict logic, in _HOST_SCRIPT_SBIN_FILES so sync_host_scripts
          # delivers it to already-installed edges on upgrade (not just fresh
          # installs via install-systemd.sh).
          cp lib/channel-health-lib.sh         channel-health-lib.sh
          # Cross-probe (P3b mesh peer-probe) lib — sourced by
          # oxpulse-channels-health-report.sh at runtime, synced to existing boxes
          # via upgrade.sh _HOST_SCRIPT_SBIN_FILES (verified against SHA256SUMS),
          # same delivery pattern as telegram-alert-lib.sh.
          cp lib/cross-probe-lib.sh            cross-probe-lib.sh
          # Metric-sink lib (P1 of the 2026-07-08 refresh-lib-extraction-
          # strangler plan) — emit_metric/emit_gauge Prometheus textfile sink,
          # sourced fail-closed by oxpulse-partner-edge-refresh.sh, synced to
          # existing boxes via upgrade.sh _HOST_SCRIPT_SBIN_FILES (verified
          # against SHA256SUMS), same delivery pattern as cross-probe-lib.sh.
          cp lib/metric-sink-lib.sh            metric-sink-lib.sh
          # Surgical-restart lib (P2 of the 2026-07-08 refresh-lib-extraction-
          # strangler plan) — the sha-diff-gated docker restart/recreate
          # mechanism, sourced fail-closed by oxpulse-partner-edge-refresh.sh,
          # synced to existing boxes via upgrade.sh _HOST_SCRIPT_SBIN_FILES
          # (verified against SHA256SUMS), same delivery pattern as
          # metric-sink-lib.sh directly above.
          cp lib/surgical-restart-lib.sh       surgical-restart-lib.sh
          # Xprb-refresh lib (P3 of the 2026-07-08 refresh-lib-extraction-
          # strangler plan) — the cross-probe (xprb_) bearer-token daily
          # re-mint leg, sourced fail-closed by
          # oxpulse-partner-edge-refresh.sh, synced to existing boxes via
          # upgrade.sh _HOST_SCRIPT_SBIN_FILES (verified against SHA256SUMS),
          # same delivery pattern as surgical-restart-lib.sh directly above.
          cp lib/xprb-refresh-lib.sh           xprb-refresh-lib.sh
          # NOTE: reconcile.sh is intentionally NOT staged as a repo-root release
          # asset. upgrade.sh's _source_lib fetches it from $REPO_RAW/lib/reconcile.sh
          # and verifies it against lib-checksums.txt (the lib/*.sh home manifest,
          # regenerated below and shipped as the lib-checksums.txt asset) — NOT against
          # the repo-root SHA256SUMS. It is not in _HOST_SCRIPT_SBIN_FILES either, so
          # nothing ever downloads a top-level reconcile.sh asset. A root cp+SHA256SUMS
          # entry would be a dead, unverified-by-any-consumer asset (review MEDIUM).
          # healthcheck-lib.sh / compose-lib.sh / host-scripts-lib.sh follow the SAME
          # reconcile.sh precedent (synthetic_green fix, P4 strangler-harden
          # follow-up): upgrade.sh's _stage_lib now fetches them from
          # $REPO_RAW/lib/<name> and verifies against lib-checksums.txt — they are
          # NOT repo-root release assets either, only entries in the regen block below.
          # Scripts exec'd by xray-update and geoip-refresh timers: staged so
          # SHA256SUMS covers them and sync_host_scripts can install + verify them.
          # oxpulse-xray-update.sh installs to /usr/local/bin/ (not sbin).
          cp scripts/oxpulse-xray-update.sh   oxpulse-xray-update.sh
          cp scripts/oxpulse-geoip-refresh.sh oxpulse-geoip-refresh.sh
          # Systemd units managed by sync_host_scripts (step 5); stage so SHA256SUMS
          # covers them and the integrity guard can be wired in upgrade.sh.
          cp systemd/oxpulse-partner-edge.service              oxpulse-partner-edge.service
          cp systemd/oxpulse-partner-edge-hydrate.service      oxpulse-partner-edge-hydrate.service
          cp systemd/oxpulse-partner-edge-refresh.service      oxpulse-partner-edge-refresh.service
          cp systemd/oxpulse-partner-edge-refresh.timer        oxpulse-partner-edge-refresh.timer
          cp systemd/oxpulse-partner-edge-sni-rotate.service   oxpulse-partner-edge-sni-rotate.service
          cp systemd/oxpulse-partner-edge-sni-rotate.timer     oxpulse-partner-edge-sni-rotate.timer
          cp systemd/oxpulse-xray-update.service               oxpulse-xray-update.service
          cp systemd/oxpulse-xray-update.timer                 oxpulse-xray-update.timer
          cp systemd/oxpulse-geoip-refresh.service             oxpulse-geoip-refresh.service
          cp systemd/oxpulse-geoip-refresh.timer               oxpulse-geoip-refresh.timer
          cp systemd/oxpulse-channels-health-report.service    oxpulse-channels-health-report.service
          cp systemd/oxpulse-channels-health-report.timer      oxpulse-channels-health-report.timer
          # Split-routing artifacts (PR #280): static unit + lib module + 3 sbin scripts.
          # The 3 sbin scripts (split-routing.sh, split-disable.sh, ru-subnets-update)
          # already live at repo root with the right name — no cp needed (same pattern as
          # oxpulse-channels-health-report.sh). Only the subdir files need staging.
          cp systemd/oxpulse-partner-edge-split-routing.service oxpulse-partner-edge-split-routing.service
          cp systemd/oxpulse-partner-edge-ru-subnets-update.service oxpulse-partner-edge-ru-subnets-update.service
          cp systemd/oxpulse-partner-edge-ru-subnets-update.timer    oxpulse-partner-edge-ru-subnets-update.timer
          # awg-params-agent unit: must be released so upgrade.sh can sync it
          # with OXPULSE_RESTART_UNIT_AFTER_APPLY to existing boxes.
          cp systemd/oxpulse-awg-params-agent.service                oxpulse-awg-params-agent.service
          cp lib/install-split-routing.sh                            install-split-routing.sh
          # Bug 8 fix: ship defaults.conf so install-systemd.sh can fetch it
          # from REPO_RAW on fresh installs (tier-4 fallback path).
          cp config/defaults.conf         config-defaults.conf
          # VERSION file: oxpulse-channels-health-report.sh reads installer_version
          # from /usr/local/share/oxpulse-partner-edge/VERSION; sync_host_scripts
          # fetches it from REPO_RAW/VERSION on upgrade.  Must be a release asset
          # so SHA256SUMS covers it.  (File is already at repo root, no cp needed.)
          # Phase 5.7 Item 3: regenerate lib-checksums.txt from the lib/*.sh
          # files at release time so the integrity is pinned to this exact
          # release. install.sh _install_lib_source validates tier-4 fetches
          # against this file. Also copy the pre-committed file as a release asset.
          (cd lib && sha256sum \
            install-args.sh \
            install-awg.sh \
            install-awg-params-agent.sh \
            install-deps.sh \
            install-firewall.sh \
            install-healthcheck.sh \
            install-network.sh \
            install-preflight.sh \
            install-split-routing.sh \
            install-systemd.sh \
            render-channel-lib.sh \
            reconcile.sh \
            telegram-alert-lib.sh \
            compose-lib.sh \
            healthcheck-lib.sh \
            host-scripts-lib.sh \
            peer-ip-guard-lib.sh \
            channel-health-lib.sh \
            cross-probe-lib.sh \
            metric-sink-lib.sh \
            surgical-restart-lib.sh \
            xprb-refresh-lib.sh \
          ) > lib/lib-checksums.txt
          cp lib/lib-checksums.txt        lib-checksums.txt
          chmod +x partner-cli-amd64 partner-cli-arm64
          chmod +x opec-amd64 opec-arm64
          chmod +x oxpulse-awg-params-agent-amd64 oxpulse-awg-params-agent-arm64
          sha256sum \
            partner-edge-installer.sh \
            partner-edge-upgrade.sh \
            partner-edge-healthcheck.sh \
            uninstall.sh \
            install-preflight.sh \
            install-deps.sh \
            install-network.sh \
            install-healthcheck.sh \
            install-systemd.sh \
            install-args.sh \
            install-awg.sh \
            install-awg-params-agent.sh \
            render-channel-lib.sh \
            lib-checksums.txt \
            config-defaults.conf \
            VERSION \
            oxpulse-channels-health-report.sh \
            hydrate.sh \
            oxpulse-partner-edge-refresh.sh \
            oxpulse-partner-edge-sni-rotate.sh \
            ghcr-auth-lib.sh \
            channel-render-lib.sh \
            oxpulse-token-lib.sh \
            telegram-alert-lib.sh \
            peer-ip-guard-lib.sh \
            channel-health-lib.sh \
            cross-probe-lib.sh \
            metric-sink-lib.sh \
            surgical-restart-lib.sh \
            xprb-refresh-lib.sh \
            oxpulse-xray-update.sh \
            oxpulse-geoip-refresh.sh \
            oxpulse-partner-edge.service \
            oxpulse-partner-edge-hydrate.service \
            oxpulse-partner-edge-refresh.service \
            oxpulse-partner-edge-refresh.timer \
            oxpulse-partner-edge-sni-rotate.service \
            oxpulse-partner-edge-sni-rotate.timer \
            oxpulse-xray-update.service \
            oxpulse-xray-update.timer \
            oxpulse-geoip-refresh.service \
            oxpulse-geoip-refresh.timer \
            oxpulse-channels-health-report.service \
            oxpulse-channels-health-report.timer \
            oxpulse-partner-edge-split-routing.sh \
            oxpulse-partner-edge-split-disable.sh \
            oxpulse-partner-edge-ru-subnets-update \
            install-split-routing.sh \
            oxpulse-partner-edge-split-routing.service \
            oxpulse-partner-edge-ru-subnets-update.service \
            oxpulse-partner-edge-ru-subnets-update.timer \
            oxpulse-awg-params-agent.service \
            oxpulse-partner-edge-enable-hy2 \
            partner-cli-amd64 \
            partner-cli-arm64 \
            opec-amd64 \
            opec-arm64 \
            oxpulse-awg-params-agent-amd64 \
            oxpulse-awg-params-agent-arm64 \
            > SHA256SUMS

      - name: Upload release assets
        env:
          GH_TOKEN: ${{ github.token }}
          VERSION: ${{ needs.extract-version.outputs.version }}
          TAG: ${{ needs.extract-version.outputs.tag }}
        run: |
          if ! gh release view "$TAG" >/dev/null 2>&1; then
            gh release create "$TAG" \
              --title "partner-edge v${VERSION}" \
              --notes "OxPulse partner-edge bundle v${VERSION}. See README.md."
          fi
          gh release upload "$TAG" \
            partner-edge-installer.sh \
            partner-edge-upgrade.sh \
            partner-edge-healthcheck.sh \
            uninstall.sh \
            install-preflight.sh \
            install-deps.sh \
            install-network.sh \
            install-healthcheck.sh \
            install-systemd.sh \
            install-args.sh \
            install-awg.sh \
            render-channel-lib.sh \
            lib-checksums.txt \
            config-defaults.conf \
            VERSION \
            oxpulse-channels-health-report.sh \
            hydrate.sh \
            oxpulse-partner-edge-refresh.sh \
            oxpulse-partner-edge-sni-rotate.sh \
            ghcr-auth-lib.sh \
            channel-render-lib.sh \
            oxpulse-token-lib.sh \
            telegram-alert-lib.sh \
            peer-ip-guard-lib.sh \
            channel-health-lib.sh \
            cross-probe-lib.sh \
            metric-sink-lib.sh \
            surgical-restart-lib.sh \
            xprb-refresh-lib.sh \
            oxpulse-xray-update.sh \
            oxpulse-geoip-refresh.sh \
            oxpulse-partner-edge.service \
            oxpulse-partner-edge-hydrate.service \
            oxpulse-partner-edge-refresh.service \
            oxpulse-partner-edge-refresh.timer \
            oxpulse-partner-edge-sni-rotate.service \
            oxpulse-partner-edge-sni-rotate.timer \
            oxpulse-xray-update.service \
            oxpulse-xray-update.timer \
            oxpulse-geoip-refresh.service \
            oxpulse-geoip-refresh.timer \
            oxpulse-channels-health-report.service \
            oxpulse-channels-health-report.timer \
            partner-cli-amd64 \
            partner-cli-arm64 \
            opec-amd64 \
            opec-arm64 \
            oxpulse-awg-params-agent-amd64 \
            oxpulse-awg-params-agent-arm64 \
            oxpulse-awg-params-agent.service \
            install-awg-params-agent.sh \
            oxpulse-partner-edge-split-routing.sh \
            oxpulse-partner-edge-split-disable.sh \
            oxpulse-partner-edge-ru-subnets-update \
            install-split-routing.sh \
            oxpulse-partner-edge-split-routing.service \
            oxpulse-partner-edge-ru-subnets-update.service \
            oxpulse-partner-edge-ru-subnets-update.timer \
            oxpulse-partner-edge-enable-hy2 \
            SHA256SUMS \
            --clobber

