6bf8bebf51
CI / Test and Build (push) Failing after 1s
CI / Migrate Dev DB (push) Has been skipped
CI / Migrate DB (push) Has been skipped
CodeQL / Analyze actions (push) Has been cancelled
CodeQL / Analyze javascript-typescript (push) Has been cancelled
CI / Detect Version (push) Has been cancelled
CI / Detect Desktop Changes (push) Has been cancelled
CI / Build AMD64 (blacksmith-2vcpu-ubuntu-2404, ./docker/cron.Dockerfile, ubuntu-latest, ghcr.io/simstudioai/cron) (push) Has been cancelled
CI / Build AMD64 (blacksmith-2vcpu-ubuntu-2404, ./docker/db.Dockerfile, ECR_MIGRATIONS, ubuntu-latest, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (blacksmith-4vcpu-ubuntu-2404, ./docker/pii.Dockerfile, ECR_PII, ubuntu-latest, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (blacksmith-4vcpu-ubuntu-2404, ./docker/realtime.Dockerfile, ECR_REALTIME, ubuntu-latest, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build AMD64 (blacksmith-8vcpu-ubuntu-2404, ./docker/app.Dockerfile, ECR_APP, linux-x64-8-core, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (blacksmith-4vcpu-ubuntu-2404-arm, ./docker/cron.Dockerfile, ubuntu-24.04-arm, ghcr.io/simstudioai/cron) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (blacksmith-4vcpu-ubuntu-2404-arm, ./docker/db.Dockerfile, ubuntu-24.04-arm, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (blacksmith-4vcpu-ubuntu-2404-arm, ./docker/pii.Dockerfile, ubuntu-24.04-arm, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (blacksmith-4vcpu-ubuntu-2404-arm, ./docker/realtime.Dockerfile, ubuntu-24.04-arm, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (blacksmith-8vcpu-ubuntu-2404-arm, ./docker/app.Dockerfile, linux-arm64-8-core, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
Publish CLI Package / publish-npm (push) Has been cancelled
Publish Python SDK / publish-pypi (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
Helm Chart / Lint, test, and validate chart (push) Has been cancelled
Helm Chart / Chart version bumped (push) Has been cancelled
Publish TypeScript SDK / publish-npm (push) Has been cancelled
CI / Build Dev ECR (blacksmith-8vcpu-ubuntu-2404, ./docker/app.Dockerfile, ECR_APP, linux-x64-8-core) (push) Has been cancelled
CI / Promote Images (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/cron) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build Dev ECR (blacksmith-2vcpu-ubuntu-2404, ./docker/db.Dockerfile, ECR_MIGRATIONS, ubuntu-latest) (push) Has been cancelled
CI / Build Dev ECR (blacksmith-4vcpu-ubuntu-2404, ./docker/pii.Dockerfile, ECR_PII, ubuntu-latest) (push) Has been cancelled
CI / Build Dev ECR (blacksmith-4vcpu-ubuntu-2404, ./docker/realtime.Dockerfile, ECR_REALTIME, ubuntu-latest) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Check Desktop Signing Secrets (push) Has been cancelled
CI / Desktop Release (push) Has been cancelled
CI / Create Desktop Prerelease (push) Has been cancelled
CI / Desktop Prerelease Build (push) Has been cancelled
CI / Publish Desktop Prerelease (push) Has been cancelled
CI / Prune Desktop Prereleases (push) Has been cancelled
Helm Chart / Install on kind and run helm test (push) Has been cancelled
844 lines
37 KiB
YAML
844 lines
37 KiB
YAML
name: CI
|
||
|
||
# Runner provider toggle, read from the CI_PROVIDER repo variable:
|
||
#
|
||
# gh variable set CI_PROVIDER --body github # fall back to GitHub-hosted
|
||
# gh variable delete CI_PROVIDER # back to Blacksmith (default)
|
||
#
|
||
# A repo variable, not a committed value: during a Blacksmith outage there is no
|
||
# working CI to merge a switchover through. Only unset/'blacksmith' selects
|
||
# Blacksmith; anything unrecognized selects GitHub so a typo can't queue jobs
|
||
# against the provider you're escaping. Every runs-on and both composite actions
|
||
# share this predicate and must change together.
|
||
#
|
||
# GitHub mode is break-glass, not a peer — cold layers, slower runs. The app image
|
||
# is the one job on a paid larger runner: next build needs ~32 GB and OOM-kills
|
||
# (exit 137) on the free 16 GB runners at any heap ceiling.
|
||
|
||
on:
|
||
push:
|
||
branches: [main, staging, dev]
|
||
pull_request:
|
||
branches: [main, staging, dev]
|
||
# Docs content and markdown don't affect the app build or images; push
|
||
# runs stay unfiltered because they feed the deploy pipeline.
|
||
paths-ignore:
|
||
- 'apps/docs/content/**'
|
||
- '**/*.md'
|
||
|
||
concurrency:
|
||
group: ci-${{ github.ref }}
|
||
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
|
||
|
||
permissions:
|
||
contents: read
|
||
|
||
jobs:
|
||
test-build:
|
||
name: Test and Build
|
||
if: github.ref != 'refs/heads/dev' || github.event_name == 'pull_request'
|
||
uses: ./.github/workflows/test-build.yml
|
||
secrets: inherit
|
||
|
||
# Detect if this is a version release commit (e.g., "v0.5.24: ...")
|
||
# Smallest runner on purpose: a few seconds of pure shell over the commit
|
||
# message, no checkout and no install.
|
||
detect-version:
|
||
name: Detect Version
|
||
runs-on: ${{ (vars.CI_PROVIDER == '' || vars.CI_PROVIDER == 'blacksmith') && 'blacksmith-2vcpu-ubuntu-2404' || 'ubuntu-latest' }}
|
||
timeout-minutes: 5
|
||
if: github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/staging' || github.ref == 'refs/heads/dev')
|
||
outputs:
|
||
version: ${{ steps.extract.outputs.version }}
|
||
is_release: ${{ steps.extract.outputs.is_release }}
|
||
steps:
|
||
- name: Extract version from commit message
|
||
id: extract
|
||
env:
|
||
COMMIT_MSG: ${{ github.event.head_commit.message }}
|
||
run: |
|
||
# Only tag versions on main branch
|
||
if [ "$GITHUB_REF" = "refs/heads/main" ] && [[ "$COMMIT_MSG" =~ ^(v[0-9]+\.[0-9]+\.[0-9]+): ]]; then
|
||
VERSION="${BASH_REMATCH[1]}"
|
||
echo "version=${VERSION}" >> $GITHUB_OUTPUT
|
||
echo "is_release=true" >> $GITHUB_OUTPUT
|
||
echo "✅ Detected release commit: ${VERSION}"
|
||
else
|
||
echo "version=" >> $GITHUB_OUTPUT
|
||
echo "is_release=false" >> $GITHUB_OUTPUT
|
||
echo "ℹ️ Not a release commit"
|
||
fi
|
||
|
||
# Detect shell-code changes on dev/staging pushes. Web-only changes never
|
||
# need a desktop build (installed shells load the web app live); changes to
|
||
# the Electron app or the bridge packages trigger a per-env prerelease build
|
||
# (dev → alpha channel, staging → beta) that the env's update feed
|
||
# (/api/desktop/update) starts offering automatically.
|
||
detect-desktop-changes:
|
||
name: Detect Desktop Changes
|
||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||
timeout-minutes: 5
|
||
if: github.event_name == 'push' && (github.ref == 'refs/heads/dev' || github.ref == 'refs/heads/staging')
|
||
outputs:
|
||
changed: ${{ steps.diff.outputs.changed }}
|
||
steps:
|
||
- name: Checkout code
|
||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
|
||
with:
|
||
fetch-depth: 50
|
||
|
||
- name: Diff desktop paths
|
||
id: diff
|
||
env:
|
||
BEFORE: ${{ github.event.before }}
|
||
run: |
|
||
# Force pushes (dev resets) can reference a BEFORE we don't have;
|
||
# fall back to the previous commit, and to no build when even that
|
||
# is unavailable.
|
||
if [ -z "$BEFORE" ] || ! git cat-file -e "$BEFORE" 2>/dev/null; then
|
||
BEFORE="$(git rev-parse HEAD^ 2>/dev/null || echo '')"
|
||
fi
|
||
if [ -z "$BEFORE" ]; then
|
||
echo "changed=false" >> "$GITHUB_OUTPUT"
|
||
echo "ℹ️ No comparable base commit; skipping desktop prerelease"
|
||
exit 0
|
||
fi
|
||
if git diff --name-only "$BEFORE" HEAD | grep -qE '^(apps/desktop/|packages/desktop-bridge/|packages/browser-protocol/)'; then
|
||
echo "changed=true" >> "$GITHUB_OUTPUT"
|
||
echo "✅ Desktop shell code changed"
|
||
else
|
||
echo "changed=false" >> "$GITHUB_OUTPUT"
|
||
echo "ℹ️ No desktop shell changes"
|
||
fi
|
||
|
||
# Run database migrations before images are promoted: the ECR latest/staging
|
||
# tag push triggers CodePipeline, so migrating first guarantees the schema is
|
||
# in place before the new app version deploys (replaces the removed ECS
|
||
# migration sidecar)
|
||
migrate:
|
||
name: Migrate DB
|
||
needs: [test-build]
|
||
# Explicit need results instead of the implicit success(): a skipped job
|
||
# anywhere in the transitive needs chain silently fails implicit success()
|
||
# and cascade-skips the deploy chain (migrate -> promote-images ->
|
||
# CodeDeploy) — this bit us on 2026-07-23. State requirements explicitly.
|
||
if: >-
|
||
!cancelled() &&
|
||
needs.test-build.result == 'success' &&
|
||
github.event_name == 'push' &&
|
||
(github.ref == 'refs/heads/main' || github.ref == 'refs/heads/staging')
|
||
uses: ./.github/workflows/migrations.yml
|
||
with:
|
||
environment: ${{ github.ref == 'refs/heads/main' && 'production' || 'staging' }}
|
||
secrets: inherit
|
||
|
||
# Same ordering for dev (schema push before the dev image lands in ECR)
|
||
migrate-dev:
|
||
name: Migrate Dev DB
|
||
if: github.event_name == 'push' && github.ref == 'refs/heads/dev'
|
||
uses: ./.github/workflows/migrations.yml
|
||
with:
|
||
environment: dev
|
||
secrets: inherit
|
||
|
||
# Dev: build all 3 images for ECR only (no GHCR, no ARM64)
|
||
build-dev:
|
||
name: Build Dev ECR
|
||
needs: [detect-version, migrate-dev]
|
||
if: github.event_name == 'push' && github.ref == 'refs/heads/dev'
|
||
runs-on: ${{ (vars.CI_PROVIDER == '' || vars.CI_PROVIDER == 'blacksmith') && matrix.bs_runner || matrix.gh_runner }}
|
||
timeout-minutes: 30
|
||
permissions:
|
||
contents: read
|
||
id-token: write
|
||
strategy:
|
||
fail-fast: false
|
||
matrix:
|
||
include:
|
||
# Only the app image needs the paid 8-core/32 GB runner: next build
|
||
# exhausts the free 16 GB one (exit 137). The others build in <5 min.
|
||
# bs_runner mirrors that per-image sizing on Blacksmith — a single
|
||
# pinned tier put every image on 8 vCPU, where the non-app builds idle
|
||
# at 12-15% CPU and under 10% memory.
|
||
- dockerfile: ./docker/app.Dockerfile
|
||
ecr_repo_secret: ECR_APP
|
||
gh_runner: linux-x64-8-core
|
||
bs_runner: blacksmith-8vcpu-ubuntu-2404
|
||
- dockerfile: ./docker/db.Dockerfile
|
||
ecr_repo_secret: ECR_MIGRATIONS
|
||
gh_runner: ubuntu-latest
|
||
bs_runner: blacksmith-2vcpu-ubuntu-2404
|
||
- dockerfile: ./docker/realtime.Dockerfile
|
||
ecr_repo_secret: ECR_REALTIME
|
||
gh_runner: ubuntu-latest
|
||
bs_runner: blacksmith-4vcpu-ubuntu-2404
|
||
- dockerfile: ./docker/pii.Dockerfile
|
||
ecr_repo_secret: ECR_PII
|
||
gh_runner: ubuntu-latest
|
||
bs_runner: blacksmith-4vcpu-ubuntu-2404
|
||
steps:
|
||
- name: Checkout code
|
||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
|
||
|
||
- name: Configure AWS credentials
|
||
uses: aws-actions/configure-aws-credentials@e7f100cf4c008499ea8adda475de1042d6975c7b # v6
|
||
with:
|
||
role-to-assume: ${{ secrets.DEV_AWS_ROLE_TO_ASSUME }}
|
||
aws-region: ${{ secrets.DEV_AWS_REGION }}
|
||
|
||
- name: Login to Amazon ECR
|
||
id: login-ecr
|
||
uses: aws-actions/amazon-ecr-login@d539f0932e70871a027e9d5a9d8fc38589180a64 # v2
|
||
|
||
- name: Login to Docker Hub
|
||
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4
|
||
with:
|
||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||
|
||
- name: Resolve ECR repo name
|
||
id: ecr-repo
|
||
run: echo "name=$ECR_REPO" >> $GITHUB_OUTPUT
|
||
env:
|
||
ECR_REPO: ${{ matrix.ecr_repo_secret == 'ECR_APP' && secrets.ECR_APP || matrix.ecr_repo_secret == 'ECR_MIGRATIONS' && secrets.ECR_MIGRATIONS || matrix.ecr_repo_secret == 'ECR_REALTIME' && secrets.ECR_REALTIME || matrix.ecr_repo_secret == 'ECR_PII' && secrets.ECR_PII || '' }}
|
||
|
||
- name: Build and push
|
||
uses: ./.github/actions/docker-build
|
||
with:
|
||
provider: ${{ vars.CI_PROVIDER }}
|
||
file: ${{ matrix.dockerfile }}
|
||
platforms: linux/amd64
|
||
tags: ${{ steps.login-ecr.outputs.registry }}/${{ steps.ecr-repo.outputs.name }}:dev
|
||
|
||
# Dev: deploy Trigger.dev background tasks to the preview "dev-sim" branch.
|
||
# Gated after migrate-dev for the same reason as build-dev — the new task
|
||
# code runs against the dev DB, so the schema must be pushed first.
|
||
deploy-trigger-dev:
|
||
name: Deploy Trigger.dev (Dev)
|
||
needs: [migrate-dev]
|
||
if: github.event_name == 'push' && github.ref == 'refs/heads/dev'
|
||
runs-on: ${{ (vars.CI_PROVIDER == '' || vars.CI_PROVIDER == 'blacksmith') && 'blacksmith-4vcpu-ubuntu-2404' || 'ubuntu-latest' }}
|
||
timeout-minutes: 15
|
||
steps:
|
||
- name: Checkout code
|
||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
|
||
|
||
- name: Setup Bun
|
||
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
|
||
with:
|
||
bun-version: 1.3.14
|
||
|
||
- name: Cache Bun dependencies
|
||
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5
|
||
with:
|
||
path: |
|
||
~/.bun/install/cache
|
||
node_modules
|
||
**/node_modules
|
||
key: ${{ runner.os }}-bun-${{ hashFiles('**/bun.lock') }}
|
||
restore-keys: |
|
||
${{ runner.os }}-bun-
|
||
|
||
- name: Install dependencies
|
||
run: bun install --frozen-lockfile --ignore-scripts
|
||
|
||
- name: Deploy to Trigger.dev
|
||
working-directory: ./apps/sim
|
||
env:
|
||
TRIGGER_ACCESS_TOKEN: ${{ secrets.DEV_TRIGGER_ACCESS_TOKEN }}
|
||
TRIGGER_PROJECT_ID: ${{ secrets.TRIGGER_PROJECT_ID }}
|
||
run: |
|
||
if [ -z "$TRIGGER_ACCESS_TOKEN" ] || [ -z "$TRIGGER_PROJECT_ID" ]; then
|
||
echo "ERROR: DEV_TRIGGER_ACCESS_TOKEN and TRIGGER_PROJECT_ID repo secrets must both be set" >&2
|
||
exit 1
|
||
fi
|
||
bunx trigger.dev@4.5.7 deploy --env preview --branch dev-sim
|
||
|
||
# Main/staging: build AMD64 images and push sha-tagged images to ECR + GHCR.
|
||
# Runs in parallel with tests — only immutable sha tags are pushed here, and
|
||
# the CodePipeline EventBridge triggers filter on exactly the
|
||
# latest/staging/dev ECR tags, so nothing deploys and no mutable tag moves
|
||
# until promote-images / create-ghcr-manifests retag after the gate.
|
||
build-amd64:
|
||
name: Build AMD64
|
||
if: >-
|
||
github.event_name == 'push' &&
|
||
(github.ref == 'refs/heads/main' || github.ref == 'refs/heads/staging')
|
||
runs-on: ${{ (vars.CI_PROVIDER == '' || vars.CI_PROVIDER == 'blacksmith') && matrix.bs_runner || matrix.gh_runner }}
|
||
timeout-minutes: 30
|
||
permissions:
|
||
contents: read
|
||
packages: write
|
||
id-token: write
|
||
strategy:
|
||
fail-fast: false
|
||
matrix:
|
||
include:
|
||
- dockerfile: ./docker/app.Dockerfile
|
||
ghcr_image: ghcr.io/simstudioai/simstudio
|
||
ecr_repo_secret: ECR_APP
|
||
gh_runner: linux-x64-8-core
|
||
bs_runner: blacksmith-8vcpu-ubuntu-2404
|
||
- dockerfile: ./docker/db.Dockerfile
|
||
ghcr_image: ghcr.io/simstudioai/migrations
|
||
ecr_repo_secret: ECR_MIGRATIONS
|
||
gh_runner: ubuntu-latest
|
||
bs_runner: blacksmith-2vcpu-ubuntu-2404
|
||
- dockerfile: ./docker/realtime.Dockerfile
|
||
ghcr_image: ghcr.io/simstudioai/realtime
|
||
ecr_repo_secret: ECR_REALTIME
|
||
gh_runner: ubuntu-latest
|
||
bs_runner: blacksmith-4vcpu-ubuntu-2404
|
||
- dockerfile: ./docker/pii.Dockerfile
|
||
ghcr_image: ghcr.io/simstudioai/pii
|
||
ecr_repo_secret: ECR_PII
|
||
gh_runner: ubuntu-latest
|
||
bs_runner: blacksmith-4vcpu-ubuntu-2404
|
||
# No ECR repo is provisioned for cron, so it publishes to GHCR only.
|
||
# The tag step below omits the ECR tag when the repo name is empty.
|
||
- dockerfile: ./docker/cron.Dockerfile
|
||
ghcr_image: ghcr.io/simstudioai/cron
|
||
gh_runner: ubuntu-latest
|
||
bs_runner: blacksmith-2vcpu-ubuntu-2404
|
||
steps:
|
||
- name: Checkout code
|
||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
|
||
|
||
- name: Configure AWS credentials
|
||
uses: aws-actions/configure-aws-credentials@e7f100cf4c008499ea8adda475de1042d6975c7b # v6
|
||
with:
|
||
role-to-assume: ${{ github.ref == 'refs/heads/main' && secrets.AWS_ROLE_TO_ASSUME || secrets.STAGING_AWS_ROLE_TO_ASSUME }}
|
||
aws-region: ${{ github.ref == 'refs/heads/main' && secrets.AWS_REGION || secrets.STAGING_AWS_REGION }}
|
||
|
||
- name: Login to Amazon ECR
|
||
id: login-ecr
|
||
uses: aws-actions/amazon-ecr-login@d539f0932e70871a027e9d5a9d8fc38589180a64 # v2
|
||
|
||
- name: Login to Docker Hub
|
||
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4
|
||
with:
|
||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||
|
||
- name: Login to GHCR
|
||
if: github.ref == 'refs/heads/main'
|
||
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4
|
||
with:
|
||
registry: ghcr.io
|
||
username: ${{ github.repository_owner }}
|
||
password: ${{ secrets.GITHUB_TOKEN }}
|
||
|
||
- name: Resolve ECR repo name
|
||
id: ecr-repo
|
||
run: echo "name=$ECR_REPO" >> $GITHUB_OUTPUT
|
||
env:
|
||
ECR_REPO: ${{ matrix.ecr_repo_secret == 'ECR_APP' && secrets.ECR_APP || matrix.ecr_repo_secret == 'ECR_MIGRATIONS' && secrets.ECR_MIGRATIONS || matrix.ecr_repo_secret == 'ECR_REALTIME' && secrets.ECR_REALTIME || matrix.ecr_repo_secret == 'ECR_PII' && secrets.ECR_PII || '' }}
|
||
|
||
# Only sha tags here — the ECR deploy tags (latest/staging) are applied
|
||
# by promote-images and the GHCR latest-amd64/version tags by
|
||
# create-ghcr-manifests, both after tests and migrations pass.
|
||
- name: Generate tags
|
||
id: meta
|
||
run: |
|
||
ECR_REGISTRY="${{ steps.login-ecr.outputs.registry }}"
|
||
ECR_REPO="${{ steps.ecr-repo.outputs.name }}"
|
||
GHCR_IMAGE="${{ matrix.ghcr_image }}"
|
||
|
||
TAGS=""
|
||
if [ -n "$ECR_REPO" ]; then
|
||
TAGS="${ECR_REGISTRY}/${ECR_REPO}:${{ github.sha }}"
|
||
fi
|
||
|
||
if [ "${{ github.ref }}" = "refs/heads/main" ] && [ -n "$GHCR_IMAGE" ]; then
|
||
if [ -n "$TAGS" ]; then
|
||
TAGS="${TAGS},${GHCR_IMAGE}:${{ github.sha }}-amd64"
|
||
else
|
||
TAGS="${GHCR_IMAGE}:${{ github.sha }}-amd64"
|
||
fi
|
||
fi
|
||
|
||
# An entry can legitimately resolve to no tags — e.g. the cron image has
|
||
# no ECR repo, so on staging/dev (where GHCR tags are not applied) there
|
||
# is nothing to push. Skip that build instead of failing the job.
|
||
if [ -z "$TAGS" ]; then
|
||
echo "No ECR repo and no GHCR tag for this entry on ${{ github.ref }} — skipping push."
|
||
echo "skip=true" >> $GITHUB_OUTPUT
|
||
else
|
||
echo "skip=false" >> $GITHUB_OUTPUT
|
||
fi
|
||
|
||
echo "tags=${TAGS}" >> $GITHUB_OUTPUT
|
||
|
||
- name: Build and push images
|
||
if: steps.meta.outputs.skip != 'true'
|
||
uses: ./.github/actions/docker-build
|
||
with:
|
||
provider: ${{ vars.CI_PROVIDER }}
|
||
file: ${{ matrix.dockerfile }}
|
||
platforms: linux/amd64
|
||
tags: ${{ steps.meta.outputs.tags }}
|
||
|
||
# Promote the sha-tagged ECR images to the deploy tags once tests and
|
||
# migrations pass. Pushing the ECR latest/staging tag is what triggers
|
||
# CodePipeline, so this seconds-long manifest retag is the deploy gate —
|
||
# the image builds themselves run in parallel with the tests. A single job
|
||
# (not a matrix) so all four sha manifests are verified before any tag
|
||
# moves; a missing image can't produce a partial mixed-version deploy.
|
||
promote-images:
|
||
name: Promote Images
|
||
needs: [migrate, build-amd64]
|
||
# Explicit results: see migrate's comment.
|
||
if: >-
|
||
!cancelled() &&
|
||
needs.migrate.result == 'success' &&
|
||
needs.build-amd64.result == 'success' &&
|
||
github.event_name == 'push' &&
|
||
(github.ref == 'refs/heads/main' || github.ref == 'refs/heads/staging')
|
||
runs-on: ${{ (vars.CI_PROVIDER == '' || vars.CI_PROVIDER == 'blacksmith') && 'blacksmith-2vcpu-ubuntu-2404' || 'ubuntu-latest' }}
|
||
timeout-minutes: 10
|
||
permissions:
|
||
contents: read
|
||
id-token: write
|
||
steps:
|
||
- name: Configure AWS credentials
|
||
uses: aws-actions/configure-aws-credentials@e7f100cf4c008499ea8adda475de1042d6975c7b # v6
|
||
with:
|
||
role-to-assume: ${{ github.ref == 'refs/heads/main' && secrets.AWS_ROLE_TO_ASSUME || secrets.STAGING_AWS_ROLE_TO_ASSUME }}
|
||
aws-region: ${{ github.ref == 'refs/heads/main' && secrets.AWS_REGION || secrets.STAGING_AWS_REGION }}
|
||
|
||
- name: Login to Amazon ECR
|
||
id: login-ecr
|
||
uses: aws-actions/amazon-ecr-login@d539f0932e70871a027e9d5a9d8fc38589180a64 # v2
|
||
|
||
# Deploy-tag moves must be monotonic: a re-run of an old run must never
|
||
# retag latest/staging back to stale code. A superseded first-attempt
|
||
# run still promotes — the ci-<ref> concurrency group executes runs
|
||
# serially in commit order, so an ancestor of head is a forward deploy.
|
||
- name: Guard against stale promotion
|
||
id: guard
|
||
env:
|
||
GH_TOKEN: ${{ github.token }}
|
||
run: |
|
||
STATUS="$(gh api "repos/${{ github.repository }}/compare/${{ github.sha }}...${GITHUB_REF_NAME}" --jq '.status' || echo "unknown")"
|
||
if [ "$STATUS" = "identical" ] || { [ "$STATUS" = "ahead" ] && [ "${{ github.run_attempt }}" = "1" ]; }; then
|
||
echo "fresh=true" >> $GITHUB_OUTPUT
|
||
else
|
||
echo "::warning::Skipping promotion of ${{ github.sha }} (branch compare: ${STATUS}, attempt ${{ github.run_attempt }}). Moving the deploy tags here could deploy stale code; push a revert commit to roll back instead."
|
||
echo "fresh=false" >> $GITHUB_OUTPUT
|
||
fi
|
||
|
||
- name: Promote images to deploy tags
|
||
if: steps.guard.outputs.fresh == 'true'
|
||
env:
|
||
ECR_REPOS: >-
|
||
${{ secrets.ECR_APP }}
|
||
${{ secrets.ECR_MIGRATIONS }}
|
||
${{ secrets.ECR_REALTIME }}
|
||
${{ secrets.ECR_PII }}
|
||
run: |
|
||
REGISTRY="${{ steps.login-ecr.outputs.registry }}"
|
||
|
||
if [ "${{ github.ref }}" = "refs/heads/main" ]; then
|
||
ECR_TAG="latest"
|
||
else
|
||
ECR_TAG="staging"
|
||
fi
|
||
|
||
# Verify every sha image exists before moving any deploy tag, so a
|
||
# missing/expired image aborts the whole promotion up front.
|
||
for repo in $ECR_REPOS; do
|
||
echo "🔍 Verifying ${repo}:${{ github.sha }}"
|
||
docker buildx imagetools inspect "${REGISTRY}/${repo}:${{ github.sha }}" > /dev/null
|
||
done
|
||
|
||
for repo in $ECR_REPOS; do
|
||
echo "🚀 Promoting ${repo}:${{ github.sha }} to ${ECR_TAG}"
|
||
docker buildx imagetools create \
|
||
-t "${REGISTRY}/${repo}:${ECR_TAG}" \
|
||
"${REGISTRY}/${repo}:${{ github.sha }}"
|
||
done
|
||
|
||
# Build ARM64 images for GHCR (main branch only, runs in parallel with
|
||
# tests). Pushes only the immutable sha tag — latest-arm64/version-arm64
|
||
# are applied by create-ghcr-manifests after the gate, so a failing run
|
||
# never moves a documented tag.
|
||
build-ghcr-arm64:
|
||
name: Build ARM64 (GHCR Only)
|
||
runs-on: ${{ (vars.CI_PROVIDER == '' || vars.CI_PROVIDER == 'blacksmith') && matrix.bs_runner || matrix.gh_runner }}
|
||
timeout-minutes: 30
|
||
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
||
permissions:
|
||
contents: read
|
||
packages: write
|
||
strategy:
|
||
fail-fast: false
|
||
matrix:
|
||
# Non-app images sit at 4 vCPU rather than the finer x64 split: the ARM
|
||
# sizing data is job-level (8 -> 4 for the whole matrix), not per-image,
|
||
# and this job only runs on push to main — an unprovisioned label would
|
||
# hang a release in `queued` rather than fail a PR.
|
||
include:
|
||
- dockerfile: ./docker/app.Dockerfile
|
||
image: ghcr.io/simstudioai/simstudio
|
||
gh_runner: linux-arm64-8-core
|
||
bs_runner: blacksmith-8vcpu-ubuntu-2404-arm
|
||
- dockerfile: ./docker/db.Dockerfile
|
||
image: ghcr.io/simstudioai/migrations
|
||
gh_runner: ubuntu-24.04-arm
|
||
bs_runner: blacksmith-4vcpu-ubuntu-2404-arm
|
||
- dockerfile: ./docker/realtime.Dockerfile
|
||
image: ghcr.io/simstudioai/realtime
|
||
gh_runner: ubuntu-24.04-arm
|
||
bs_runner: blacksmith-4vcpu-ubuntu-2404-arm
|
||
- dockerfile: ./docker/pii.Dockerfile
|
||
image: ghcr.io/simstudioai/pii
|
||
gh_runner: ubuntu-24.04-arm
|
||
bs_runner: blacksmith-4vcpu-ubuntu-2404-arm
|
||
- dockerfile: ./docker/cron.Dockerfile
|
||
image: ghcr.io/simstudioai/cron
|
||
gh_runner: ubuntu-24.04-arm
|
||
bs_runner: blacksmith-4vcpu-ubuntu-2404-arm
|
||
|
||
steps:
|
||
- name: Checkout code
|
||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
|
||
|
||
- name: Login to GHCR
|
||
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4
|
||
with:
|
||
registry: ghcr.io
|
||
username: ${{ github.repository_owner }}
|
||
password: ${{ secrets.GITHUB_TOKEN }}
|
||
|
||
- name: Build and push ARM64 to GHCR
|
||
uses: ./.github/actions/docker-build
|
||
with:
|
||
provider: ${{ vars.CI_PROVIDER }}
|
||
file: ${{ matrix.dockerfile }}
|
||
platforms: linux/arm64
|
||
tags: ${{ matrix.image }}:${{ github.sha }}-arm64
|
||
|
||
# Publish all mutable GHCR tags (latest, latest-amd64/arm64, version tags)
|
||
# and the multi-arch manifests from the immutable sha tags — only on main,
|
||
# after the deploy gate (promote-images) and the ARM64 build both pass.
|
||
create-ghcr-manifests:
|
||
name: Create GHCR Manifests
|
||
runs-on: ${{ (vars.CI_PROVIDER == '' || vars.CI_PROVIDER == 'blacksmith') && 'blacksmith-2vcpu-ubuntu-2404' || 'ubuntu-latest' }}
|
||
timeout-minutes: 10
|
||
needs: [promote-images, build-ghcr-arm64, detect-version]
|
||
# Explicit results: see migrate's comment.
|
||
if: >-
|
||
!cancelled() &&
|
||
needs.promote-images.result == 'success' &&
|
||
needs.build-ghcr-arm64.result == 'success' &&
|
||
needs.detect-version.result == 'success' &&
|
||
github.event_name == 'push' && github.ref == 'refs/heads/main'
|
||
permissions:
|
||
contents: read
|
||
packages: write
|
||
strategy:
|
||
matrix:
|
||
include:
|
||
- image: ghcr.io/simstudioai/simstudio
|
||
- image: ghcr.io/simstudioai/migrations
|
||
- image: ghcr.io/simstudioai/realtime
|
||
- image: ghcr.io/simstudioai/pii
|
||
- image: ghcr.io/simstudioai/cron
|
||
|
||
steps:
|
||
- name: Login to GHCR
|
||
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4
|
||
with:
|
||
registry: ghcr.io
|
||
username: ${{ github.repository_owner }}
|
||
password: ${{ secrets.GITHUB_TOKEN }}
|
||
|
||
# Same monotonic guard as promote-images, applied to the public latest
|
||
# tags only — immutable sha and version tags are always published.
|
||
- name: Guard against stale latest tags
|
||
id: guard
|
||
env:
|
||
GH_TOKEN: ${{ github.token }}
|
||
run: |
|
||
STATUS="$(gh api "repos/${{ github.repository }}/compare/${{ github.sha }}...${GITHUB_REF_NAME}" --jq '.status' || echo "unknown")"
|
||
if [ "$STATUS" = "identical" ] || { [ "$STATUS" = "ahead" ] && [ "${{ github.run_attempt }}" = "1" ]; }; then
|
||
echo "fresh=true" >> $GITHUB_OUTPUT
|
||
else
|
||
echo "::warning::Publishing immutable tags for ${{ github.sha }} but skipping the latest tags (branch compare: ${STATUS}, attempt ${{ github.run_attempt }})."
|
||
echo "fresh=false" >> $GITHUB_OUTPUT
|
||
fi
|
||
|
||
- name: Publish tags and manifests
|
||
run: |
|
||
IMAGE="${{ matrix.image }}"
|
||
SHA="${{ github.sha }}"
|
||
|
||
# Multi-arch manifest from the immutable per-arch sha tags
|
||
docker buildx imagetools create -t "${IMAGE}:${SHA}" \
|
||
"${IMAGE}:${SHA}-amd64" "${IMAGE}:${SHA}-arm64"
|
||
|
||
if [ "${{ needs.detect-version.outputs.is_release }}" = "true" ]; then
|
||
VERSION="${{ needs.detect-version.outputs.version }}"
|
||
echo "📦 Publishing version tags: ${VERSION}"
|
||
docker buildx imagetools create -t "${IMAGE}:${VERSION}-amd64" "${IMAGE}:${SHA}-amd64"
|
||
docker buildx imagetools create -t "${IMAGE}:${VERSION}-arm64" "${IMAGE}:${SHA}-arm64"
|
||
docker buildx imagetools create -t "${IMAGE}:${VERSION}" \
|
||
"${IMAGE}:${SHA}-amd64" "${IMAGE}:${SHA}-arm64"
|
||
fi
|
||
|
||
if [ "${{ steps.guard.outputs.fresh }}" = "true" ]; then
|
||
docker buildx imagetools create -t "${IMAGE}:latest-amd64" "${IMAGE}:${SHA}-amd64"
|
||
docker buildx imagetools create -t "${IMAGE}:latest-arm64" "${IMAGE}:${SHA}-arm64"
|
||
docker buildx imagetools create -t "${IMAGE}:latest" \
|
||
"${IMAGE}:${SHA}-amd64" "${IMAGE}:${SHA}-arm64"
|
||
fi
|
||
|
||
# Check if docs changed
|
||
# Smallest runner on purpose: a depth-2 checkout plus a path filter, no
|
||
# install and no build.
|
||
check-docs-changes:
|
||
name: Check Docs Changes
|
||
runs-on: ${{ (vars.CI_PROVIDER == '' || vars.CI_PROVIDER == 'blacksmith') && 'blacksmith-2vcpu-ubuntu-2404' || 'ubuntu-latest' }}
|
||
timeout-minutes: 5
|
||
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
||
outputs:
|
||
docs_changed: ${{ steps.filter.outputs.docs }}
|
||
steps:
|
||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
|
||
with:
|
||
fetch-depth: 2 # Need at least 2 commits to detect changes
|
||
- uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4
|
||
id: filter
|
||
with:
|
||
filters: |
|
||
docs:
|
||
- 'apps/docs/content/docs/en/**'
|
||
- 'apps/sim/scripts/process-docs.ts'
|
||
- 'apps/sim/lib/chunkers/**'
|
||
|
||
# Process docs embeddings (only when docs change, after images are promoted)
|
||
process-docs:
|
||
name: Process Docs
|
||
needs: [promote-images, check-docs-changes]
|
||
# Explicit results: see migrate's comment.
|
||
if: >-
|
||
!cancelled() &&
|
||
needs.promote-images.result == 'success' &&
|
||
needs.check-docs-changes.result == 'success' &&
|
||
needs.check-docs-changes.outputs.docs_changed == 'true'
|
||
uses: ./.github/workflows/docs-embeddings.yml
|
||
secrets: inherit
|
||
|
||
# Create GitHub Release (only for version commits on main, after all builds complete)
|
||
create-release:
|
||
name: Create GitHub Release
|
||
runs-on: ${{ (vars.CI_PROVIDER == '' || vars.CI_PROVIDER == 'blacksmith') && 'blacksmith-4vcpu-ubuntu-2404' || 'ubuntu-latest' }}
|
||
timeout-minutes: 10
|
||
needs: [create-ghcr-manifests, detect-version]
|
||
# Explicit results: see migrate's comment.
|
||
if: >-
|
||
!cancelled() &&
|
||
needs.create-ghcr-manifests.result == 'success' &&
|
||
needs.detect-version.result == 'success' &&
|
||
needs.detect-version.outputs.is_release == 'true'
|
||
permissions:
|
||
contents: write
|
||
steps:
|
||
- name: Checkout code
|
||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
|
||
with:
|
||
fetch-depth: 0
|
||
|
||
- name: Setup Bun
|
||
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
|
||
with:
|
||
bun-version: 1.3.14
|
||
|
||
- name: Install dependencies
|
||
run: bun install --frozen-lockfile --ignore-scripts
|
||
|
||
- name: Create release
|
||
env:
|
||
GH_PAT: ${{ secrets.GITHUB_TOKEN }}
|
||
run: bun run scripts/create-single-release.ts ${{ needs.detect-version.outputs.version }}
|
||
|
||
# Desktop release: builds, signs, notarizes, and attaches the macOS app to
|
||
# the GitHub release created above. Gated on the Apple signing secrets so a
|
||
# release pipeline run skips cleanly (instead of failing) until the Apple
|
||
# Developer account is provisioned — the moment the six secrets exist, the
|
||
# next vX.Y.Z release ships desktop artifacts with no further changes.
|
||
# Job-level `if:` cannot read the secrets context, hence the probe job.
|
||
check-desktop-signing:
|
||
name: Check Desktop Signing Secrets
|
||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||
timeout-minutes: 2
|
||
needs: [detect-version, detect-desktop-changes]
|
||
# !cancelled(): detect-desktop-changes is skipped on main (and
|
||
# detect-version tags only on main); either path may need the probe.
|
||
if: ${{ !cancelled() && (needs.detect-version.outputs.is_release == 'true' || needs.detect-desktop-changes.outputs.changed == 'true') }}
|
||
outputs:
|
||
configured: ${{ steps.check.outputs.configured }}
|
||
steps:
|
||
- name: Probe Apple signing secrets
|
||
id: check
|
||
env:
|
||
CONFIGURED: ${{ secrets.CSC_LINK != '' && secrets.CSC_KEY_PASSWORD != '' && secrets.APPLE_API_KEY_P8 != '' && secrets.APPLE_API_KEY_ID != '' && secrets.APPLE_API_ISSUER != '' && secrets.APPLE_TEAM_ID != '' }}
|
||
run: |
|
||
echo "configured=${CONFIGURED}" >> "$GITHUB_OUTPUT"
|
||
if [ "$CONFIGURED" != "true" ]; then
|
||
echo "::warning::Desktop release skipped: Apple signing secrets are not configured (CSC_LINK, CSC_KEY_PASSWORD, APPLE_API_KEY_P8, APPLE_API_KEY_ID, APPLE_API_ISSUER, APPLE_TEAM_ID)."
|
||
fi
|
||
|
||
desktop-release:
|
||
name: Desktop Release
|
||
needs: [create-release, check-desktop-signing, detect-version]
|
||
if: needs.check-desktop-signing.outputs.configured == 'true'
|
||
permissions:
|
||
contents: write
|
||
uses: ./.github/workflows/desktop-release.yml
|
||
with:
|
||
version: ${{ needs.detect-version.outputs.version }}
|
||
publish: true
|
||
secrets: inherit
|
||
|
||
# Per-env desktop prereleases: a dev/staging push that touches shell code
|
||
# publishes a channel-tagged GitHub prerelease (vX.Y.Z-alpha.N from dev,
|
||
# vX.Y.Z-beta.N from staging). Each environment's /api/desktop/update feed
|
||
# offers only its channel, so dev-pointed shells pick up alpha builds,
|
||
# staging-pointed shells beta builds, and prod-pointed shells stable
|
||
# releases — independently. Unlike stable releases, prereleases build even
|
||
# before the Apple signing secrets exist — unsigned, so the update pipeline
|
||
# is testable end to end; installed shells detect the missing Developer ID
|
||
# and offer a manual download instead of a Squirrel install.
|
||
create-desktop-prerelease:
|
||
name: Create Desktop Prerelease
|
||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||
timeout-minutes: 5
|
||
needs: [detect-desktop-changes, check-desktop-signing]
|
||
# Requires the signing probe to have actually succeeded (not just "not
|
||
# cancelled") so a probe failure can't produce a release with no build.
|
||
if: ${{ !cancelled() && needs.detect-desktop-changes.outputs.changed == 'true' && needs.check-desktop-signing.result == 'success' }}
|
||
permissions:
|
||
contents: write
|
||
outputs:
|
||
version: ${{ steps.version.outputs.version }}
|
||
steps:
|
||
- name: Checkout code
|
||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
|
||
|
||
- name: Compute prerelease version and create draft release
|
||
id: version
|
||
env:
|
||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||
GH_REPO: ${{ github.repository }}
|
||
SIGNED: ${{ needs.check-desktop-signing.outputs.configured }}
|
||
run: |
|
||
if [ "$GITHUB_REF" = "refs/heads/dev" ]; then CHANNEL=alpha; APP_NAME="Sim Dev"; else CHANNEL=beta; APP_NAME="Sim Staging"; fi
|
||
# Prerelease core = next patch after the latest stable release, so
|
||
# channel builds always outrank the stable they are built on top of
|
||
# and are always superseded by the next stable. The run-attempt
|
||
# suffix keeps re-runs of the same workflow from colliding on the
|
||
# tag while preserving semver ordering.
|
||
# Fail loudly if the query itself fails: silently falling back to
|
||
# v0.0.0 would publish a channel build that sorts below the shipped
|
||
# stable, and installed shells would never see it as an update.
|
||
if ! LATEST="$(gh release list --exclude-pre-releases --limit 1 --json tagName --jq '.[0].tagName')"; then
|
||
echo "::error::Could not query the latest stable release."
|
||
exit 1
|
||
fi
|
||
# An empty release list makes jq print "null", which ${VAR:-default}
|
||
# does not treat as empty. Anything that is not a bare vX.Y.Z means
|
||
# "no stable release to build on top of" — start the channel at 0.0.1.
|
||
if [[ ! "$LATEST" =~ ^v?[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
||
LATEST="v0.0.0"
|
||
fi
|
||
IFS='.' read -r MAJOR MINOR PATCH <<< "${LATEST#v}"
|
||
TAG="v${MAJOR}.${MINOR}.$((PATCH + 1))-${CHANNEL}.${GITHUB_RUN_NUMBER}.${GITHUB_RUN_ATTEMPT}"
|
||
NOTES="Automated ${CHANNEL}-channel desktop build from ${GITHUB_REF_NAME} @ ${GITHUB_SHA::7}."
|
||
if [ "$SIGNED" != "true" ]; then
|
||
NOTES="$NOTES
|
||
|
||
⚠️ Unsigned test build (Apple signing secrets not configured). Gatekeeper will quarantine a downloaded copy: right-click → Open, or clear the flag with \`xattr -dr com.apple.quarantine \"/Applications/${APP_NAME}.app\"\`."
|
||
fi
|
||
# Draft until the build uploads its artifacts: drafts are invisible
|
||
# to the update feed, so a failed or in-flight build can never take
|
||
# the channel down with an assetless release. Publishing later also
|
||
# defers tag creation, so failed builds strand no tags.
|
||
gh release create "$TAG" \
|
||
--draft \
|
||
--prerelease \
|
||
--target "$GITHUB_SHA" \
|
||
--title "$TAG" \
|
||
--notes "$NOTES"
|
||
echo "version=$TAG" >> "$GITHUB_OUTPUT"
|
||
echo "✅ Created draft prerelease $TAG"
|
||
|
||
desktop-prerelease:
|
||
name: Desktop Prerelease Build
|
||
needs: [create-desktop-prerelease, check-desktop-signing]
|
||
permissions:
|
||
contents: write
|
||
uses: ./.github/workflows/desktop-release.yml
|
||
with:
|
||
version: ${{ needs.create-desktop-prerelease.outputs.version }}
|
||
publish: true
|
||
sign: ${{ needs.check-desktop-signing.outputs.configured == 'true' }}
|
||
secrets: inherit
|
||
|
||
# The draft only becomes visible to the update feed once its artifacts are
|
||
# attached — this is what makes a dev/staging push atomic from the shell's
|
||
# point of view.
|
||
publish-desktop-prerelease:
|
||
name: Publish Desktop Prerelease
|
||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||
timeout-minutes: 5
|
||
needs: [create-desktop-prerelease, desktop-prerelease]
|
||
permissions:
|
||
contents: write
|
||
env:
|
||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||
GH_REPO: ${{ github.repository }}
|
||
TAG: ${{ needs.create-desktop-prerelease.outputs.version }}
|
||
steps:
|
||
- name: Publish the draft release
|
||
run: gh release edit "$TAG" --draft=false
|
||
|
||
# Keep the release list tidy: per channel, retain the newest 5 prereleases
|
||
# and delete the rest (with their tags, so dev force-resets don't strand
|
||
# commits behind stale tags). Leftover drafts (failed or superseded builds)
|
||
# are always garbage by this point — the current run's release is published.
|
||
prune-desktop-prereleases:
|
||
name: Prune Desktop Prereleases
|
||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||
timeout-minutes: 5
|
||
needs: [publish-desktop-prerelease]
|
||
permissions:
|
||
contents: write
|
||
env:
|
||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||
GH_REPO: ${{ github.repository }}
|
||
steps:
|
||
- name: Delete stale prereleases
|
||
run: |
|
||
if [ "$GITHUB_REF" = "refs/heads/dev" ]; then CHANNEL=alpha; else CHANNEL=beta; fi
|
||
gh release list --limit 100 --json tagName,isPrerelease,isDraft,createdAt \
|
||
--jq "[.[] | select(.isPrerelease and (.isDraft | not) and (.tagName | test(\"-${CHANNEL}\\\\.\")))] | sort_by(.createdAt) | reverse | .[5:] | .[].tagName" |
|
||
while read -r TAG; do
|
||
[ -n "$TAG" ] || continue
|
||
echo "Deleting stale prerelease $TAG"
|
||
gh release delete "$TAG" --cleanup-tag --yes
|
||
done
|
||
|
||
- name: Delete leftover draft prereleases
|
||
run: |
|
||
if [ "$GITHUB_REF" = "refs/heads/dev" ]; then CHANNEL=alpha; else CHANNEL=beta; fi
|
||
# Drafts have no tag ref, so delete by release id via the API
|
||
# (gh release delete resolves by tag, which is ambiguous for drafts).
|
||
gh api "repos/${GH_REPO}/releases?per_page=100" \
|
||
--jq ".[] | select(.draft and (.tag_name | test(\"-${CHANNEL}\\\\.\"))) | .id" |
|
||
while read -r ID; do
|
||
[ -n "$ID" ] || continue
|
||
echo "Deleting leftover draft release $ID"
|
||
gh api -X DELETE "repos/${GH_REPO}/releases/${ID}"
|
||
done
|