Release #342
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 |