Skip to content

Deploying to staging #431

Deploying to staging

Deploying to staging #431

Workflow file for this run

name: Deploying to staging
run-name: Deploying to staging
concurrency:
group: deploy-staging-pr-${{ github.event.client_payload.slash_command.args.named.prId }}
cancel-in-progress: true
on:
repository_dispatch:
types: [deploy-command]
# this should only be used for testing purposes.
workflow_dispatch:
inputs:
prId:
description: "PR to deploy"
required: true
permissions:
contents: write
issues: write
pull-requests: write
statuses: write
jobs:
getPRHead:
name: Get PR head SHA
runs-on: ubuntu-latest
outputs:
sha: ${{ steps.pr-head.outputs.result }}
steps:
- name: Get PR head SHA
id: pr-head
uses: actions/github-script@v7
with:
result-encoding: string
script: |
const prId = ${{
(github.event_name == 'repository_dispatch' && github.event.client_payload.slash_command.args.named.prId)
|| github.event.inputs.prId
}};
const { data: pr } = await github.rest.pulls.get({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: parseInt(prId, 10)
});
return pr.head.sha;
sendMessage:
name: Send deploying to staging message
runs-on: ubuntu-latest
needs: getPRHead
steps:
- name: Comment on PR
uses: actions/github-script@v7
with:
script: |
const prId = ${{
(github.event_name == 'repository_dispatch' && github.event.client_payload.slash_command.args.named.prId)
|| github.event.inputs.prId
}};
const runUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`;
const commitHash = '${{ needs.getPRHead.outputs.sha }}';
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: parseInt(prId, 10),
body: `The command to deploy to staging for the commit ${commitHash} has been triggered. [View action run](${runUrl})`
});
checkExistingImage:
name: Check if image already exists
runs-on: ubuntu-latest
needs: [getPRHead, sendMessage]
outputs:
image-exists: ${{ steps.check-image.outputs.exists }}
git-sha: ${{ steps.set-sha.outputs.sha }}
steps:
- name: Set SHA
id: set-sha
run: echo "sha=$(echo "${{ needs.getPRHead.outputs.sha }}" | cut -c1-7)" >> $GITHUB_OUTPUT
- name: Check if Docker image exists
id: check-image
run: |
SHA7=$(echo "${{ needs.getPRHead.outputs.sha }}" | cut -c1-7)
if docker manifest inspect tahminator/codebloom:staging-$SHA7 > /dev/null 2>&1; then
echo "exists=true" >> $GITHUB_OUTPUT
echo "Image staging-$SHA7 already exists"
else
echo "exists=false" >> $GITHUB_OUTPUT
echo "Image staging-$SHA7 does not exist"
fi
promoteExisting:
name: Promote existing image to staging-latest
runs-on: ubuntu-latest
needs: [getPRHead, checkExistingImage]
if: needs.checkExistingImage.outputs.image-exists == 'true'
steps:
- name: Checkout Repository
uses: actions/checkout@v4
with:
ref: ${{ needs.getPRHead.outputs.sha }}
- name: Load secrets
uses: ./.github/composite/load-secrets
with:
GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }}
GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }}
UNLOAD_ENVIRONMENTS: ci
- name: Log in to Docker Hub
uses: docker/login-action@v3
with:
registry: docker.io
username: tahminator
password: ${{ env.DOCKER_HUB_PAT }}
- name: Promote existing image
run: |
SHA7=${{ needs.checkExistingImage.outputs.git-sha }}
docker pull tahminator/codebloom:staging-$SHA7
docker tag tahminator/codebloom:staging-$SHA7 tahminator/codebloom:staging-latest
docker push tahminator/codebloom:staging-latest
echo "Promoted staging-$SHA7 to staging-latest"
backendPreTest:
name: Backend Compile Test
runs-on: ubuntu-latest
needs: [getPRHead, checkExistingImage]
if: needs.checkExistingImage.outputs.image-exists == 'false'
steps:
- name: Checkout Repository
uses: actions/checkout@v4
with:
ref: ${{ needs.getPRHead.outputs.sha }}
- name: Run backend pre test
uses: ./.github/composite/test/backend-pre-test
backendTests:
name: Backend Tests
runs-on: ubuntu-latest
needs: [getPRHead, checkExistingImage, backendPreTest]
if: needs.checkExistingImage.outputs.image-exists == 'false'
steps:
- name: Checkout Repository
uses: actions/checkout@v4
with:
ref: ${{ needs.getPRHead.outputs.sha }}
- name: Disable man-db
uses: ./.github/composite/disable-mandb
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: 20
- name: Set up OpenJDK 25
uses: actions/setup-java@v4
with:
distribution: "temurin"
java-version: "25"
cache: "maven"
- name: Verify Java version
run: |
java -version
javac -version
echo "JAVA_HOME=$JAVA_HOME"
- name: Load secrets
uses: ./.github/composite/load-secrets
with:
GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }}
GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }}
UNLOAD_ENVIRONMENTS: ci-app
- name: Run script
run: bash .github/scripts/run-backend-tests.sh
frontendTests:
name: Frontend Tests
runs-on: ubuntu-latest
needs: [getPRHead, checkExistingImage, backendPreTest]
if: needs.checkExistingImage.outputs.image-exists == 'false'
steps:
- name: Checkout Repository
uses: actions/checkout@v4
with:
ref: ${{ needs.getPRHead.outputs.sha }}
- name: Disable man-db
uses: ./.github/composite/disable-mandb
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: 20
- name: Set up OpenJDK 25
uses: actions/setup-java@v4
with:
distribution: "temurin"
java-version: "25"
cache: "maven"
- name: Verify Java version
run: |
java -version
javac -version
echo "JAVA_HOME=$JAVA_HOME"
- name: Fix a bug with corepack by installing corepack globally
run: npm i -g corepack@latest
working-directory: js
- name: Load secrets
uses: ./.github/composite/load-secrets
with:
GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }}
GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }}
UNLOAD_ENVIRONMENTS: ci-app
- name: Run script
run: bash .github/scripts/run-frontend-tests.sh
validateDBSchema:
name: Validate DB Schema on Staging DB
runs-on: ubuntu-latest
needs: [getPRHead, checkExistingImage, backendPreTest]
if: needs.checkExistingImage.outputs.image-exists == 'false'
steps:
- name: Checkout Repository
uses: actions/checkout@v4
with:
ref: ${{ needs.getPRHead.outputs.sha }}
- name: Disable man-db
uses: ./.github/composite/disable-mandb
- name: Set up OpenJDK 25
uses: actions/setup-java@v4
with:
distribution: "temurin"
java-version: "25"
cache: "maven"
- name: Verify Java version
run: |
java -version
javac -version
echo "JAVA_HOME=$JAVA_HOME"
- name: Load secrets
uses: ./.github/composite/load-secrets
with:
GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }}
GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }}
UNLOAD_ENVIRONMENTS: staging
- name: Validate DB Schema
run: ./mvnw flyway:validate -Dflyway.locations=filesystem:./db/migration -Dflyway.ignoreMigrationPatterns="*:pending"
buildImage:
name: Build Docker Image & Upload to Registry
runs-on: ubuntu-latest
needs:
[
getPRHead,
checkExistingImage,
frontendTests,
backendTests,
validateDBSchema,
backendPreTest,
]
if: needs.checkExistingImage.outputs.image-exists == 'false'
steps:
- name: Checkout Repository
uses: actions/checkout@v4
with:
ref: ${{ needs.getPRHead.outputs.sha }}
- name: Disable man-db
uses: ./.github/composite/disable-mandb
- name: Set up OpenJDK 25
uses: actions/setup-java@v4
with:
distribution: "temurin"
java-version: "25"
cache: "maven"
- name: Verify Java version
run: |
java -version
javac -version
echo "JAVA_HOME=$JAVA_HOME"
- name: Load secrets
uses: ./.github/composite/load-secrets
with:
GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }}
GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }}
UNLOAD_ENVIRONMENTS: ci,ci-app
- name: Run script
run: bash .github/scripts/build-image.sh
env:
TAG_PREFIX: staging-
SERVER_PROFILES: stg
redeploy:
name: Redeploy on DigitalOcean
runs-on: ubuntu-latest
needs: [buildImage, promoteExisting, getPRHead]
if: always() && (needs.buildImage.result == 'success' || needs.promoteExisting.result == 'success')
steps:
- name: Checkout Repository
uses: actions/checkout@v4
with:
ref: ${{ needs.getPRHead.outputs.sha }}
- name: Disable man-db
uses: ./.github/composite/disable-mandb
- name: Set up OpenJDK 25
uses: actions/setup-java@v4
with:
distribution: "temurin"
java-version: "25"
cache: "maven"
- name: Verify Java version
run: |
java -version
javac -version
echo "JAVA_HOME=$JAVA_HOME"
- name: Load secrets
uses: ./.github/composite/load-secrets
with:
GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }}
GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }}
UNLOAD_ENVIRONMENTS: ci,staging
- name: Install doctl
uses: digitalocean/action-doctl@v2
with:
token: ${{ env.DIGITAL_OCEAN_PAT }}
- name: Check for DB changes
id: db-changes
run: |
git fetch origin main:main
if git diff --name-only main...${{ needs.getPRHead.outputs.sha }} | grep -q '^db/'; then
echo "changed=true" >> $GITHUB_OUTPUT
echo "DB changes detected"
else
echo "changed=false" >> $GITHUB_OUTPUT
echo "No DB changes detected"
fi
- name: Migrate Staging DB
if: steps.db-changes.outputs.changed == 'true'
run: ./mvnw flyway:migrate -Dflyway.locations=filesystem:./db/migration
- name: Transform App Spec with secrets
run: bash .github/scripts/transform-yaml-with-env.sh .do/stg/app.yml .env.staging
- name: Update App Spec on DigitalOcean
run: |
OUTPUT=$(doctl apps update ${{ env.DIGITAL_OCEAN_APP_ID }} --spec .do/stg/app.yml --update-sources --output json)
DEPLOYMENT_ID=$(echo "$OUTPUT" | jq -r '.[0].pending_deployment.id // .[0].in_progress_deployment.id // .[0].active_deployment.id // empty' | head -n 1)
if [ -z "$DEPLOYMENT_ID" ] || [ "$DEPLOYMENT_ID" == "null" ]; then
echo "Failed to extract deployment ID from app update response"
exit 1
fi
echo "DEPLOYMENT_ID=$DEPLOYMENT_ID" >> $GITHUB_ENV
- name: Poll Deployment Status
run: |
if [ -z "$DEPLOYMENT_ID" ]; then
echo "Deployment ID is empty."
exit 1
fi
echo "Waiting for deployment to be promoted."
for i in {1..60}; do
RESPONSE=$(curl -s -X GET \
-H "Content-Type: application/json" \
-H "Authorization: Bearer ${{ env.DIGITAL_OCEAN_PAT }}" \
"https://api.digitalocean.com/v2/apps/${{ env.DIGITAL_OCEAN_APP_ID }}/deployments/$DEPLOYMENT_ID")
PHASE=$(echo "$RESPONSE" | jq -r '.deployment.phase')
echo "Deployment phase: $PHASE"
if [ "$PHASE" == "ACTIVE" ]; then
echo "Deployment is active."
exit 0
elif [ "$PHASE" == "SUPERSEDED" ] || [ "$PHASE" == "ERROR" ] || [ "$PHASE" == "CANCELED" ]; then
echo "Deployment failed with phase: $PHASE"
exit 1
fi
echo "Waiting for deployment to complete, sleep 10... ($i/60)"
sleep 10
done
echo "Deployment did not reach a valid state within 10 minutes."
exit 1
notifyStatus:
name: Notify on deployment status
runs-on: ubuntu-latest
needs:
[
getPRHead,
checkExistingImage,
sendMessage,
backendTests,
backendPreTest,
frontendTests,
validateDBSchema,
buildImage,
promoteExisting,
redeploy,
]
if: always()
steps:
- name: Comment on PR about deployment status
uses: actions/github-script@v7
with:
script: |
const prId = ${{
(github.event_name == 'repository_dispatch' && github.event.client_payload.slash_command.args.named.prId)
|| github.event.inputs.prId
}};
const runUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`;
const commitHash = '${{ needs.getPRHead.outputs.sha }}';
const needsObj = ${{ toJSON(needs) }};
const jobNames = Object.keys(needsObj);
const failedJobs = jobNames.filter((name) => needsObj[name].result === 'failure');
const cancelledJobs = jobNames.filter((name) => needsObj[name].result === 'cancelled');
const successJobs = jobNames.filter((name) => needsObj[name].result === 'success');
let statusMessage;
let status = false;
if (failedJobs.length > 0 && cancelledJobs.length > 0) {
statusMessage = `**Staging deployment failed and was cancelled** for commit ${commitHash}`;
status = false;
} else if (failedJobs.length > 0) {
statusMessage = `**Staging deployment failed** for commit ${commitHash}`;
status = false;
} else if (cancelledJobs.length > 0) {
statusMessage = `**Staging deployment was cancelled** for commit ${commitHash}`;
status = false;
} else {
statusMessage = `**Staging deployment succeeded** for commit ${commitHash}`;
status = true;
}
await github.rest.repos.createCommitStatus({
owner: context.repo.owner,
repo: context.repo.repo,
sha: commitHash,
state: status ? 'success' : 'failure',
context: "Successful deployment to staging",
description: status ? undefined : "Successful deployment to staging is required",
});
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: parseInt(prId, 10),
body: `${statusMessage}\n\n[View run](${runUrl})`
});