mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
* fix(analytics): bake real Sentry DSN and lower trace sampling The bake script emitted a placeholder Sentry DSN even in on mode, so every published image initialized Sentry against a dead endpoint and no events ever reached the project. Point it at the real snapotter project DSN. Also drop tracesSampleRate from 1 to 0.1. It governs only performance transactions (errors are always captured), so 100% fleet-wide tracing would drain Sentry quota for no benefit. * fix(analytics): point baked Sentry DSN at the snapotter org * refactor(analytics): inject Sentry DSN + PostHog key from build env #336 replaced the analytics creds with placeholders but never added a way to put real values back at build time, so any image built from the repo since then ships dead analytics (the live fleet only still reports because publishing is paused and it runs a pre-placeholder image). Restore the pipeline the clean way: bake-analytics.mjs reads SNAPOTTER_SENTRY_DSN and SNAPOTTER_POSTHOG_KEY from the environment; the official image's CI supplies them from repo secrets via build args. A build with neither stays disabled, so building from source never phones home. Both values are public (they ship in the browser bundle), so this is about not making source builds report, not secrecy. Supersedes the hardcoded DSN: real creds are no longer committed to the repo.
461 lines
17 KiB
YAML
461 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@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
|
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@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
|
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@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
|
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 }}
|
|
build-args: |
|
|
SNAPOTTER_ANALYTICS=on
|
|
SNAPOTTER_POSTHOG_KEY=${{ secrets.SNAPOTTER_POSTHOG_KEY }}
|
|
SNAPOTTER_SENTRY_DSN=${{ secrets.SNAPOTTER_SENTRY_DSN }}
|
|
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@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
|
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 ' *)
|