Skip to content

ci(test): consolidate backend coverage job (#1830) #1

ci(test): consolidate backend coverage job (#1830)

ci(test): consolidate backend coverage job (#1830) #1

# Self-host image releases (#980). Cutting a `selfhost-v<semver>` tag builds the multi-arch image, pushes it
# to GHCR with version + latest + sha tags (with provenance + SBOM), and opens a GitHub Release.
#
# git tag selfhost-v0.1.0 && git push origin selfhost-v0.1.0
#
# Pull: docker pull ghcr.io/<owner>/gittensory-selfhost:0.1.0
name: release-selfhost
on:
push:
tags:
- "selfhost-v*"
workflow_dispatch:
inputs:
version:
description: "Version to publish (e.g. 0.1.0)"
required: true
permissions:
contents: write # create the GitHub Release
packages: write # push to GHCR
concurrency:
group: release-selfhost-${{ github.ref_name }}
cancel-in-progress: false
jobs:
release:
runs-on: ubuntu-latest
timeout-minutes: 40
# Environment gate — requires reviewer approval before a release runs (configure under repo Settings > Environments).
environment: release
env:
SENTRY_ORG: jsonbored
SENTRY_PROJECT: gittensory
SENTRY_CLI_PACKAGE: "@sentry/cli@3.6.0"
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
fetch-depth: 0
persist-credentials: false
- name: Verify release commit is on main
env:
RELEASE_SHA: ${{ github.sha }}
run: |
git fetch --no-tags origin main
if ! git merge-base --is-ancestor "$RELEASE_SHA" origin/main; then
echo "::error::Self-host releases must be cut from a commit reachable from main."
exit 1
fi
- name: Resolve version
id: version
env:
EVENT_NAME: ${{ github.event_name }}
INPUT_VERSION: ${{ github.event.inputs.version }}
REF_NAME: ${{ github.ref_name }}
run: |
set -euo pipefail
if [ "$EVENT_NAME" = "workflow_dispatch" ]; then
VERSION="$INPUT_VERSION"
else
case "$REF_NAME" in
selfhost-v*) VERSION="${REF_NAME#selfhost-v}" ;;
*) echo "expected a selfhost-v<semver> tag, got $REF_NAME" >&2; exit 1 ;;
esac
fi
if ! printf '%s' "$VERSION" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+$'; then
echo "expected semver version X.Y.Z, got $VERSION" >&2
exit 1
fi
echo "v=${VERSION}" >> "$GITHUB_OUTPUT"
echo "release=gittensory-selfhost@${VERSION}" >> "$GITHUB_OUTPUT"
# Release jobs receive publishing/Sentry credentials, so avoid shared dependency caches here.
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
node-version: "24.18.0"
- name: Install deps
run: npm ci --ignore-scripts
- name: Build self-host bundle for release
run: node scripts/build-selfhost.mjs --all
- name: Validate release source map
run: node scripts/validate-selfhost-sourcemap.mjs
- name: Detect Sentry release token
id: sentry
env:
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
run: |
if [ -n "$SENTRY_AUTH_TOKEN" ]; then
echo "enabled=true" >> "$GITHUB_OUTPUT"
else
echo "enabled=false" >> "$GITHUB_OUTPUT"
fi
- name: Require Sentry token for official release
if: github.repository == 'JSONbored/gittensory' && steps.sentry.outputs.enabled != 'true'
run: |
echo "::error::Configure SENTRY_AUTH_TOKEN in the release environment before publishing official self-host images."
exit 1
- name: Upload Sentry source maps
if: steps.sentry.outputs.enabled == 'true'
env:
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
SENTRY_ORG: ${{ vars.SENTRY_ORG || 'jsonbored' }}
SENTRY_PROJECT: ${{ vars.SENTRY_PROJECT || 'gittensory' }}
SENTRY_URL: ${{ vars.SENTRY_URL }}
SENTRY_RELEASE: ${{ steps.version.outputs.release }}
SENTRY_REPOSITORY: ${{ github.repository }}
SENTRY_COMMIT_SHA: ${{ github.sha }}
run: |
set -euo pipefail
test -n "$SENTRY_AUTH_TOKEN"
test -n "$SENTRY_ORG"
test -n "$SENTRY_PROJECT"
if [ -z "${SENTRY_URL:-}" ]; then unset SENTRY_URL; fi
npx -y "$SENTRY_CLI_PACKAGE" releases new "$SENTRY_RELEASE"
npx -y "$SENTRY_CLI_PACKAGE" releases set-commits "$SENTRY_RELEASE" --commit "$SENTRY_REPOSITORY@$SENTRY_COMMIT_SHA" --ignore-missing
npx -y "$SENTRY_CLI_PACKAGE" sourcemaps inject dist
node scripts/validate-selfhost-sourcemap.mjs
npx -y "$SENTRY_CLI_PACKAGE" sourcemaps upload --release="$SENTRY_RELEASE" --validate --wait --strict dist
- uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3
- uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3
- name: Log in to GHCR
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Image metadata
id: meta
uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6
with:
images: ghcr.io/${{ github.repository_owner }}/gittensory-selfhost
tags: |
type=raw,value=${{ steps.version.outputs.v }}
type=raw,value=latest
type=sha,format=short
labels: |
org.opencontainers.image.title=gittensory-selfhost
org.opencontainers.image.description=Self-hostable Gittensory review engine
org.opencontainers.image.version=${{ steps.version.outputs.v }}
org.opencontainers.image.revision=${{ github.sha }}
- name: Build + push (linux/amd64 + linux/arm64)
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6
with:
context: .
target: runtime-prebuilt
platforms: linux/amd64,linux/arm64
push: true
build-args: |
GITTENSORY_VERSION=${{ steps.version.outputs.release }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
provenance: true
sbom: true
- name: Finalize Sentry release
if: steps.sentry.outputs.enabled == 'true'
env:
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
SENTRY_ORG: ${{ vars.SENTRY_ORG || 'jsonbored' }}
SENTRY_PROJECT: ${{ vars.SENTRY_PROJECT || 'gittensory' }}
SENTRY_URL: ${{ vars.SENTRY_URL }}
SENTRY_RELEASE: ${{ steps.version.outputs.release }}
run: |
set -euo pipefail
if [ -z "${SENTRY_URL:-}" ]; then unset SENTRY_URL; fi
npx -y "$SENTRY_CLI_PACKAGE" releases finalize "$SENTRY_RELEASE"
- name: Validate Sentry release
env:
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
SENTRY_ORG: ${{ vars.SENTRY_ORG || 'jsonbored' }}
SENTRY_PROJECT: ${{ vars.SENTRY_PROJECT || 'gittensory' }}
SENTRY_URL: ${{ vars.SENTRY_URL }}
SENTRY_RELEASE: ${{ steps.version.outputs.release }}
SENTRY_REPOSITORY: ${{ github.repository }}
SENTRY_COMMIT_SHA: ${{ github.sha }}
SENTRY_REQUIRE_COMMITS: "true"
SENTRY_REQUIRE_DEPLOY: "false"
SENTRY_REQUIRE_FINALIZED: "true"
run: node review-enrichment/scripts/validate-sentry-release.mjs
- name: GitHub Release
if: github.event_name == 'push'
uses: softprops/action-gh-release@3bb12739c298aeb8a4eeaf626c5b8d85266b0e65 # v2
with:
generate_release_notes: true
body: |
Self-host container image:
```bash
docker pull ghcr.io/${{ github.repository_owner }}/gittensory-selfhost:${{ steps.version.outputs.v }}
```
Multi-arch (linux/amd64 + linux/arm64). See https://gittensory.aethereal.dev/docs/maintainer-self-hosting for setup.
Includes the Claude Code / Codex subscription CLIs by default; credentials stay runtime-only.
Sentry release id baked into the image: `${{ steps.version.outputs.release }}`.