Skip to content

feat(selfhost): gate priority label on linked-issue label propagation… #2

feat(selfhost): gate priority label on linked-issue label propagation…

feat(selfhost): gate priority label on linked-issue label propagation… #2

# Orb image releases (#980). Cutting an `orb-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 orb-v0.1.0 && git push origin orb-v0.1.0
#
# Pull: docker pull ghcr.io/<owner>/gittensory-selfhost:orb-v0.1.0
#
# Prerelease tags (#1937): orb-v0.1.0-rc.1 / orb-v0.1.0-beta.1 run the identical pipeline but never move
# `latest` and are marked prerelease on the GitHub Release -- for beta-testing an image before it becomes
# the stable/latest recommendation.
#
# git tag orb-v0.1.0-rc.1 && git push origin orb-v0.1.0-rc.1
name: release-orb
on:
push:
tags:
- "orb-v*"
workflow_dispatch:
inputs:
version:
description: "Version to publish (e.g. 0.1.0, or a prerelease 0.1.0-rc.1 / 0.1.0-beta.1)"
required: true
permissions:
contents: write # create the GitHub Release
packages: write # push to GHCR
concurrency:
group: release-orb-${{ 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
orb-v*) VERSION="${REF_NAME#orb-v}" ;;
*) echo "expected an orb-v<semver> tag, got $REF_NAME" >&2; exit 1 ;;
esac
fi
# #1937: a stable X.Y.Z tag is the only kind that ever moved `latest` or an unmarked GitHub
# Release; a prerelease tag (X.Y.Z-rc.N / X.Y.Z-beta.N) publishes the SAME image/provenance/SBOM/
# Sentry pipeline below, just flagged as prerelease and never pushed under `latest` (see the
# "Resolve image tags" and "GitHub Release" steps).
if ! printf '%s' "$VERSION" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+(-(rc|beta)\.[0-9]+)?$'; then
echo "expected semver version X.Y.Z, or a prerelease X.Y.Z-rc.N / X.Y.Z-beta.N, got $VERSION" >&2
exit 1
fi
PRERELEASE=false
if printf '%s' "$VERSION" | grep -Eq -- '-(rc|beta)\.[0-9]+$'; then
PRERELEASE=true
fi
{
echo "v=${VERSION}"
echo "tag=orb-v${VERSION}"
echo "release=gittensory-orb@${VERSION}"
echo "prerelease=${PRERELEASE}"
} >> "$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 Orb 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 }}
# #1937: `latest` must never move to a prerelease build -- an operator who blindly pulls `latest`
# for a trial should always land on the newest STABLE image, not an in-flight rc/beta.
- name: Resolve image tags
id: tags
env:
PRERELEASE: ${{ steps.version.outputs.prerelease }}
VERSION_TAG: ${{ steps.version.outputs.tag }}
run: |
set -euo pipefail
{
echo "list<<GTORBTAGS"
echo "type=raw,value=${VERSION_TAG}"
if [ "$PRERELEASE" != "true" ]; then
echo "type=raw,value=latest"
fi
echo "type=sha,format=short"
echo "GTORBTAGS"
} >> "$GITHUB_OUTPUT"
- name: Image metadata
id: meta
uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6
with:
images: ghcr.io/${{ github.repository_owner }}/gittensory-selfhost
tags: ${{ steps.tags.outputs.list }}
labels: |
org.opencontainers.image.title=gittensory-orb
org.opencontainers.image.description=Self-hostable Gittensory review engine
org.opencontainers.image.version=${{ steps.version.outputs.tag }}
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
# Matches selfhost.yml's CI build (same Dockerfile, same default GHA cache scope), so a release
# can inherit layers selfhost.yml already built and cached for the identical commit -- without
# this, every release rebuilt runtime-base from scratch under QEMU emulation for arm64, the most
# expensive leg (#2502).
cache-from: type=gha
cache-to: type=gha,mode=max
- 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'
env:
GH_TOKEN: ${{ github.token }}
REF_NAME: ${{ github.ref_name }}
RELEASE_VERSION: ${{ steps.version.outputs.v }}
RELEASE_TAG: ${{ steps.version.outputs.tag }}
RELEASE_ID: ${{ steps.version.outputs.release }}
REPOSITORY_OWNER: ${{ github.repository_owner }}
PRERELEASE: ${{ steps.version.outputs.prerelease }}
run: |
set -euo pipefail
NOTES="$(cat <<EOF
Gittensory Orb container image:
\`\`\`bash
docker pull ghcr.io/${REPOSITORY_OWNER}/gittensory-selfhost:${RELEASE_TAG}
\`\`\`
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: \`${RELEASE_ID}\`.
EOF
)"
# #1937: a prerelease tag never becomes the repo's "Latest release" and is visibly marked as such
# on GitHub -- distinct from the image-tag `latest` decision above (Resolve image tags), which
# this flag also drives at the version-resolution step.
PRERELEASE_ARGS=()
if [ "$PRERELEASE" = "true" ]; then
PRERELEASE_ARGS=(--prerelease --latest=false)
fi
if gh release view "$REF_NAME" --repo "$GITHUB_REPOSITORY" >/dev/null 2>&1; then
gh release edit "$REF_NAME" --repo "$GITHUB_REPOSITORY" \
--title "gittensory-orb ${RELEASE_TAG}" \
--notes "$NOTES" \
"${PRERELEASE_ARGS[@]}"
else
gh release create "$REF_NAME" --repo "$GITHUB_REPOSITORY" \
--verify-tag \
--title "gittensory-orb ${RELEASE_TAG}" \
"${PRERELEASE_ARGS[@]}" \
--notes "$NOTES" \
--generate-notes
fi