Files
SnapOtter/.github/workflows/release.yml
T
SnapOtterandGitHub 9b64a96bcd fix(docker): restore Postgres/Redis in GPU compose stack + pause image publishing
Restore Postgres 17 + Redis 8 services in docker-compose-gpu.yml; re-add if:false publish pause on the release.yml docker job (main had lost it).
2026-06-19 18:39:35 +08:00

457 lines
17 KiB
YAML

name: Release
on:
workflow_dispatch:
permissions: {}
jobs:
release:
name: Semantic Release
runs-on: ubuntu-latest
permissions:
contents: write
issues: write
pull-requests: write
outputs:
new_version: ${{ steps.check.outputs.version }}
steps:
- name: Checkout
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
fetch-depth: 0
persist-credentials: false
- uses: ./.github/actions/setup
- name: Save release notes
id: notes
run: |
if [ -f .release-notes.md ]; then
cp .release-notes.md /tmp/release-notes.md
echo "has_notes=true" >> "$GITHUB_OUTPUT"
echo "Custom release notes found -- will apply after release."
else
echo "No .release-notes.md found -- using default release notes."
fi
- name: Run semantic-release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: npx semantic-release
- name: Check for new release
id: check
run: |
if [ -f .release-version ]; then
echo "version=$(cat .release-version)" >> "$GITHUB_OUTPUT"
else
# semantic-release found no new commits — tag already exists from a
# previous run. Fall back to the latest git tag so the Docker build
# jobs still run (useful when re-triggering after a push failure).
latest=$(git describe --tags --abbrev=0 2>/dev/null | sed 's/^v//')
if [ -n "$latest" ]; then
echo "version=$latest" >> "$GITHUB_OUTPUT"
echo "Re-using existing tag v${latest} for Docker build."
else
echo "::error::semantic-release did not produce a new version. No releasable commits found."
exit 1
fi
fi
- name: Update GitHub release notes
if: steps.notes.outputs.has_notes == 'true' && steps.check.outputs.version
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
echo "Updating release v${{ steps.check.outputs.version }} with custom notes..."
gh release edit "v${{ steps.check.outputs.version }}" \
--notes-file /tmp/release-notes.md
echo "Release notes updated successfully."
- name: Update docs changelog
if: steps.notes.outputs.has_notes == 'true' && steps.check.outputs.version
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
VERSION: ${{ steps.check.outputs.version }}
run: |
CHANGELOG="apps/docs/changelog.md"
if [ ! -f "$CHANGELOG" ]; then
echo "No docs changelog found, skipping."
exit 0
fi
NOTES="/tmp/release-notes.md"
# Build the new entry: ## vX.Y.Z header + release notes body (skip the first ## Highlights/Upgrade sections wrapper)
{
echo ""
echo "## v${VERSION}"
echo ""
# Strip the ## Highlights header and ## Upgrade section, keep the rest
sed '1{/^## Highlights$/d}' "$NOTES" | sed '/^## Upgrade$/,/^---$/d' | sed '/^---$/d'
echo ""
echo "[Full diff on GitHub](https://github.com/snapotter-hq/SnapOtter/compare/v$(git tag --sort=-v:refname | grep -E '^v[0-9]' | sed -n '2p' | sed 's/^v//')...v${VERSION})"
echo ""
echo "---"
echo ""
} > /tmp/changelog-entry.md
# Insert after the "# Changelog" header
sed -i '/^# Changelog$/r /tmp/changelog-entry.md' "$CHANGELOG"
# Commit and push
git config user.name "SnapOtter"
git config user.email "snapotter.hq@gmail.com"
git add "$CHANGELOG"
git commit -m "docs: update changelog for v${VERSION}" || true
git push origin HEAD:main || true
echo "Docs changelog updated for v${VERSION}."
prebuilt:
name: Archive (${{ matrix.arch }})
needs: release
if: needs.release.outputs.new_version
permissions:
contents: write
strategy:
fail-fast: false
matrix:
include:
- runner: ubuntu-latest
arch: amd64
- runner: ubuntu-24.04-arm
arch: arm64
runs-on: ${{ matrix.runner }}
steps:
- name: Checkout release tag
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
ref: v${{ needs.release.outputs.new_version }}
- uses: ./.github/actions/setup
- name: Build web frontend
run: pnpm --filter @snapotter/web build
- name: Prune to production dependencies
run: |
rm -rf node_modules apps/*/node_modules packages/*/node_modules
npm pkg delete scripts.prepare
pnpm install --prod --frozen-lockfile
- name: Create archive
env:
VERSION: ${{ needs.release.outputs.new_version }}
ARCH: ${{ matrix.arch }}
run: |
rm -rf apps/web/src apps/web/public apps/web/index.html apps/web/tsconfig.json
rm -rf apps/landing apps/docs apps/demo
rm -rf tests .husky scripts
rm -rf .releaserc.json biome.json .editorconfig .gitattributes
rm -f CHANGELOG.md README.md CONTRIBUTING.md SECURITY.md
ARCHIVE_NAME="snapotter-v${VERSION}-linux-${ARCH}.tar.gz"
cd ..
mv SnapOtter snapotter
tar czf "/tmp/${ARCHIVE_NAME}" --exclude='.git' --exclude='.github' --exclude='.gitignore' snapotter/
mv snapotter SnapOtter
cd SnapOtter
echo "archive_name=${ARCHIVE_NAME}" >> "$GITHUB_ENV"
echo "Archive: ${ARCHIVE_NAME} ($(du -sh /tmp/${ARCHIVE_NAME} | cut -f1))"
- name: Generate checksum
run: cd /tmp && sha256sum "${archive_name}" > "${archive_name}.sha256"
- name: Upload to GitHub Release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
VERSION: ${{ needs.release.outputs.new_version }}
run: gh release upload "v${VERSION}" "/tmp/${archive_name}" "/tmp/${archive_name}.sha256" --clobber --repo snapotter-hq/SnapOtter
docker:
name: Build (${{ matrix.platform }})
needs: release
# PUBLISHING PAUSED (2026-06-17): building and pushing the app image to
# Docker Hub + GHCR is disabled while the app is still being stabilized.
# The scan, sbom, ai-bundles, and manifest jobs all `need` this job, so
# they are skipped too and nothing reaches either registry. The release,
# changelog, and source-archive (prebuilt) jobs still run.
# To resume publishing, delete the `if: ${{ false }}` line below.
if: ${{ false }}
permissions:
contents: read
packages: write
strategy:
fail-fast: false
matrix:
include:
- platform: linux/amd64
runner: ubuntu-latest
- platform: linux/arm64
runner: ubuntu-24.04-arm
runs-on: ${{ matrix.runner }}
steps:
- name: Free disk space
run: |
sudo rm -rf /usr/share/dotnet /usr/local/lib/android /opt/ghc \
/usr/local/share/boost /opt/hostedtoolcache/CodeQL
sudo docker system prune -af
df -h /
- name: Prepare
run: |
platform=${{ matrix.platform }}
echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
- name: Checkout release tag
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
ref: v${{ needs.release.outputs.new_version }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0
- name: Log in to Docker Hub
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Log in to GitHub Container Registry
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GHCR_TOKEN }}
- name: Extract metadata
id: meta
uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6.1.0
with:
images: |
snapotter/snapotter
ghcr.io/snapotter-hq/snapotter
- name: Build and push by digest
id: build
uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0
with:
context: .
file: docker/Dockerfile
platforms: ${{ matrix.platform }}
labels: ${{ steps.meta.outputs.labels }}
outputs: type=image,"name=snapotter/snapotter,ghcr.io/snapotter-hq/snapotter",push-by-digest=true,name-canonical=true,push=true
cache-from: type=registry,ref=ghcr.io/snapotter-hq/snapotter:cache-${{ env.PLATFORM_PAIR }}
cache-to: type=registry,ref=ghcr.io/snapotter-hq/snapotter:cache-${{ env.PLATFORM_PAIR }},mode=max
- 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@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: digests-${{ env.PLATFORM_PAIR }}
path: /tmp/digests/*
if-no-files-found: error
retention-days: 1
scan:
name: Trivy Container Scan
needs: [release, docker]
runs-on: ubuntu-latest
permissions:
contents: write
packages: read
security-events: write
steps:
- name: Download amd64 digest
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: digests-linux-amd64
path: /tmp/digests
- name: Log in to GitHub Container Registry
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GHCR_TOKEN }}
- name: Get digest
id: digest
run: |
sha=$(ls /tmp/digests | head -1)
echo "sha=$sha" >> "$GITHUB_OUTPUT"
- name: Checkout for trivyignore
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
sparse-checkout: .trivyignore
sparse-checkout-cone-mode: false
- name: Run Trivy vulnerability scanner
uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0
with:
image-ref: "ghcr.io/snapotter-hq/snapotter@sha256:${{ steps.digest.outputs.sha }}"
format: "table"
exit-code: "1"
ignore-unfixed: true
severity: "CRITICAL,HIGH"
trivyignores: ".trivyignore"
- name: Upload results to GitHub Security
if: always()
uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0
with:
image-ref: "ghcr.io/snapotter-hq/snapotter@sha256:${{ steps.digest.outputs.sha }}"
format: "sarif"
output: "trivy-results.sarif"
ignore-unfixed: true
severity: "CRITICAL,HIGH"
- name: Upload SARIF
uses: github/codeql-action/upload-sarif@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v3
if: always()
with:
sarif_file: "trivy-results.sarif"
- name: Run Trivy (JSON report)
if: always()
uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0
with:
image-ref: "ghcr.io/snapotter-hq/snapotter@sha256:${{ steps.digest.outputs.sha }}"
format: "json"
output: "snapotter-v${{ needs.release.outputs.new_version }}-trivy.json"
ignore-unfixed: true
- name: Upload Trivy report to GitHub Release
if: always()
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
VERSION: ${{ needs.release.outputs.new_version }}
run: |
gh release upload "v${VERSION}" \
"snapotter-v${VERSION}-trivy.json" \
--clobber --repo snapotter-hq/SnapOtter
sbom:
name: Generate SBOM
needs: [release, docker]
runs-on: ubuntu-latest
permissions:
contents: write
packages: read
steps:
- name: Download amd64 digest
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: digests-linux-amd64
path: /tmp/digests
- name: Log in to GitHub Container Registry
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GHCR_TOKEN }}
- name: Get digest
id: digest
run: |
sha=$(ls /tmp/digests | head -1)
echo "sha=$sha" >> "$GITHUB_OUTPUT"
- name: Install Syft
uses: anchore/sbom-action/download-syft@e22c389904149dbc22b58101806040fa8d37a610 # v0.24.0
- name: Generate SBOMs
env:
IMAGE: "ghcr.io/snapotter-hq/snapotter@sha256:${{ steps.digest.outputs.sha }}"
VERSION: ${{ needs.release.outputs.new_version }}
run: |
syft scan "$IMAGE" -o "cyclonedx-json=snapotter-v${VERSION}-sbom.cdx.json"
syft scan "$IMAGE" -o "spdx-json=snapotter-v${VERSION}-sbom.spdx.json"
- name: Upload SBOMs to GitHub Release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
VERSION: ${{ needs.release.outputs.new_version }}
run: |
gh release upload "v${VERSION}" \
"snapotter-v${VERSION}-sbom.cdx.json" \
"snapotter-v${VERSION}-sbom.spdx.json" \
--clobber --repo snapotter-hq/SnapOtter
ai-bundles:
name: AI Bundles
needs: [release, docker]
if: needs.release.outputs.new_version
# The top-level `permissions: {}` default means this reusable-workflow call
# grants no token scopes by default. ai-bundles.yml's jobs declare
# `contents: read` / `packages: read`, and GitHub rejects a called workflow
# requesting scopes the caller never granted -- failing at startup before any
# job runs. Grant them here so the call passes startup validation.
permissions:
contents: read
packages: read
uses: ./.github/workflows/ai-bundles.yml
with:
version: ${{ needs.release.outputs.new_version }}
secrets: inherit
manifest:
name: Create Multi-Arch Manifests
needs: [release, docker, scan]
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Download digests
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
path: /tmp/digests
pattern: digests-*
merge-multiple: true
- name: Log in to Docker Hub
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Log in to GitHub Container Registry
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GHCR_TOKEN }}
- name: Extract metadata
id: meta
uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6.1.0
with:
images: |
snapotter/snapotter
ghcr.io/snapotter-hq/snapotter
tags: |
type=semver,pattern={{version}},value=v${{ needs.release.outputs.new_version }}
type=semver,pattern={{major}}.{{minor}},value=v${{ needs.release.outputs.new_version }}
type=semver,pattern={{major}},value=v${{ needs.release.outputs.new_version }}
type=raw,value=latest
- name: Create Docker Hub manifest
working-directory: /tmp/digests
run: |
docker buildx imagetools create \
$(jq -cr '.tags | map(select(startswith("snapotter/")) | "-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
$(printf 'snapotter/snapotter@sha256:%s ' *)
- name: Create GHCR manifest
working-directory: /tmp/digests
run: |
docker buildx imagetools create \
$(jq -cr '.tags | map(select(startswith("ghcr.io/")) | "-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
$(printf 'ghcr.io/snapotter-hq/snapotter@sha256:%s ' *)