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 }}"