mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
Harden Docker runtime packaging, preserve async job response semantics, fix Redis subscriber startup connections, clear lint warnings, and harden enterprise S3 object body handling.
464 lines
17 KiB
YAML
464 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_PROJECT_ID=${{ secrets.SNAPOTTER_POSTHOG_KEY }}
|
|
SNAPOTTER_SENTRY_DSN=${{ secrets.SNAPOTTER_SENTRY_DSN }}
|
|
SENTRY_RELEASE=${{ needs.release.outputs.new_version }}
|
|
secrets: |
|
|
sentry_auth_token=${{ secrets.SENTRY_AUTH_TOKEN }}
|
|
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 ' *)
|