name: partner-edge-release

on:
  push:
    tags:
      - 'partner-edge-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. Discovered 2026-04-29 on tag partner-edge-v0.12.4 (recovery
  # of the per-peer-token fix). Until release-please is configured with
  # a Personal Access Token, this workflow_dispatch is the operator-
  # facing escape hatch.
  workflow_dispatch:
    inputs:
      tag:
        description: 'Existing tag to re-publish (e.g. partner-edge-v0.12.4)'
        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

  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" =~ ^partner-edge-v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
            echo "::error::tag '$tag' does not match partner-edge-vX.Y.Z"
            exit 1
          fi
          version="${tag#partner-edge-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 piter+rvpn, arm64 on krolik).
  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 }}
        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
          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
          # 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
          # 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
          # 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-healthcheck.sh \
            install-network.sh \
            install-preflight.sh \
            install-systemd.sh \
            render-channel-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 \
            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 \
            partner-cli-amd64 \
            partner-cli-arm64 \
            opec-amd64 \
            opec-arm64 \
            oxpulse-awg-params-agent-amd64 \
            oxpulse-awg-params-agent-arm64 \
            install-awg-params-agent.sh \
            SHA256SUMS \
            --clobber

