Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
116 commits
Select commit Hold shift + click to select a range
6f3fade
ci(e2e): make e2e suite a required merge gate
G4614 Jun 10, 2026
b8f39d7
test(e2e): add BOXLITE_E2E_SKIP_PATH_VERIFY toggle for cloud CI
G4614 Jun 10, 2026
a62066e
ci: add IAM OIDC role provisioning for e2e-cloud workflow
G4614 Jun 10, 2026
578509d
ci(e2e): rename e2e-stack → e2e-cloud, target deployed Tokyo stack
G4614 Jun 10, 2026
527143f
docs(workflows): document e2e-cloud and fix stale setup-aws-oidc.sh ref
G4614 Jun 10, 2026
f358c67
ci(e2e-cloud): drop gate job — defer 'required check' until Tokyo pro…
G4614 Jun 10, 2026
d7f4fb8
ci: fix setup-e2e-cloud-oidc.sh em-dash
G4614 Jun 10, 2026
784e4e1
ci(e2e-cloud): hardcode AWS_REGION + AWS_ROLE_ARN to fix fork-PR vars…
G4614 Jun 10, 2026
49a95e2
ci(e2e-cloud): build Api image with repo-root context, not apps/api/
G4614 Jun 10, 2026
442950a
ci(e2e-cloud): build Api image with development config (skip strict t…
G4614 Jun 10, 2026
27f1315
ci(e2e-cloud): swap to source-mode Api image (tsx runtime, no webpack…
G4614 Jun 10, 2026
ad6d12b
ci(e2e-cloud): swap runner build to Go + prebuilt libboxlite.a
G4614 Jun 10, 2026
3a126be
ci: widen e2e-cloud OIDC trust policy to cover workflow_dispatch + an…
G4614 Jun 10, 2026
eb4cd04
ci(e2e-cloud): skip runner rebuild + deploy (Go runner needs libboxli…
G4614 Jun 10, 2026
4919ce1
ci(e2e-cloud): switch source-mode runtime from tsx to ts-node (TypeOR…
G4614 Jun 10, 2026
dfc03d9
ci(e2e-cloud): grant ecs:ExecuteCommand on cluster ARN too
G4614 Jun 10, 2026
e50fe99
ci(e2e-cloud): install postgresql-client in source-mode image
G4614 Jun 10, 2026
a656b6d
ci(e2e-cloud): use renamed _per_box quota columns
G4614 Jun 10, 2026
73c4bba
ci(e2e-cloud): disable psql pager + use compact -A -t output
G4614 Jun 10, 2026
02d71b5
ci(e2e-cloud): strip CR before grep on ECS Exec output
G4614 Jun 10, 2026
272ae5e
ci(e2e-cloud): set BOXLITE_DEPS_STUB=1 for Python SDK build
G4614 Jun 10, 2026
ed0428c
ci(e2e-cloud): apt-install protoc for SDK build
G4614 Jun 10, 2026
36abe01
ci(e2e-cloud): resolve path_prefix from /api/v1/me
G4614 Jun 10, 2026
23b8dd7
ci(e2e-cloud): pre-register snapshots before pytest
G4614 Jun 10, 2026
ded8d4b
ci(e2e-cloud): move AWS account ID/region/role ARN to repo vars
G4614 Jun 11, 2026
30fd403
ci(e2e-cloud): rebuild + redeploy runner via reusable build-runner-bi…
G4614 Jun 11, 2026
923d5b1
ci(e2e-cloud): grant contents:write to build_runner caller for reusab…
G4614 Jun 11, 2026
f647a93
ci(build-runner-binary): install gperf + libssl-dev for build mode
G4614 Jun 11, 2026
865f0fd
ci(build-runner-binary): apt-install llvm for llvm-objcopy
G4614 Jun 11, 2026
8f1da6d
ci(e2e-cloud): reuse build-c.yml for libboxlite.a instead of inline b…
G4614 Jun 11, 2026
b7d4f58
ci(e2e-cloud): drop IAM bootstrap + use S3 polling instead of SSM Sen…
G4614 Jun 11, 2026
7d815e3
Merge remote-tracking branch 'origin/main' into codex/pr724-merge-main
law-chain-hot Jun 11, 2026
63e7eaf
ci(e2e-cloud): split paths-filter per component, skip unrelated rebuilds
G4614 Jun 11, 2026
7dbd60a
ci(e2e-cloud): align Dockerfile.source to apps/ workspace root + asse…
G4614 Jun 11, 2026
c9ce34b
ci(e2e-cloud): run runner deploy before API build/deploy
G4614 Jun 11, 2026
71bb996
Merge remote-tracking branch 'upstream/main' into chore/e2e-required-…
G4614 Jun 11, 2026
d472f1d
ci: split runner build+deploy into reusable deploy-runner.yml
G4614 Jun 11, 2026
ca17223
ci(deploy-runner): add push trigger w/ runner-chain paths-filter (pre…
G4614 Jun 11, 2026
61bbecd
ci(deploy-runner): gate build+deploy on actual runner source changes
G4614 Jun 11, 2026
5f3b624
sdks/go: add stub WithPort BoxOption (unblock runner build)
G4614 Jun 11, 2026
c7d45d6
ci(deploy-runner): add runner_artifact_run_id input — deploy-only mode
G4614 Jun 11, 2026
b641355
ci(deploy-runner): commit-msg [runner-from: N] override + paths-filte…
G4614 Jun 11, 2026
200ebed
ci(deploy-runner): print sts identity + raw list-buckets for debug
G4614 Jun 11, 2026
4968a35
ci(deploy-runner): switch to SSM Run Command (main's pattern) + pre-s…
G4614 Jun 11, 2026
68f60ee
ci(deploy-runner): dynamically resolve storagebucket + list all bucke…
G4614 Jun 11, 2026
018bbac
ci(deploy-runner): trigger after creating storagebucket-pr724
G4614 Jun 11, 2026
9392f87
ci(deploy-runner): trigger after creating storagebucket-pr724
G4614 Jun 11, 2026
ef272cf
deploy-runner: switch from SSM Run Command to SSH+SCP via Instance Co…
G4614 Jun 11, 2026
9f61849
ci(deploy-runner): exercise SSH+SCP path against Tokyo EC2 [runner-fr…
G4614 Jun 11, 2026
ba2162f
ci(deploy-runner): force unconditional redeploy of run 27345482938 ar…
G4614 Jun 11, 2026
9bfd28c
ci(deploy-api): extract API deploy into reusable workflow + revert de…
G4614 Jun 11, 2026
c40da30
ci(deploy-api): exercise ECS PassRole + UpdateService [api-redeploy]
G4614 Jun 11, 2026
96c69db
ci(deploy-api): fix ECS cluster pattern to match SST's auto-generated…
G4614 Jun 11, 2026
6ce9a8b
ci(deploy-runner): re-verify SSH+SCP path post-boundary-removal [runn…
G4614 Jun 11, 2026
a218bfe
ci(deploy-runner): touch file to trigger standalone run [runner-from:…
G4614 Jun 11, 2026
1640831
ci(deploy-api): touch file to trigger standalone run [api-redeploy]
G4614 Jun 11, 2026
97c8bff
ci(deploy-runner): verify swap via PID + ActiveEnterTimestamp + sha25…
G4614 Jun 11, 2026
b4c4080
ci(deploy-api): retest after recreating Api{Task,Execution}Role [api-…
G4614 Jun 11, 2026
608e7e8
ci(deploy-api): touch to trigger after Api roles recreated [api-redep…
G4614 Jun 11, 2026
f793f03
ci(deploy-api): trigger full API build + deploy (no [api-redeploy] th…
G4614 Jun 11, 2026
02110c2
ci(deploy-api): strip stale S3_ACCESS_KEY env when cloning TD
G4614 Jun 11, 2026
5ba523b
Merge remote-tracking branch 'upstream/main' into chore/e2e-required-…
G4614 Jun 11, 2026
cd9e1fe
ci: trigger fresh API build after main merge + TD env strip patch
G4614 Jun 11, 2026
e4d2d95
ci(deploy-api): dynamically patch S3_DEFAULT_BUCKET + SST_RESOURCE_St…
G4614 Jun 11, 2026
fc229dd
ci: trigger redeploy-current with bucket env patch [api-redeploy]
G4614 Jun 11, 2026
4e7383d
ci(deploy-api): touch file to trigger run [api-redeploy] (re-register…
G4614 Jun 11, 2026
98a2c25
ci: rebuild api image with current src so VolumeManager uses task role
G4614 Jun 11, 2026
27ba50d
ci: rebuild api after DB reset — fresh baseline migration on boot
G4614 Jun 11, 2026
42b7c2a
chore(apps): remove TypeScript cloud SDK (#738)
law-chain-hot Jun 11, 2026
a556629
feat(box): create box from one of 3 curated images (no DB table)
law-chain-hot Jun 10, 2026
fa9304d
fix(api): guard reserved image-ref label, default warm-pool image, ec…
law-chain-hot Jun 11, 2026
ee9da05
test(e2e): adapt the e2e stack to curated image keys
law-chain-hot Jun 11, 2026
bac3977
feat(apps): wire curated image selection through SDK and dashboard
law-chain-hot Jun 11, 2026
9cb845c
test(e2e): patch quotas through default organization membership
law-chain-hot Jun 11, 2026
03f2cc6
chore(openapi): drop curated image description churn
law-chain-hot Jun 11, 2026
02d599f
refactor(box): make image a first-class Box column and rename the run…
law-chain-hot Jun 12, 2026
52feb39
ci(deploy-app-services): new reusable workflow for Proxy / OtelCollec…
G4614 Jun 12, 2026
baa0591
ci(deploy-app-services): touch apps/proxy/Dockerfile to test workflow…
G4614 Jun 12, 2026
53a65b9
ci(deploy-app-services): grant id-token:write on calling jobs (fix st…
G4614 Jun 12, 2026
be82c1f
refactor(box): pass full OCI image refs end-to-end, drop the curated …
law-chain-hot Jun 12, 2026
f290ba6
ci(deploy-app-services): retest Proxy after OIDC UpdateService scope …
G4614 Jun 12, 2026
ae79f4e
ci(_deploy-single-service): override task/exec role with Api's to byp…
G4614 Jun 12, 2026
8b66ae1
ci(deploy-app-services): touch OtelCollector + SshGateway Dockerfiles…
G4614 Jun 12, 2026
0e6b875
refactor(box): collapse the dual uuid/boxId identity into a single 12…
law-chain-hot Jun 12, 2026
457a5cd
Merge remote-tracking branch 'upstream/main' into chore/e2e-required-…
G4614 Jun 12, 2026
c303d9d
ci(e2e-cloud): compose deploy-api.yml + deploy-runner.yml via workflo…
G4614 Jun 12, 2026
95b5bab
fix(sdks/go): drop obsolete WithPort stub now that #741 landed proper…
G4614 Jun 12, 2026
6c4776a
chore(apps): regenerate API clients for box image column and workspac…
G4614 Jun 12, 2026
a0ccf05
ci(e2e-cloud): wire deploy-app-services.yml into the pipeline
G4614 Jun 12, 2026
e25b95d
ci(e2e-cloud): resolve admin-org id dynamically (not hardcoded) for q…
G4614 Jun 12, 2026
4ae44d2
fix(otel-collector,apps): fold in #745 and #746 to unblock cloud deploy
G4614 Jun 12, 2026
bbc1b09
fix(otel-collector): drop python3/make/g++ hack now that dockerode is…
G4614 Jun 12, 2026
c22216d
ci(e2e-cloud): drop pull_request trigger
G4614 Jun 12, 2026
f260801
ci(deploy): serialize deploys against the shared Tokyo stack
G4614 Jun 12, 2026
3da0967
fix(e2e-cloud): assert exactly one target group on ApiLoadBalancer
G4614 Jun 12, 2026
875b338
ci(build-c): default GITHUB_TOKEN to contents: read
G4614 Jun 12, 2026
9af953c
fix(api): run source-mode container as non-root node user (Trivy DS-0…
G4614 Jun 12, 2026
fc0cebf
fix(e2e): only treat BOXLITE_E2E_SKIP_PATH_VERIFY=1/true/yes/on as skip
G4614 Jun 12, 2026
75403a5
ci(e2e-cloud): default GITHUB_TOKEN to contents: read
G4614 Jun 12, 2026
0032b43
ci(e2e-cloud): split the test suite into its own workflow
G4614 Jun 12, 2026
5b09b24
ci(deploy-runner): skip libboxlite rebuild on Go-only diffs
G4614 Jun 12, 2026
6d77048
fix(e2e-cloud-test): base64-encode quota SQL to survive shell escape …
G4614 Jun 12, 2026
ee0966c
ci(e2e-cloud-test): TEMPORARY push trigger to register the workflow
G4614 Jun 12, 2026
31f0de0
fix(e2e-cloud-test): drop obsolete /api/snapshots pre-registration
G4614 Jun 12, 2026
eecc315
merge: pull #735 image-resolution work into fix/api-image-resolution-…
G4614 Jun 12, 2026
178420d
fix(api/Dockerfile.source): drop COPY of deleted libs/sdk-typescript
G4614 Jun 12, 2026
89f9a50
fix(e2e): skip path_verification cases when BOXLITE_E2E_SKIP_PATH_VER…
G4614 Jun 12, 2026
66ca10e
fix(api/migration): add box.image column for stacks that ran the squa…
G4614 Jun 12, 2026
b4e952d
fix(api/migration): also drop the orphan box.boxId column
G4614 Jun 12, 2026
cd3ef9e
fix(api/migration): also DROP all obsolete box columns
G4614 Jun 12, 2026
a2e5031
fix(api/migration): drop obsolete box columns in a fresh migration class
G4614 Jun 12, 2026
b28f294
fix: add public docker.io test image to bypass Tokyo's missing ghcr auth
G4614 Jun 12, 2026
883913a
fix: bump test image to alpine:3.23 to match the local stack baseline
G4614 Jun 12, 2026
2cc7e1a
fix(e2e): drop strict=True from xfail markers tripped by current clou…
G4614 Jun 12, 2026
fc8fce9
fix(api): clear box.pending when CREATE_BOX fails so destroy can run
G4614 Jun 12, 2026
ad46915
fix(api): honour ?force=true on DELETE /boxes to bypass pending+state…
G4614 Jun 12, 2026
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
71 changes: 70 additions & 1 deletion .github/workflows/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,7 @@ Runs VM-based E2E integration tests on an ephemeral AWS EC2 self-hosted runner.
- `AWS_SUBNET_ID` - Subnet with auto-assign public IP
- `AWS_SECURITY_GROUP_ID` - Security group allowing outbound HTTPS

**Required AWS resources** (provisioned by `scripts/ci/setup-aws-oidc.sh`):
**Required AWS resources** (provisioned by `scripts/ci/setup-ci-runner.sh`):
- OIDC identity provider (`token.actions.githubusercontent.com`)
- IAM role `boxlite-e2e-github-actions` with trust policy for this repo
- IAM instance profile `boxlite-e2e-runner` with `ec2:TerminateInstances` on self
Expand All @@ -207,6 +207,75 @@ Runs VM-based E2E integration tests on an ephemeral AWS EC2 self-hosted runner.
3. `e2e-tests` - Build runtime, run Rust/CLI/Python/Node/C integration tests
4. `stop-runner` - Terminate instance, deregister runner

### `e2e-cloud.yml`

Runs the SDK → API → Runner → libkrun VM regression suite against the
**always-on Tokyo stack** (`boxlite-e2e-ci-*`) instead of a per-run KVM
runner. Built for fast iteration on REST-path bugs (`e2e-test.yml` is
slower because it spins up a fresh EC2 every run).

**Why:** The same regression goal as `e2e-test.yml`, but the cost
profile is different — `e2e-cloud.yml` deploys-and-tests against an
already-deployed cloud stack, so a typical run is 8-15 min and adds
no per-run EC2 cost. `e2e-test.yml` and this workflow are
complementary, not redundant — they exercise the same test code but
the deployed-stack path also catches infra-only regressions (LB
config, RDS schema, ECS task def drift) that the self-bootstrap
path doesn't.

**Architecture:** Three-job required-gate pattern:
1. `changes` (ubuntu-latest) — paths-filter cheap detector
2. `e2e` (ubuntu-latest, only if `changes` says relevant or `workflow_dispatch`) —
builds Api image + runner binary from this checkout, deploys to Tokyo,
builds the Python SDK from source, runs `pytest scripts/test/e2e/cases/`
3. `e2e-gate` (always runs) — collapses outcome into one required check

**Triggers:**
- Push to `main`
- Pull request to `main`
- Manual dispatch (`workflow_dispatch`)

Path matching happens inside the `changes` job (so branch
protection can require the gate's status check on every PR).

**Cost:** Build + deploy + test on GitHub-hosted ubuntu-latest (free for
public repos / billable minutes for private). The Tokyo stack itself
runs 24×7 — its baseline is the cost driver, not this workflow.

**Authentication:** GitHub OIDC → AWS STS (no stored AWS credentials),
**separate** IAM role from `e2e-test.yml`:

| Workflow | Role | Region | Scope |
|----------|------|--------|-------|
| `e2e-test.yml` | `boxlite-e2e-github-actions` | us-east-1 | self-hosted runner provisioning (ec2:RunInstances, terminate, register/deregister GH runner) |
| `e2e-cloud.yml` | `boxlite-e2e-cloud-github-actions` | ap-northeast-1 | Tokyo stack deploy + exec (ecr push, ecs update-service / execute-command, ssm send-command to runner, s3 builds/, ssm parameter read) |

**Required variables** (Settings → Variables → Actions):
- `AWS_ACCOUNT_ID` (shared)
- `AWS_E2E_CLOUD_REGION` = `ap-northeast-1`
- `AWS_E2E_CLOUD_ROLE_ARN` = `arn:aws:iam::<acct>:role/boxlite-e2e-cloud-github-actions`

**Required AWS resources** (provisioned by `scripts/ci/setup-e2e-cloud-oidc.sh`):
- OIDC identity provider (`token.actions.githubusercontent.com`) — shared
with `e2e-test.yml`
- IAM role `boxlite-e2e-cloud-github-actions` with trust policy limited
to `repo:boxlite-ai/boxlite:{ref:refs/heads/main, pull_request, environment:e2e-cloud}`
- SSM SecureString parameter `/boxlite/e2e-ci/admin-api-key` (sourced
by the workflow at runtime; never committed)
- Tokyo `boxlite-e2e-ci-*` stack already deployed (SST `e2e-ci` stage)

**Concurrency:** `e2e-cloud-shared` — singleton lock across every PR
and every push. The Tokyo stack is a shared singleton, so per-ref
grouping would let PRs race each other's ECS rolling updates.

**Stack state after a run:** The Tokyo stack is left running THIS
workflow run's Api image + runner binary. There's no auto-restore to
`main` HEAD between runs (cost trade-off). Console / direct stack
inspection therefore reflects "whatever the last e2e-cloud run
deployed" — a `git log --grep e2e-cloud` on `main` and the ECS task
definition's image tag (`api-<sha>`) together identify the running
revision.

## Trigger Behavior

| Change | warm-caches | build-runtime | build-wheels | build-node |
Expand Down
183 changes: 183 additions & 0 deletions .github/workflows/_deploy-single-service.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
# Reusable inner workflow: build + deploy ONE ECS service.
# Called from deploy-app-services.yml's matrix-equivalent (3 jobs, one
# per service). Parameterized by service_name + dockerfile so the same
# logic services Proxy / OtelCollector / SshGateway.
#
# Mirrors deploy-api.yml's deploy job (minus the S3-related env patches
# which only apply to Api):
# 1. Resolve cluster
# 2. ECR login + buildx build of the parameterized Dockerfile
# 3. Clone live TD, swap image, strip readonly fields, register
# 4. UpdateService --force-new-deployment + wait services-stable
# 5. Assert PRIMARY taskDefinition == NEW_TD_ARN
#
# Unlike Api, these services don't sit behind ApiLoadBalancer (only Api
# has an ALB target group; Proxy/SshGateway have their own LBs;
# OtelCollector is internal). So the ALB-healthy poll is omitted here —
# `wait services-stable` + PRIMARY assertion are sufficient.
name: _deploy-single-service

on:
workflow_call:
inputs:
service_name:
description: 'Exact ECS service name (e.g. Proxy, OtelCollector, SshGateway)'
type: string
required: true
dockerfile:
description: 'Repo-relative Dockerfile path (build context = repo root)'
type: string
required: true

# Belt-and-suspenders against the outer-workflow concurrency: also
# serialize per-service here, so any caller (deploy-app-services or
# a future direct dispatch) racing on the same ECS service queues
# instead of fighting over TD revisions. Different services parallelize.
concurrency:
group: deploy-single-service-${{ inputs.service_name }}
cancel-in-progress: false

permissions:
contents: read
id-token: write

env:
AWS_REGION: ${{ vars.AWS_E2E_CLOUD_REGION }}
AWS_ROLE_ARN: ${{ vars.AWS_E2E_CLOUD_ROLE_ARN }}
STACK_PREFIX: boxlite-e2e-ci
ECS_CLUSTER_PATTERN: boxlite-e2e-ci-ClusterCluster-
ECR_REPO: sst-asset

jobs:
deploy:
name: Deploy ${{ inputs.service_name }}
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@v5

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

- name: Resolve cluster
id: resources
run: |
set -euo pipefail
CLUSTER_COUNT=$(aws ecs list-clusters \
--query "length(clusterArns[?contains(@, '${ECS_CLUSTER_PATTERN}')])" --output text)
[ "$CLUSTER_COUNT" -eq 1 ] || { echo "::error::Expected 1 cluster, found $CLUSTER_COUNT"; exit 1; }
CLUSTER=$(aws ecs list-clusters \
--query "clusterArns[?contains(@, '${ECS_CLUSTER_PATTERN}')]|[0]" \
--output text | awk -F/ '{print $NF}')
echo "cluster=$CLUSTER" >> "$GITHUB_OUTPUT"
echo "::notice::cluster=$CLUSTER service=${{ inputs.service_name }}"

- name: ECR login
id: ecr_login
uses: aws-actions/amazon-ecr-login@v2

- name: Build & push image (${{ inputs.dockerfile }})
id: build
run: |
set -euo pipefail
REGISTRY="${{ steps.ecr_login.outputs.registry }}"
# Lowercase service for the ECR tag (deterministic + matches
# the convention "${service-lowercase}-${sha}").
TAG_NAME=$(echo "${{ inputs.service_name }}" | tr '[:upper:]' '[:lower:]')
IMAGE="${REGISTRY}/${ECR_REPO}:${TAG_NAME}-${{ github.sha }}"
docker buildx build --platform linux/amd64 --load \
-f "${{ inputs.dockerfile }}" -t "$IMAGE" .
docker push "$IMAGE"
echo "image=$IMAGE" >> "$GITHUB_OUTPUT"
echo "::notice::Built and pushed $IMAGE"

- name: Resolve Api role ARNs (borrow source for missing per-service roles)
id: api_roles
run: |
set -euo pipefail
# The e2e-ci stack's per-service Task/Execution roles
# (Proxy/Otel/SshGw{Task,Execution}Role-*) were deleted from
# IAM out-of-band, but the live ECS TDs still reference the
# dead ARNs → AssumeRole NoSuchEntity → new tasks can't start.
# As a temporary unblock (until admin runs `sst deploy
# --stage e2e-ci` to recreate properly), borrow the Api
# service's roles which were recreated earlier in this PR's
# session. Both Api roles trust ecs-tasks.amazonaws.com (no
# principal restriction), so any service can assume them. The
# cost is least-privilege: borrowed task role grants S3 +
# CloudWatch perms the borrower may not need. Acceptable for
# e2e-ci which has no production traffic.
API_TD=$(aws ecs describe-services --cluster "${{ steps.resources.outputs.cluster }}" \
--services Api --query 'services[0].taskDefinition' --output text)
read -r TASK_ROLE EXEC_ROLE <<<"$(aws ecs describe-task-definition \
--task-definition "$API_TD" \
--query 'taskDefinition.[taskRoleArn,executionRoleArn]' --output text)"
[ -n "$TASK_ROLE" ] && [ "$TASK_ROLE" != "None" ] \
|| { echo "::error::Could not resolve Api taskRoleArn"; exit 1; }
[ -n "$EXEC_ROLE" ] && [ "$EXEC_ROLE" != "None" ] \
|| { echo "::error::Could not resolve Api executionRoleArn"; exit 1; }
echo "task_role_arn=$TASK_ROLE" >> "$GITHUB_OUTPUT"
echo "exec_role_arn=$EXEC_ROLE" >> "$GITHUB_OUTPUT"
echo "::notice::Will borrow Api roles for ${{ inputs.service_name }}: $(basename "$TASK_ROLE"), $(basename "$EXEC_ROLE")"

- name: Register new TD + UpdateService + wait stable
env:
CLUSTER: ${{ steps.resources.outputs.cluster }}
IMAGE: ${{ steps.build.outputs.image }}
SERVICE: ${{ inputs.service_name }}
TASK_ROLE: ${{ steps.api_roles.outputs.task_role_arn }}
EXEC_ROLE: ${{ steps.api_roles.outputs.exec_role_arn }}
run: |
set -euo pipefail

OLD_TD_ARN=$(aws ecs describe-services --cluster "$CLUSTER" --services "$SERVICE" \
--query 'services[0].taskDefinition' --output text)
echo "Old TD: $OLD_TD_ARN"

# Clone old TD, swap container image, OVERRIDE task & execution
# roles to point at Api's (the per-service roles are gone from
# IAM — see api_roles step above), strip readonly fields.
# Plaintext env (e.g. credentials) lives in TD — do NOT cat the file.
aws ecs describe-task-definition --task-definition "$OLD_TD_ARN" \
--query 'taskDefinition' --output json \
| jq --arg img "$IMAGE" --arg trole "$TASK_ROLE" --arg erole "$EXEC_ROLE" '
.containerDefinitions[0].image = $img
| .taskRoleArn = $trole
| .executionRoleArn = $erole
| del(.taskDefinitionArn, .revision, .status, .requiresAttributes,
.compatibilities, .registeredAt, .registeredBy)' \
> /tmp/new-td.json

NEW_TD_ARN=$(aws ecs register-task-definition \
--cli-input-json file:///tmp/new-td.json \
--query 'taskDefinition.taskDefinitionArn' --output text)
echo "::notice::New TD: $NEW_TD_ARN"

aws ecs update-service --cluster "$CLUSTER" --service "$SERVICE" \
--task-definition "$NEW_TD_ARN" --force-new-deployment >/dev/null
aws ecs wait services-stable --cluster "$CLUSTER" --services "$SERVICE"
echo "Service stable — verifying PRIMARY..."

PRIMARY_TD=$(aws ecs describe-services --cluster "$CLUSTER" --services "$SERVICE" \
--query 'services[0].deployments[?status==`PRIMARY`]|[0].taskDefinition' \
--output text)
if [ "$PRIMARY_TD" != "$NEW_TD_ARN" ]; then
echo "::error::ECS rolled back. PRIMARY=$PRIMARY_TD expected $NEW_TD_ARN"
aws ecs list-tasks --cluster "$CLUSTER" --service-name "$SERVICE" \
--desired-status STOPPED --max-results 5 \
--query 'taskArns' --output json \
| python3 -c "import json,sys; [print(t) for t in json.load(sys.stdin)]" \
| while read TASK; do
[ -n "$TASK" ] || continue
echo "--- stopped task $(basename "$TASK") ---"
aws ecs describe-tasks --cluster "$CLUSTER" --tasks "$TASK" \
--query 'tasks[0].[stoppedReason,containers[0].exitCode,containers[0].reason]' \
--output text || true
done
exit 1
fi
echo "::notice::PRIMARY deployment confirmed for ${SERVICE} on $NEW_TD_ARN"
47 changes: 45 additions & 2 deletions .github/workflows/build-c.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,28 @@
# Triggers:
# - release: When a GitHub release is published
# - workflow_dispatch: Manual trigger for testing
# - workflow_call: Reusable from .github/workflows/e2e-cloud.yml.
# Pass target_filter='linux-x64-gnu' to skip the
# macOS/arm64 matrix entries (only the Tokyo
# runner target is needed).
name: Build C SDK

# Default GITHUB_TOKEN to read-only. The upload_release job opts into
# `contents: write` explicitly when it needs to publish release assets.
permissions:
contents: read

on:
release:
types: [ published ]
workflow_dispatch:
workflow_call:
inputs:
target_filter:
description: 'Optional. Build only the given target (e.g. "linux-x64-gnu"). Empty = build all matrix platforms.'
type: string
default: ''
required: false

env:
CARGO_TERM_COLOR: always
Expand All @@ -30,6 +46,33 @@ jobs:
config:
uses: ./.github/workflows/config.yml

# Compute the matrix the `build` job iterates over. When called via
# workflow_call with target_filter set, narrow to that single entry.
# `matrix` context is not available in job-level if:, so we filter
# the JSON here and have `build` consume `setup_matrix.outputs.platforms`.
setup_matrix:
name: Setup matrix
needs: config
runs-on: ubuntu-latest
outputs:
platforms: ${{ steps.filter.outputs.platforms }}
steps:
- id: filter
env:
ALL_PLATFORMS: ${{ needs.config.outputs.platforms }}
TARGET_FILTER: ${{ inputs.target_filter }}
run: |
if [ -n "${TARGET_FILTER:-}" ]; then
FILTERED=$(printf '%s' "$ALL_PLATFORMS" | jq -c --arg t "$TARGET_FILTER" '[.[] | select(.target == $t)]')
COUNT=$(printf '%s' "$FILTERED" | jq 'length')
[ "$COUNT" -ge 1 ] || { echo "::error::No matrix entry matches target_filter=$TARGET_FILTER"; exit 1; }
echo "Filtered to $COUNT entry(ies): $FILTERED"
else
FILTERED="$ALL_PLATFORMS"
echo "No filter — full matrix"
fi
echo "platforms=$FILTERED" >> "$GITHUB_OUTPUT"
# ============================================================================
# BUILD
# ============================================================================
Expand All @@ -48,12 +91,12 @@ jobs:
# ============================================================================
build:
name: Build (${{ matrix.target }})
needs: config
needs: [config, setup_matrix]
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
include: ${{ fromJson(needs.config.outputs.platforms) }}
include: ${{ fromJson(needs.setup_matrix.outputs.platforms) }}

steps:
- uses: actions/checkout@v4
Expand Down
Loading
Loading