Skip to content

Deploy lik-ui @ 2c0904a8b26b654e994de7f886010cac8591eeac by @yoomlam #46

Deploy lik-ui @ 2c0904a8b26b654e994de7f886010cac8591eeac by @yoomlam

Deploy lik-ui @ 2c0904a8b26b654e994de7f886010cac8591eeac by @yoomlam #46

Workflow file for this run

# Build each service image, push it to its Lightsail registry, then deploy it.
# The build/push runs in CI so the large image upload does not run from a maintainer's
# (slow) uplink. The `apply` job then runs `terraform apply` — but ONLY when the plan is a
# clean image swap (`Plan: N to add, 0 to change, N to destroy.`). Any other plan (config
# drift, anything with "to change") is left for the maintainer to review and apply locally
# with `./tf.sh apply` (the step summary echoes the exact -var image refs to use).
#
# Prerequisites (set once in repo Settings):
# - Variable AWS_DEPLOY_ROLE_ARN = the github_image_push_role_arn Terraform output.
# - Variable AWS_APPLY_ROLE_ARN = the github_apply_role_arn Terraform output.
# - Variable AWS_REGION = us-east-1.
# Auth is GitHub OIDC (no stored AWS keys). Both IAM roles' trust is scoped to this repo's
# `prod` environment (see infra/iam_github_oidc.tf).
name: Build and deploy images
run-name: Deploy ${{ inputs.service }} @ ${{ github.sha }} by @${{ github.actor }}
on:
workflow_dispatch:
inputs:
service:
description: Which service(s) to build and push
type: choice
options: [lik-ui, lik-mcp, both]
default: lik-ui
permissions:
id-token: write # required for OIDC
contents: read
jobs:
push:
runs-on: ubuntu-latest
# The prod environment scopes AWS_DEPLOY_ROLE_ARN / AWS_REGION (and enables optional
# deployment protection rules). Env-scoped vars only resolve when the job declares this.
environment: prod
strategy:
matrix:
include:
- name: lik-mcp
service: lik-mcp-prod
- name: lik-ui
service: lik-ui-prod
steps:
- name: Skip services not selected
id: gate
run: |
if [ "${{ inputs.service }}" != "both" ] && [ "${{ inputs.service }}" != "${{ matrix.name }}" ]; then
echo "skip=true" >> "$GITHUB_OUTPUT"
fi
- uses: actions/checkout@v6
if: steps.gate.outputs.skip != 'true'
- name: Configure AWS credentials (OIDC)
if: steps.gate.outputs.skip != 'true'
uses: aws-actions/configure-aws-credentials@v6
with:
role-to-assume: ${{ vars.AWS_DEPLOY_ROLE_ARN }}
aws-region: ${{ vars.AWS_REGION }}
- name: Install lightsailctl (required by push-container-image)
if: steps.gate.outputs.skip != 'true'
run: |
sudo curl -sL "https://s3.us-west-2.amazonaws.com/lightsailctl/latest/linux-amd64/lightsailctl" \
-o /usr/local/bin/lightsailctl
sudo chmod +x /usr/local/bin/lightsailctl
- name: Build image
if: steps.gate.outputs.skip != 'true'
run: docker build -t ${{ matrix.name }}:${{ github.sha }} ./${{ matrix.name }}
- name: Push to Lightsail registry
if: steps.gate.outputs.skip != 'true'
run: |
out=$(aws lightsail push-container-image \
--region "${{ vars.AWS_REGION }}" \
--service-name "${{ matrix.service }}" \
--label app \
--image "${{ matrix.name }}:${{ github.sha }}")
echo "$out"
# push-container-image prints: Refer to this image as ":svc.app.N" in deployments.
ref=$(echo "$out" | grep -oE '":[^"]+"' | tr -d '"' | tail -1)
# Hand the ref to the apply job via an artifact (matrix job outputs collide).
mkdir -p image-ref
printf '%s' "$ref" > "image-ref/${{ matrix.name }}.txt"
{
echo "### ${{ matrix.name }} pushed"
echo ""
echo "Image ref for \`terraform apply\`:"
echo ""
echo '```'
echo "$ref"
echo '```'
} >> "$GITHUB_STEP_SUMMARY"
- name: Upload image ref
if: steps.gate.outputs.skip != 'true'
uses: actions/upload-artifact@v7
with:
name: image-ref-${{ matrix.name }}
path: image-ref/${{ matrix.name }}.txt
if-no-files-found: error
# Deploy the pushed image(s) — but auto-apply only for a routine image swap. The gate is
# the plan summary: a per-service deployment_version replacement reads as "N to add,
# 0 to change, N to destroy" (see infra/lik_mcp.tf / lik_ui.tf). Anything else is left
# for the maintainer to review and apply locally.
apply:
needs: push
runs-on: ubuntu-latest
environment: prod
env:
AWS_REGION: ${{ vars.AWS_REGION }}
# Single deployment environment, so the custom-domain URLs are hardcoded here (they
# must be passed, or the empty default detaches the live domain — see infra/lik_ui.tf).
MCP_CUSTOM_DOMAIN_URL: https://mcp.lik.navapbc.com
UI_CUSTOM_DOMAIN_URL: https://ui.lik.navapbc.com
steps:
- uses: actions/checkout@v6
- name: Show deployed commit
run: |
{
echo "### Commit"
echo ""
echo "\`${{ github.sha }}\`"
echo ""
git log -1 --pretty='format:%s%n%n%b' "${{ github.sha }}"
} >> "$GITHUB_STEP_SUMMARY"
- name: Configure AWS credentials (OIDC)
uses: aws-actions/configure-aws-credentials@v6
with:
role-to-assume: ${{ vars.AWS_APPLY_ROLE_ARN }}
aws-region: ${{ vars.AWS_REGION }}
- uses: hashicorp/setup-terraform@v3
with:
terraform_version: "1.10.5"
terraform_wrapper: false
- name: Download image refs
uses: actions/download-artifact@v8
with:
pattern: image-ref-*
path: image-refs
merge-multiple: true
- name: Resolve image refs (built this run, else currently deployed)
run: |
set -euo pipefail
# For each service: use the ref just pushed (artifact) if present, otherwise read
# the currently-deployed ref from Lightsail. Both must be non-empty — passing an
# empty image var would DESTROY that service's deployment (count guard).
resolve() {
local name="$1" svc="$2" file ref
file="image-refs/${name}.txt"
if [ -f "$file" ]; then
ref=$(cat "$file")
else
ref=$(aws lightsail get-container-services --region "$AWS_REGION" \
--service-name "$svc" \
--query "containerServices[0].currentDeployment.containers.\"${name}\".image" \
--output text)
fi
if [ -z "$ref" ] || [ "$ref" = "None" ]; then
echo "::error::could not resolve image ref for ${name} (service ${svc})"
exit 1
fi
printf '%s' "$ref"
}
MCP_REF=$(resolve lik-mcp lik-mcp-prod)
UI_REF=$(resolve lik-ui lik-ui-prod)
echo "MCP_REF=$MCP_REF" >> "$GITHUB_ENV"
echo "UI_REF=$UI_REF" >> "$GITHUB_ENV"
echo "Resolved refs: lik-mcp=$MCP_REF lik-ui=$UI_REF"
- name: Terraform init
run: terraform -chdir=infra init -input=false
- name: Terraform plan
run: |
set -o pipefail # else `| tee` masks a plan failure and it leaks into the gate
terraform -chdir=infra plan -input=false -no-color -out=tfplan \
-var "lik_mcp_image=$MCP_REF" \
-var "lik_ui_image=$UI_REF" \
-var "mcp_custom_domain_url=$MCP_CUSTOM_DOMAIN_URL" \
-var "ui_custom_domain_url=$UI_CUSTOM_DOMAIN_URL" | tee plan.out
- name: Apply only a clean image swap
run: |
set -euo pipefail
summary=$(grep -E '^(Plan:|No changes\.)' plan.out | tail -1 || true)
echo "Plan summary: ${summary:-<none>}"
case "$summary" in
"Plan: 1 to add, 0 to change, 1 to destroy."|"Plan: 2 to add, 0 to change, 2 to destroy.")
echo "Clean image swap — applying."
terraform -chdir=infra apply -input=false tfplan
{
echo "### ✅ Deployed"
echo ""
echo "- lik-mcp: \`$MCP_REF\`"
echo "- lik-ui: \`$UI_REF\`"
} >> "$GITHUB_STEP_SUMMARY"
;;
"No changes."*)
{
echo "### Nothing to deploy"
echo ""
echo "Terraform reports no changes — the running deployment already matches."
} >> "$GITHUB_STEP_SUMMARY"
;;
*)
{
echo "### ⚠️ Not a clean image swap — apply skipped"
echo ""
echo "The plan below was not a routine image swap, so it was left for review:"
echo ""
echo '```'
echo "${summary:-see the Terraform plan step log}"
echo '```'
echo ""
echo "Review and apply locally with the image refs from this run:"
echo ""
echo '```'
echo "cd infra && ./tf.sh apply -var \"lik_mcp_image=$MCP_REF\" -var \"lik_ui_image=$UI_REF\""
echo '```'
} >> "$GITHUB_STEP_SUMMARY"
echo "::error::Plan is not a clean image swap — apply skipped, review and apply locally."
exit 1
;;
esac