-
Notifications
You must be signed in to change notification settings - Fork 0
SW-1336: Add reusable E2E workflow for CodeBuild-based testing #13
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
51 commits
Select commit
Hold shift + click to select a range
d237a9e
add reusable E2E workflow for CodeBuild-based testing
darwinboersma 574bb14
rename tdp-backend-e2e to tdp-e2e
darwinboersma ab457b8
address security and review comments
darwinboersma 80429b0
remove unnecessary +5 buffer from job timeout
darwinboersma ee39290
make JFrog secrets required, simplify env var assembly
darwinboersma c5af998
add optional deploy-before-e2e: push to env branch and wait for CI
darwinboersma b290ef1
remove infra_config_ref input — only needed during dev
darwinboersma f6c5828
accept env_vars_json as flat object, convert to CodeBuild format inte…
darwinboersma a3ced6f
address review: force-with-lease, set -euo pipefail, error handling, …
darwinboersma 844e7eb
re-add infra_config_ref as optional input for pre-merge testing
darwinboersma 850f08d
embed default buildspec inline, make test_command configurable
darwinboersma be1d152
use yarn config set instead of raw echo for JFrog registry config
darwinboersma 9946b06
move default buildspec to a file, copy into source zip before upload
darwinboersma 1693c32
document e2e-codebuild reusable workflow in README
darwinboersma b728a23
stream CodeBuild logs into GHA output and job summary
darwinboersma e69caf1
fix README: contents: read in examples, clarify PAT description
darwinboersma 643674f
add deploy_paths input to conditionally skip deploy for test-only cha…
darwinboersma c345a42
make deploy_paths required — callers must be explicit about what trig…
darwinboersma 9b95684
remove deploy_before_e2e — deploy_paths controls everything
darwinboersma 2454d4c
allow empty deploy_paths to skip deploy entirely
darwinboersma d69ac81
make environment required — no silent defaults
darwinboersma bf094dc
remove test_command input and E2E_TEST_COMMAND — just use yarn test:e2e
darwinboersma ba51236
remove test_command input and E2E_TEST_COMMAND env var
darwinboersma 5eeb845
rename ci_workflow to deploy_workflow for clarity
darwinboersma 12ee721
update README to reflect latest API: required inputs, deploy_paths, d…
darwinboersma e4bb9a0
expand infrastructure prerequisites with environment setup steps
darwinboersma 9ccf9db
add working_directory input for publishing subdirectory packages
darwinboersma 3cc6d55
enable corepack in publish workflow for Yarn 4+ repos
darwinboersma 3eb0935
remove SSM auth — pass E2E_AUTH_TOKEN from GitHub secrets instead
darwinboersma cdbfbb6
revert to SSM for E2E auth token — per-account secrets required
darwinboersma a2eda1a
use service-scoped SSM path for E2E auth token
darwinboersma 299a5fc
fix: fetch SSM token in install phase (parameter-store can't interpol…
darwinboersma 2bc020b
pass E2E_SERVICE_NAME to CodeBuild for service-scoped SSM path
darwinboersma e13a3bc
make buildspec required, remove default buildspec pattern
darwinboersma 7797e65
rewrite e2e-codebuild README section — concise, matches current API
darwinboersma d485808
remove default buildspec — each service provides its own
darwinboersma 11cbc9c
feat: pass E2E_ENVIRONMENT to CodeBuild
darwinboersma 57d3e7c
refactor: remove env_vars_json input
darwinboersma 98ca3b2
docs: update README for environments.json pattern
darwinboersma 67b8e15
fix: add 5 min buffer to e2e job timeout
darwinboersma dcc54ae
fix: simplify timeout expression to fix workflow parse error
darwinboersma 66626e4
fix: revert timeout to simple input, bump default to 25
darwinboersma a29a842
refactor: hardcode infra_config_ref, remove caller input
darwinboersma b0bebeb
fix: improve change detection and timeout handling
darwinboersma 570d8d5
docs: move e2e-codebuild section to bottom of README
darwinboersma 6a56def
docs: restore full publish-npm-package and check-links documentation
darwinboersma 9c47027
refactor: inline environment config, remove external config dependency
darwinboersma b115fc7
feat: add ECS stabilization wait after deploy
darwinboersma 4ff606f
docs: fix e2e-codebuild README to match actual implementation
darwinboersma 8fccbfe
refactor: remove ecs_cluster/ecs_service inputs
darwinboersma 211d3f2
Move permissions from top-level to job-level
darwinboersma File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,363 @@ | ||
| name: E2E Tests (CodeBuild) | ||
|
|
||
| on: | ||
| workflow_call: | ||
| inputs: | ||
| environment: | ||
| description: "Target environment (e.g. predev3, dev)" | ||
| type: string | ||
| required: true | ||
| deploy_paths: | ||
| description: "Space-separated glob patterns — deploy only if changed files match. Empty string = never deploy." | ||
| type: string | ||
| required: true | ||
| buildspec: | ||
| description: "Path to the buildspec file in the caller repo" | ||
| type: string | ||
| required: true | ||
| deploy_workflow: | ||
| description: "Workflow file that builds and deploys the service (waited on after pushing to env branch)" | ||
| type: string | ||
| default: "ci.yml" | ||
| required: false | ||
| image_override: | ||
| description: "Override the CodeBuild base image (e.g. mcr.microsoft.com/playwright:v1.52.0-noble)" | ||
| type: string | ||
| required: false | ||
| compute_type_override: | ||
| description: "Override the CodeBuild compute type (e.g. BUILD_GENERAL1_MEDIUM for 7GB RAM)" | ||
| type: string | ||
| required: false | ||
| timeout_minutes: | ||
| description: "Max minutes for the E2E job — includes buffer for setup steps" | ||
| type: number | ||
| default: 25 | ||
|
darwinboersma marked this conversation as resolved.
|
||
| required: false | ||
| secrets: | ||
| JFROG_ARTIFACTORY_NPM_VIRTUAL_URL: | ||
| required: true | ||
| JFROG_ARTIFACTORY_READ_NPM_AUTH: | ||
| required: true | ||
| GITHUB_PAT: | ||
| description: "PAT with cross-repo access and contents:write for deploy push" | ||
| required: true | ||
|
|
||
| jobs: | ||
| check-changes: | ||
| if: ${{ inputs.deploy_paths != '' }} | ||
| runs-on: ubuntu-latest | ||
| permissions: | ||
| contents: read | ||
| outputs: | ||
| should_deploy: ${{ steps.check.outputs.should_deploy }} | ||
| steps: | ||
| - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 | ||
| with: | ||
| fetch-depth: 0 | ||
|
|
||
| - name: Check if service code changed | ||
| id: check | ||
| env: | ||
| DEPLOY_PATHS: ${{ inputs.deploy_paths }} | ||
| BEFORE_SHA: ${{ github.event.before }} | ||
| run: | | ||
| set -euo pipefail | ||
| if [[ -n "${GITHUB_BASE_REF:-}" ]]; then | ||
| BASE="origin/$GITHUB_BASE_REF" | ||
| elif [[ -n "$BEFORE_SHA" && "$BEFORE_SHA" != "0000000000000000000000000000000000000000" ]]; then | ||
| BASE="$BEFORE_SHA" | ||
| else | ||
| BASE="HEAD~1" | ||
| fi | ||
|
|
||
| CHANGED_FILES=$(git diff --name-only "$BASE" HEAD 2>/dev/null || echo "") | ||
|
darwinboersma marked this conversation as resolved.
|
||
| if [[ -z "$CHANGED_FILES" ]]; then | ||
| echo "No changed files detected, will deploy" | ||
| echo "should_deploy=true" >> "$GITHUB_OUTPUT" | ||
| exit 0 | ||
| fi | ||
|
|
||
| echo "Changed files:" | ||
| echo "$CHANGED_FILES" | ||
|
|
||
| for pattern in $DEPLOY_PATHS; do | ||
| while IFS= read -r file; do | ||
| if [[ -n "$file" ]] && [[ "$file" == $pattern ]]; then | ||
| echo "Match: $file matches $pattern" | ||
| echo "should_deploy=true" >> "$GITHUB_OUTPUT" | ||
| exit 0 | ||
| fi | ||
| done <<< "$CHANGED_FILES" | ||
| done | ||
|
|
||
| echo "No service code changes — skipping deploy" | ||
| echo "should_deploy=false" >> "$GITHUB_OUTPUT" | ||
|
|
||
| deploy: | ||
| needs: check-changes | ||
| if: ${{ needs.check-changes.outputs.should_deploy == 'true' }} | ||
| runs-on: ubuntu-latest | ||
|
darwinboersma marked this conversation as resolved.
|
||
| timeout-minutes: 20 | ||
| permissions: | ||
| contents: read | ||
| steps: | ||
| - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 | ||
| with: | ||
| fetch-depth: 0 | ||
| token: ${{ secrets.GITHUB_PAT }} | ||
|
|
||
| - name: Push to environment branch | ||
| env: | ||
| TARGET_ENV: ${{ inputs.environment }} | ||
| run: | | ||
| set -euo pipefail | ||
| git push origin HEAD:refs/heads/$TARGET_ENV --force-with-lease | ||
| echo "Pushed $(git rev-parse HEAD) to $TARGET_ENV" | ||
|
|
||
| - name: Wait for deploy | ||
| env: | ||
| GH_TOKEN: ${{ secrets.GITHUB_PAT }} | ||
|
darwinboersma marked this conversation as resolved.
|
||
| TARGET_ENV: ${{ inputs.environment }} | ||
| DEPLOY_WORKFLOW: ${{ inputs.deploy_workflow }} | ||
| run: | | ||
| set -euo pipefail | ||
| COMMIT_SHA=$(git rev-parse HEAD) | ||
| echo "Waiting for $DEPLOY_WORKFLOW on $TARGET_ENV branch (commit $COMMIT_SHA)..." | ||
|
|
||
| for i in $(seq 1 20); do | ||
| sleep 15 | ||
| RUN_ID=$(gh run list --branch "$TARGET_ENV" --workflow "$DEPLOY_WORKFLOW" --limit 20 --json databaseId,headSha,status \ | ||
| | jq -r --arg sha "$COMMIT_SHA" '.[] | select(.headSha == $sha) | .databaseId' | head -1) | ||
| if [[ -n "$RUN_ID" ]]; then | ||
| echo "Found deploy run: $RUN_ID" | ||
| break | ||
| fi | ||
|
darwinboersma marked this conversation as resolved.
|
||
| done | ||
|
|
||
| if [[ -z "$RUN_ID" ]]; then | ||
| echo "::error::Deploy workflow run not found after 5 minutes" | ||
| exit 1 | ||
| fi | ||
|
|
||
| gh run watch "$RUN_ID" --exit-status | ||
| echo "Deploy completed successfully" | ||
|
|
||
|
|
||
| e2e: | ||
| needs: [check-changes, deploy] | ||
| if: | | ||
| always() && | ||
| (needs.deploy.result == 'success' || needs.deploy.result == 'skipped') && | ||
| (needs.check-changes.result == 'success' || needs.check-changes.result == 'skipped') | ||
| runs-on: ubuntu-latest | ||
| timeout-minutes: ${{ inputs.timeout_minutes }} | ||
| permissions: | ||
| id-token: write | ||
| contents: read | ||
|
|
||
| steps: | ||
| - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 | ||
|
|
||
| - name: Resolve environment | ||
| id: env | ||
| env: | ||
| TARGET_ENV: ${{ inputs.environment }} | ||
| run: | | ||
| set -euo pipefail | ||
|
|
||
| # Environment → AWS account mapping. | ||
| # The E2E infra (CodeBuild, OIDC role, S3 bucket) is deployed as a | ||
| # substack of the TDP service stack via EnableE2E=true. | ||
| # Account IDs and regions are stable infrastructure facts. | ||
| declare -A ACCOUNTS=( | ||
| [predev]=611255221147 | ||
| [predev2]=383434869204 | ||
| [predev3]=866984931759 | ||
| [predev4]=456932089055 | ||
| [predev5]=734018046548 | ||
| [predev6]=326392129485 | ||
| [predev7]=901926888715 | ||
| [predev8]=581141123528 | ||
| [dev]=706717599419 | ||
| [preuat]=555180171822 | ||
| ) | ||
|
|
||
| declare -A REGIONS=( | ||
| [preuat]=us-west-2 | ||
| ) | ||
|
|
||
| # CloudFormation Environment value used in IAM role names. | ||
| # All predev/dev environments use "development"; preuat uses "preuat". | ||
| declare -A CF_ENVIRONMENTS=( | ||
| [preuat]=preuat | ||
| ) | ||
|
|
||
| ACCOUNT="${ACCOUNTS[$TARGET_ENV]:-}" | ||
| REGION="${REGIONS[$TARGET_ENV]:-us-east-2}" | ||
| CF_ENV="${CF_ENVIRONMENTS[$TARGET_ENV]:-development}" | ||
|
|
||
| if [[ -z "$ACCOUNT" ]]; then | ||
| echo "::error::Unknown environment: $TARGET_ENV" | ||
| exit 1 | ||
| fi | ||
|
|
||
| echo "env=$TARGET_ENV" >> "$GITHUB_OUTPUT" | ||
| echo "account=$ACCOUNT" >> "$GITHUB_OUTPUT" | ||
| echo "region=$REGION" >> "$GITHUB_OUTPUT" | ||
| echo "cf_env=$CF_ENV" >> "$GITHUB_OUTPUT" | ||
| echo "source_bucket=tdp-e2e-source-${ACCOUNT}" >> "$GITHUB_OUTPUT" | ||
|
|
||
| - name: Configure AWS credentials | ||
| uses: aws-actions/configure-aws-credentials@7474bc4690e29a8392af63c5b98e7449536d5c3a # v4 | ||
| with: | ||
| aws-region: ${{ steps.env.outputs.region }} | ||
| role-to-assume: arn:aws:iam::${{ steps.env.outputs.account }}:role/gha-tdp-e2e-${{ steps.env.outputs.cf_env }} | ||
| role-session-name: e2e-codebuild | ||
| role-skip-session-tagging: true | ||
|
darwinboersma marked this conversation as resolved.
|
||
|
|
||
| - name: Upload source to S3 | ||
| id: source | ||
| env: | ||
| COMMIT_SHA: ${{ github.event.pull_request.head.sha || github.sha }} | ||
| REPO_NAME: ${{ github.repository }} | ||
| SOURCE_BUCKET: ${{ steps.env.outputs.source_bucket }} | ||
| run: | | ||
| set -euo pipefail | ||
| S3_KEY="${REPO_NAME}/${COMMIT_SHA}.zip" | ||
| zip -r -q /tmp/source.zip . -x '.git/*' | ||
| aws s3 cp /tmp/source.zip "s3://${SOURCE_BUCKET}/${S3_KEY}" | ||
| echo "s3_location=${SOURCE_BUCKET}/${S3_KEY}" >> "$GITHUB_OUTPUT" | ||
|
|
||
| - name: Start CodeBuild | ||
| id: codebuild | ||
| env: | ||
| TARGET_ENV: ${{ inputs.environment }} | ||
| JFROG_URL: ${{ secrets.JFROG_ARTIFACTORY_NPM_VIRTUAL_URL }} | ||
| JFROG_AUTH: ${{ secrets.JFROG_ARTIFACTORY_READ_NPM_AUTH }} | ||
| S3_LOCATION: ${{ steps.source.outputs.s3_location }} | ||
| BUILDSPEC_PATH: ${{ inputs.buildspec }} | ||
| IMAGE_OVERRIDE: ${{ inputs.image_override }} | ||
| COMPUTE_TYPE_OVERRIDE: ${{ inputs.compute_type_override }} | ||
| run: | | ||
| set -euo pipefail | ||
|
|
||
| ENV_VARS=$(jq -nc '[ | ||
| {"name":"E2E_ENVIRONMENT","value":env.TARGET_ENV}, | ||
| {"name":"JFROG_ARTIFACTORY_URL","value":env.JFROG_URL}, | ||
| {"name":"JFROG_ARTIFACTORY_AUTH","value":env.JFROG_AUTH} | ||
|
darwinboersma marked this conversation as resolved.
|
||
| ]') | ||
|
darwinboersma marked this conversation as resolved.
|
||
|
|
||
| CMD=(aws codebuild start-build | ||
| --project-name tdp-e2e | ||
|
darwinboersma marked this conversation as resolved.
|
||
| --source-type-override S3 | ||
|
darwinboersma marked this conversation as resolved.
|
||
| --source-location-override "$S3_LOCATION" | ||
| --buildspec-override "$BUILDSPEC_PATH" | ||
| --environment-variables-override "$ENV_VARS") | ||
|
|
||
| if [[ -n "$IMAGE_OVERRIDE" ]]; then | ||
| CMD+=(--image-override "$IMAGE_OVERRIDE") | ||
| fi | ||
| if [[ -n "$COMPUTE_TYPE_OVERRIDE" ]]; then | ||
| CMD+=(--compute-type-override "$COMPUTE_TYPE_OVERRIDE") | ||
| fi | ||
|
|
||
| startBuild=$("${CMD[@]}") | ||
| buildId=$(echo "$startBuild" | jq -r '.build.id') | ||
| if [[ -z "$buildId" || "$buildId" == "null" ]]; then | ||
| echo "::error::Failed to start CodeBuild — no build ID returned" | ||
| exit 1 | ||
| fi | ||
| echo "build_id=$buildId" >> "$GITHUB_OUTPUT" | ||
| echo "Started CodeBuild (tdp-e2e): $buildId" | ||
|
|
||
| - name: Wait for CodeBuild | ||
| id: wait | ||
| env: | ||
| BUILD_ID: ${{ steps.codebuild.outputs.build_id }} | ||
| TIMEOUT: ${{ inputs.timeout_minutes }} | ||
| run: | | ||
| set -euo pipefail | ||
| MAX_SECONDS=$((TIMEOUT * 60)) | ||
| buildStatus="IN_PROGRESS" | ||
| while [[ $SECONDS -le $MAX_SECONDS ]]; do | ||
| sleep 15 | ||
| buildInfo=$(aws codebuild batch-get-builds --ids "$BUILD_ID") | ||
| buildStatus=$(echo "$buildInfo" | jq -r '.builds[0].buildStatus') | ||
| if [[ -z "$buildStatus" || "$buildStatus" == "null" ]]; then | ||
| echo "::warning::Failed to get build status, retrying..." | ||
| continue | ||
| fi | ||
| if [[ "$buildStatus" != "IN_PROGRESS" ]]; then | ||
| break | ||
| fi | ||
| done | ||
|
|
||
| LOG_GROUP=$(echo "$buildInfo" | jq -r '.builds[0].logs.groupName') | ||
| LOG_STREAM=$(echo "$buildInfo" | jq -r '.builds[0].logs.streamName') | ||
| LOG_LINK=$(echo "$buildInfo" | jq -r '.builds[0].logs.deepLink') | ||
|
|
||
| echo "log_group=$LOG_GROUP" >> "$GITHUB_OUTPUT" | ||
| echo "log_stream=$LOG_STREAM" >> "$GITHUB_OUTPUT" | ||
| echo "log_link=$LOG_LINK" >> "$GITHUB_OUTPUT" | ||
| echo "build_status=$buildStatus" >> "$GITHUB_OUTPUT" | ||
|
|
||
| if [[ $SECONDS -gt $MAX_SECONDS ]]; then | ||
| echo "::error::Timeout exceeded ($TIMEOUT min); stopping CodeBuild build $BUILD_ID" | ||
| aws codebuild stop-build --id "$BUILD_ID" 2>/dev/null || true | ||
| exit 1 | ||
| fi | ||
|
|
||
| - name: Fetch CodeBuild logs | ||
| if: always() && steps.wait.outputs.log_stream != '' | ||
| env: | ||
| LOG_GROUP: ${{ steps.wait.outputs.log_group }} | ||
| LOG_STREAM: ${{ steps.wait.outputs.log_stream }} | ||
| run: | | ||
| set -euo pipefail | ||
| echo "::group::CodeBuild output" | ||
| aws logs get-log-events \ | ||
| --log-group-name "$LOG_GROUP" \ | ||
| --log-stream-name "$LOG_STREAM" \ | ||
| --start-from-head \ | ||
| --query 'events[].message' \ | ||
| --output text 2>/dev/null || echo "(failed to fetch logs)" | ||
| echo "::endgroup::" | ||
|
|
||
| - name: Write job summary | ||
| if: always() && steps.wait.outputs.build_status != '' | ||
| env: | ||
| BUILD_STATUS: ${{ steps.wait.outputs.build_status }} | ||
| LOG_LINK: ${{ steps.wait.outputs.log_link }} | ||
| BUILD_ID: ${{ steps.codebuild.outputs.build_id }} | ||
| ENVIRONMENT: ${{ inputs.environment }} | ||
| run: | | ||
| if [[ "$BUILD_STATUS" == "SUCCEEDED" ]]; then | ||
| ICON="white_check_mark" | ||
| else | ||
| ICON="x" | ||
| fi | ||
|
|
||
| cat >> "$GITHUB_STEP_SUMMARY" <<EOF | ||
| ## :${ICON}: E2E Tests — ${BUILD_STATUS} | ||
|
|
||
| | | | | ||
| |---|---| | ||
| | **Environment** | \`${ENVIRONMENT}\` | | ||
| | **Build ID** | \`${BUILD_ID}\` | | ||
| | **Status** | ${BUILD_STATUS} | | ||
| | **CloudWatch Logs** | [View logs](${LOG_LINK}) | | ||
| EOF | ||
|
|
||
| - name: Check result | ||
| if: always() | ||
| env: | ||
| BUILD_STATUS: ${{ steps.wait.outputs.build_status }} | ||
| LOG_LINK: ${{ steps.wait.outputs.log_link }} | ||
| run: | | ||
| if [[ "$BUILD_STATUS" == "SUCCEEDED" ]]; then | ||
| echo "E2E tests passed" | ||
| else | ||
| echo "::error::E2E tests failed — status: $BUILD_STATUS" | ||
| echo "::error::CloudWatch logs: $LOG_LINK" | ||
| exit 1 | ||
| fi | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.