Skip to content
Open
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
199 changes: 199 additions & 0 deletions .github/workflows/build-and-push.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
# Build and Push to ECR
# This workflow builds the Docker image and pushes it to AWS ECR
# Triggered on pushes to stage branch (tag-based releases can be enabled later)
#
# On pull_request: build-only (validates Dockerfile, no AWS auth required)
# On push to stage: build + push to ECR (requires OIDC role below)
#
# Authentication: GitHub OIDC
# Prerequisites:
# 1. AWS OIDC provider for token.actions.githubusercontent.com (already exists)
# 2. IAM role with trust policy condition:
# "StringEquals": { "token.actions.githubusercontent.com:aud": "sts.amazonaws.com" }
# "StringLike": { "token.actions.githubusercontent.com:sub": "repo:thunderbird/addons-server:ref:refs/heads/stage" }
# 3. Repository variable: AWS_ROLE_ARN (role ARN from step 2)
# Note: Can later be moved to an environment for stricter controls
# See: https://tinyurl.com/ghAwsOidc
#
# Publishing is gated on BOTH:
# - Event type (push, not pull_request)
# - vars.AWS_ROLE_ARN is set
# If either condition fails, then build succeeds but publish is skipped
#
# Required IAM permissions for the OIDC role:
# - ecr:GetAuthorizationToken
# - ecr:BatchCheckLayerAvailability
# - ecr:BatchGetImage
# - ecr:CompleteLayerUpload
# - ecr:DescribeImages
# - ecr:InitiateLayerUpload
# - ecr:GetDownloadUrlForLayer
# - ecr:ListImages
# - ecr:UploadLayerPart
# - ecr:PutImage

name: Build and Push to ECR

on:
push:
branches:
- stage
# tags:
# - 'v*' # Uncomment when tag-based releases are defined
pull_request:
branches:
- stage
- master

env:
AWS_REGION: us-west-2
ECR_REPOSITORY: atn-stage-addons-server
AWS_ACCOUNT_ID: "768512802988"

jobs:
# Build job: always runs, validates Dockerfile, no AWS permissions needed
build:
name: Build
runs-on: ubuntu-latest
permissions:
contents: read
# Note: no id-token here - minimum privilege for PR/build-only scenarios

steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3

- name: Extract metadata for Docker
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.AWS_ACCOUNT_ID }}.dkr.ecr.${{ env.AWS_REGION }}.amazonaws.com/${{ env.ECR_REPOSITORY }}
tags: |
type=ref,event=branch
type=ref,event=pr
type=sha,prefix=
# type=semver,pattern={{version}} # Enable when tag triggers are added
# type=semver,pattern={{major}}.{{minor}}

- name: Build Docker image
uses: docker/build-push-action@v5
with:
context: .
file: ./Dockerfile.ecs
push: false
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
build-args: |
OLYMPIA_UID=9500
OLYMPIA_GID=9500

# Informational job: shows why publishing skipped when not configured role
publish-disabled:
name: Publish (skipped - AWS_ROLE_ARN not set)
needs: build
runs-on: ubuntu-latest
if: github.event_name == 'push' && github.ref == 'refs/heads/stage' && vars.AWS_ROLE_ARN == ''
steps:
- name: Publishing not configured
run: |
echo "::notice::Publish skipped: AWS_ROLE_ARN repo variable not set (OIDC role not configured yet)"
echo "See workflow header comments for IAM role setup instructions"

# Publish job: only runs on push to stage when OIDC role is configured
publish:
name: Publish to ECR
needs: build
runs-on: ubuntu-latest
if: github.event_name == 'push' && github.ref == 'refs/heads/stage' && vars.AWS_ROLE_ARN != ''
concurrency:
group: ecr-stage-publish
cancel-in-progress: true
permissions:
contents: read
id-token: write # Required for OIDC authn - only granted when actually publishing

steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3

- name: Configure AWS credentials (OIDC)
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: ${{ vars.AWS_ROLE_ARN }}
aws-region: ${{ env.AWS_REGION }}

- name: Login to Amazon ECR
id: login-ecr
uses: aws-actions/amazon-ecr-login@v2

- name: Extract metadata for Docker
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.AWS_ACCOUNT_ID }}.dkr.ecr.${{ env.AWS_REGION }}.amazonaws.com/${{ env.ECR_REPOSITORY }}
tags: |
type=ref,event=branch
type=sha,prefix=
type=raw,value=stage-latest

- name: Build and push Docker image
id: build-image
uses: docker/build-push-action@v5
with:
context: .
file: ./Dockerfile.ecs
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
build-args: |
OLYMPIA_UID=9500
OLYMPIA_GID=9500

# Generate build metadata (future: bake into image or upload to S3 for traceability)
- name: Generate version.json
run: |
echo '{
"commit": "${{ github.sha }}",
"version": "${{ github.ref_name }}",
"source": "https://github.com/${{ github.repository }}",
"build": "https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}"
}' > version.json
cat version.json

- name: Image digest
run: echo "Image pushed with digest ${{ steps.build-image.outputs.digest }}"

# Deploy to ECS (optional - we would uncomment this when ready, or move to separate deploy.yml)
# deploy:
# name: Deploy to ECS
# needs: publish
# runs-on: ubuntu-latest
# permissions:
# contents: read
# id-token: write
#
# steps:
# - name: Configure AWS credentials (OIDC)
# uses: aws-actions/configure-aws-credentials@v4
# with:
# role-to-assume: ${{ vars.AWS_ROLE_ARN }}
# aws-region: ${{ env.AWS_REGION }}
#
# - name: Update ECS services
# run: |
# for service in web worker versioncheck; do
# aws ecs update-service \
# --cluster thunderbird-addons-stage-${service}-cluster \
# --service thunderbird-addons-stage-${service}-service \
# --force-new-deployment
# done
171 changes: 171 additions & 0 deletions .github/workflows/deploy-stage.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
---
# Deploy to Stage
#
# Runs `pulumi up` against the stage stack after a successful image push to ECR
# This requires manual approval via the _staging_ environment protection rule
#
# Trigger options
# - workflow_dispatch: manual trigger from GH Actions UI
# - workflow_run: automatically after build-and-push succeeds on stage
#
# Authentication: GH OIDC -> AWS IAM role
# Required secrets: PULUMI_ACCESS_TOKEN, PULUMI_PASSPHRASE
# Required variables: AWS_ROLE_ARN

name: deploy-stage

concurrency:
group: deploy-stage
cancel-in-progress: false # To avoid cancelling in-progress deploys

on:
workflow_dispatch:
inputs:
preview_only:
description: "Run pulumi preview only (no apply)"
required: false
default: "false"
type: choice
options:
- "false"
- "true"

workflow_run:
workflows: ["Build and Push to ECR"]
types: [completed]
branches: [stage]

permissions:
contents: read
id-token: write # OIDC authn

env:
AWS_REGION: us-west-2
PULUMI_STACK: thunderbird/thunderbird-addons/stage

jobs:
# Gate: only proceed if the triggering workflow succeeded (or manual)
check-trigger:
runs-on: ubuntu-latest
outputs:
should-deploy: ${{ steps.check.outputs.result }}
steps:
- name: Evaluate trigger
id: check
run: |
if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then
echo "result=true" >> $GITHUB_OUTPUT
elif [[ "${{ github.event.workflow_run.conclusion }}" == "success" ]]; then
echo "result=true" >> $GITHUB_OUTPUT
else
echo "result=false" >> $GITHUB_OUTPUT
echo "Skipping: triggering workflow did not succeed"
fi

deploy:
needs: check-trigger
if: needs.check-trigger.outputs.should-deploy == 'true'
runs-on: ubuntu-latest
timeout-minutes: 30
environment: staging # Would require approval if environment protection rules are set
steps:
- name: Checkout repository
uses: actions/checkout@v4

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

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.13"

- name: Install Pulumi CLI
uses: pulumi/actions@v6
with:
command: version

- name: Install dependencies
working-directory: infra/pulumi
run: |
python -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt

- name: Pulumi preview
working-directory: infra/pulumi
env:
PULUMI_ACCESS_TOKEN: ${{ secrets.PULUMI_ACCESS_TOKEN }}
PULUMI_CONFIG_PASSPHRASE: ${{ secrets.PULUMI_PASSPHRASE }}
run: |
source .venv/bin/activate
pulumi stack select ${{ env.PULUMI_STACK }}
pulumi preview --diff

- name: Pulumi up
if: github.event.inputs.preview_only != 'true'
working-directory: infra/pulumi
env:
PULUMI_ACCESS_TOKEN: ${{ secrets.PULUMI_ACCESS_TOKEN }}
PULUMI_CONFIG_PASSPHRASE: ${{ secrets.PULUMI_PASSPHRASE }}
run: |
source .venv/bin/activate
pulumi stack select ${{ env.PULUMI_STACK }}
pulumi up --yes --diff

- name: "Post-deploy: scale services to 0"
if: github.event.inputs.preview_only != 'true'
run: |
echo "Scaling ECS services to 0 (safety: prevents writes to shared stage DB)"
echo "Scale up manually after RO healthcheck validation"
PREFIX="thunderbird-addons-stage"
for svc in web worker versioncheck; do
CLUSTER="${PREFIX}-${svc}"
SERVICE="${PREFIX}-${svc}"
RESOURCE_ID="service/${CLUSTER}/${SERVICE}"

echo " Suspending autoscaling for ${svc}..."
aws application-autoscaling register-scalable-target \
--service-namespace ecs \
--scalable-dimension ecs:service:DesiredCount \
--resource-id "${RESOURCE_ID}" \
--suspended-state '{"DynamicScalingInSuspended":true,"DynamicScalingOutSuspended":true,"ScheduledScalingSuspended":true}' \
--region ${{ env.AWS_REGION }} 2>/dev/null || echo " (no autoscaling target for ${svc}, skipping)"

echo " Scaling ${svc} to 0..."
aws ecs update-service \
--cluster "${CLUSTER}" \
--service "${SERVICE}" \
--desired-count 0 \
--region ${{ env.AWS_REGION }} \
--query 'service.[serviceName,desiredCount]' \
--output text
done

- name: Post-deploy summary
if: github.event.inputs.preview_only != 'true'
working-directory: infra/pulumi
env:
PULUMI_ACCESS_TOKEN: ${{ secrets.PULUMI_ACCESS_TOKEN }}
PULUMI_CONFIG_PASSPHRASE: ${{ secrets.PULUMI_PASSPHRASE }}
run: |
source .venv/bin/activate
echo "## Deploy Summary" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "Stack: ${{ env.PULUMI_STACK }}" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
pulumi stack output --json | python -c "
import sys, json
outputs = json.load(sys.stdin)
print('| Output | Value |')
print('|--------|-------|')
for k, v in sorted(outputs.items()):
if isinstance(v, list):
v = ', '.join(str(i) for i in v)
print(f'| {k} | \`{v}\` |')
" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "Services scaled to 0. Run RO healthcheck before scaling up" >> $GITHUB_STEP_SUMMARY
Loading