ci: add e2e install-test workflow for aws-lambda #1
Workflow file for this run
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
| # End-to-end install test for the aws-lambda app. | |
| # | |
| # Creates a real install in the stage org, applies the Nuon CloudFormation | |
| # install stack into a test AWS account (assumed via GitHub OIDC), waits for the | |
| # runner + sandbox + components to go active, verifies the public endpoint via | |
| # the http_healthcheck action, then tears everything down. | |
| # | |
| # Prerequisites (configured on the `stage` GitHub Environment): | |
| # - secret NUON_CONFIG : ~/.nuon contents (api_url, org_id, api_token) for stage | |
| # - secret AWS_ROLE_ARN : IAM role to assume via OIDC; must be able to create | |
| # the install stack (IAM, VPC/EC2, Lambda, CFN nested | |
| # stacks, Logs, custom resources) and delete it | |
| # | |
| # NOTE: the AWS_ROLE_ARN role is currently provisioned MANUALLY (not IaC) in the | |
| # sandbox-ht account (504178855485) as gha-example-app-configs-e2e: | |
| # - trust: GitHub OIDC, sub=repo:nuonco/example-app-configs:environment:stage | |
| # - policy: AdministratorAccess | |
| # TODO: move this to mono IaC (infra/aws/lib/accounts.ts) in the demo account. | |
| # | |
| # Components are pinned to branch=main, so this always tests released config. | |
| name: E2E Install Test (aws-lambda) | |
| on: | |
| # TEMPORARY: remove before merging to main. Lets us test the workflow from the | |
| # feature branch (workflow_dispatch only works once the file is on main). | |
| push: | |
| branches: [ht/install-test] | |
| workflow_dispatch: | |
| inputs: | |
| install_name: | |
| description: "Install name (default: aws-lambda-ci-<UTC yyyymmdd-HHMMSS>)" | |
| type: string | |
| default: "" | |
| region: | |
| description: AWS region to install into | |
| type: string | |
| default: us-east-1 | |
| keep_alive: | |
| description: Skip teardown (leave the install running for debugging) | |
| type: boolean | |
| default: false | |
| NUON_DEBUG: | |
| description: Enable nuon CLI debug logging | |
| type: boolean | |
| default: false | |
| permissions: | |
| contents: read | |
| id-token: write | |
| defaults: | |
| run: | |
| shell: bash | |
| jobs: | |
| install-test: | |
| name: Install, verify, teardown | |
| runs-on: ubuntu-latest | |
| environment: stage | |
| timeout-minutes: 45 | |
| env: | |
| NUON_DEBUG: ${{ inputs.NUON_DEBUG }} | |
| AWS_REGION: ${{ inputs.region || 'us-east-1' }} | |
| APP: aws-lambda | |
| INSTALL_NAME_PREFIX: aws-lambda-ci | |
| INSTALL_CONFIG: aws-lambda/installs/aws-lambda-ci-stage.toml | |
| STACK_PREFIX: nuon-e2e | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| - name: Install nuon CLI | |
| run: ./scripts/install-cli.sh | |
| - name: Configure nuon auth | |
| env: | |
| NUON_CONFIG: ${{ secrets.NUON_CONFIG }} | |
| run: printf '%s' "$NUON_CONFIG" > "$HOME/.nuon" | |
| - name: Configure AWS credentials (OIDC) | |
| uses: aws-actions/configure-aws-credentials@v4 | |
| with: | |
| role-to-assume: ${{ secrets.AWS_ROLE_ARN }} | |
| aws-region: ${{ env.AWS_REGION }} | |
| - name: Sync app config | |
| run: nuon apps sync "$APP" --create | |
| - name: Create install | |
| env: | |
| INPUT_NAME: ${{ inputs.install_name }} | |
| run: | | |
| set -euo pipefail | |
| INSTALL_NAME="$INPUT_NAME" | |
| if [ -z "$INSTALL_NAME" ]; then | |
| INSTALL_NAME="${INSTALL_NAME_PREFIX}-$(date -u +%Y%m%d-%H%M%S)" | |
| fi | |
| echo "INSTALL_NAME=$INSTALL_NAME" >> "$GITHUB_ENV" | |
| echo "Using install name: $INSTALL_NAME" | |
| # Render a per-run config from the template with a unique name so | |
| # concurrent runs don't collide (installs sync is non-pruning). | |
| cfg="$(mktemp -d)/install.toml" | |
| sed -E "s|^name[[:space:]]*=.*|name = \"${INSTALL_NAME}\"|" "$INSTALL_CONFIG" > "$cfg" | |
| nuon installs sync -a "$APP" -d "$cfg" -y | |
| INSTALL_ID=$(nuon installs list -a "$APP" --json \ | |
| | jq -r --arg n "$INSTALL_NAME" '.[] | select(.name==$n) | .id' | head -1) | |
| if [ -z "$INSTALL_ID" ] || [ "$INSTALL_ID" = "null" ]; then | |
| echo "::error::could not resolve install id for $INSTALL_NAME" | |
| exit 1 | |
| fi | |
| echo "INSTALL_ID=$INSTALL_ID" >> "$GITHUB_ENV" | |
| echo "STACK_NAME=${STACK_PREFIX}-${INSTALL_ID}" >> "$GITHUB_ENV" | |
| echo "Created install $INSTALL_ID" | |
| - name: Apply CloudFormation install stack | |
| run: | | |
| set -euo pipefail | |
| TEMPLATE_URL=$(nuon installs stacks latest --install-id "$INSTALL_ID" --json \ | |
| | jq -r '.template_url') | |
| echo "Applying stack $STACK_NAME from $TEMPLATE_URL" | |
| aws cloudformation create-stack \ | |
| --stack-name "$STACK_NAME" \ | |
| --template-url "$TEMPLATE_URL" \ | |
| --capabilities CAPABILITY_NAMED_IAM CAPABILITY_AUTO_EXPAND \ | |
| --region "$AWS_REGION" | |
| aws cloudformation wait stack-create-complete \ | |
| --stack-name "$STACK_NAME" --region "$AWS_REGION" | |
| echo "Stack $STACK_NAME create complete" | |
| - name: Wait for install to go active | |
| run: | | |
| set -euo pipefail | |
| deadline=$(( $(date +%s) + 1800 )) | |
| while :; do | |
| json=$(nuon installs get -i "$INSTALL_ID" --json) | |
| runner=$(echo "$json" | jq -r '.runner_status // "unknown"') | |
| sandbox=$(echo "$json" | jq -r '.sandbox_status // "unknown"') | |
| comp=$(echo "$json" | jq -r '.composite_component_status // "unknown"') | |
| echo "runner=$runner sandbox=$sandbox components=$comp" | |
| if [ "$runner" = "active" ] && [ "$sandbox" = "active" ] && [ "$comp" = "active" ]; then | |
| echo "install is active" | |
| break | |
| fi | |
| case "$runner $sandbox $comp" in | |
| *error*|*failed*) | |
| echo "::error::install entered a failed state" | |
| echo "$json" | jq '{runner_status,sandbox_status,composite_component_status,composite_component_status_description}' | |
| exit 1 | |
| ;; | |
| esac | |
| if [ "$(date +%s)" -ge "$deadline" ]; then | |
| echo "::error::timed out waiting for install to go active" | |
| exit 1 | |
| fi | |
| sleep 20 | |
| done | |
| - name: Verify public endpoint (http_healthcheck) | |
| run: | | |
| set -euo pipefail | |
| ACW=$(nuon installs actions list -i "$INSTALL_ID" --json \ | |
| | jq -r '.[] | select(.action_workflow.name=="http_healthcheck") | .action_workflow.id' | head -1) | |
| if [ -z "$ACW" ] || [ "$ACW" = "null" ]; then | |
| echo "::error::http_healthcheck action not found" | |
| exit 1 | |
| fi | |
| nuon actions create-run -w "$ACW" -i "$INSTALL_ID" | |
| deadline=$(( $(date +%s) + 300 )) | |
| while :; do | |
| run=$(nuon actions recent-runs -w "$ACW" -i "$INSTALL_ID" --json | jq -r '.[0]') | |
| rid=$(echo "$run" | jq -r '.id') | |
| status=$(echo "$run" | jq -r '.status') | |
| echo "run=$rid status=$status" | |
| case "$status" in | |
| finished) break ;; | |
| error|failed) | |
| echo "::error::http_healthcheck run failed" | |
| exit 1 | |
| ;; | |
| esac | |
| if [ "$(date +%s)" -ge "$deadline" ]; then | |
| echo "::error::timed out waiting for http_healthcheck" | |
| exit 1 | |
| fi | |
| sleep 10 | |
| done | |
| code=$(nuon actions get-run --run-id "$rid" -i "$INSTALL_ID" --json \ | |
| | jq -r '.outputs.http_status_code // .outputs.steps.check.http_status_code') | |
| echo "http_status_code=$code" | |
| if [ "$code" != "200" ]; then | |
| echo "::error::endpoint returned $code, expected 200" | |
| exit 1 | |
| fi | |
| echo "endpoint healthy (200)" | |
| - name: Teardown | |
| if: ${{ always() && inputs.keep_alive != true && env.INSTALL_ID != '' }} | |
| run: | | |
| set -uo pipefail | |
| echo "Deprovisioning install $INSTALL_ID" | |
| nuon installs deprovision -i "$INSTALL_ID" || true | |
| # sandbox_status reports "unknown" once torn down, so key off a | |
| # completed deprovision sandbox run plus all components inactive. | |
| deadline=$(( $(date +%s) + 900 )) | |
| while :; do | |
| comps=$(nuon installs get -i "$INSTALL_ID" --json 2>/dev/null \ | |
| | jq -r '(.component_statuses // {}) | [.[]] as $s | ($s|length>0) and ($s|all(.=="inactive"))' 2>/dev/null || echo false) | |
| deprov=$(nuon installs sandbox-runs --install-id "$INSTALL_ID" --json 2>/dev/null \ | |
| | jq -r 'any(.[]; .run_type=="deprovision" and .status=="deprovisioned")' 2>/dev/null || echo false) | |
| echo "components_inactive=$comps deprovisioned=$deprov" | |
| if [ "$comps" = "true" ] && [ "$deprov" = "true" ]; then | |
| echo "deprovision complete" | |
| break | |
| fi | |
| [ "$(date +%s)" -ge "$deadline" ] && { echo "::warning::deprovision wait timed out, continuing to stack delete"; break; } | |
| sleep 20 | |
| done | |
| if [ -n "${STACK_NAME:-}" ]; then | |
| echo "Deleting stack $STACK_NAME" | |
| aws cloudformation delete-stack --stack-name "$STACK_NAME" --region "$AWS_REGION" || true | |
| aws cloudformation wait stack-delete-complete --stack-name "$STACK_NAME" --region "$AWS_REGION" || true | |
| fi | |
| echo "Deleting install $INSTALL_ID" | |
| nuon installs delete -i "$INSTALL_ID" --confirm || true |