Skip to content

release: bump version to 1.9.11 #154

release: bump version to 1.9.11

release: bump version to 1.9.11 #154

Workflow file for this run

name: Deploy
on:
push:
branches: [main]
workflow_dispatch:
env:
REGISTRY: ghcr.io
IMAGE_NAME: hughkantsime/odin
jobs:
# ── Validate build before deploying ──
validate:
name: Validate Build
runs-on: mac-mini-runner
steps:
- uses: actions/checkout@v4
- name: Add Homebrew to PATH
run: echo "$HOME/homebrew/bin:/opt/homebrew/bin" | tr ':' '\n' >> $GITHUB_PATH
- name: Set up Node 20
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Validate design tokens are committed
run: |
node design/generate.mjs --local-only
if ! git diff --exit-code frontend/src/design-tokens.css; then
echo "::error::design-tokens.css is out of sync with tokens.json"
echo "Run 'make tokens' and commit the result before releasing"
exit 1
fi
- name: Validate frontend build
run: |
cd frontend
npm ci --no-audit --no-fund
npx vite build
- name: Validate backend deps
run: pip3.11 install --break-system-packages -r backend/requirements.txt
# ── Auto-tag from VERSION file ──
tag:
name: Auto-Tag Release
needs: validate
runs-on: mac-mini-runner
outputs:
version: ${{ steps.version.outputs.version }}
tag_created: ${{ steps.check.outputs.tag_created }}
permissions:
contents: write
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Read VERSION
id: version
run: echo "version=v$(cat VERSION)" >> $GITHUB_OUTPUT
- name: Check if tag exists
id: check
run: |
TAG="${{ steps.version.outputs.version }}"
if git rev-parse "$TAG" >/dev/null 2>&1; then
TAG_SHA=$(git rev-parse "$TAG")
HEAD_SHA=$(git rev-parse HEAD)
if [ "$TAG_SHA" = "$HEAD_SHA" ]; then
echo "Tag $TAG exists and points to HEAD — proceeding (make release pushed it)"
echo "tag_created=true" >> $GITHUB_OUTPUT
else
echo "Tag $TAG exists but points to $TAG_SHA (HEAD is $HEAD_SHA) — skipping"
echo "tag_created=false" >> $GITHUB_OUTPUT
fi
else
echo "tag_created=true" >> $GITHUB_OUTPUT
fi
- name: Create tag
if: steps.check.outputs.tag_created == 'true'
run: |
TAG="${{ steps.version.outputs.version }}"
if git rev-parse "$TAG" >/dev/null 2>&1; then
echo "Tag $TAG already exists, skipping creation"
else
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git tag "$TAG"
git push origin "$TAG"
fi
# ── Build and push Docker image ──
build:
name: Build & Push Docker Image
needs: tag
if: needs.tag.outputs.tag_created == 'true'
runs-on: mac-mini-runner
timeout-minutes: 30
permissions:
contents: read
packages: write
steps:
- name: Add Homebrew to PATH
run: echo "$HOME/homebrew/bin:/opt/homebrew/bin" | tr ':' '\n' >> $GITHUB_PATH
- uses: actions/checkout@v4
with:
lfs: true
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to GHCR
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: |
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ needs.tag.outputs.version }}
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
platforms: linux/amd64,linux/arm64
provenance: false
# ── Phase 3: Create GitHub Release ──
release:
name: Create GitHub Release
needs: [tag, build]
if: needs.tag.outputs.tag_created == 'true'
runs-on: mac-mini-runner
continue-on-error: true
permissions:
contents: write
outputs:
release_ok: ${{ steps.create.outcome == 'success' }}
steps:
- name: Add Homebrew to PATH
run: echo "$HOME/homebrew/bin:/opt/homebrew/bin" | tr ':' '\n' >> $GITHUB_PATH
- uses: actions/checkout@v4
- name: Extract changelog body
id: changelog
run: |
VER="${{ needs.tag.outputs.version }}"
VER="${VER#v}"
python3 -c "
import sys, re
content = open('CHANGELOG.md').read()
ver = '$VER'
m = re.search(r'## \[' + re.escape(ver) + r'\][^\n]*\n(.*?)(?=\n## \[|\Z)', content, re.DOTALL)
if not m:
print(f'ERROR: no changelog section for {ver}', file=sys.stderr); sys.exit(1)
body = m.group(1).strip()
if not body:
print(f'ERROR: empty changelog section for {ver}', file=sys.stderr); sys.exit(1)
print(body)
" > /tmp/changelog_body.txt
if [ ! -s /tmp/changelog_body.txt ]; then
echo "ERROR: changelog body is empty for v$VER"
exit 1
fi
echo "Extracted $(wc -l < /tmp/changelog_body.txt) lines for v$VER"
- name: Create GitHub Release
id: create
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
VER="${{ needs.tag.outputs.version }}"
gh release create "$VER" \
--title "O.D.I.N. $VER" \
--notes-file /tmp/changelog_body.txt \
install/install.sh
# ── Phase 4: Sync odin-site ──
sync-site:
name: Sync odin-site
needs: [release, tag]
if: needs.tag.outputs.tag_created == 'true'
runs-on: mac-mini-runner
continue-on-error: true
outputs:
sync_ok: ${{ steps.poll.outcome == 'success' }}
steps:
- name: Add Homebrew to PATH
run: echo "$HOME/homebrew/bin:/opt/homebrew/bin" | tr ':' '\n' >> $GITHUB_PATH
- name: Dispatch odin-release event
id: dispatch
env:
# Cross-repo dispatch requires a PAT with repo scope — GITHUB_TOKEN
# cannot trigger workflows in other repositories.
GH_TOKEN: ${{ secrets.ODIN_SITE_PAT }}
run: |
VER="${{ needs.tag.outputs.version }}"
VER="${VER#v}"
TAG="${{ needs.tag.outputs.version }}"
SHA="${{ github.sha }}"
DISPATCH_TIME=$(date -u +%Y-%m-%dT%H:%M:%SZ)
echo "dispatch_time=$DISPATCH_TIME" >> $GITHUB_OUTPUT
gh api repos/HughKantsime/odin-site/dispatches \
--method POST \
--input - <<EOF
{"event_type":"odin-release","client_payload":{"version":"$VER","tag":"$TAG","source_repo":"HughKantsime/runsodin","source_sha":"$SHA"}}
EOF
- name: Poll for odin-site workflow completion
id: poll
env:
GH_TOKEN: ${{ secrets.ODIN_SITE_PAT }}
run: |
DISPATCH_TIME="${{ steps.dispatch.outputs.dispatch_time }}"
for i in $(seq 1 20); do
sleep 15
RESULT=$(gh run list --repo HughKantsime/odin-site --event repository_dispatch --limit 5 --json status,conclusion,createdAt,databaseId 2>/dev/null)
CONCLUSION=$(echo "$RESULT" | python3 -c "
import sys, json
data = json.load(sys.stdin)
dispatch_time = '$DISPATCH_TIME'
for run in data:
if run.get('createdAt', '') >= dispatch_time:
print(run.get('conclusion') or 'pending')
break
else:
print('not_found')
")
echo "Poll $i/20: $CONCLUSION"
if [ "$CONCLUSION" = "success" ]; then exit 0; fi
if [ "$CONCLUSION" = "failure" ] || [ "$CONCLUSION" = "cancelled" ]; then
echo "::error::odin-site sync workflow failed with: $CONCLUSION"
exit 1
fi
done
echo "::error::odin-site sync workflow timed out after 5 minutes"
exit 1
# ── Phase 5: Verify prod deployment ──
#
# IMPORTANT: this job does NOT use continue-on-error. Prior to 2026-04-16
# it had `continue-on-error: true`, which meant a failure here (prod
# hasn't picked up the new image) reported as the whole Deploy workflow
# succeeding — GitHub showed a green ✓ badge while prod was actually
# crash-looping. That's how v1.9.0's prod outage went undetected for
# 19 hours. The notify job below still runs via `if: always()` so we
# still get the summary+notification on partial failure; the change is
# just that the overall workflow status now reflects reality.
verify-prod:
name: Verify Prod
needs: [build, tag]
if: needs.tag.outputs.tag_created == 'true'
runs-on: mac-mini-runner
outputs:
prod_ok: ${{ steps.verify.outcome == 'success' }}
steps:
- name: Add Homebrew to PATH
run: echo "$HOME/homebrew/bin:/opt/homebrew/bin" | tr ':' '\n' >> $GITHUB_PATH
- uses: actions/checkout@v4
- name: Verify prod version and readiness
id: verify
run: |
VER="${{ needs.tag.outputs.version }}"
ops/prod_verify_public.sh "$VER"
# ── Phase 6: Notification ──
notify:
name: Deploy Notification
needs: [tag, build, release, sync-site, verify-prod]
if: always() && needs.tag.outputs.tag_created == 'true'
runs-on: mac-mini-runner
steps:
- name: Add Homebrew to PATH
run: echo "$HOME/homebrew/bin:/opt/homebrew/bin" | tr ':' '\n' >> $GITHUB_PATH
- name: Build status and notify
run: |
VER="${{ needs.tag.outputs.version }}"
# Collect phase results
BUILD="${{ needs.build.result }}"
RELEASE="${{ needs.release.result }}"
SYNC="${{ needs.sync-site.result }}"
PROD="${{ needs.verify-prod.result }}"
icon() { [ "$1" = "success" ] && echo "✓" || echo "✗"; }
STATUS="GHCR $(icon $BUILD) | Release $(icon $RELEASE) | Site $(icon $SYNC) | Prod $(icon $PROD)"
echo "## Deploy Summary" >> $GITHUB_STEP_SUMMARY
echo "- **Version:** $VER" >> $GITHUB_STEP_SUMMARY
echo "- **GHCR:** $BUILD" >> $GITHUB_STEP_SUMMARY
echo "- **Release:** $RELEASE" >> $GITHUB_STEP_SUMMARY
echo "- **Site sync:** $SYNC" >> $GITHUB_STEP_SUMMARY
echo "- **Prod verify:** $PROD" >> $GITHUB_STEP_SUMMARY
# Determine priority
if [ "$BUILD" = "success" ] && [ "$RELEASE" = "success" ] && [ "$SYNC" = "success" ] && [ "$PROD" = "success" ]; then
PRIORITY="default"
TITLE="O.D.I.N. $VER deployed"
elif [ "$BUILD" != "success" ]; then
PRIORITY="high"
TITLE="O.D.I.N. $VER — build FAILED"
else
PRIORITY="high"
TITLE="O.D.I.N. $VER — partial failure"
fi
curl -sf \
-H "Title: $TITLE" \
-H "Priority: $PRIORITY" \
-H "Tags: rocket" \
-d "$STATUS" \
"${{ secrets.NTFY_URL }}" || echo "::warning::ntfy notification failed"