name: Release on: push: tags: # GitHub Actions uses glob patterns here, not regex. Match versioned # tags broadly at the trigger layer, then enforce strict semver below. - "v*.*.*" - "!v*-dirty*" permissions: contents: write packages: write jobs: verify: runs-on: ubuntu-latest outputs: tag_name: ${{ steps.release_meta.outputs.tag_name }} is_stable: ${{ steps.release_meta.outputs.is_stable }} steps: - name: Checkout uses: actions/checkout@v4 with: fetch-depth: 0 - name: Validate tag name id: release_meta shell: bash run: | tag="${GITHUB_REF_NAME}" echo "Triggered by tag: $tag" if [[ ! "$tag" =~ ^v[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?$ ]]; then echo "::error::Release tags must look like vX.Y.Z or vX.Y.Z-suffix; got '$tag'." exit 1 fi if [[ "$tag" == *-dirty* ]]; then echo "::error::Refusing to release from dirty tag '$tag'." exit 1 fi echo "tag_name=$tag" >> "$GITHUB_OUTPUT" if [[ "$tag" == *-* ]]; then echo "is_stable=false" >> "$GITHUB_OUTPUT" else echo "is_stable=true" >> "$GITHUB_OUTPUT" fi - name: Setup Go uses: actions/setup-go@v5 with: go-version-file: server/go.mod cache-dependency-path: server/go.sum - name: Run tests run: cd server && go test ./... release: needs: verify # Only run on the canonical upstream repo. Forks don't have the # HOMEBREW_TAP_GITHUB_TOKEN secret and should not be publishing to # `multica-ai/homebrew-tap` anyway. Without this guard, every fork's # tag push fails this job (401 against the upstream tap), which makes # downstream CI go red without affecting the actual artifact pipeline. if: github.repository_owner == 'multica-ai' runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 with: fetch-depth: 0 - name: Setup Go uses: actions/setup-go@v5 with: go-version-file: server/go.mod cache-dependency-path: server/go.sum - name: Run GoReleaser uses: goreleaser/goreleaser-action@v6 with: version: "~> v2" args: release --clean env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} HOMEBREW_TAP_GITHUB_TOKEN: ${{ secrets.HOMEBREW_TAP_GITHUB_TOKEN }} # Multi-arch images are built natively per platform on dedicated runners # (amd64 on ubuntu-latest, arm64 on ubuntu-24.04-arm) and merged into a # manifest list. This avoids QEMU emulation, which was making the Next.js # arm64 build run for 30+ minutes per release. docker-backend-build: needs: verify strategy: fail-fast: false matrix: include: - platform: linux/amd64 runs-on: ubuntu-latest - platform: linux/arm64 runs-on: ubuntu-24.04-arm runs-on: ${{ matrix.runs-on }} steps: - name: Prepare run: | platform=${{ matrix.platform }} echo "PLATFORM_PAIR=${platform//\//-}" >> "$GITHUB_ENV" - name: Checkout uses: actions/checkout@v4 - name: Compute backend image labels id: meta uses: docker/metadata-action@v5 with: images: ghcr.io/${{ github.repository_owner }}/multica-backend labels: | org.opencontainers.image.title=Multica Backend org.opencontainers.image.description=Multica self-hosted backend - name: Setup Docker Buildx uses: docker/setup-buildx-action@v3 - name: Login to GHCR uses: docker/login-action@v3 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - name: Build and push by digest id: build uses: docker/build-push-action@v6 with: context: . file: Dockerfile pull: true platforms: ${{ matrix.platform }} labels: ${{ steps.meta.outputs.labels }} cache-from: type=gha,scope=release-backend-${{ env.PLATFORM_PAIR }} cache-to: type=gha,mode=max,scope=release-backend-${{ env.PLATFORM_PAIR }} build-args: | VERSION=${{ needs.verify.outputs.tag_name }} COMMIT=${{ github.sha }} outputs: type=image,name=ghcr.io/${{ github.repository_owner }}/multica-backend,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-backend-${{ env.PLATFORM_PAIR }} path: /tmp/digests/* if-no-files-found: error retention-days: 1 docker-backend-merge: needs: [verify, docker-backend-build] runs-on: ubuntu-latest concurrency: group: release-docker-backend-${{ github.ref }} cancel-in-progress: true steps: - name: Download digests uses: actions/download-artifact@v4 with: path: /tmp/digests pattern: digests-backend-* merge-multiple: true - name: Setup Docker Buildx uses: docker/setup-buildx-action@v3 - name: Compute backend image tags id: meta uses: docker/metadata-action@v5 with: images: ghcr.io/${{ github.repository_owner }}/multica-backend flavor: | latest=false tags: | type=raw,value=latest,enable=${{ needs.verify.outputs.is_stable == 'true' }} type=raw,value=${{ needs.verify.outputs.tag_name }} type=sha,prefix=sha- - name: Login to GHCR uses: docker/login-action@v3 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - name: Create manifest list and push working-directory: /tmp/digests run: | docker buildx imagetools create \ $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \ $(printf 'ghcr.io/${{ github.repository_owner }}/multica-backend@sha256:%s ' *) - name: Inspect image run: | docker buildx imagetools inspect \ ghcr.io/${{ github.repository_owner }}/multica-backend:${{ steps.meta.outputs.version }} docker-web-build: needs: verify strategy: fail-fast: false matrix: include: - platform: linux/amd64 runs-on: ubuntu-latest - platform: linux/arm64 runs-on: ubuntu-24.04-arm runs-on: ${{ matrix.runs-on }} steps: - name: Prepare run: | platform=${{ matrix.platform }} echo "PLATFORM_PAIR=${platform//\//-}" >> "$GITHUB_ENV" - name: Checkout uses: actions/checkout@v4 - name: Compute web image labels id: meta uses: docker/metadata-action@v5 with: images: ghcr.io/${{ github.repository_owner }}/multica-web labels: | org.opencontainers.image.title=Multica Web org.opencontainers.image.description=Multica self-hosted web frontend - name: Setup Docker Buildx uses: docker/setup-buildx-action@v3 - name: Login to GHCR uses: docker/login-action@v3 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - name: Build and push by digest id: build uses: docker/build-push-action@v6 with: context: . file: Dockerfile.web pull: true platforms: ${{ matrix.platform }} labels: ${{ steps.meta.outputs.labels }} cache-from: type=gha,scope=release-web-${{ env.PLATFORM_PAIR }} cache-to: type=gha,mode=max,scope=release-web-${{ env.PLATFORM_PAIR }} build-args: | REMOTE_API_URL=http://backend:8080 NEXT_PUBLIC_APP_VERSION=${{ needs.verify.outputs.tag_name }} outputs: type=image,name=ghcr.io/${{ github.repository_owner }}/multica-web,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-web-${{ env.PLATFORM_PAIR }} path: /tmp/digests/* if-no-files-found: error retention-days: 1 docker-web-merge: needs: [verify, docker-web-build] runs-on: ubuntu-latest concurrency: group: release-docker-web-${{ github.ref }} cancel-in-progress: true steps: - name: Download digests uses: actions/download-artifact@v4 with: path: /tmp/digests pattern: digests-web-* merge-multiple: true - name: Setup Docker Buildx uses: docker/setup-buildx-action@v3 - name: Compute web image tags id: meta uses: docker/metadata-action@v5 with: images: ghcr.io/${{ github.repository_owner }}/multica-web flavor: | latest=false tags: | type=raw,value=latest,enable=${{ needs.verify.outputs.is_stable == 'true' }} type=raw,value=${{ needs.verify.outputs.tag_name }} type=sha,prefix=sha- - name: Login to GHCR uses: docker/login-action@v3 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - name: Create manifest list and push working-directory: /tmp/digests run: | docker buildx imagetools create \ $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \ $(printf 'ghcr.io/${{ github.repository_owner }}/multica-web@sha256:%s ' *) - name: Inspect image run: | docker buildx imagetools inspect \ ghcr.io/${{ github.repository_owner }}/multica-web:${{ steps.meta.outputs.version }} helm-chart: needs: [verify, docker-backend-merge, docker-web-merge] if: github.repository_owner == 'multica-ai' runs-on: ubuntu-latest concurrency: group: release-helm-chart-${{ github.ref }} cancel-in-progress: true env: CHART_DIR: deploy/helm/multica OCI_REGISTRY: oci://ghcr.io/${{ github.repository_owner }}/charts steps: - name: Checkout uses: actions/checkout@v4 - name: Setup Helm uses: azure/setup-helm@v4 - name: Sync chart metadata with release tag env: TAG_NAME: ${{ needs.verify.outputs.tag_name }} run: | chart_version="${TAG_NAME#v}" sed -i -E "s/^version:.*/version: ${chart_version}/" "$CHART_DIR/Chart.yaml" sed -i -E "s/^appVersion:.*/appVersion: \"${TAG_NAME}\"/" "$CHART_DIR/Chart.yaml" echo "CHART_VERSION=${chart_version}" >> "$GITHUB_ENV" - name: Lint chart run: helm lint "$CHART_DIR" - name: Package chart run: helm package "$CHART_DIR" --destination .chart-packages - name: Login to GHCR run: echo "${{ secrets.GITHUB_TOKEN }}" | helm registry login ghcr.io --username "${{ github.actor }}" --password-stdin - name: Push chart to GHCR run: helm push ".chart-packages/multica-${CHART_VERSION}.tgz" "$OCI_REGISTRY" - name: Verify published chart run: helm show chart "$OCI_REGISTRY/multica" --version "$CHART_VERSION" # Build the Desktop installers for Linux and Windows and upload them to # the GitHub Release that the `release` job above just published. macOS # Desktop continues to ship via the manual `release-desktop` skill so it # can be signed + notarized with Apple Developer credentials that are # not (yet) wired into CI. desktop: needs: release strategy: fail-fast: false matrix: include: - os: ubuntu-latest target: linux - os: windows-latest target: win runs-on: ${{ matrix.os }} steps: - name: Checkout uses: actions/checkout@v4 with: fetch-depth: 0 - name: Install rpmbuild (Linux) if: matrix.target == 'linux' run: sudo apt-get update && sudo apt-get install -y rpm - name: Setup Go uses: actions/setup-go@v5 with: go-version-file: server/go.mod cache-dependency-path: server/go.sum - name: Setup pnpm uses: pnpm/action-setup@v4 - name: Setup Node uses: actions/setup-node@v4 with: node-version: 22 cache: pnpm - name: Install dependencies run: pnpm install --frozen-lockfile - name: Package Desktop installers (${{ matrix.target }}) working-directory: apps/desktop env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} # electron-builder's GitHub publisher reads this: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Disable code signing on Linux/Windows for now — the public # release is unsigned for these platforms, the CLI carries the # trust boundary. Set CSC_LINK in repo secrets to enable # Windows signing later. CSC_IDENTITY_AUTO_DISCOVERY: "false" run: node scripts/package.mjs --${{ matrix.target }} --x64 --arm64 --publish always