Skip to content

Release: 0.2.0-rc0

Release: 0.2.0-rc0 #3

Workflow file for this run

name: Tagged releases
# Sequential publish: e2e → docker → github-release → screenshots-pr. The
# GitHub Release lands LAST so a Docker-push failure can't strand the project
# with an immutable Release pointing at a non-existent image. Pages deployment
# is decoupled into deploy-pages.yml — it runs from a workflow_run trigger.
on:
push:
tags:
- 'v*'
permissions:
contents: read
concurrency:
group: release-${{ github.ref_name }}
cancel-in-progress: false
jobs:
validate-tag:
name: Validate tag
runs-on: ubuntu-22.04
steps:
- name: Check tag-name format
env:
REF_NAME: ${{ github.ref_name }}
run: |
set -euo pipefail
# Allow vM.m.p as well (suffix-free, reserved for future "stable" releases) —
# the prerelease format vM.m.p-(rc|beta)N is the only one bump.sh currently
# emits, but accepting both makes :latest Docker promotion sensible.
if [[ ! "${REF_NAME}" =~ ^v[0-9]{1,2}\.[0-9]{1,2}\.[0-9]{1,2}(-(rc|beta)[0-9]{1,2})?$ ]]; then
echo "Tag '${REF_NAME}' does not match vM.m.p or vM.m.p-(rc|beta)N" >&2
echo "Releases must be cut via the 'Release prepare' workflow." >&2
exit 1
fi
- name: Checkout source code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0
persist-credentials: false
- name: Verify tag commit is on main
run: |
set -euo pipefail
git fetch origin main --no-tags
if ! git merge-base --is-ancestor HEAD origin/main; then
echo "::error::Tag commit is not reachable from origin/main. Tags must come from main." >&2
exit 1
fi
- name: Setup environment
uses: ./.github/actions/common-setup
- name: Verify package.json#version matches tag
env:
REF_NAME: ${{ github.ref_name }}
run: |
set -euo pipefail
tag_name="${REF_NAME#v}"
parsed=$(./tools/release/bump.sh --mode=check)
current_name=$(echo "$parsed" | grep -E '^current_name=' | cut -d= -f2)
if [[ "$current_name" != "$tag_name" ]]; then
echo "Tag '${REF_NAME}' does not match package.json#version '$current_name'" >&2
echo "package.json must equal the tag — releases must be cut via 'Release prepare'." >&2
exit 1
fi
e2e:
needs: validate-tag
name: E2E + screenshots (real sync-server)
runs-on: ubuntu-22.04
timeout-minutes: 25
# Boots the same digest-pinned sync-server the smoke job uses, seeds a fake
# Pixel 7 peer, drives the SPA through Playwright, captures screenshots.
# Failure here BLOCKS docker/github-release/pages — see job ordering below.
services:
sync-server:
image: ghcr.io/d4rken-org/octi-server@sha256:3829efba5ca5a4d407a0d0a048b8d0c20264ad2f9e389aa087c857ea0d0bddaa
ports:
- 18080:8080
env:
OCTI_CORS_ALLOWED_ORIGINS: 'http://127.0.0.1:4173,http://localhost:4173'
steps:
- name: Checkout source code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with: { persist-credentials: false }
- name: Setup environment
uses: ./.github/actions/common-setup
- name: Wait for sync-server
run: |
set -euo pipefail
for attempt in $(seq 1 30); do
if curl -sf http://127.0.0.1:18080/v1/status > /dev/null; then
echo "sync-server up after ${attempt}s"
exit 0
fi
sleep 1
done
echo "sync-server did not respond within 30s" >&2
docker logs ${{ job.services.sync-server.id }} || true
exit 1
- name: Bootstrap fake phone peer
env:
SYNC_SERVER_URL: http://127.0.0.1:18080
OUTPUT_PATH: bootstrap-peer.json
run: pnpm bootstrap-peer
- name: Install Playwright browsers
run: pnpm exec playwright install --with-deps chromium
- name: Build SPA (stable channel, tag version)
env:
VITE_CHANNEL: stable
VITE_COMMIT_SHA: ${{ github.sha }}
VITE_APP_VERSION: ${{ github.ref_name }}
run: pnpm build
- name: Run E2E screenshot capture
env:
BOOTSTRAP_PEER_FILE: bootstrap-peer.json
run: pnpm e2e
- name: Generate thumbnails
run: pnpm exec node scripts/generate-thumbs.mjs
- name: Upload screenshots artifact
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: screenshots-${{ github.ref_name }}
path: screenshots/
retention-days: 14
- name: Upload Playwright trace on failure
if: failure()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: playwright-trace-${{ github.run_id }}
path: |
test-results/
playwright-report/
if-no-files-found: ignore
- name: Dump sync-server logs on failure
if: failure()
run: docker logs ${{ job.services.sync-server.id }} || true
release-docker:
needs: e2e
name: Publish Docker image (ghcr.io)
runs-on: ubuntu-22.04
permissions:
contents: read
packages: write
steps:
- name: Checkout source code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Log in to ghcr.io
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
# `:latest` only on suffix-free tags (vM.m.p) — RCs and betas don't become
# the default. Today bump.sh always emits a suffix; this is forward-compat
# for when we cut a stable 1.0.0.
- name: Compute Docker tags
id: tags
env:
REF_NAME: ${{ github.ref_name }}
run: |
set -euo pipefail
tags=("ghcr.io/${{ github.repository }}:${REF_NAME}")
if [[ ! "${REF_NAME}" == *-* ]]; then
tags+=("ghcr.io/${{ github.repository }}:latest")
fi
printf '%s\n' "${tags[@]}"
# Join with newlines for docker/build-push-action's tags input.
{
echo "list<<EOF"
printf '%s\n' "${tags[@]}"
echo "EOF"
} >> "$GITHUB_OUTPUT"
- name: Build and push image
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
with:
context: .
push: true
tags: ${{ steps.tags.outputs.list }}
platforms: linux/amd64,linux/arm64
build-args: |
VITE_BASE=/
VITE_CHANNEL=stable
VITE_COMMIT_SHA=${{ github.sha }}
VITE_APP_VERSION=${{ github.ref_name }}
release-github:
needs: release-docker
name: Create GitHub release
permissions:
contents: write
runs-on: ubuntu-22.04
steps:
- name: Checkout source code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0
persist-credentials: false
- name: Setup environment
uses: ./.github/actions/common-setup
- name: Build production bundle
env:
VITE_CHANNEL: stable
VITE_COMMIT_SHA: ${{ github.sha }}
VITE_APP_VERSION: ${{ github.ref_name }}
run: pnpm build
- name: Package dist tarball
env:
REF_NAME: ${{ github.ref_name }}
run: |
set -euo pipefail
tar -czf "octi-web-${REF_NAME}.tar.gz" -C dist .
ls -la "octi-web-${REF_NAME}.tar.gz"
# Use `gh release create` directly so the release is created WITH its assets
# in a single API call. softprops/action-gh-release creates-then-attaches,
# which fails on repos with "Immutable Releases" enabled (the release is
# frozen at creation time and assets can't be added after the fact).
- name: Create release with assets
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
REF_NAME: ${{ github.ref_name }}
run: |
set -euo pipefail
flags=(
--title "${REF_NAME}"
--generate-notes
)
# Pre-release for -rc/-beta tags (anything with a suffix); stable for vM.m.p.
if [[ "${REF_NAME}" == *-* ]]; then
flags+=(--prerelease)
else
flags+=(--latest)
fi
gh release create "${REF_NAME}" "${flags[@]}" "octi-web-${REF_NAME}.tar.gz"
screenshots-pr:
needs: release-github
name: Open screenshots PR
runs-on: ubuntu-22.04
permissions:
contents: read
steps:
- name: Mint App token
# Same App as release-prepare — needed to bypass main branch ruleset
# protection when pushing the screenshots/refresh branch.
id: app-token
uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1
with:
client-id: ${{ secrets.RELEASE_APP_CLIENT_ID }}
private-key: ${{ secrets.RELEASE_APP_PRIVATE_KEY }}
- name: Resolve bot identity
id: bot
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
APP_SLUG: ${{ steps.app-token.outputs.app-slug }}
run: |
set -euo pipefail
user_id=$(gh api "/users/${APP_SLUG}%5Bbot%5D" --jq .id)
echo "user_name=${APP_SLUG}[bot]" >> "$GITHUB_OUTPUT"
echo "user_email=${user_id}+${APP_SLUG}[bot]@users.noreply.github.com" >> "$GITHUB_OUTPUT"
- name: Checkout main (App-authenticated)
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: main
fetch-depth: 1
persist-credentials: true
token: ${{ steps.app-token.outputs.token }}
- name: Setup environment
uses: ./.github/actions/common-setup
- name: Download screenshots artifact (from this run)
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
with:
name: screenshots-${{ github.ref_name }}
path: screenshots/
- name: Re-generate thumbnails defensively
run: pnpm exec node scripts/generate-thumbs.mjs
- name: Open / refresh PR
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
REF_NAME: ${{ github.ref_name }}
BOT_USER_NAME: ${{ steps.bot.outputs.user_name }}
BOT_USER_EMAIL: ${{ steps.bot.outputs.user_email }}
run: |
set -euo pipefail
git config user.name "${BOT_USER_NAME}"
git config user.email "${BOT_USER_EMAIL}"
# Fixed branch name → at most one open "screenshots/refresh" PR exists.
# Each new release force-updates the branch with the new PNGs.
branch="screenshots/refresh"
git checkout -B "${branch}"
git add screenshots/
if git diff --staged --quiet; then
echo "No screenshot changes — nothing to PR."
exit 0
fi
git commit -m "Refresh screenshots for ${REF_NAME}"
git push --force --set-upstream origin "${branch}"
# Open or update the PR (gh pr create errors if one already exists for
# the branch; in that case the push above already updated the diff).
if gh pr view "${branch}" >/dev/null 2>&1; then
echo "PR already open for ${branch}; force-push updated it."
else
gh pr create --base main --head "${branch}" \
--title "Refresh screenshots (${REF_NAME})" \
--body "Automated refresh from the ${REF_NAME} release. Review the diffs and merge if the new shots look right."
fi