Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 60 additions & 0 deletions .github/actions/discord-notify/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
name: Discord deploy notification
description: Post a green (healthy) or red (failed) deploy-status embed to Discord.

inputs:
webhook-url:
description: Discord webhook URL. When empty, the step is a no-op.
required: true
name:
description: Deploy target name shown in the title (e.g. kaapi-staging).
required: true
release:
description: Release identifier (tag or run number).
required: true
sha:
description: Full commit SHA; truncated to 7 chars for display.
required: true
run-url:
description: Link to the workflow run.
required: true
ok:
description: "'true' for a healthy deploy; anything else renders as failed."
required: true
failure-reason:
description: Optional extra line explaining a failure (e.g. CI blocked the release).
required: false
default: ""

runs:
using: composite
steps:
- shell: bash
env:
DISCORD_WEBHOOK_URL: ${{ inputs.webhook-url }}
NAME: ${{ inputs.name }}
RELEASE: ${{ inputs.release }}
SHA: ${{ inputs.sha }}
RUN_URL: ${{ inputs.run-url }}
OK: ${{ inputs.ok }}
REASON: ${{ inputs.failure-reason }}
run: |
[ -z "$DISCORD_WEBHOOK_URL" ] && { echo "No webhook configured, skipping"; exit 0; }
if [ "$OK" = "true" ]; then
TITLE="🟢 $NAME deployment healthy"; COLOR=3066993 # green
else
TITLE="🔴 $NAME deployment failed"; COLOR=15158332 # red
fi
SHA_SHORT=$(echo "$SHA" | cut -c1-7)

fields=$(jq -n --arg release "$RELEASE" --arg sha "$SHA_SHORT" \
'[{name:"Release",value:$release,inline:true},{name:"SHA",value:$sha,inline:true}]')
# Only a failed deploy with a stated cause gets the extra Reason line.
if [ "$OK" != "true" ] && [ -n "$REASON" ]; then
fields=$(echo "$fields" | jq --arg r "$REASON" '. + [{name:"Reason",value:$r,inline:false}]')
fi

payload=$(jq -n --arg title "$TITLE" --argjson color "$COLOR" \
--arg url "$RUN_URL" --argjson fields "$fields" \
'{embeds:[{title:$title,url:$url,color:$color,fields:$fields,timestamp:(now|todate)}]}')
curl -sf -H "Content-Type: application/json" -X POST -d "$payload" "$DISCORD_WEBHOOK_URL" \
|| echo "Discord notification failed to send"

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just wanted to check if we should use exit 1 here when the Discord notification fails to send. This way, we can at least flag that something went wrong with the webhook, even if the deployment itself was healthy.

If this behavior is intentional, feel free to ignore my comment.

78 changes: 66 additions & 12 deletions .github/workflows/create-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,45 +5,82 @@ on:
tags:
- "v[0-9]+.[0-9]+.[0-9]+" # Deploy only when tags like v1.0.0, v2.1.0, etc., are created

concurrency:
group: production-ec2-deploy
cancel-in-progress: false

jobs:
verify-ci:
runs-on: ubuntu-latest
permissions:
contents: read
actions: read
steps:
- name: Confirm CI passed on the tagged commit
env:
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
SHA="${{ github.sha }}"
echo "Checking Kaapi CI conclusion for ${SHA}"
CONCLUSION=$(gh api \
"repos/${{ github.repository }}/actions/runs?head_sha=${SHA}" \
--jq 'first(.workflow_runs[]
| select(.name == "Kaapi CI" and .status == "completed")
| .conclusion) // "missing"')
echo "Kaapi CI conclusion: ${CONCLUSION}"
if [ "${CONCLUSION}" != "success" ]; then
echo "::error::Kaapi CI has not passed for ${SHA} (conclusion: ${CONCLUSION}). Aborting release."
exit 1
fi

deploy:
needs: verify-ci
runs-on: ubuntu-latest
environment: AWS_ENV

permissions:
packages: write
contents: read
attestations: write
id-token: write
contents: read

steps:
- name: Checkout Repository
uses: actions/checkout@v6

- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v6
with:
role-to-assume: ${{ secrets.AWS_ROLE_ARN }}
aws-region: ${{ secrets.AWS_REGION }}

- name: Deploy via SSM
- name: Trigger deploy on EC2 via SSM
id: ssm
env:
BUILD_DIRECTORY: ${{ secrets.BUILD_DIRECTORY }}
APP_NAME: ${{ secrets.PM2_APP_NAME }}
AWS_REGION: ${{ secrets.AWS_REGION }}
INSTANCE_ID: ${{ secrets.EC2_INSTANCE_ID }}
ROOT_USER: ${{ secrets.USER }}
DEPLOY_SHA: ${{ github.sha }}
run: |
REMOTE_CMD="export HOME=/home/$ROOT_USER && export NVM_DIR="/home/$ROOT_USER/.nvm" && [ -s "\$NVM_DIR/nvm.sh" ] && \. "\$NVM_DIR/nvm.sh" && git config --global --add safe.directory ${BUILD_DIRECTORY} && set -e && cd ${BUILD_DIRECTORY} && git pull origin main && npm ci && npm run build && sudo -iu ${ROOT_USER} pm2 restart ${APP_NAME}"
: "${BUILD_DIRECTORY:?BUILD_DIRECTORY secret is not set}"

REMOTE_CMD="git config --global --add safe.directory $BUILD_DIRECTORY \
&& cd $BUILD_DIRECTORY \
&& git fetch origin --tags --prune \
&& git checkout $DEPLOY_SHA \
&& npm ci \
&& npm run build \
&& pm2 restart $APP_NAME"

# Build the parameters as JSON so quoting in REMOTE_CMD can't break it.
PARAMS=$(jq -n --arg cmd "$REMOTE_CMD" '{commands: [$cmd]}')

CMD_ID=$(aws ssm send-command \
--instance-ids "$INSTANCE_ID" \
--document-name "AWS-RunShellScript" \
--parameters commands="[\"$REMOTE_CMD\"]" \
--comment "Deploy kaapi-frontend ${{ github.ref_name }}" \
--parameters "$PARAMS" \
--region "$AWS_REGION" \
--query 'Command.CommandId' \
--output text)
--query "Command.CommandId" --output text)
echo "cmd_id=$CMD_ID" >> "$GITHUB_OUTPUT"
echo "Sent SSM command: $CMD_ID"

- name: Wait for SSM command to finish
timeout-minutes: 20
Expand Down Expand Up @@ -83,3 +120,20 @@ jobs:
--instance-id "$INSTANCE_ID" \
--query '{Status:Status,Stdout:StandardOutputContent,Stderr:StandardErrorContent}' \
--output json

notify:
needs: [verify-ci, deploy]
if: always()
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: ./.github/actions/discord-notify
with:
webhook-url: ${{ secrets.DISCORD_WEBHOOK_URL }}
name: kaapi-frontend-production
release: ${{ github.ref_name }}
sha: ${{ github.sha }}
run-url: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
ok: ${{ needs.deploy.result == 'success' }}
# Call out the specific case where the release was blocked by CI.
failure-reason: ${{ needs.verify-ci.result != 'success' && 'CI never passed on the tagged commit — release blocked.' || '' }}
76 changes: 58 additions & 18 deletions .github/workflows/deploy-staging.yml
Original file line number Diff line number Diff line change
@@ -1,56 +1,81 @@
name: Deploy Kaapi to EC2 Staging

on:
push:
branches:
- main # Deploy only when changes are pushed to the main branch
workflow_run:
workflows: ["Kaapi CI"]
branches: [main]
types: [completed]
workflow_dispatch:

concurrency:
group: staging-ec2-deploy
cancel-in-progress: false

jobs:
deploy:
runs-on: ubuntu-latest
environment: AWS_ENV
environment: Staging
# workflow_dispatch runs unconditionally; workflow_run only on a green CI from a push to main (not PRs).
if: >-
${{ github.event_name == 'workflow_dispatch' ||
github.event.workflow_run.conclusion == 'success' }}

permissions:
packages: write
contents: read
attestations: write
id-token: write
contents: read

steps:
- name: Checkout Repository
uses: actions/checkout@v6

- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v6
with:
role-to-assume: ${{ secrets.AWS_ROLE_ARN }}
role-to-assume: ${{ secrets.DEPLOY_ROLE_ARN }}
aws-region: ${{ secrets.AWS_REGION }}

- name: Deploy via SSM
- name: Trigger deploy on EC2 via SSM
id: ssm
env:
BUILD_DIRECTORY: ${{ secrets.BUILD_DIRECTORY }}
APP_NAME: ${{ secrets.PM2_APP_NAME }}
AWS_REGION: ${{ secrets.AWS_REGION }}
INSTANCE_ID: ${{ secrets.EC2_STAGING_INSTANCE_ID }}
INSTANCE_ID: ${{ secrets.INSTANCE_ID }}
ROOT_USER: ${{ secrets.USER }}
DEPLOY_SHA: ${{ github.event.workflow_run.head_sha || github.sha }}
run: |
REMOTE_CMD="export HOME=/home/$ROOT_USER && export NVM_DIR="/home/$ROOT_USER/.nvm" && [ -s "\$NVM_DIR/nvm.sh" ] && \. "\$NVM_DIR/nvm.sh" && git config --global --add safe.directory ${BUILD_DIRECTORY} && set -e && cd ${BUILD_DIRECTORY} && git pull origin main && npm ci && npm run build && sudo -iu ${ROOT_USER} pm2 restart ${APP_NAME}"
: "${BUILD_DIRECTORY:?BUILD_DIRECTORY secret is not set}"
: "${ROOT_USER:?USER secret is not set}"

REMOTE_CMD="export HOME=/home/$ROOT_USER \
&& export NVM_DIR=/home/$ROOT_USER/.nvm \
&& . \$NVM_DIR/nvm.sh \
&& git config --global --add safe.directory $BUILD_DIRECTORY \
&& cd $BUILD_DIRECTORY \
&& git fetch origin --tags --prune \
&& git checkout $DEPLOY_SHA \
&& npm ci \
&& npm run build \
&& sudo -iu $ROOT_USER pm2 restart $APP_NAME"

# Build the parameters as JSON so quoting in REMOTE_CMD can't break it.
PARAMS=$(jq -n --arg cmd "$REMOTE_CMD" '{commands: [$cmd]}')

CMD_ID=$(aws ssm send-command \
--instance-ids "$INSTANCE_ID" \
--document-name "AWS-RunShellScript" \
--parameters commands="[\"$REMOTE_CMD\"]" \
--comment "Deploy kaapi-frontend kaapi-staging" \
--parameters "$PARAMS" \
--region "$AWS_REGION" \
--query 'Command.CommandId' \
--output text)
--query "Command.CommandId" --output text)
echo "cmd_id=$CMD_ID" >> "$GITHUB_OUTPUT"
echo "Sent SSM command: $CMD_ID"

- name: Wait for SSM command to finish
timeout-minutes: 20
env:
INSTANCE_ID: ${{ secrets.EC2_STAGING_INSTANCE_ID }}
INSTANCE_ID: ${{ secrets.INSTANCE_ID }}
CMD_ID: ${{ steps.ssm.outputs.cmd_id }}
run: |
: "${INSTANCE_ID:?INSTANCE_ID secret is not set}"
: "${CMD_ID:?SSM command ID missing from previous step}"
while true; do
if aws ssm wait command-executed \
--command-id "$CMD_ID" \
Expand Down Expand Up @@ -83,3 +108,18 @@ jobs:
--instance-id "$INSTANCE_ID" \
--query '{Status:Status,Stdout:StandardOutputContent,Stderr:StandardErrorContent}' \
--output json

notify:
needs: [deploy]
if: ${{ always() && needs.deploy.result != 'skipped' }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: ./.github/actions/discord-notify
with:
webhook-url: ${{ secrets.DISCORD_WEBHOOK_URL }}
name: kaapi-frontend-staging
release: "#${{ github.run_number }}"
sha: ${{ github.event.workflow_run.head_sha || github.sha }}
run-url: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
ok: ${{ needs.deploy.result == 'success' }}
Loading