name: Release

on:
  push:
    tags:
      - "v*.*.*"
  workflow_dispatch:

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

permissions:
  contents: read

env:
  GO_VERSION: "1.26.1"
  NODE_VERSION: "24"
  PNPM_VERSION: "10"
  ARTIFACT_RETENTION_DAYS: 60
  ARTIFACT_PREFIX: memos

jobs:
  prepare:
    name: Extract Version
    runs-on: ubuntu-latest
    outputs:
      version: ${{ steps.version.outputs.version }}
      tag: ${{ steps.version.outputs.tag }}
      major_minor: ${{ steps.version.outputs.major_minor }}
      is_prerelease: ${{ steps.version.outputs.is_prerelease }}
    steps:
      - name: Extract version
        id: version
        env:
          REF_NAME: ${{ github.ref_name }}
          EVENT_NAME: ${{ github.event_name }}
        run: |
          if [ "$EVENT_NAME" = "workflow_dispatch" ]; then
            echo "tag=" >> "$GITHUB_OUTPUT"
            echo "version=manual-${GITHUB_SHA::7}" >> "$GITHUB_OUTPUT"
            echo "major_minor=" >> "$GITHUB_OUTPUT"
            echo "is_prerelease=false" >> "$GITHUB_OUTPUT"
            exit 0
          fi

          if [[ ! "$REF_NAME" =~ ^v([0-9]+\.[0-9]+\.[0-9]+)(-rc\.[0-9]+)?$ ]]; then
            echo "Unsupported release tag format: $REF_NAME" >&2
            exit 1
          fi

          version="${BASH_REMATCH[1]}${BASH_REMATCH[2]}"
          major_minor="${BASH_REMATCH[1]%.*}"
          is_prerelease=false
          if [ -n "${BASH_REMATCH[2]}" ]; then
            is_prerelease=true
          fi

          echo "tag=${REF_NAME}" >> "$GITHUB_OUTPUT"
          echo "version=${version}" >> "$GITHUB_OUTPUT"
          echo "major_minor=${major_minor}" >> "$GITHUB_OUTPUT"
          echo "is_prerelease=${is_prerelease}" >> "$GITHUB_OUTPUT"

  build-frontend:
    name: Build Frontend
    needs: prepare
    runs-on: ubuntu-latest
    steps:
      - name: Checkout code
        uses: actions/checkout@v6

      - name: Setup pnpm
        uses: pnpm/action-setup@v4.2.0
        with:
          version: ${{ env.PNPM_VERSION }}

      - name: Setup Node.js
        uses: actions/setup-node@v6
        with:
          node-version: ${{ env.NODE_VERSION }}
          cache: pnpm
          cache-dependency-path: web/pnpm-lock.yaml

      - name: Get pnpm store directory
        id: pnpm-cache
        shell: bash
        run: echo "STORE_PATH=$(pnpm store path)" >> "$GITHUB_OUTPUT"

      - name: Setup pnpm cache
        uses: actions/cache@v4
        with:
          path: ${{ steps.pnpm-cache.outputs.STORE_PATH }}
          key: ${{ runner.os }}-pnpm-store-${{ hashFiles('web/pnpm-lock.yaml') }}
          restore-keys: ${{ runner.os }}-pnpm-store-

      - name: Install dependencies
        working-directory: web
        run: pnpm install --frozen-lockfile

      - name: Build frontend release assets
        working-directory: web
        run: pnpm release

      - name: Upload frontend artifacts
        uses: actions/upload-artifact@v4
        with:
          name: frontend-dist
          path: server/router/frontend/dist
          retention-days: 1

  build-binaries:
    name: Build ${{ matrix.goos }}-${{ matrix.goarch }}${{ matrix.goarm && format('v{0}', matrix.goarm) || '' }}
    needs: [prepare, build-frontend]
    runs-on: ubuntu-latest
    strategy:
      fail-fast: false
      matrix:
        include:
          - goos: linux
            goarch: amd64
          - goos: linux
            goarch: arm64
          - goos: linux
            goarch: arm
            goarm: "7"
          - goos: darwin
            goarch: amd64
          - goos: darwin
            goarch: arm64
          - goos: windows
            goarch: amd64
    steps:
      - name: Checkout code
        uses: actions/checkout@v6

      - name: Setup Go
        uses: actions/setup-go@v6
        with:
          go-version: ${{ env.GO_VERSION }}
          cache: true

      - name: Download frontend artifacts
        uses: actions/download-artifact@v4
        with:
          name: frontend-dist
          path: server/router/frontend/dist

      - name: Build binary
        env:
          GOOS: ${{ matrix.goos }}
          GOARCH: ${{ matrix.goarch }}
          GOARM: ${{ matrix.goarm }}
          CGO_ENABLED: "0"
        run: |
          output_name="memos"
          if [ "$GOOS" = "windows" ]; then
            output_name="memos.exe"
          fi

          mkdir -p build

          go build \
            -trimpath \
            -ldflags="-s -w -X github.com/usememos/memos/internal/version.Version=${{ needs.prepare.outputs.version }} -extldflags '-static'" \
            -tags netgo,osusergo \
            -o "build/${output_name}" \
            ./cmd/memos

      - name: Package binary
        env:
          VERSION: ${{ needs.prepare.outputs.version }}
          GOOS: ${{ matrix.goos }}
          GOARCH: ${{ matrix.goarch }}
          GOARM: ${{ matrix.goarm }}
        run: |
          cd build

          package_name="${ARTIFACT_PREFIX}_${VERSION}_${GOOS}_${GOARCH}"
          if [ -n "$GOARM" ]; then
            package_name="${package_name}v${GOARM}"
          fi

          if [ "$GOOS" = "windows" ]; then
            artifact_name="${package_name}.zip"
            zip -q "${artifact_name}" memos.exe
          else
            artifact_name="${package_name}.tar.gz"
            tar czf "${artifact_name}" memos
          fi

          echo "artifact_name=${artifact_name}" >> "$GITHUB_ENV"

      - name: Upload binary artifact
        uses: actions/upload-artifact@v4
        with:
          name: ${{ env.artifact_name }}
          path: build/${{ env.artifact_name }}
          retention-days: ${{ env.ARTIFACT_RETENTION_DAYS }}

  checksums:
    name: Generate Checksums
    needs: [prepare, build-binaries]
    runs-on: ubuntu-latest
    steps:
      - name: Download binary artifacts
        uses: actions/download-artifact@v4
        with:
          path: artifacts
          pattern: ${{ env.ARTIFACT_PREFIX }}_*
          merge-multiple: true

      - name: Generate checksums
        working-directory: artifacts
        run: sha256sum * > checksums.txt

      - name: Upload checksum artifact
        uses: actions/upload-artifact@v4
        with:
          name: checksums
          path: artifacts/checksums.txt
          retention-days: ${{ env.ARTIFACT_RETENTION_DAYS }}

  release:
    name: Publish GitHub Release
    needs: [prepare, build-binaries, checksums]
    if: github.event_name != 'workflow_dispatch'
    runs-on: ubuntu-latest
    permissions:
      contents: write
    steps:
      - name: Download binary artifacts
        uses: actions/download-artifact@v4
        with:
          path: artifacts
          pattern: ${{ env.ARTIFACT_PREFIX }}_*
          merge-multiple: true

      - name: Download checksum artifact
        uses: actions/download-artifact@v4
        with:
          name: checksums
          path: artifacts

      - name: Publish release assets
        uses: softprops/action-gh-release@v2
        with:
          tag_name: ${{ needs.prepare.outputs.tag }}
          name: ${{ needs.prepare.outputs.tag }}
          prerelease: ${{ needs.prepare.outputs.is_prerelease == 'true' }}
          files: artifacts/*

  build-push:
    name: Build Image ${{ matrix.platform }}
    needs: [prepare, build-frontend]
    if: github.event_name != 'workflow_dispatch'
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write
    strategy:
      fail-fast: false
      matrix:
        platform:
          - linux/amd64
          - linux/arm/v7
          - linux/arm64
    steps:
      - name: Checkout code
        uses: actions/checkout@v6

      - name: Download frontend artifacts
        uses: actions/download-artifact@v4
        with:
          name: frontend-dist
          path: server/router/frontend/dist

      - name: Set up QEMU
        uses: docker/setup-qemu-action@v3

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3

      - name: Login to Docker Hub
        uses: docker/login-action@v3
        with:
          username: ${{ secrets.DOCKER_HUB_USERNAME }}
          password: ${{ secrets.DOCKER_HUB_TOKEN }}

      - name: Login to GitHub Container Registry
        uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ github.token }}

      - name: Build and push by digest
        id: build
        uses: docker/build-push-action@v6
        with:
          context: .
          file: ./scripts/Dockerfile
          platforms: ${{ matrix.platform }}
          build-args: |
            VERSION=${{ needs.prepare.outputs.version }}
            COMMIT=${{ github.sha }}
          cache-from: type=gha,scope=release-${{ matrix.platform }}
          cache-to: type=gha,mode=max,scope=release-${{ matrix.platform }}
          outputs: type=image,name=neosmemo/memos,push-by-digest=true,name-canonical=true,push=true

      - name: Export digest
        run: |
          mkdir -p /tmp/digests
          digest="${{ steps.build.outputs.digest }}"
          touch "/tmp/digests/${digest#sha256:}"

      - name: Upload digest
        uses: actions/upload-artifact@v4
        with:
          name: digests-${{ strategy.job-index }}
          path: /tmp/digests/*
          if-no-files-found: error
          retention-days: 1

  merge-images:
    name: Publish Release Image Tags
    needs: [prepare, build-push]
    if: github.event_name != 'workflow_dispatch'
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write
    steps:
      - name: Download digests
        uses: actions/download-artifact@v4
        with:
          pattern: digests-*
          merge-multiple: true
          path: /tmp/digests

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3

      - name: Login to Docker Hub
        uses: docker/login-action@v3
        with:
          username: ${{ secrets.DOCKER_HUB_USERNAME }}
          password: ${{ secrets.DOCKER_HUB_TOKEN }}

      - name: Login to GitHub Container Registry
        uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ github.token }}

      - name: Create manifest list and push
        working-directory: /tmp/digests
        run: |
          version="${{ needs.prepare.outputs.version }}"
          if [ "${{ needs.prepare.outputs.is_prerelease }}" = "true" ]; then
            docker buildx imagetools create \
              -t "neosmemo/memos:${version}" \
              -t "ghcr.io/usememos/memos:${version}" \
              $(printf 'neosmemo/memos@sha256:%s ' *)
            exit 0
          fi

          docker buildx imagetools create \
            -t "neosmemo/memos:${version}" \
            -t "neosmemo/memos:${{ needs.prepare.outputs.major_minor }}" \
            -t "neosmemo/memos:stable" \
            -t "ghcr.io/usememos/memos:${version}" \
            -t "ghcr.io/usememos/memos:${{ needs.prepare.outputs.major_minor }}" \
            -t "ghcr.io/usememos/memos:stable" \
            $(printf 'neosmemo/memos@sha256:%s ' *)

      - name: Inspect images
        run: |
          docker buildx imagetools inspect neosmemo/memos:${{ needs.prepare.outputs.version }}
          if [ "${{ needs.prepare.outputs.is_prerelease }}" = "true" ]; then
            exit 0
          fi

          docker buildx imagetools inspect neosmemo/memos:stable
