Skip to content

Data tables v1

Data tables v1 #344

name: Preview
on:
pull_request:
types: [opened, synchronize, reopened]
branches-ignore:
- "main"
concurrency:
group: pr-${{ github.event.pull_request.number }}-build-images
cancel-in-progress: true
jobs:
create_deployment:
name: Create Deployment
runs-on: blacksmith-2vcpu-ubuntu-2204
if: github.head_ref != 'qa' || github.base_ref != 'main'
permissions:
contents: read
pull-requests: write
issues: write
deployments: write
outputs:
deployment_id: ${{ steps.deployment.outputs.id }}
steps:
- name: Find Build & Deploy Images Comment
uses: peter-evans/find-comment@v3
id: preview-build-deploy-images-comment
with:
issue-number: ${{ github.event.pull_request.number }}
comment-author: "github-actions[bot]"
body-includes: "Preview - Build & Deploy Images"
- name: Create initial status comment
uses: peter-evans/create-or-update-comment@v3
with:
comment-id: ${{ steps.preview-build-deploy-images-comment.outputs.comment-id }}
issue-number: ${{ github.event.pull_request.number }}
body: |
## Preview - Build & Deploy Images
⏳ Building images...
⏳ Deploy images
🔍 Track progress in the [workflow run](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})
edit-mode: replace
- name: Deactivate previous deployments
run: |
gh api \
--method GET \
-H "Accept: application/vnd.github+json" \
/repos/${{ github.repository }}/deployments \
-f ref="${{ github.event.pull_request.head.ref }}" \
-f environment=preview-pr-${{ github.event.pull_request.number }} \
--jq '.[] | .id' | while read -r id; do
gh api \
--method POST \
-H "Accept: application/vnd.github+json" \
/repos/${{ github.repository }}/deployments/$id/statuses \
-f state="inactive" \
-f environment="preview-pr-${{ github.event.pull_request.number }}" \
-f description="Superseded by newer deployment"
done
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Create deployment
id: deployment
run: |
DEPLOYMENT_ID=$(gh api \
--method POST \
-H "Accept: application/vnd.github+json" \
/repos/${{ github.repository }}/deployments \
-f ref="${{ github.event.pull_request.head.ref }}" \
-f description="Building and deploying PR #${{ github.event.pull_request.number }} (${GITHUB_SHA::7})" \
-F auto_merge=false \
-f required_contexts\[\] \
-f environment=preview-pr-${{ github.event.pull_request.number }} \
-F transient_environment=true \
-F production_environment=false \
--jq '.id')
echo "id=$DEPLOYMENT_ID" >> $GITHUB_OUTPUT
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Update deployment status (building images)
run: |
gh api \
--method POST \
-H "Accept: application/vnd.github+json" \
/repos/${{ github.repository }}/deployments/${{ steps.deployment.outputs.id }}/statuses \
-f state="in_progress" \
-f description="Building Docker images..." \
-f environment=preview-pr-${{ github.event.pull_request.number }} \
-f log_url="${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
build_images:
name: Build Images
needs: create_deployment
permissions:
contents: read
pull-requests: write
issues: write
deployments: write
runs-on: ${{ matrix.runner }}
if: github.head_ref != 'qa' || github.base_ref != 'main'
timeout-minutes: 30
strategy:
matrix:
include:
- service: client
runner: blacksmith-8vcpu-ubuntu-2204
- service: api
runner: blacksmith-2vcpu-ubuntu-2204
- service: connection
runner: blacksmith-4vcpu-ubuntu-2204
- service: files
runner: blacksmith-4vcpu-ubuntu-2204
- service: multiplayer
runner: blacksmith-4vcpu-ubuntu-2204
fail-fast: true
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Generate Build Metadata
id: build-metadata
run: |
echo "BUILD_TIME=$(date -u +'%Y-%m-%dT%H:%M:%SZ')" >> $GITHUB_OUTPUT
echo "GIT_SHA_SHORT=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
echo "BRANCH_NAME=$(echo "${{ github.head_ref }}" | tr '/' '-')" >> $GITHUB_OUTPUT
- name: Configure AWS Credentials
uses: aws-actions/configure-aws-credentials@v4
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID_DEVELOPMENT }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY_DEVELOPMENT }}
aws-region: ${{ secrets.AWS_REGION }}
- name: Login to Amazon ECR Private
id: login-ecr
uses: aws-actions/amazon-ecr-login@v2
- name: Define repository name
id: repo-name
run: |
echo "REPO_NAME=quadratic-${{ matrix.service }}" >> $GITHUB_OUTPUT
- name: Create Private ECR Repository
id: create-ecr
env:
REPO_NAME: ${{ steps.repo-name.outputs.REPO_NAME }}
run: |
# Try to describe the repository first
if ! aws ecr describe-repositories --repository-names $REPO_NAME 2>/dev/null; then
# Repository doesn't exist, create it
aws ecr create-repository --repository-name $REPO_NAME || true
fi
# Get the repository URI either way
REPO_INFO=$(aws ecr describe-repositories --repository-names $REPO_NAME)
ECR_URL=$(echo $REPO_INFO | jq -r '.repositories[0].repositoryUri')
echo "ECR_URL=$ECR_URL" >> $GITHUB_OUTPUT
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
with:
driver-opts: |
image=moby/buildkit:latest
network=host
- name: Build and push
uses: useblacksmith/build-push-action@v1
with:
context: .
file: quadratic-${{ matrix.service }}/Dockerfile
push: true
tags: ${{ steps.create-ecr.outputs.ECR_URL }}:pr-${{ github.event.pull_request.number }}
build-args: |
BUILD_TIME=${{ steps.build-metadata.outputs.BUILD_TIME }}
GIT_SHA_SHORT=${{ steps.build-metadata.outputs.GIT_SHA_SHORT }}
BRANCH_NAME=${{ steps.build-metadata.outputs.BRANCH_NAME }}
PR_NUMBER=${{ github.event.pull_request.number }}
labels: |
org.opencontainers.image.created=${{ steps.build-metadata.outputs.BUILD_TIME }}
org.opencontainers.image.revision=${{ steps.build-metadata.outputs.GIT_SHA_SHORT }}
- name: Find Build & Deploy Images Comment
if: failure()
uses: peter-evans/find-comment@v3
id: preview-build-deploy-images-comment
with:
issue-number: ${{ github.event.pull_request.number }}
comment-author: "github-actions[bot]"
body-includes: "Preview - Build & Deploy Images"
- name: Update comment on build images failure
if: failure()
uses: peter-evans/create-or-update-comment@v3
with:
comment-id: ${{ steps.preview-build-deploy-images-comment.outputs.comment-id }}
issue-number: ${{ github.event.pull_request.number }}
body: |
## Preview - Build & Deploy Images
❌ Build images
❌ Deploy images
🔍 Please check the [workflow run](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}) for details.
edit-mode: replace
- name: Update deployment status (failure)
if: failure()
run: |
gh api \
--method POST \
-H "Accept: application/vnd.github+json" \
/repos/${{ github.repository }}/deployments/${{ needs.create_deployment.outputs.deployment_id }}/statuses \
-f state="failure" \
-f description="Failed to build ${{ matrix.service }} image" \
-f environment=preview-pr-${{ github.event.pull_request.number }} \
-f log_url="${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
deploy_images:
name: Deploy Images
needs: [create_deployment, build_images]
permissions:
contents: read
pull-requests: write
issues: write
deployments: write
runs-on: blacksmith-2vcpu-ubuntu-2204
if: github.head_ref != 'qa' || github.base_ref != 'main'
timeout-minutes: 30
env:
STACK_NAME: pr-${{ github.event.pull_request.number }}
MAX_ATTEMPTS: 50
steps:
- name: Find Build & Deploy Images Comment
uses: peter-evans/find-comment@v3
id: preview-build-deploy-images-comment-start
with:
issue-number: ${{ github.event.pull_request.number }}
comment-author: "github-actions[bot]"
body-includes: "Preview - Build & Deploy Images"
- name: Update comment on deploy images start
uses: peter-evans/create-or-update-comment@v3
with:
comment-id: ${{ steps.preview-build-deploy-images-comment-start.outputs.comment-id }}
issue-number: ${{ github.event.pull_request.number }}
body: |
## Preview - Build & Deploy Images
✅ Build images
⏳ Deploying images...
🔍 Track progress in the [workflow run](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})
edit-mode: replace
- name: Update deployment status (deploying)
run: |
gh api \
--method POST \
-H "Accept: application/vnd.github+json" \
/repos/${{ github.repository }}/deployments/${{ needs.create_deployment.outputs.deployment_id }}/statuses \
-f state="in_progress" \
-f description="Deploying images..." \
-f environment=preview-pr-${{ github.event.pull_request.number }} \
-f log_url="${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Configure AWS Credentials
uses: aws-actions/configure-aws-credentials@v4
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID_DEVELOPMENT }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY_DEVELOPMENT }}
aws-region: ${{ secrets.AWS_REGION }}
- name: Wait for stack deployment
id: check-stack
run: |
ATTEMPTS=0
echo "Waiting for stack deployment..."
while [ $ATTEMPTS -lt ${{ env.MAX_ATTEMPTS }} ]; do
if ! STATUS=$(aws cloudformation describe-stacks \
--stack-name ${{ env.STACK_NAME }} \
--query 'Stacks[0].StackStatus' \
--output text 2>&1); then
echo "Error getting stack status: $STATUS"
echo "Stack might not exist yet. Waiting..."
sleep 30
ATTEMPTS=$((ATTEMPTS + 1))
continue
fi
echo "Current stack status: $STATUS"
# Fail if stack is in a failed or rollback state
if [[ $STATUS == *FAILED* ]] || [[ $STATUS == *ROLLBACK* ]]; then
echo "::error::Stack is in a failed or rollback state: $STATUS"
exit 1
fi
# Continue if stack is ready
if [[ $STATUS == "CREATE_COMPLETE" ]] || [[ $STATUS == "UPDATE_COMPLETE" ]]; then
echo "::notice::Stack is ready with status: $STATUS"
break
fi
# Wait and check again if stack is still being created/updated
if [[ $STATUS == *IN_PROGRESS* ]]; then
echo "Stack operation in progress. Waiting 30 seconds..."
sleep 30
ATTEMPTS=$((ATTEMPTS + 1))
continue
fi
done
if [ $ATTEMPTS -eq ${{ env.MAX_ATTEMPTS }} ]; then
echo "::error::Timeout waiting for stack to be ready"
exit 1
fi
- name: Get EC2 Instance ID
id: get-instance
run: |
INSTANCE_ID=$(aws cloudformation describe-stack-resources \
--stack-name ${{ env.STACK_NAME }} \
--logical-resource-id EC2Instance \
--query 'StackResources[0].PhysicalResourceId' \
--output text)
if [ -z "$INSTANCE_ID" ]; then
echo "::error::Failed to get EC2 instance ID"
exit 1
fi
echo "instance_id=$INSTANCE_ID" >> $GITHUB_OUTPUT
- name: Wait for instance to be ready
run: |
aws ec2 wait instance-status-ok \
--instance-ids ${{ steps.get-instance.outputs.instance_id }}
- name: Run deployment script on EC2
id: deploy
run: |
COMMAND_ID=$(aws ssm send-command \
--instance-ids ${{ steps.get-instance.outputs.instance_id }} \
--document-name "AWS-RunShellScript" \
--parameters commands=["cd /quadratic-selfhost && ./login.sh && ./pull_start.sh"] \
--comment "Deploying new images after build" \
--query 'Command.CommandId' \
--output text)
# Wait for command completion
ATTEMPTS=0
echo "Waiting for deployment command to complete..."
while [ $ATTEMPTS -lt ${{ env.MAX_ATTEMPTS }} ]; do
STATUS=$(aws ssm get-command-invocation \
--command-id "$COMMAND_ID" \
--instance-id ${{ steps.get-instance.outputs.instance_id }} \
--query "Status" \
--output text 2>/dev/null || echo "Pending")
echo "Command status: $STATUS"
if [ "$STATUS" = "Success" ]; then
echo "Deployment completed successfully"
exit 0
elif [ "$STATUS" = "Failed" ] || [ "$STATUS" = "Cancelled" ] || [ "$STATUS" = "TimedOut" ]; then
echo "Deployment failed with status: $STATUS"
# Get command output for debugging
aws ssm get-command-invocation \
--command-id "$COMMAND_ID" \
--instance-id ${{ steps.get-instance.outputs.instance_id }} \
--query "StandardOutputContent" \
--output text
exit 1
fi
echo "Waiting for deployment to complete... (Attempt $((ATTEMPTS + 1))/${{ env.MAX_ATTEMPTS }})"
sleep 30
ATTEMPTS=$((ATTEMPTS + 1))
done
echo "Deployment timed out after ${{ env.MAX_ATTEMPTS }} attempts"
exit 1
- name: Delete untagged (old) images
run: |
for service in client api connection files multiplayer; do
REPO_NAME="quadratic-${service}"
# Get list of untagged image digests
UNTAGGED_IMAGES=$(aws ecr list-images \
--repository-name "$REPO_NAME" \
--filter "tagStatus=UNTAGGED" \
--query 'imageIds[*].imageDigest' \
--output json)
# Delete untagged images if any exist
if [ "$UNTAGGED_IMAGES" != "[]" ]; then
echo "Deleting untagged images from $REPO_NAME"
aws ecr batch-delete-image \
--repository-name "$REPO_NAME" \
--image-ids "$(echo $UNTAGGED_IMAGES | jq -r 'map({imageDigest: .}) | @json')"
else
echo "No untagged images found in $REPO_NAME"
fi
done
- name: Generate Deploy Metadata
id: deploy-metadata
run: |
# Sanitize branch name for DNS
BRANCH_NAME="${{ github.event.pull_request.head.ref }}"
SANITIZED_BRANCH_NAME=$(echo "$BRANCH_NAME" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9-]/-/g' | sed 's/-\+/-/g' | sed 's/^-\|-$//')
echo "WebsiteURL=https://${SANITIZED_BRANCH_NAME}.quadratic-preview.com" >> $GITHUB_OUTPUT
echo "DEPLOY_TIME=$(date -u +'%b %d, %Y at %I:%M %p UTC')" >> $GITHUB_OUTPUT
- name: Find Build & Deploy Images Comment
if: always()
uses: peter-evans/find-comment@v3
id: preview-build-deploy-images-comment-update
with:
issue-number: ${{ github.event.pull_request.number }}
comment-author: "github-actions[bot]"
body-includes: "Preview - Build & Deploy Images"
- name: Update comment on deploy images success
if: success()
uses: peter-evans/create-or-update-comment@v3
with:
comment-id: ${{ steps.preview-build-deploy-images-comment-update.outputs.comment-id }}
issue-number: ${{ github.event.pull_request.number }}
body: |
## Preview - Build & Deploy Images
✅ Build images
✅ Deploy images
🕒 Last deployed: ${{ steps.deploy-metadata.outputs.DEPLOY_TIME }}
🔗 URL: ${{ steps.deploy-metadata.outputs.WebsiteURL }}
edit-mode: replace
- name: Update comment on deploy images failure
if: failure()
uses: peter-evans/create-or-update-comment@v3
with:
comment-id: ${{ steps.preview-build-deploy-images-comment-update.outputs.comment-id }}
issue-number: ${{ github.event.pull_request.number }}
body: |
## Preview - Build & Deploy Images
✅ Build images
❌ Deploy images
🔍 Please check the [workflow run](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}) for details.
edit-mode: replace
- name: Update deployment status (success)
if: success()
run: |
gh api \
--method POST \
-H "Accept: application/vnd.github+json" \
/repos/${{ github.repository }}/deployments/${{ needs.create_deployment.outputs.deployment_id }}/statuses \
-f state="success" \
-f environment_url="${{ steps.deploy-metadata.outputs.WebsiteURL }}" \
-f description="Deployment successful!" \
-f environment=preview-pr-${{ github.event.pull_request.number }} \
-f log_url="${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Update deployment status (failure)
if: failure()
run: |
gh api \
--method POST \
-H "Accept: application/vnd.github+json" \
/repos/${{ github.repository }}/deployments/${{ needs.create_deployment.outputs.deployment_id }}/statuses \
-f state="failure" \
-f description="Deployment failed" \
-f environment=preview-pr-${{ github.event.pull_request.number }} \
-f log_url="${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}