π Release v2026.8.11-835b0a1c #300
Workflow file for this run
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: Deploy to AWS ECS | |
| on: | |
| push: | |
| tags: | |
| - 'v[0-9]+.[0-9]+.[0-9]+-*' | |
| concurrency: | |
| group: deploy-production | |
| cancel-in-progress: false | |
| # The workflow only talks to AWS; contents: read is all checkout needs. | |
| permissions: | |
| contents: read | |
| env: | |
| AWS_REGION: eu-west-1 | |
| ECR_REGISTRY: 525984396332.dkr.ecr.eu-west-1.amazonaws.com | |
| ECR_REPOSITORY: b10cks/cms | |
| ECS_CLUSTER: nb-clients01 | |
| ECS_SERVICE: b10cks-cms | |
| ECS_TASK_DEFINITION: b10cks-cms | |
| PHP_VERSION: '8.5' | |
| VITE_S3_BUCKET: ${{ secrets.VITE_S3_BUCKET }} | |
| DOCS_S3_BUCKET: b10cks-production-docs | |
| DOCS_CLOUDFRONT_DISTRIBUTION_ID: E2DCMG9O2W1BIA | |
| jobs: | |
| # Same gate as release.yml: a calver tag can land on any commit, and without | |
| # this a red one would deploy straight to production. Reuses tests.yml so the | |
| # gate cannot drift from what runs on pull requests. | |
| test: | |
| uses: ./.github/workflows/tests.yml | |
| # The deploy job's own Discord notification never runs when the gate fails | |
| # (the job is skipped, not failed), so a blocked deploy would otherwise be | |
| # silent outside the Actions UI. | |
| notify-blocked: | |
| needs: test | |
| if: failure() | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 5 | |
| steps: | |
| - name: Post blocked-deploy notification to Discord | |
| env: | |
| DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK_URL }} | |
| run: | | |
| VERSION="${GITHUB_REF#refs/tags/v}" | |
| PAYLOAD=$(jq -n \ | |
| --arg title "π« Deployment Blocked β Tests Failed" \ | |
| --arg description "**Project:** https://github.com/b10cks/cms | |
| **Version:** \`${VERSION}\` | |
| The test suite failed, so this tag was not deployed. | |
| ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}" \ | |
| --arg timestamp "$(date -u +%Y-%m-%dT%H:%M:%S.000Z)" \ | |
| '{ | |
| "embeds": [{ | |
| "title": $title, | |
| "description": $description, | |
| "color": 15158332, | |
| "timestamp": $timestamp | |
| }] | |
| }') | |
| curl -H "Content-Type: application/json" \ | |
| -d "$PAYLOAD" \ | |
| "$DISCORD_WEBHOOK" | |
| deploy: | |
| needs: test | |
| runs-on: ubuntu-latest | |
| # must leave room for a worst-case docs build (2 Γ 5 min timeout) | |
| # on top of the normal ~10 min pipeline | |
| timeout-minutes: 30 | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| fetch-depth: 0 # Fetch all history for commit comparison | |
| - name: Extract version from tag | |
| id: version | |
| run: | | |
| VERSION=${GITHUB_REF#refs/tags/v} | |
| echo "version=${VERSION}" >> $GITHUB_OUTPUT | |
| echo "tag=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT | |
| echo "short_sha=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT | |
| - name: Setup Bun | |
| uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 | |
| with: | |
| bun-version: 1.3.14 | |
| - name: Setup PHP | |
| uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # 2.37.2 | |
| with: | |
| php-version: ${{ env.PHP_VERSION }} | |
| extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, bcmath, soap, intl, gd, exif, iconv, imagick, redis | |
| tools: composer:v2 | |
| - name: Install production Composer dependencies | |
| run: composer install --prefer-dist --no-interaction --no-progress --optimize-autoloader --no-dev | |
| - name: Restore committed OpenAPI specs | |
| # composer's post-autoload-dump runs `artisan docs:generate`, which | |
| # regenerates docs/public/specs without a real app environment; the | |
| # degenerate output sends the vitepress-openapi spec parser into an | |
| # infinite loop during docs SSR. The committed specs are the source | |
| # of truth for CI builds. | |
| run: git checkout -- docs/public/specs | |
| - name: Configure AWS credentials | |
| uses: aws-actions/configure-aws-credentials@e6de054238d6b7531b4efff3b6587d9aade6a06c # v6.2.3 | |
| with: | |
| aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} | |
| aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} | |
| aws-region: ${{ env.AWS_REGION }} | |
| - name: Cache Bun dependencies | |
| uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 | |
| with: | |
| path: ~/.bun/install/cache | |
| key: ${{ runner.os }}-bun-${{ hashFiles('**/bun.lock') }} | |
| restore-keys: | | |
| ${{ runner.os }}-bun- | |
| - name: Install dependencies | |
| run: bun install --frozen-lockfile | |
| - name: Build frontend | |
| run: bun run build | |
| env: | |
| NODE_ENV: production | |
| - name: Build docs | |
| id: docs | |
| # vitepress repeatedly hangs in the SSR render phase in this job | |
| # (fine in isolation) β bound it, retry once, and never block the | |
| # app deploy on it: on failure we skip docs publishing instead. | |
| run: | | |
| if timeout 300 bun run docs:build || { | |
| echo "::warning::docs build failed or timed out β retrying once" | |
| rm -rf public/docs | |
| timeout 300 bun run docs:build | |
| }; then | |
| echo "built=true" >> $GITHUB_OUTPUT | |
| else | |
| echo "::warning::docs build failed twice β deploying without docs update" | |
| echo "built=false" >> $GITHUB_OUTPUT | |
| fi | |
| env: | |
| NODE_ENV: production | |
| APP_URL: ${{ vars.APP_FRONTEND_URL || 'https://app.b10cks.com' }} | |
| - name: Login to Amazon ECR | |
| id: login-ecr | |
| uses: aws-actions/amazon-ecr-login@d539f0932e70871a027e9d5a9d8fc38589180a64 # v2.1.6 | |
| - name: Set up Docker Buildx | |
| uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0 | |
| - name: Check for existing image | |
| id: image-exists | |
| # tags are immutable β a re-run after a partial failure must not | |
| # re-push the tag its first attempt already published | |
| env: | |
| IMAGE_TAG: main-${{ steps.version.outputs.version }} | |
| run: | | |
| if aws ecr describe-images \ | |
| --repository-name ${{ env.ECR_REPOSITORY }} \ | |
| --image-ids imageTag=$IMAGE_TAG >/dev/null 2>&1; then | |
| echo "::notice::${IMAGE_TAG} already in ECR β skipping build and push" | |
| echo "exists=true" >> $GITHUB_OUTPUT | |
| else | |
| echo "exists=false" >> $GITHUB_OUTPUT | |
| fi | |
| - name: Build and push Docker image | |
| if: steps.image-exists.outputs.exists != 'true' | |
| uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0 | |
| with: | |
| context: . | |
| push: true | |
| tags: ${{ env.ECR_REGISTRY }}/${{ env.ECR_REPOSITORY }}:main-${{ steps.version.outputs.version }} | |
| build-args: | | |
| APP_VERSION=${{ steps.version.outputs.version }} | |
| cache-from: type=registry,ref=${{ env.ECR_REGISTRY }}/${{ env.ECR_REPOSITORY }}:cache | |
| cache-to: type=registry,ref=${{ env.ECR_REGISTRY }}/${{ env.ECR_REPOSITORY }}:cache,mode=max | |
| provenance: false | |
| - name: Download current task definition | |
| run: | | |
| aws ecs describe-task-definition \ | |
| --task-definition ${{ env.ECS_TASK_DEFINITION }} \ | |
| --query taskDefinition > task-definition.json | |
| - name: Update task definition with new image | |
| id: task-def | |
| uses: aws-actions/amazon-ecs-render-task-definition@138c24f321fdbdf7edee4a685519d253cae2cdea # v1.9.0 | |
| with: | |
| task-definition: task-definition.json | |
| container-name: ${{ env.ECS_TASK_DEFINITION }} | |
| image: ${{ env.ECR_REGISTRY }}/${{ env.ECR_REPOSITORY }}:main-${{ steps.version.outputs.version }} | |
| - name: Download reverb task definition | |
| run: | | |
| aws ecs describe-task-definition \ | |
| --task-definition b10cks-reverb \ | |
| --query taskDefinition > reverb-task-definition.json | |
| - name: Update reverb task definition with new image | |
| id: reverb-task-def-image | |
| uses: aws-actions/amazon-ecs-render-task-definition@138c24f321fdbdf7edee4a685519d253cae2cdea # v1.9.0 | |
| with: | |
| task-definition: reverb-task-definition.json | |
| container-name: b10cks-reverb | |
| image: ${{ env.ECR_REGISTRY }}/${{ env.ECR_REPOSITORY }}:main-${{ steps.version.outputs.version }} | |
| - name: Switch reverb task definition to supervisord config | |
| id: reverb-task-def | |
| run: | | |
| jq '.containerDefinitions[0].entryPoint = ["/usr/bin/supervisord", "-c", "/etc/supervisord-reverb.conf", "-n"]' \ | |
| ${{ steps.reverb-task-def-image.outputs.task-definition }} > reverb-task-definition-final.json | |
| echo "task-definition=reverb-task-definition-final.json" >> $GITHUB_OUTPUT | |
| - name: Sync static assets to S3 | |
| run: | | |
| aws s3 sync public/build/ "s3://${{ env.VITE_S3_BUCKET }}/build" \ | |
| --delete \ | |
| --cache-control "max-age=7776000,s-maxage=604800,public" | |
| - name: Publish docs to S3 | |
| if: steps.docs.outputs.built == 'true' | |
| run: | | |
| # HTML gets short-lived cache (invalidated below), hashed assets long-lived | |
| aws s3 sync public/docs/ "s3://${{ env.DOCS_S3_BUCKET }}/docs" \ | |
| --delete \ | |
| --exclude "*.html" \ | |
| --cache-control "max-age=7776000,s-maxage=604800,public" | |
| aws s3 sync public/docs/ "s3://${{ env.DOCS_S3_BUCKET }}/docs" \ | |
| --exclude "*" \ | |
| --include "*.html" \ | |
| --cache-control "max-age=0,s-maxage=604800,public,must-revalidate" | |
| - name: Invalidate docs CloudFront cache | |
| if: steps.docs.outputs.built == 'true' | |
| run: | | |
| aws cloudfront create-invalidation \ | |
| --distribution-id ${{ env.DOCS_CLOUDFRONT_DISTRIBUTION_ID }} \ | |
| --paths "/docs*" | |
| - name: Deploy CMS service to Amazon ECS | |
| uses: aws-actions/amazon-ecs-deploy-task-definition@c465972ecbd160473f22e683363b422a5412a3de # v2.6.3 | |
| with: | |
| task-definition: ${{ steps.task-def.outputs.task-definition }} | |
| service: ${{ env.ECS_SERVICE }} | |
| cluster: ${{ env.ECS_CLUSTER }} | |
| wait-for-service-stability: true | |
| wait-for-minutes: 10 | |
| - name: Deploy Reverb service to Amazon ECS | |
| uses: aws-actions/amazon-ecs-deploy-task-definition@c465972ecbd160473f22e683363b422a5412a3de # v2.6.3 | |
| with: | |
| task-definition: ${{ steps.reverb-task-def.outputs.task-definition }} | |
| service: b10cks-reverb | |
| cluster: ${{ env.ECS_CLUSTER }} | |
| wait-for-service-stability: false | |
| - name: Get commits since last release | |
| id: commits | |
| run: | | |
| # Get the previous tag | |
| PREVIOUS_TAG=$(git describe --tags --abbrev=0 HEAD^ 2>/dev/null || echo "") | |
| if [ -z "$PREVIOUS_TAG" ]; then | |
| # If no previous tag, get all commits | |
| COMMITS=$(git log --pretty=format:"β’ %s" --no-merges) | |
| else | |
| # Get commits between previous tag and current | |
| COMMITS=$(git log ${PREVIOUS_TAG}..HEAD --pretty=format:"β’ %s" --no-merges) | |
| fi | |
| COMMITS=$(echo -n "$COMMITS" | base64 | tr -d '\n') | |
| echo "commits=${COMMITS}" >> $GITHUB_OUTPUT | |
| echo "previous_tag=${PREVIOUS_TAG:-'Initial Release'}" >> $GITHUB_OUTPUT | |
| - name: Post deployment notification to Discord | |
| if: always() | |
| env: | |
| DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK_URL }} | |
| run: | | |
| if [ "${{ job.status }}" == "success" ]; then | |
| COLOR="3066993" # Green | |
| STATUS="β Deployment Successful" | |
| else | |
| COLOR="15158332" # Red | |
| STATUS="β Deployment Failed" | |
| fi | |
| # Use jq to properly escape the commits for JSON | |
| COMMITS=$(echo "${{ steps.commits.outputs.commits }}" | base64 --decode) | |
| DESCRIPTION=$(echo -e "**Project:** https://github.com/b10cks/cms\n**Version:** \`${{ steps.version.outputs.version }}\`\n\n**Changes since ${{ steps.commits.outputs.previous_tag }}:**\n${COMMITS}") | |
| # Truncate if too long (Discord has a 4096 char limit for description) | |
| if [ ${#DESCRIPTION} -gt 3800 ]; then | |
| DESCRIPTION=$(echo "${DESCRIPTION:0:3800}... (truncated)") | |
| fi | |
| # Create JSON payload with proper escaping | |
| PAYLOAD=$(jq -n \ | |
| --arg title "$STATUS" \ | |
| --arg description "$DESCRIPTION" \ | |
| --arg color "$COLOR" \ | |
| --arg timestamp "$(date -u +%Y-%m-%dT%H:%M:%S.000Z)" \ | |
| --arg footer_text "Commit: ${{ steps.version.outputs.short_sha }}" \ | |
| '{ | |
| "embeds": [{ | |
| "title": $title, | |
| "description": $description, | |
| "color": $color|tonumber, | |
| "timestamp": $timestamp, | |
| "footer": { | |
| "text": $footer_text | |
| } | |
| }] | |
| }') | |
| # Send to Discord | |
| curl -H "Content-Type: application/json" \ | |
| -d "$PAYLOAD" \ | |
| $DISCORD_WEBHOOK | |
| if [ ${{ job.status }} == 'success' ]; then | |
| echo "β Deployment successful for version ${{ steps.version.outputs.version }}" | |
| else | |
| echo "β Deployment failed for version ${{ steps.version.outputs.version }}" | |
| fi |