Part 8: CI/CD with GitHub Actions¶
Overview¶
The project has two GitHub Actions workflows:
| Workflow | Trigger | Purpose |
|---|---|---|
CI (.github/workflows/ci.yml) |
PR to main, push to main | Typecheck, lint, test |
Deploy (.github/workflows/deploy.yml) |
Push to main (source changes) | Build, push to ECR, update runtime |
Plus a local pre-commit hook that catches issues before they reach CI.
Local Quality Gates¶
Pre-commit Hook¶
.githooks/pre-commit runs three checks before every commit:
#!/usr/bin/env bash
set -euo pipefail
echo "==> typecheck"
npm run typecheck
echo "==> lint + format"
npm run check
echo "==> test"
npm test
This is configured automatically during npm install via the prepare script:
If any check fails, the commit is rejected. Fix the issue, re-stage, and commit again.
CI Workflow¶
.github/workflows/ci.yml — runs on every PR and push to main:
name: CI
on:
pull_request:
branches: [main]
push:
branches: [main]
jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
- run: npm ci
- name: Typecheck
run: npm run typecheck
- name: Lint & format
run: npm run check
- name: Test
run: npm run test:coverage
This mirrors the pre-commit hook but runs in a clean environment and includes coverage reporting. The CI job uses Node 22 (latest LTS) for validation — the runtime uses Node 20 (matching the Docker image).
GitHub OIDC Setup¶
The deploy workflow uses GitHub OIDC to authenticate with AWS — no long-lived credentials needed. GitHub Actions requests a short-lived token from AWS STS, scoped to your repository.
Prerequisites¶
- A GitHub OIDC identity provider must exist in your AWS account. If it doesn't:
aws iam create-open-id-connect-provider \
--url https://token.actions.githubusercontent.com \
--client-id-list sts.amazonaws.com \
--thumbprint-list 6938fd4d98bab03faadb97b34396831e3780aea1
- The AgentCore runtime must already be created (see Part 7).
scripts/setup-github-iam.sh¶
This script creates the github-actions-recipe-agent IAM role:
#!/usr/bin/env bash
set -euo pipefail
REGION="${AWS_REGION:-us-west-2}"
ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)
ROLE_NAME="github-actions-recipe-agent"
REPO="cdot65/agentcore-recipe-agent"
ECR_REPO="recipe-extraction-agent"
RUNTIME_ID="${AGENTCORE_RUNTIME_ID:-recipe_extraction_agent-wkubdE7YBy}"
Trust Policy¶
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Principal": {
"Federated": "arn:aws:iam::ACCOUNT_ID:oidc-provider/token.actions.githubusercontent.com"
},
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {
"StringEquals": {
"token.actions.githubusercontent.com:aud": "sts.amazonaws.com"
},
"StringLike": {
"token.actions.githubusercontent.com:sub": "repo:cdot65/agentcore-recipe-agent:*"
}
}
}]
}
The StringLike condition scopes the role to your specific GitHub repository. Only workflows running from this repo can assume the role.
ECR Push Policy¶
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": "ecr:GetAuthorizationToken",
"Resource": "*"
},
{
"Effect": "Allow",
"Action": [
"ecr:BatchCheckLayerAvailability",
"ecr:GetDownloadUrlForLayer",
"ecr:BatchGetImage",
"ecr:PutImage",
"ecr:InitiateLayerUpload",
"ecr:UploadLayerPart",
"ecr:CompleteLayerUpload"
],
"Resource": "arn:aws:ecr:us-west-2:ACCOUNT_ID:repository/recipe-extraction-agent"
}
]
}
AgentCore Deploy Policy¶
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"bedrock-agentcore:UpdateAgentRuntime",
"bedrock-agentcore:GetAgentRuntime"
],
"Resource": "arn:aws:bedrock-agentcore:us-west-2:ACCOUNT_ID:runtime/RUNTIME_ID"
},
{
"Effect": "Allow",
"Action": "iam:PassRole",
"Resource": "arn:aws:iam::ACCOUNT_ID:role/BedrockAgentCoreRecipeAgent",
"Condition": {
"StringEquals": {
"iam:PassedToService": "bedrock-agentcore.amazonaws.com"
}
}
}
]
}
The iam:PassRole permission is required because update-agent-runtime passes the execution role to AgentCore.
Run the setup script¶
The script outputs the commands to set GitHub secrets:
Set GitHub repo secrets:
gh secret set AWS_ROLE_ARN -b "arn:aws:iam::123456789012:role/github-actions-recipe-agent"
gh secret set AGENTCORE_RUNTIME_ID -b "your-runtime-id"
gh secret set AGENTCORE_ROLE_ARN -b "arn:aws:iam::123456789012:role/BedrockAgentCoreRecipeAgent"
GitHub Secrets¶
Four secrets are needed in your repository:
| Secret | Value | Purpose |
|---|---|---|
AWS_ROLE_ARN |
arn:aws:iam::...:role/github-actions-recipe-agent |
OIDC role for CI/CD |
AGENTCORE_RUNTIME_ID |
Runtime ID from first deploy | Target runtime to update |
AGENTCORE_ROLE_ARN |
arn:aws:iam::...:role/BedrockAgentCoreRecipeAgent |
Execution role passed to runtime |
PRISMA_AIRS_PROFILE_NAME |
AIRS security profile name | Prisma AIRS profile for prompt/response scanning |
Set them with the gh CLI:
gh secret set AWS_ROLE_ARN -b "arn:aws:iam::123456789012:role/github-actions-recipe-agent"
gh secret set AGENTCORE_RUNTIME_ID -b "your-runtime-id"
gh secret set AGENTCORE_ROLE_ARN -b "arn:aws:iam::123456789012:role/BedrockAgentCoreRecipeAgent"
gh secret set PRISMA_AIRS_PROFILE_NAME -b "your-airs-profile-name"
Deploy Workflow¶
.github/workflows/deploy.yml:
name: Deploy to AgentCore
on:
push:
branches: [main]
paths:
- "src/**"
- "package.json"
- "package-lock.json"
- "Dockerfile"
- "tsconfig.json"
workflow_dispatch:
env:
AWS_REGION: us-west-2
ECR_REGISTRY: ${{ steps.account.outputs.id }}.dkr.ecr.us-west-2.amazonaws.com
ECR_REPOSITORY: recipe-extraction-agent
permissions:
id-token: write
contents: read
Triggers¶
- Push to main — only when source files, dependencies, or the Dockerfile change (via
pathsfilter). README changes don't trigger a deploy. - Manual dispatch —
workflow_dispatchlets you trigger from the GitHub Actions UI.
Permissions¶
id-token: write is required for GitHub OIDC — it allows the workflow to request a token from AWS STS.
Build & Push Steps¶
steps:
- uses: actions/checkout@v4
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: ${{ secrets.AWS_ROLE_ARN }}
aws-region: ${{ env.AWS_REGION }}
- name: Login to Amazon ECR
uses: aws-actions/amazon-ecr-login@v2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Build and push
uses: docker/build-push-action@v6
with:
context: .
push: true
platforms: linux/arm64
tags: |
${{ env.ECR_REGISTRY }}/${{ env.ECR_REPOSITORY }}:latest
${{ env.ECR_REGISTRY }}/${{ env.ECR_REPOSITORY }}:sha-${{ github.sha }}
cache-from: type=gha
cache-to: type=gha,mode=max
Image tagging: Each build produces two tags:
- latest — always points to the newest image
- sha-{commit} — immutable tag tied to the exact commit
Build caching: cache-from: type=gha uses GitHub Actions cache for Docker layer caching. Subsequent builds only rebuild changed layers.
Update Runtime¶
- name: Update AgentCore runtime
run: |
aws bedrock-agentcore-control update-agent-runtime \
--agent-runtime-id "${{ secrets.AGENTCORE_RUNTIME_ID }}" \
--agent-runtime-artifact "{\"containerConfiguration\":{\"containerUri\":\"${ECR_REGISTRY}/${ECR_REPOSITORY}:sha-${GITHUB_SHA}\"}}" \
--role-arn "${{ secrets.AGENTCORE_ROLE_ARN }}" \
--network-configuration '{"networkMode":"PUBLIC"}' \
--protocol-configuration '{"serverProtocol":"HTTP"}' \
--environment-variables "{\"AWS_REGION\":\"${AWS_REGION}\",...}" \
--region "${AWS_REGION}"
Note: The deploy workflow uses the sha-{commit} tag (not latest) for the container URI. This ensures the runtime pulls the exact image that was just built.
Poll for READY¶
- name: Wait for READY status
timeout-minutes: 5
run: |
for i in $(seq 1 30); do
STATUS=$(aws bedrock-agentcore-control get-agent-runtime \
--agent-runtime-id "${{ secrets.AGENTCORE_RUNTIME_ID }}" \
--region "${AWS_REGION}" \
--query 'status' --output text 2>/dev/null || echo "UNKNOWN")
echo "Status: ${STATUS} (attempt ${i}/30)"
if [[ "${STATUS}" == "READY" ]]; then
echo "Runtime is READY!"
exit 0
elif [[ "${STATUS}" == "FAILED" ]]; then
echo "Runtime FAILED" >&2
exit 1
fi
sleep 10
done
echo "Timeout waiting for READY" >&2
exit 1
End-to-End Flow¶
graph LR
Push["Push to main<br/>(src/ changes)"] --> CI["CI Job<br/>typecheck + lint + test"]
Push --> Deploy["Deploy Job"]
Deploy --> OIDC["OIDC Auth<br/>→ AWS credentials"]
OIDC --> ECR["ECR Login<br/>+ Docker Build<br/>+ Push"]
ECR --> Update["update-agent-runtime<br/>(sha-tagged image)"]
Update --> Poll["Poll for READY<br/>(5 min timeout)"]
Both CI and Deploy run in parallel on push to main. CI validates code quality; Deploy builds and ships to AgentCore.