name: Build

on:
  push:
    branches:
      - develop
      - master
      - 'releases/**'
  pull_request:
  release:
    types: [ published ]

concurrency:
  group: ${{ github.ref }}
  cancel-in-progress: true

env:
  COMPILER_IMAGE: ghcr.io/stashapp/compiler:13

jobs:
  # Job 1: Generate code and build UI
  # Runs natively (no Docker) — go generate/gqlgen and node don't need cross-compilers.
  # Produces artifacts (generated Go files + UI build) consumed by test and build jobs.
  generate:
    runs-on: ubuntu-24.04
    steps:
    - uses: actions/checkout@v6
      with:
        fetch-depth: 0
        fetch-tags: true
    - name: Setup Go
      uses: actions/setup-go@v6

    # pnpm version is read from the packageManager field in package.json
    # very broken (4.3, 4.4)
    - name: Install pnpm
      uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061
      with:
        package_json_file: ui/v2.5/package.json

    - name: Setup Node.js
      uses: actions/setup-node@v6
      with:
        node-version: '20'
        cache: 'pnpm'
        cache-dependency-path: ui/v2.5/pnpm-lock.yaml

    - name: Install UI dependencies
      run: cd ui/v2.5 && pnpm install --frozen-lockfile

    - name: Generate
      run: make generate

    - name: Cache UI build
      uses: actions/cache@v5
      id: cache-ui
      with:
        path: ui/v2.5/build
        key: ${{ runner.os }}-ui-build-${{ hashFiles('ui/v2.5/pnpm-lock.yaml', 'ui/v2.5/public/**', 'ui/v2.5/src/**', 'graphql/**/*.graphql') }}

    - name: Validate UI
      # skip UI validation for pull requests if UI is unchanged
      if: ${{ github.event_name != 'pull_request' || steps.cache-ui.outputs.cache-hit != 'true' }}
      run: make validate-ui

    - name: Build UI
      # skip UI build for pull requests if UI is unchanged (UI was cached)
      if: ${{ github.event_name != 'pull_request' || steps.cache-ui.outputs.cache-hit != 'true' }}
      run: make ui

    # Bundle generated Go files + UI build for downstream jobs (test + build)
    - name: Upload generated artifacts
      uses: actions/upload-artifact@v7
      with:
        name: generated
        retention-days: 1
        path: |
          internal/api/generated_exec.go
          internal/api/generated_models.go
          ui/v2.5/build/
          ui/login/locales/

  # Job 2: Integration tests
  # Runs natively (no Docker) — only needs Go + GCC (for CGO/SQLite), both on ubuntu-22.04.
  # Runs in parallel with the build matrix jobs.
  test:
    needs: generate
    runs-on: ubuntu-24.04
    steps:
    - uses: actions/checkout@v6

    - name: Setup Go
      uses: actions/setup-go@v6
      with:
        go-version-file: 'go.mod'

    # Places generated Go files + UI build into the working tree so the build compiles
    - name: Download generated artifacts
      uses: actions/download-artifact@v8
      with:
        name: generated

    - name: Test Backend
      run: make it

  # Job 3: Cross-compile for all platforms
  # Each platform gets its own runner and Docker container (ghcr.io/stashapp/compiler:13).
  # Each build-cc-* make target is self-contained (sets its own GOOS/GOARCH/CC),
  # so running them in separate containers is functionally identical to one container.
  # Runs in parallel with the test job.
  build:
    needs: generate
    runs-on: ubuntu-24.04
    strategy:
      fail-fast: false
      matrix:
        include:
        - platform: windows
          make-target: build-cc-windows
          artifact-paths: |
            dist/stash-win.exe
          tag: win
        - platform: macos
          make-target: build-cc-macos
          artifact-paths: |
            dist/stash-macos
            dist/Stash.app.zip
          tag: osx
        - platform: linux
          make-target: build-cc-linux
          artifact-paths: |
            dist/stash-linux
          tag: linux
        - platform: linux-arm64v8
          make-target: build-cc-linux-arm64v8
          artifact-paths: |
            dist/stash-linux-arm64v8
          tag: arm
        - platform: linux-arm32v7
          make-target: build-cc-linux-arm32v7
          artifact-paths: |
            dist/stash-linux-arm32v7
          tag: arm
        - platform: linux-arm32v6
          make-target: build-cc-linux-arm32v6
          artifact-paths: |
            dist/stash-linux-arm32v6
          tag: arm
        - platform: freebsd
          make-target: build-cc-freebsd
          artifact-paths: |
            dist/stash-freebsd
          tag: freebsd

    steps:
    - uses: actions/checkout@v6
      with:
        fetch-depth: 0
        fetch-tags: true

    - name: Download generated artifacts
      uses: actions/download-artifact@v8
      with:
        name: generated

    - name: Cache Go build
      uses: actions/cache@v5
      with:
        path: .go-cache
        key: ${{ runner.os }}-go-cache-${{ matrix.platform }}-${{ hashFiles('go.mod', '**/go.sum') }}

    # kept seperate to test timings
    - name: pull compiler image
      run: docker pull $COMPILER_IMAGE

    - name: Start build container
      env:
        official-build: ${{ (github.event_name == 'push' && github.ref == 'refs/heads/develop') || (github.event_name == 'release' && github.ref != 'refs/tags/latest_develop') }}
      run: |
        mkdir -p .go-cache
        docker run -d --name build --mount type=bind,source="$(pwd)",target=/stash,consistency=delegated --mount type=bind,source="$(pwd)/.go-cache",target=/root/.cache/go-build,consistency=delegated --env OFFICIAL_BUILD=${{ env.official-build }} -w /stash $COMPILER_IMAGE tail -f /dev/null

    - name: Build (${{ matrix.platform }})
      run: docker exec -t build /bin/bash -c "make ${{ matrix.make-target }}"

    - name: Cleanup build container
      run: docker rm -f -v build

    - name: Upload build artifact
      uses: actions/upload-artifact@v7
      with:
        name: build-${{ matrix.platform }}
        retention-days: 1
        path: ${{ matrix.artifact-paths }}

  # Job 4: Release
  # Waits for both test and build to pass, then collects all platform artifacts
  # into dist/ for checksums, GitHub releases, and multi-arch Docker push.
  release:
    needs: [test, build]
    runs-on: ubuntu-24.04
    steps:
    - uses: actions/checkout@v6
      with:
        fetch-depth: 0
        fetch-tags: true

    # Downloads all artifacts (generated + 7 platform builds) into artifacts/ subdirectories
    - name: Download all build artifacts
      uses: actions/download-artifact@v8
      with:
        path: artifacts

    # Reassemble platform binaries from matrix job artifacts into a single dist/ directory
    # make sure that artifacts have executable bit set
    # upload-artifact@v4 strips the common path prefix (dist/), so files are at the artifact root
    - name: Collect binaries
      run: |
        mkdir -p dist
        cp artifacts/build-*/* dist/
        chmod +x dist/*

    - name: Zip UI
      run: |
        cd artifacts/generated/ui/v2.5/build && zip -r ../../../../../dist/stash-ui.zip .

    - name: Generate checksums
      run: |
        git describe --tags --exclude latest_develop | tee CHECKSUMS_SHA1
        sha1sum dist/Stash.app.zip dist/stash-* dist/stash-ui.zip | sed 's/dist\///g' | tee -a CHECKSUMS_SHA1
        echo "STASH_VERSION=$(git describe --tags --exclude latest_develop)" >> $GITHUB_ENV
        echo "RELEASE_DATE=$(date +'%Y-%m-%d %H:%M:%S %Z')" >> $GITHUB_ENV

    - name: Upload Windows binary
      # only upload binaries for pull requests
      if: ${{ github.event_name == 'pull_request' && github.base_ref != 'refs/heads/develop' && github.base_ref != 'refs/heads/master'}}
      uses: actions/upload-artifact@v7
      with:
        name: stash-win.exe
        path: dist/stash-win.exe

    - name: Upload macOS binary
      # only upload binaries for pull requests
      if: ${{ github.event_name == 'pull_request' && github.base_ref != 'refs/heads/develop' && github.base_ref != 'refs/heads/master'}}
      uses: actions/upload-artifact@v7
      with:
        name: stash-macos
        path: dist/stash-macos

    - name: Upload macOS bundle
      # only upload binaries for pull requests
      if: ${{ github.event_name == 'pull_request' && github.base_ref != 'refs/heads/develop' && github.base_ref != 'refs/heads/master'}}
      uses: actions/upload-artifact@v7
      with:
        name: Stash.app.zip
        path: dist/Stash.app.zip

    - name: Upload Linux binary
      # only upload binaries for pull requests
      if: ${{ github.event_name == 'pull_request' && github.base_ref != 'refs/heads/develop' && github.base_ref != 'refs/heads/master'}}
      uses: actions/upload-artifact@v7
      with:
        name: stash-linux
        path: dist/stash-linux

    - name: Upload UI
      # only upload for pull requests
      if: ${{ github.event_name == 'pull_request' && github.base_ref != 'refs/heads/develop' && github.base_ref != 'refs/heads/master'}}
      uses: actions/upload-artifact@v7
      with:
        name: stash-ui.zip
        path: dist/stash-ui.zip

    - name: Update latest_develop tag
      if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/develop' }}
      run: git tag -f latest_develop; git push -f --tags

    - name: Development Release
      if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/develop' }}
      uses: marvinpinto/action-automatic-releases@v1.1.2
      with:
        repo_token: "${{ secrets.GITHUB_TOKEN }}"
        prerelease: true
        automatic_release_tag: latest_develop
        title: "${{ env.STASH_VERSION }}: Latest development build"
        files: |
          dist/Stash.app.zip
          dist/stash-macos
          dist/stash-win.exe
          dist/stash-linux
          dist/stash-linux-arm64v8
          dist/stash-linux-arm32v7
          dist/stash-linux-arm32v6
          dist/stash-freebsd
          dist/stash-ui.zip
          CHECKSUMS_SHA1

    - name: Master release
      # NOTE: this isn't perfect, but should cover most scenarios
      # DON'T create tag names starting with "v" if they are not stable releases
      if: ${{ github.event_name == 'release' && startsWith(github.ref, 'refs/tags/v') }}
      uses: WithoutPants/github-release@v2.0.4
      with:
        token: "${{ secrets.GITHUB_TOKEN }}"
        allow_override: true
        files: |
          dist/Stash.app.zip
          dist/stash-macos
          dist/stash-win.exe
          dist/stash-linux
          dist/stash-linux-arm64v8
          dist/stash-linux-arm32v7
          dist/stash-linux-arm32v6
          dist/stash-freebsd
          dist/stash-ui.zip
          CHECKSUMS_SHA1
        gzip: false

    - name: Development Docker
      if: ${{ github.repository == 'stashapp/stash' && github.event_name == 'push' && github.ref == 'refs/heads/develop' }}
      env:
        DOCKER_CLI_EXPERIMENTAL: enabled
        DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
        DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
      run: |
        docker run --rm --privileged tonistiigi/binfmt
        docker info
        docker buildx create --name builder --use
        docker buildx inspect --bootstrap
        docker buildx ls
        bash ./docker/ci/x86_64/docker_push.sh development

    - name: Release Docker
      # NOTE: this isn't perfect, but should cover most scenarios
      # DON'T create tag names starting with "v" if they are not stable releases
      if: ${{ github.repository == 'stashapp/stash' && github.event_name == 'release' && startsWith(github.ref, 'refs/tags/v') }}
      env:
        DOCKER_CLI_EXPERIMENTAL: enabled
        DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
        DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
      run: |
        docker run --rm --privileged tonistiigi/binfmt
        docker info
        docker buildx create --name builder --use
        docker buildx inspect --bootstrap
        docker buildx ls
        bash ./docker/ci/x86_64/docker_push.sh latest "${{ github.event.release.tag_name }}"
