Skip to content

Release

Release #342

Workflow file for this run

name: Release
on:
workflow_run:
workflows: ["Verify"]
types: [completed]
branches: [main]
workflow_dispatch:
permissions:
contents: write
packages: write
concurrency:
group: release-main
cancel-in-progress: false
jobs:
release:
if: |
(
github.event_name == 'workflow_dispatch' &&
github.ref == 'refs/heads/main'
) ||
(
github.event.workflow_run.conclusion == 'success' &&
github.event.workflow_run.event == 'push' &&
github.event.workflow_run.head_branch == 'main'
)
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- name: Checkout main
uses: actions/checkout@v6
with:
ref: main
fetch-depth: 0
- name: Determine release eligibility
id: freshness
env:
EVENT_NAME: ${{ github.event_name }}
WORKFLOW_HEAD_SHA: ${{ github.event.workflow_run.head_sha }}
run: |
if [ "$EVENT_NAME" = "workflow_run" ]; then
CURRENT_MAIN_SHA="$(git rev-parse origin/main)"
if [ "$CURRENT_MAIN_SHA" != "$WORKFLOW_HEAD_SHA" ]; then
echo "should_release=false" >> "$GITHUB_OUTPUT"
echo "Skipping release: verify run is not for the latest main commit."
echo "Current main: $CURRENT_MAIN_SHA"
echo "Verify head: $WORKFLOW_HEAD_SHA"
exit 0
fi
fi
echo "should_release=true" >> "$GITHUB_OUTPUT"
- name: Setup Node
if: steps.freshness.outputs.should_release == 'true'
uses: actions/setup-node@v6
with:
node-version: 20
- name: Detect release version
if: steps.freshness.outputs.should_release == 'true'
id: version
run: |
CURRENT_VERSION="$(node -p "require('./package.json').version || ''")"
if [ -z "$CURRENT_VERSION" ]; then
echo "Missing package.json version"
exit 1
fi
if git cat-file -e HEAD^:package.json 2>/dev/null; then
PREVIOUS_VERSION="$(
git show HEAD^:package.json | node -e "const fs=require('node:fs');const raw=fs.readFileSync(0,'utf8');try{process.stdout.write(JSON.parse(raw).version||'')}catch{process.stdout.write('')}"
)"
else
PREVIOUS_VERSION="0.0.0"
fi
if [ -z "$PREVIOUS_VERSION" ]; then
PREVIOUS_VERSION="0.0.0"
fi
VERSION_RELATION="$(
node -e "const parse=(v)=>{const m=/^(\\d+)\\.(\\d+)\\.(\\d+)$/.exec(v||'');return m?m.slice(1).map(Number):null};const cmp=(a,b)=>a[0]!==b[0]?Math.sign(a[0]-b[0]):a[1]!==b[1]?Math.sign(a[1]-b[1]):Math.sign(a[2]-b[2]);const current=parse(process.argv[1]);const previous=parse(process.argv[2]);if(!current||!previous){process.stdout.write('invalid');process.exit(0)}const value=cmp(current,previous);process.stdout.write(value>0?'gt':value<0?'lt':'eq')" "$CURRENT_VERSION" "$PREVIOUS_VERSION"
)"
if [ "$VERSION_RELATION" = "invalid" ]; then
echo "Invalid semantic version. Current: $CURRENT_VERSION Previous: $PREVIOUS_VERSION"
exit 1
fi
echo "version=$CURRENT_VERSION" >> "$GITHUB_OUTPUT"
echo "previous_version=$PREVIOUS_VERSION" >> "$GITHUB_OUTPUT"
echo "tag=v$CURRENT_VERSION" >> "$GITHUB_OUTPUT"
echo "version_relation=$VERSION_RELATION" >> "$GITHUB_OUTPUT"
if [ "$VERSION_RELATION" = "gt" ]; then
echo "has_version_bump=true" >> "$GITHUB_OUTPUT"
echo "Detected release version bump: $PREVIOUS_VERSION -> $CURRENT_VERSION"
else
echo "has_version_bump=false" >> "$GITHUB_OUTPUT"
fi
if [ "$CURRENT_VERSION" = "0.0.0" ] || [ "$VERSION_RELATION" = "lt" ]; then
echo "can_publish_version=false" >> "$GITHUB_OUTPUT"
echo "No release candidate. Current version: $CURRENT_VERSION Previous version: $PREVIOUS_VERSION"
else
echo "can_publish_version=true" >> "$GITHUB_OUTPUT"
if [ "$VERSION_RELATION" = "eq" ]; then
echo "Version unchanged ($CURRENT_VERSION). CI will publish only if tag is still missing."
fi
fi
- name: Check tag availability
if: steps.freshness.outputs.should_release == 'true' && steps.version.outputs.can_publish_version == 'true'
id: tag
env:
TAG: ${{ steps.version.outputs.tag }}
run: |
git fetch --tags --force
if git rev-parse -q --verify "refs/tags/$TAG" >/dev/null; then
echo "tag_available=false" >> "$GITHUB_OUTPUT"
echo "Tag $TAG already exists; skipping release."
else
echo "tag_available=true" >> "$GITHUB_OUTPUT"
fi
- name: Configure git identity
if: steps.freshness.outputs.should_release == 'true' && steps.version.outputs.can_publish_version == 'true' && steps.tag.outputs.tag_available == 'true'
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
- name: Determine image repository
if: steps.freshness.outputs.should_release == 'true' && steps.version.outputs.can_publish_version == 'true' && steps.tag.outputs.tag_available == 'true'
id: image
run: |
IMAGE_REPO="ghcr.io/$(echo "$GITHUB_REPOSITORY" | tr '[:upper:]' '[:lower:]')"
echo "repo=$IMAGE_REPO" >> "$GITHUB_OUTPUT"
- name: Set up Docker Buildx
if: steps.freshness.outputs.should_release == 'true' && steps.version.outputs.can_publish_version == 'true' && steps.tag.outputs.tag_available == 'true'
uses: docker/setup-buildx-action@v4
- name: Log in to GHCR
if: steps.freshness.outputs.should_release == 'true' && steps.version.outputs.can_publish_version == 'true' && steps.tag.outputs.tag_available == 'true'
uses: docker/login-action@v4
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push app image
if: steps.freshness.outputs.should_release == 'true' && steps.version.outputs.can_publish_version == 'true' && steps.tag.outputs.tag_available == 'true'
uses: docker/build-push-action@v7
with:
context: .
file: ./docker/app.Dockerfile
push: true
tags: |
${{ steps.image.outputs.repo }}:${{ steps.version.outputs.tag }}
${{ steps.image.outputs.repo }}:${{ steps.version.outputs.version }}
${{ steps.image.outputs.repo }}:latest
build-args: |
NEXT_PUBLIC_API_URL=
NEXT_PUBLIC_SHIPMENT_BASE_FEE_CENTS=250
NEXT_PUBLIC_SHIPMENT_FEE_PER_UNIT_CENTS=15
- name: Smoke check release image endpoints
if: steps.freshness.outputs.should_release == 'true' && steps.version.outputs.can_publish_version == 'true' && steps.tag.outputs.tag_available == 'true'
env:
APP_IMAGE_REPO: ${{ steps.image.outputs.repo }}
APP_IMAGE_TAG: ${{ steps.version.outputs.version }}
COMPOSE_NETWORK_NAME: corpsim-release-smoke-${{ github.run_id }}
BETTER_AUTH_SECRET: release-smoke-only-secret-do-not-use
BETTER_AUTH_URL: http://127.0.0.1:4310
run: |
docker compose -f docker-compose.preview.yml down -v --remove-orphans || true
docker compose -f docker-compose.preview.yml up -d frontend
for i in $(seq 1 90); do
if curl -fsS "http://127.0.0.1:4311/meta/version" >/tmp/meta-version.json; then
break
fi
sleep 2
done
curl -fsS "http://127.0.0.1:4311/meta/version" | node -e "let s='';process.stdin.on('data',d=>s+=d);process.stdin.on('end',()=>{const j=JSON.parse(s);if(typeof j.version!=='string'||j.version.trim().length===0){process.exit(1)}})"
curl -fsS "http://127.0.0.1:4311/health/maintenance" | node -e "let s='';process.stdin.on('data',d=>s+=d);process.stdin.on('end',()=>{const j=JSON.parse(s);if(typeof j.enabled!=='boolean'){process.exit(1)}})"
curl -fsS "http://127.0.0.1:4311/v1/world/health" | node -e "let s='';process.stdin.on('data',d=>s+=d);process.stdin.on('end',()=>{const j=JSON.parse(s);if(typeof j.currentTick!=='number'){process.exit(1)}})"
- name: Cleanup smoke test stack
if: always() && steps.freshness.outputs.should_release == 'true' && steps.version.outputs.can_publish_version == 'true' && steps.tag.outputs.tag_available == 'true'
env:
APP_IMAGE_REPO: ${{ steps.image.outputs.repo }}
APP_IMAGE_TAG: ${{ steps.version.outputs.version }}
COMPOSE_NETWORK_NAME: corpsim-release-smoke-${{ github.run_id }}
run: docker compose -f docker-compose.preview.yml down -v --remove-orphans
- name: Create annotated tag
if: steps.freshness.outputs.should_release == 'true' && steps.version.outputs.can_publish_version == 'true' && steps.tag.outputs.tag_available == 'true'
env:
TAG: ${{ steps.version.outputs.tag }}
run: git tag -a "$TAG" -m "Release $TAG"
- name: Push release tag
if: steps.freshness.outputs.should_release == 'true' && steps.version.outputs.can_publish_version == 'true' && steps.tag.outputs.tag_available == 'true'
env:
TAG: ${{ steps.version.outputs.tag }}
run: git push origin "$TAG"
- name: Publish GitHub release
if: steps.freshness.outputs.should_release == 'true' && steps.version.outputs.can_publish_version == 'true' && steps.tag.outputs.tag_available == 'true'
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ steps.version.outputs.tag }}
name: CorpSim ${{ steps.version.outputs.tag }}
generate_release_notes: true