From 8549ea773213f8e4db821c1f6e8b501af15fbc3b Mon Sep 17 00:00:00 2001 From: edersonbrilhante Date: Thu, 22 Oct 2026 12:16:12 +0200 Subject: [PATCH 1/7] test: refactor smoke test --- .github/dependabot.yml | 12 + .github/workflows/ministack.yml | 2 - .github/workflows/smoke-tests.yml | 149 +--- .gitignore | 2 + .../.terraform.lock.hcl | 71 ++ examples/multi-runner-orchestration/README.md | 57 ++ examples/multi-runner-orchestration/main.tf | 202 +++++ .../outputs.tf | 5 +- .../multi-runner-orchestration/providers.tf | 9 + .../multi-runner-orchestration/variables.tf | 85 +++ .../versions.tf | 7 +- .../.terraform.lock.hcl | 93 --- .../.terraform.lock.hcl.tofu | 150 ---- examples/multi-runner-scale-set/README.md | 86 --- examples/multi-runner-scale-set/main.tf | 215 ------ examples/multi-runner-scale-set/providers.tf | 9 - examples/multi-runner-scale-set/variables.tf | 72 -- examples/multi-runner-v2/.terraform.lock.hcl | 93 --- .../multi-runner-v2/.terraform.lock.hcl.tofu | 150 ---- examples/multi-runner-v2/README.md | 77 -- examples/multi-runner-v2/main.tf | 180 ----- examples/multi-runner-v2/outputs.tf | 8 - examples/multi-runner-v2/providers.tf | 9 - examples/multi-runner-v2/variables.tf | 39 - examples/multi-runner-v2/versions.tf | 17 - tests/ministack/README.md | 67 +- tests/ministack/github-api-expectations.json | 77 -- tests/ministack/initializerJson.json | 138 ---- tests/ministack/multi-runner-scale-set.tfvars | 57 -- tests/ministack/multi-runner-v2.tfvars | 33 - tests/ministack/run-scale-set-integration.sh | 600 --------------- tests/ministack/run-smoke.sh | 721 ------------------ tests/ministack/workflow_job_event.json | 26 - tests/multi-runner-orchestration/Dockerfile | 18 + tests/multi-runner-orchestration/README.md | 101 +++ .../multi-runner-orchestration/buildkitd.toml | 5 + .../docker-compose.yml | 175 +++++ .../fixtures/ministack-fixtures.json | 38 + .../multi-runner-orchestration.tfvars | 48 ++ .../fixtures/seed_ministack_fixtures.py | 198 +++++ .../01-github-app---list-installations.json | 26 + ...ithub-app---organization-installation.json | 24 + ...-github-app---repository-installation.json | 24 + ...pp---create-installation-access-token.json | 22 + .../05-runner---org-registration-token.json | 17 + .../06-runner---repo-registration-token.json | 17 + ...unner---enterprise-registration-token.json | 17 + .../08-runner---org-remove-token.json | 17 + .../09-runner---repo-remove-token.json | 17 + .../10-runner---enterprise-remove-token.json | 17 + .../11-runner---list-org-runners.json | 37 + .../12-runner---list-repository-runners.json | 37 + .../13-runner---list-enterprise-runners.json | 37 + .../14-runner---delete-org-runner.json | 10 + .../15-runner---delete-repository-runner.json | 10 + .../16-runner---delete-enterprise-runner.json | 10 + .../17-runner---org-runner-groups.json | 24 + .../18-runner---repo-workflow-job.json | 27 + .../mappings/19-runner---org-jit-config.json | 20 + .../mappings/20-runner---repo-jit-config.json | 20 + .../21-runner---enterprise-jit-config.json | 20 + ...unner-release---latest-runner-release.json | 14 + .../23-runner-release---runner-archive.json | 14 + ...-set---actions-registration-handshake.json | 14 + .../25-scale-set---list-runner-groups.json | 14 + .../26-scale-set---list-scale-sets.json | 40 + .../27-scale-set---create-scale-set.json | 35 + .../28-scale-set---get-scale-set-by-id.json | 35 + .../29-scale-set---update-scale-set.json | 35 + .../30-scale-set---delete-scale-set.json | 10 + ...cale-set---generate-jit-runner-config.json | 14 + ...32-scale-set---create-message-session.json | 14 + ...3-scale-set---refresh-message-session.json | 14 + .../34-scale-set---close-message-session.json | 10 + ...-scale-set---poll-message-queue-empty.json | 11 + .../36-scale-set---acknowledge-message.json | 10 + .../mappings/37-scale-set---acquire-jobs.json | 16 + .../38-scale-set---get-runner-by-id.json | 14 + .../39-scale-set---list-runners-by-name.json | 14 + .../40-scale-set---remove-runner.json | 10 + .../mappings/41-runner---get-org-runner.json | 13 + .../42-runner---get-repository-runner.json | 32 + .../smoke/prepare-scale-set-credentials.mjs | 75 ++ .../smoke/reporter.mjs | 15 + .../smoke/scale-set.test.mjs | 218 ++++++ .../smoke/support.mjs | 226 ++++++ .../smoke/webhook.test.mjs | 160 ++++ 87 files changed, 2571 insertions(+), 3057 deletions(-) create mode 100644 examples/multi-runner-orchestration/.terraform.lock.hcl create mode 100644 examples/multi-runner-orchestration/README.md create mode 100644 examples/multi-runner-orchestration/main.tf rename examples/{multi-runner-scale-set => multi-runner-orchestration}/outputs.tf (72%) create mode 100644 examples/multi-runner-orchestration/providers.tf create mode 100644 examples/multi-runner-orchestration/variables.tf rename examples/{multi-runner-scale-set => multi-runner-orchestration}/versions.tf (76%) delete mode 100644 examples/multi-runner-scale-set/.terraform.lock.hcl delete mode 100644 examples/multi-runner-scale-set/.terraform.lock.hcl.tofu delete mode 100644 examples/multi-runner-scale-set/README.md delete mode 100644 examples/multi-runner-scale-set/main.tf delete mode 100644 examples/multi-runner-scale-set/providers.tf delete mode 100644 examples/multi-runner-scale-set/variables.tf delete mode 100644 examples/multi-runner-v2/.terraform.lock.hcl delete mode 100644 examples/multi-runner-v2/.terraform.lock.hcl.tofu delete mode 100644 examples/multi-runner-v2/README.md delete mode 100644 examples/multi-runner-v2/main.tf delete mode 100644 examples/multi-runner-v2/outputs.tf delete mode 100644 examples/multi-runner-v2/providers.tf delete mode 100644 examples/multi-runner-v2/variables.tf delete mode 100644 examples/multi-runner-v2/versions.tf delete mode 100644 tests/ministack/github-api-expectations.json delete mode 100644 tests/ministack/initializerJson.json delete mode 100644 tests/ministack/multi-runner-scale-set.tfvars delete mode 100644 tests/ministack/multi-runner-v2.tfvars delete mode 100755 tests/ministack/run-scale-set-integration.sh delete mode 100644 tests/ministack/run-smoke.sh delete mode 100644 tests/ministack/workflow_job_event.json create mode 100644 tests/multi-runner-orchestration/Dockerfile create mode 100644 tests/multi-runner-orchestration/README.md create mode 100644 tests/multi-runner-orchestration/buildkitd.toml create mode 100644 tests/multi-runner-orchestration/docker-compose.yml create mode 100644 tests/multi-runner-orchestration/fixtures/ministack-fixtures.json create mode 100644 tests/multi-runner-orchestration/fixtures/multi-runner-orchestration.tfvars create mode 100644 tests/multi-runner-orchestration/fixtures/seed_ministack_fixtures.py create mode 100644 tests/multi-runner-orchestration/mappings/01-github-app---list-installations.json create mode 100644 tests/multi-runner-orchestration/mappings/02-github-app---organization-installation.json create mode 100644 tests/multi-runner-orchestration/mappings/03-github-app---repository-installation.json create mode 100644 tests/multi-runner-orchestration/mappings/04-github-app---create-installation-access-token.json create mode 100644 tests/multi-runner-orchestration/mappings/05-runner---org-registration-token.json create mode 100644 tests/multi-runner-orchestration/mappings/06-runner---repo-registration-token.json create mode 100644 tests/multi-runner-orchestration/mappings/07-runner---enterprise-registration-token.json create mode 100644 tests/multi-runner-orchestration/mappings/08-runner---org-remove-token.json create mode 100644 tests/multi-runner-orchestration/mappings/09-runner---repo-remove-token.json create mode 100644 tests/multi-runner-orchestration/mappings/10-runner---enterprise-remove-token.json create mode 100644 tests/multi-runner-orchestration/mappings/11-runner---list-org-runners.json create mode 100644 tests/multi-runner-orchestration/mappings/12-runner---list-repository-runners.json create mode 100644 tests/multi-runner-orchestration/mappings/13-runner---list-enterprise-runners.json create mode 100644 tests/multi-runner-orchestration/mappings/14-runner---delete-org-runner.json create mode 100644 tests/multi-runner-orchestration/mappings/15-runner---delete-repository-runner.json create mode 100644 tests/multi-runner-orchestration/mappings/16-runner---delete-enterprise-runner.json create mode 100644 tests/multi-runner-orchestration/mappings/17-runner---org-runner-groups.json create mode 100644 tests/multi-runner-orchestration/mappings/18-runner---repo-workflow-job.json create mode 100644 tests/multi-runner-orchestration/mappings/19-runner---org-jit-config.json create mode 100644 tests/multi-runner-orchestration/mappings/20-runner---repo-jit-config.json create mode 100644 tests/multi-runner-orchestration/mappings/21-runner---enterprise-jit-config.json create mode 100644 tests/multi-runner-orchestration/mappings/22-runner-release---latest-runner-release.json create mode 100644 tests/multi-runner-orchestration/mappings/23-runner-release---runner-archive.json create mode 100644 tests/multi-runner-orchestration/mappings/24-scale-set---actions-registration-handshake.json create mode 100644 tests/multi-runner-orchestration/mappings/25-scale-set---list-runner-groups.json create mode 100644 tests/multi-runner-orchestration/mappings/26-scale-set---list-scale-sets.json create mode 100644 tests/multi-runner-orchestration/mappings/27-scale-set---create-scale-set.json create mode 100644 tests/multi-runner-orchestration/mappings/28-scale-set---get-scale-set-by-id.json create mode 100644 tests/multi-runner-orchestration/mappings/29-scale-set---update-scale-set.json create mode 100644 tests/multi-runner-orchestration/mappings/30-scale-set---delete-scale-set.json create mode 100644 tests/multi-runner-orchestration/mappings/31-scale-set---generate-jit-runner-config.json create mode 100644 tests/multi-runner-orchestration/mappings/32-scale-set---create-message-session.json create mode 100644 tests/multi-runner-orchestration/mappings/33-scale-set---refresh-message-session.json create mode 100644 tests/multi-runner-orchestration/mappings/34-scale-set---close-message-session.json create mode 100644 tests/multi-runner-orchestration/mappings/35-scale-set---poll-message-queue-empty.json create mode 100644 tests/multi-runner-orchestration/mappings/36-scale-set---acknowledge-message.json create mode 100644 tests/multi-runner-orchestration/mappings/37-scale-set---acquire-jobs.json create mode 100644 tests/multi-runner-orchestration/mappings/38-scale-set---get-runner-by-id.json create mode 100644 tests/multi-runner-orchestration/mappings/39-scale-set---list-runners-by-name.json create mode 100644 tests/multi-runner-orchestration/mappings/40-scale-set---remove-runner.json create mode 100644 tests/multi-runner-orchestration/mappings/41-runner---get-org-runner.json create mode 100644 tests/multi-runner-orchestration/mappings/42-runner---get-repository-runner.json create mode 100644 tests/multi-runner-orchestration/smoke/prepare-scale-set-credentials.mjs create mode 100644 tests/multi-runner-orchestration/smoke/reporter.mjs create mode 100644 tests/multi-runner-orchestration/smoke/scale-set.test.mjs create mode 100644 tests/multi-runner-orchestration/smoke/support.mjs create mode 100644 tests/multi-runner-orchestration/smoke/webhook.test.mjs diff --git a/.github/dependabot.yml b/.github/dependabot.yml index aea4db326e..931a5424e8 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -108,3 +108,15 @@ updates: - "python" commit-message: prefix: "chore(docs)" + + - package-ecosystem: "docker-compose" + directory: "/tests/multi-runner-orchestration" + schedule: + interval: "weekly" + cooldown: + default-days: 7 + labels: + - "dependencies" + - "docker-compose" + commit-message: + prefix: "chore(docker-compose)" diff --git a/.github/workflows/ministack.yml b/.github/workflows/ministack.yml index b8945d4b34..de902585c4 100644 --- a/.github/workflows/ministack.yml +++ b/.github/workflows/ministack.yml @@ -52,8 +52,6 @@ jobs: - default - ephemeral - multi-runner - - multi-runner-v2 - - multi-runner-scale-set - migration-test - termination-watcher services: diff --git a/.github/workflows/smoke-tests.yml b/.github/workflows/smoke-tests.yml index bdc147596b..4f947d717c 100644 --- a/.github/workflows/smoke-tests.yml +++ b/.github/workflows/smoke-tests.yml @@ -2,7 +2,13 @@ name: "Smoke Tests" on: pull_request: - paths: ["**/*.tf", "**/*.hcl", ".github/workflows/smoke-tests.yml"] + paths: + - "**/*.tf" + - "**/*.hcl" + - "lambdas/**" + - "tests/ministack/**" + - "tests/multi-runner-orchestration/**" + - ".github/workflows/smoke-tests.yml" workflow_dispatch: concurrency: @@ -12,33 +18,14 @@ concurrency: permissions: contents: read -env: - AWS_ACCESS_KEY_ID: "000000000000" - AWS_DEFAULT_REGION: eu-west-1 - AWS_EC2_METADATA_DISABLED: "true" - AWS_ENDPOINT_URL: http://localhost:4566 - AWS_REGION: eu-west-1 - AWS_SECRET_ACCESS_KEY: test-only - TF_IN_AUTOMATION: "true" - TF_INPUT: "false" - jobs: - control_plane_smoke: - name: Run webhook and pool lifecycle smoke test against MiniStack + ministack_smoke: + name: Run webhook and scale-set smoke tests against MiniStack runs-on: ubuntu-latest - timeout-minutes: 30 - services: - ministack: - image: ghcr.io/ministackorg/ministack:1.5.13@sha256:ce3c906f2866ff953ce4c56f06b1fa3e453bc32e41c00de17b5f5a8672c5a42c - ports: - - 4566:4566 - options: --add-host=host.docker.internal:host-gateway - env: - MINISTACK_ACCOUNT_ID: "000000000000" - MINISTACK_REGION: eu-west-1 + timeout-minutes: 120 steps: - name: Harden the runner (Audit all outbound calls) - uses: step-security/harden-runner@ccd8616d44fd3846e67624a50d5aad6d37bf2d25 # v2.22.1 + uses: step-security/harden-runner@e14015d583714f6e62063499dc959a02595150a1 # v2.21.1 with: egress-policy: audit @@ -47,116 +34,6 @@ jobs: with: persist-credentials: false - - name: Setup Node.js - uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 - with: - node-version-file: lambdas/.nvmrc - package-manager-cache: false - - - name: Setup Terraform - uses: hashicorp/setup-terraform@dfe3c3f87815947d99a8997f908cb6525fc44e9e # v4.0.1 - with: - terraform_version: latest - terraform_wrapper: false - - - name: Install Lambda dependencies - working-directory: lambdas - run: yarn install --frozen-lockfile - - - name: Build smoke-test Lambda distributions - working-directory: lambdas + - name: Build and start the complete Compose stack run: | - yarn workspace @aws-github-runner/webhook dist - yarn workspace @aws-github-runner/control-plane dist - - - name: Start MockServer - id: mockserver - uses: mock-server/setup-mockserver@24612c2ccef1f83d587f331ed77cc5cef441e0b1 # v1.0.0 - with: - version: '7.6.0@sha256:80b3b1a26f3553d0c81a3f3896b5b7274c17b2a2e52f0fd2b28e246bc9efa290' - port: '1080' - startup-timeout: '60' - - - name: Run webhook and pool lifecycle smoke test - env: - MINISTACK_GITHUB_MOCK_HOST: host.docker.internal - MINISTACK_GITHUB_MOCK_PORT: "1080" - MINISTACK_GITHUB_MOCK_URL: ${{ steps.mockserver.outputs.url }} - run: sh tests/ministack/run-smoke.sh - - scale_set_integration_smoke: - name: Run scale-set ECS smoke test against MiniStack and MockServer - runs-on: ubuntu-latest - timeout-minutes: 30 - services: - ministack: - image: ghcr.io/ministackorg/ministack:1.5.12@sha256:41fe1ce2e666c6cc410c6047a9db8bf1df69cd0028ebc0a6c6e5517c3a83d6e0 - ports: - - 4566:4566 - # MiniStack launches nested service containers for the integration smoke test. - # This digest-pinned, non-fork workflow is the narrowly scoped Docker-daemon exception. - options: >- - --add-host=host.docker.internal:host-gateway - --volume /var/run/docker.sock:/var/run/docker.sock - env: - MINISTACK_ACCOUNT_ID: "000000000000" - MINISTACK_REGION: eu-west-1 - steps: - - name: Harden the runner (Audit all outbound calls) - uses: step-security/harden-runner@ccd8616d44fd3846e67624a50d5aad6d37bf2d25 # v2.22.1 - with: - egress-policy: audit - - - name: Checkout - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - persist-credentials: false - - - name: Setup Node.js - uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 - with: - node-version-file: lambdas/.nvmrc - package-manager-cache: false - - - name: Setup Terraform - uses: hashicorp/setup-terraform@dfe3c3f87815947d99a8997f908cb6525fc44e9e # v4.0.1 - with: - terraform_version: latest - terraform_wrapper: false - - - name: Install Lambda dependencies - working-directory: lambdas - run: yarn install --frozen-lockfile - - - name: Build smoke-test Lambda distributions - working-directory: lambdas - run: | - yarn workspace @aws-github-runner/webhook dist - yarn workspace @aws-github-runner/control-plane dist - - - name: Start MockServer - id: mockserver - uses: mock-server/setup-mockserver@24612c2ccef1f83d587f331ed77cc5cef441e0b1 # v1.0.0 - with: - version: '7.6.0@sha256:80b3b1a26f3553d0c81a3f3896b5b7274c17b2a2e52f0fd2b28e246bc9efa290' - port: '1080' - startup-timeout: '60' - - - name: Connect MockServer to MiniStack network - shell: bash - run: | - set -euo pipefail - ministack_container="$(docker ps --format '{{.ID}} {{.Image}}' | awk '$2 ~ /ministack/ {print $1; exit}')" - network="$(docker inspect --format '{{range $name, $_ := .NetworkSettings.Networks}}{{println $name}}{{end}}' "$ministack_container" | sed -n '1p')" - docker network connect --alias mockserver "$network" mockserver - - - name: Mark repository as safe - shell: sh - run: git config --global --add safe.directory "$GITHUB_WORKSPACE" - - - name: Run scale-set ECS/MockServer smoke test - env: - MINISTACK_GITHUB_MOCK_HOST: mockserver - MINISTACK_GITHUB_MOCK_PORT: "1080" - MINISTACK_GITHUB_MOCK_URL: ${{ steps.mockserver.outputs.url }} - run: sh tests/ministack/run-scale-set-integration.sh + docker compose -f tests/multi-runner-orchestration/docker-compose.yml up --build --force-recreate diff --git a/.gitignore b/.gitignore index 276fe10733..a215e2b9d5 100644 --- a/.gitignore +++ b/.gitignore @@ -29,3 +29,5 @@ secrets.auto.tfvars node_modules/ site/ + +lambda_output \ No newline at end of file diff --git a/examples/multi-runner-orchestration/.terraform.lock.hcl b/examples/multi-runner-orchestration/.terraform.lock.hcl new file mode 100644 index 0000000000..e393f93ab1 --- /dev/null +++ b/examples/multi-runner-orchestration/.terraform.lock.hcl @@ -0,0 +1,71 @@ +# This file is maintained automatically by "terraform init". +# Manual edits may be lost in future updates. + +provider "registry.terraform.io/hashicorp/aws" { + version = "6.69.1" + constraints = ">= 5.0.0, >= 6.21.0, >= 6.33.0" + hashes = [ + "h1:VCLG26frd0ZRApDQ0l4pqjMCBUi1VPm0/M6c3pnUDgg=", + "h1:j8OcftKysEw0hTrFA5FczfGrXOFGMyorSTEiQihUw4o=", + "zh:0ad1319a9fea21dacf084152e236cfd6c150ccc91854ad547c5ab9bc1cddad9a", + "zh:113d7ceaca21f0e4b84f45e69369263311512f858a402955f9e9a8a5bb7f3151", + "zh:1828581462a8a985361c2f4e9af90b2bd5b7d4603b00146bb23a88c5b7d50888", + "zh:28d454d8ac98a88903540bf17bf63d6935dd2e08e572e41152d06db926a5333a", + "zh:2a3b594a98fedc396a4eb5f0fd324e641f83db0906d0129c2ddeffd8002fbe86", + "zh:32c3103af70480b5c3be86ab00af2f46d3a2a6f269ddadf8c1e53a7a3ea0031b", + "zh:3df1dd01ec3a65291f51f26b4d28dcd1bf7b4c73ed0bd3ab1e94e1153ce9b13d", + "zh:42699fbd59bf14adb1cf2836c80c209b3ce815d252f4e983afc8defa754b1dfe", + "zh:5522afd762a1ee95e6ef0d7ad1f165185303a3039e45e1632a3cd13314b7a1a4", + "zh:69f3537ccb012cf24cc11af3cfc3b4be5602b4a3bfbaae2dcab78e41b40f280b", + "zh:9b12af85486a96aedd8d7984b0ff811a4b42e3d88dad1a3fb4c0b580d04fa425", + "zh:a0c5bf13ba5e17a993aab381603a32d6c9dc2ae8d9f982b6a8c6407b5d7c4a50", + "zh:b9f8a73e11e0b82ba51474b6a6eb4ac3080d7f268087162cb10884655878670a", + "zh:be1f9728404d67dee586d16b69a445931e5e4dafbf7b996df3a7eed519a557b0", + "zh:dc15fb3443312a42deb8b13f92c8d10a701e46eca0155f0709ad763057a85a1d", + "zh:e9f6567a13f21123838aaf78d94f76da8c8f7783eff7f7aa781fd1c21d18d86c", + ] +} + +provider "registry.terraform.io/hashicorp/null" { + version = "3.3.2" + constraints = "~> 3.0, ~> 3.2" + hashes = [ + "h1:/Wbz7DHFO92KAqyDBMxGA8nwOJYECM8iLGhR7+wVWhQ=", + "h1:IQ1qrkht1sC1nibUR+AJ3ulryyhVDHfCHZhoJi0sg2Y=", + "zh:10ec43b8b7b18d5639238c7fb9e111f6a4b038523dd66c7a426bf27b25fa4c08", + "zh:60beb9cc2ad5b871c710860cee75b42850cc6acd43db0d77cb5e00fda7288b55", + "zh:62538582d0a4a2f10ad8a8d9a6c3cd3f05af6c6d91c6641ffc78d4f0e8e69b27", + "zh:64a8f9ce7852d9efc5b464c12306c946366d59f5e2757def97969c9fd64bd1d6", + "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3", + "zh:92a374fb736a52f465283326d0a5bf4f495132eb99be209dfb4c75ec803fe8db", + "zh:98da9c42785d27a50f0604758bcb61a30f6278b9f2acd92bb3b2046e0e71916c", + "zh:b0f7896fae554729cdf4a24ac06359a050cff5817e6cd8597cba8a4ae01a7409", + "zh:bc8179ee35d67c72fb03012e7023b9f9816f033a7ec4109c001dd6d29752e812", + "zh:d23a598f713bfb6098bc003571d7de90b5a33b78f9be240488252fe5f3c2a60d", + "zh:d2855b922ea345dbd89ea287e4c6c4757e38bc0aaffeb2b79aa0b8004f9c53ff", + "zh:d3a60422bc6a2f9244d076c5222c07060c826ef91bdbaf4634cb752b86057473", + "zh:faa01928c25d2a6ecd9c7eb8b88134cb08de55a6b11ca6c703ac0092845344ba", + ] +} + +provider "registry.terraform.io/hashicorp/random" { + version = "3.9.1" + constraints = "~> 3.0" + hashes = [ + "h1:PlW+UZ4EElQF3NQwf41KQwavFujab3Czc51zu9dyVM8=", + "h1:g40qr7yDmIpaur4SsK5BcOda3HSo1RJ6zHVMqN4EJ+0=", + "zh:05f4734c1f0be840b711b3eff259ebc5fca436784c728955b1678078466f48d7", + "zh:0b91bf19371d012434eba1deeb6aab77158def9b39601dcbd94450b3974a2a26", + "zh:0ee6eacd47ec00183d55d726a4b6c4ce951a199f944bf22f1aa58392ebdfa7a2", + "zh:19388a4074b76a89a43a6c8328d7ae8ee2e7de3d346af51e80d3e6d3d12925f1", + "zh:23e74d48c5e2ac2e823fd527f49fee9db37d32a1990c9e3bf126ead697b843eb", + "zh:3cabf7fbd096c520064aae3aba61aba670af83ab91291a71fa1b1332929c2b7f", + "zh:5c0a3b8af0be60be4eca12ddee385cfa8babc1ec8e98cdf9de2f2274c73eabfa", + "zh:60b4f8a8ef18f52bf8e19215229dae408bee732825964092db7c989fd2de4097", + "zh:7359015acfedcbd6366f2329c854cf8d3c8ca5cd0faa89d2d37db358d6eba6c5", + "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3", + "zh:7b38758402f0e13a1071162da28994023cd2ac676e54af350c9ffd8dfa73fa7b", + "zh:7c7fbb8895eb75bb4de1f933e98553bd99c8d048c89a925ddba490aa5a67f7dc", + "zh:8c2b8c6a7ccdec16b73e2fb9f3700ea097f58c592571e4c5de60c93d2301732c", + ] +} diff --git a/examples/multi-runner-orchestration/README.md b/examples/multi-runner-orchestration/README.md new file mode 100644 index 0000000000..ac7743ec48 --- /dev/null +++ b/examples/multi-runner-orchestration/README.md @@ -0,0 +1,57 @@ +# Multi-runner orchestration example + + +## Requirements + +| Name | Version | +|------|---------| +| [terraform](#requirement\_terraform) | >= 1.5.6 | +| [aws](#requirement\_aws) | >= 6.33 | +| [null](#requirement\_null) | ~> 3.0 | +| [random](#requirement\_random) | ~> 3.0 | + +## Providers + +| Name | Version | +|------|---------| +| [aws](#provider\_aws) | 6.66.0 | + +## Modules + +| Name | Source | Version | +|------|--------|---------| +| [base](#module\_base) | ../base | n/a | +| [microvm\_foundation](#module\_microvm\_foundation) | ../../modules/microvm-foundation | n/a | +| [runners](#module\_runners) | ../../modules/multi-runner | n/a | + +## Resources + +| Name | Type | +|------|------| +| [aws_cloudwatch_log_group.aws_cloudwatch_log_group_microvm](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/cloudwatch_log_group) | resource | +| [aws_ecr_repository.base_ubuntu24](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/ecr_repository) | resource | +| [aws_ecr_repository_policy.repository_policy](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/ecr_repository_policy) | resource | +| [aws_caller_identity.current](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/caller_identity) | data source | +| [aws_iam_policy_document.ecr_repository_policy](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| [aws\_region](#input\_aws\_region) | AWS Region where the runner control plane and compute provider resources are deployed. | `string` | `"eu-west-1"` | no | +| [compute\_provider](#input\_compute\_provider) | Provider-specific settings for the EC2 and MicroVM runner lanes. |
object({
aws = object({
ec2 = object({
instance_types = list(string)
ami = object({
filter = optional(map(list(string)), { state = ["available"] })
owners = optional(list(string), ["amazon"])
id_ssm_parameter = optional(object({
arn = string
}), null)
kms_key = optional(object({
arn = string
}), null)
})
})
microvm = object({
image_arn = string
image_version = optional(string, null)
ingress_network_connectors = optional(list(string), [])
egress_network_connectors = list(string)
})
})
})
| n/a | yes | +| [environment](#input\_environment) | Name prefix for the example resources. | `string` | n/a | yes | +| [github](#input\_github) | Optional GitHub endpoint and scale-set ownership settings. |
object({
url = optional(string, null)
ssl_verify = optional(bool, true)
runner_owner = optional(string, null)
registration_level = optional(string, "organization")
})
| `{}` | no | +| [github\_app](#input\_github\_app) | GitHub App ID, base64-encoded private key, and installation ID. |
object({
id = string
key_base64 = string
installation_id = optional(string, null)
webhook_secret = string
})
| n/a | yes | +| [runners\_lambda\_zip](#input\_runners\_lambda\_zip) | Local ZIP file for the runner-control Lambda. | `string` | n/a | yes | +| [scale\_set](#input\_scale\_set) | GitHub Actions scale-set configuration. |
object({
name = string
runner_group_name = optional(string, "Default")
min_runners = optional(number, 0)
container = optional(object({
image = optional(string, null)
}), {})
})
| n/a | yes | +| [webhook\_lambda\_zip](#input\_webhook\_lambda\_zip) | Local ZIP file for the webhook Lambda. | `string` | n/a | yes | + +## Outputs + +| Name | Description | +|------|-------------| +| [microvm](#output\_microvm) | n/a | +| [webhook\_endpoint](#output\_webhook\_endpoint) | n/a | +| [webhook\_secret](#output\_webhook\_secret) | n/a | + diff --git a/examples/multi-runner-orchestration/main.tf b/examples/multi-runner-orchestration/main.tf new file mode 100644 index 0000000000..ee771b4fe1 --- /dev/null +++ b/examples/multi-runner-orchestration/main.tf @@ -0,0 +1,202 @@ +module "base" { + source = "../base" + + prefix = var.environment + aws_region = var.aws_region +} + +module "runners" { + source = "../../modules/multi-runner" + + prefix = var.environment + aws_region = var.aws_region + + experimental_features = ["multi-runner-v2"] + + global_config = { + tags = { + Example = var.environment + Project = "MiniStack" + } + runner = { + os = "linux" + architecture = "x64" + } + } + + global_config_github = { + app = { + key_base64 = var.github_app.key_base64 + id = var.github_app.id + installation_id = var.github_app.installation_id + webhook_secret = var.github_app.webhook_secret + } + enterprise_server = { + url = var.github.url + ssl_verify = var.github.ssl_verify + } + runner_owner = var.github.runner_owner + runner_registration_level = var.github.registration_level + + } + + global_config_lambda = { + architecture = "x86_64" + } + + global_config_observability = { + logs = { + level = "debug" + } + } + + global_config_orchestration_provider = { + webhook = { + runner = { + ephemeral = true + jit_config_enabled = true + # The smoke test keeps the ephemeral resources alive until each provider's + # scale-down phase, so it needs capacity for standard, dynamic, and pool runners. + maximum_count = 3 + boot_time_in_minutes = 0 + } + lambda = { + artifact = { + zip = var.runners_lambda_zip + } + scale = { + up = { + job_queued_check_enabled = true + } + down = { + # Smoke scenarios create and remove runners immediately; do not + # wait for the Linux five-minute minimum runtime before checking + # the GitHub runner state. + minimum_running_time_in_minutes = 0 + } + } + pool = { + config = [{ + schedule_expression = "cron(0 0 1 1 ? 2099)" + schedule_expression_timezone = "UTC" + size = 1 + }] + runner_owner = "test-owner" + } + webhook = { + artifact = { + zip = var.webhook_lambda_zip + } + } + } + } + scale_set = { + grouping = { + strategy = "runner_config" + } + container = var.scale_set.container + network = { + vpc_id = module.base.vpc.vpc_id + subnet_ids = module.base.vpc.private_subnets + } + ecs = { + task = { + cpu_architecture = var.scale_set.ecs.task.architecture + } + } + } + } + + global_config_storage_provider = { + aws = { + ssm = { + paths = { + root = "/github-action-runners/${var.environment}" + } + housekeeper = { + lambda = { + artifact = { + zip = var.runners_lambda_zip + } + } + } + } + } + } + + global_config_compute_provider = { + aws = { + ec2 = { + vpc_id = module.base.vpc.vpc_id + subnet_ids = module.base.vpc.private_subnets + ssm_enabled = true + runner_binaries = { + enabled = false + } + } + } + } + + multi_runner_config = { + ec2 = { + runner = { + os = "linux" + architecture = "x64" + name_prefix = "ec2-" + extra_labels = ["ec2"] + } + orchestration_provider = { + webhook = { + github = { + organization_runners = true + } + matcherConfig = { + labelMatchers = [["self-hosted", "linux", "x64", "ec2"]] + bidirectionalLabelMatch = true + dynamic_labels_enabled = true + awsDynamicLabelsPolicy = { + restricted_keys = { + "instance-type" = { allowed = ["m5.*"] } + } + } + } + } + } + compute_provider = { + aws = { + ec2 = { + instance_types = var.compute_provider.aws.ec2.instance_types + ami = var.compute_provider.aws.ec2.ami + } + } + } + } + ec2_scalet_set = { + runner = { + os = "linux" + architecture = "x64" + name_prefix = "ec2_scalet_set-" + extra_labels = ["self-hosted", "linux", "x64", "ec2", "scale-set"] + group_name = var.scale_set.runner_group_name + } + orchestration_provider = { + scale_set = { + name = var.scale_set.name + runner = { + min_runners = var.scale_set.min_runners + max_runners = 10 + boot_time_in_minutes = 10 + } + } + } + compute_provider = { + aws = { + ec2 = { + instance_types = var.compute_provider.aws.ec2.instance_types + ami = var.compute_provider.aws.ec2.ami + } + } + } + } + } +} diff --git a/examples/multi-runner-scale-set/outputs.tf b/examples/multi-runner-orchestration/outputs.tf similarity index 72% rename from examples/multi-runner-scale-set/outputs.tf rename to examples/multi-runner-orchestration/outputs.tf index 1feaf2e671..2e9144910e 100644 --- a/examples/multi-runner-scale-set/outputs.tf +++ b/examples/multi-runner-orchestration/outputs.tf @@ -1,8 +1,7 @@ output "webhook_endpoint" { value = module.runners.webhook.endpoint } - output "webhook_secret" { sensitive = true - value = random_id.random.hex -} + value = var.github_app.webhook_secret +} \ No newline at end of file diff --git a/examples/multi-runner-orchestration/providers.tf b/examples/multi-runner-orchestration/providers.tf new file mode 100644 index 0000000000..f24f950b27 --- /dev/null +++ b/examples/multi-runner-orchestration/providers.tf @@ -0,0 +1,9 @@ +provider "aws" { + region = var.aws_region + + default_tags { + tags = { + Example = var.environment + } + } +} diff --git a/examples/multi-runner-orchestration/variables.tf b/examples/multi-runner-orchestration/variables.tf new file mode 100644 index 0000000000..f1ed23bbbf --- /dev/null +++ b/examples/multi-runner-orchestration/variables.tf @@ -0,0 +1,85 @@ +variable "aws_region" { + description = "AWS Region where the runner control plane and compute provider resources are deployed." + type = string + default = "eu-west-1" +} + +variable "environment" { + description = "Name prefix for the example resources." + type = string +} + +variable "github_app" { + description = "GitHub App ID, base64-encoded private key, and installation ID." + + type = object({ + id = string + key_base64 = string + installation_id = optional(string, null) + webhook_secret = string + }) + sensitive = true +} + +variable "github" { + description = "Optional GitHub endpoint and scale-set ownership settings." + + type = object({ + url = optional(string, null) + ssl_verify = optional(bool, true) + runner_owner = optional(string, null) + registration_level = optional(string, "organization") + }) + + default = {} +} + +variable "runners_lambda_zip" { + description = "Local ZIP file for the runner-control Lambda." + type = string +} + +variable "webhook_lambda_zip" { + description = "Local ZIP file for the webhook Lambda." + type = string +} + +variable "compute_provider" { + description = "Provider-specific settings for compute providers." + + type = object({ + aws = object({ + ec2 = object({ + instance_types = list(string) + ami = object({ + filter = optional(map(list(string)), { state = ["available"] }) + owners = optional(list(string), ["amazon"]) + id_ssm_parameter = optional(object({ + arn = string + }), null) + kms_key = optional(object({ + arn = string + }), null) + }) + }) + }) + }) +} + +variable "scale_set" { + description = "GitHub Actions scale-set configuration." + + type = object({ + name = string + runner_group_name = optional(string, "Default") + min_runners = optional(number, 0) + container = optional(object({ + image = optional(string, null) + }), {}) + ecs = optional(object({ + task = optional(object({ + architecture = optional(string, "x86_64") + }), {}) + }), {}) + }) +} \ No newline at end of file diff --git a/examples/multi-runner-scale-set/versions.tf b/examples/multi-runner-orchestration/versions.tf similarity index 76% rename from examples/multi-runner-scale-set/versions.tf rename to examples/multi-runner-orchestration/versions.tf index 6af69ab915..6883c62423 100644 --- a/examples/multi-runner-scale-set/versions.tf +++ b/examples/multi-runner-orchestration/versions.tf @@ -4,14 +4,15 @@ terraform { source = "hashicorp/aws" version = ">= 6.33" } - local = { - source = "hashicorp/local" - version = "~> 2.0" + null = { + source = "hashicorp/null" + version = "~> 3.0" } random = { source = "hashicorp/random" version = "~> 3.0" } } + required_version = ">= 1.5.6" } diff --git a/examples/multi-runner-scale-set/.terraform.lock.hcl b/examples/multi-runner-scale-set/.terraform.lock.hcl deleted file mode 100644 index c96d2b19bf..0000000000 --- a/examples/multi-runner-scale-set/.terraform.lock.hcl +++ /dev/null @@ -1,93 +0,0 @@ -# This file is maintained automatically by "terraform init". -# Manual edits may be lost in future updates. - -provider "registry.terraform.io/hashicorp/aws" { - version = "6.63.0" - constraints = ">= 5.0.0, >= 6.21.0, >= 6.33.0" - hashes = [ - "h1:9cre7jh1lSs/9igpgAcENMUAUlYW3HCtkav3up4oit0=", - "h1:dRlYHkc+r6fgzF57WC7Zjcmb6sF/6TTGDEgwGK+LAZY=", - "zh:005d56736afd17d963998c405cee6f434dbc23a415109f9435ff1542879ae611", - "zh:026ef126321a86ad7080b5d858e2527f96f5289678cbcd8856296e229c43339d", - "zh:06e0b58b2d1eddb5137fc86bee7ad2d07953c0bc3f57cccfc5ae0d2456068a3a", - "zh:07221735d61ababed84734e5ffcfc5bd59d01f29f029166ba5f2175895dceed1", - "zh:1a72db00583112bdb8c19b213a78a3f5de754fffc08f07e061f4e326289fab7d", - "zh:32968e74a53b03e97a084dc7050c22ef661fb5b3ea8a44f5a63e47bc45ad0e7c", - "zh:4b357dfe4b820e3e4acd2881cff8288b2186491e63416751f0d12692ba478ceb", - "zh:81e30884d7de686265e7d87bb92527e802878c65a378470ede2a1e9f4e40ccc9", - "zh:82e137297f6a5a08b9ce2138f7aabea245ad99495d9d9eff502f752d6ca90dbd", - "zh:8eb83b67099f0ea9df238a979dff933ff50ce06a2e3ff05a48556a10f10dd204", - "zh:9b12af85486a96aedd8d7984b0ff811a4b42e3d88dad1a3fb4c0b580d04fa425", - "zh:d0ba30886cbe41850fee689f51ef9088578f323cfd21817bb409951d43c465eb", - "zh:dd48e7089784454bc03d713e9057f5ca0ea1613bd402125054a51894957b7925", - "zh:f250fa81e54cf60fcb0e9c0fc4ac043f1ecc2ac24967f628b3609364fcab3d04", - "zh:f38fc09fc25a8d2cf89a4d4cd6a5ef7cb1aad72798dbdcad58b8876b6a551a54", - "zh:f7c7380fdf126e1901f2084588dbfd724c76cb131ccfa795a541219111103c06", - ] -} - -provider "registry.terraform.io/hashicorp/local" { - version = "2.9.0" - constraints = "~> 2.0" - hashes = [ - "h1:9rBZCMNpxKwMlRbWH2QpwD3kqUCAejdOZQ/aiiDObXQ=", - "h1:m24fjcInWvTVZ1XSo2MaNuKPe+X/gfG8SIi09rA7a7M=", - "zh:0baa4566cf77f1ff52f4293d1c8536202dd23edc197c3196413a28343c3ac3a0", - "zh:16b5559c3c07088ddad11a9bb9e9c0799999363c2958e9a5be2bcbbf2cd9ca64", - "zh:197c79015a10d1cce904a8ea722cbc750c42aeae2da53f44a6a0751d9fd1aa90", - "zh:29d0b03e5343a80677ebfeb2e2c31cbe4b1f65e736e53417454a4277fec2544c", - "zh:4896bfa6cf1d2fd562b47ef2e87f47862ae92a04f8ad5d764380f0c6653473b8", - "zh:531f8529cbca49f681883e57761a05a8398afaef6d1ab0d205d26bf12f4428e8", - "zh:6aaf5011d83161c86d2bfb80c0923ec934e578288758da2f37acb7aec129004b", - "zh:7430275253d3d3c40aa6179e0ec0d63212874dbbc06c5a51b9d07ec590f9756c", - "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3", - "zh:be17dc611e95e26cdf6cad79dfccf1064f0e32032a2efeb939a9bbe7fb1cbfe9", - "zh:f0e3b0aa644202e1d79d2000dca91f6019425da71e9800fa23f27e51c034f195", - "zh:f62bae4519e4ead49182ddc8afe8cf61e2a4c3ba3973b0fbba967736a2696aa3", - "zh:fcafa360a5b0b96244f26f4e3a6d642b716a376557142c2442ff2fb12d11da18", - ] -} - -provider "registry.terraform.io/hashicorp/null" { - version = "3.3.1" - constraints = "~> 3.0, ~> 3.2" - hashes = [ - "h1:TuxJq10DVnRP7c5HBZPyyvQGcckNVfijyU1eXEu5e4M=", - "h1:m5FqidbIgh+E9OigiZh8/xbkvpUQFSj3hZo/jqNLCLQ=", - "zh:08c59776542ea16e5a8545752787b17ff412922182b4cfabe16139197be8ac44", - "zh:123109cc7e5ed6d515787fbc212f2a3fd5e75647bb24ab7c801ccd4d4ed42451", - "zh:14b3fa4372754b54844b41d5dbd4671a292d8d6828b90169061feb4d7b15dd05", - "zh:56a4daaa3212f57b764bf3d1f333141c6610c5f21abb240e0111221f7c7fa4d4", - "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3", - "zh:7e888a026dbacd2474a42264227ae35f639780f0f0c613529d10a95cd61988b3", - "zh:85a53646267e87d600df7124e4767ffde9bba3b6356d45d961618bdd68131cc7", - "zh:8ffa0e9c7c39b2ab0905b472465d6e35ef0b776b3f6273bb34c150340b61bff1", - "zh:9846510a1841530d4403f4818e233f91e3b3bade7441047599fbf800742f65be", - "zh:afa98d44860875f037c6def0a7e6ff208e042712ba771f620482b143cd336891", - "zh:bdca130d9ef27488ae0b13bc8fd8019e8bbdd4f2ceff29da066bd333165d68c5", - "zh:cb3b94cbca88210dd0d1f11e2b8a89333f48c3857faf8f70f589072ce7c28610", - "zh:f0c0ba87925fe32f84b80f7513b1efb1b0866f51f899ba825e95ad59ff09b018", - ] -} - -provider "registry.terraform.io/hashicorp/random" { - version = "3.9.0" - constraints = "~> 3.0" - hashes = [ - "h1:OO+IuvQJSPmWdN8AyyIEvPJbLvDQpgX/zbktoa9KsJE=", - "h1:UlBuNVuCGJ39tTv2c5gz2NRZnQbXfbIWbTzWcth5o74=", - "zh:161ad0bd9a75768c82f53fb6e7172a9d8be2d4889b012645a34795031aaf1bf1", - "zh:19dc9a5b17729725ccfc4f45b0500af0ee5bc6b6b160c7adb8f2bf617d2c80ea", - "zh:269eda8fe42daa7974d5a34d166c3ba9defe80cde86c01e4dadcfdf2e1f05e5f", - "zh:373f7c65566f8f2cc7f45d698654feb9d988996957e1266a69ca00c52d6d16d0", - "zh:5599d16804c41c83009ec621b6d6b6f74e102f5827678a4750f8809055546b61", - "zh:583be0440469a22bff70dcfa56593b01566860b29607437264adb51060cf46fc", - "zh:5f211d8ec3f2e1f414870d9584bfe26e6995560ef81c748f8447a48164767398", - "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3", - "zh:7b547fd16216761ef86efc3ed516ac5ac0c5c42b7c7eb24a08cef2d93f69ed5e", - "zh:7e7c0679daf2a382151d05068c8c3f0dae6b7b7dccf818827b73dd08638df2ef", - "zh:8089dec888a8038b9b4fb23b3df7e1057293dbc5b60b42cc47ff690d69d4b61b", - "zh:c51f15a031edfd6f23ce8ced3446ca7f8d8d647e2499890d7d5d10d5016d7257", - "zh:c94784f005708890dc6895afd53636ec00ec1e430b15d41e5aebfb1d4b39bd04", - ] -} diff --git a/examples/multi-runner-scale-set/.terraform.lock.hcl.tofu b/examples/multi-runner-scale-set/.terraform.lock.hcl.tofu deleted file mode 100644 index 577519c08d..0000000000 --- a/examples/multi-runner-scale-set/.terraform.lock.hcl.tofu +++ /dev/null @@ -1,150 +0,0 @@ -# This file is maintained automatically by "tofu init". -# Manual edits may be lost in future updates. - -provider "registry.opentofu.org/hashicorp/aws" { - version = "6.63.0" - constraints = ">= 5.0.0, >= 6.21.0, >= 6.33.0" - hashes = [ - "h1:1jhQJPHOPu2mzDG/ke3tK8PNcEqQHA4vhF05WWlM/yg=", - "h1:3+pvT0KN/bkJ6TBuExj+gxptEozhnpo80Ztblwq85eo=", - "h1:5aTequ87wZS7Mh4dEIayDGKcFdaFgHtw74NtqY5Idi0=", - "h1:AMRlrrM3z1SmrslOtotqKq02zapxLKtXaSN9Jbs0Oho=", - "h1:OTjECFWTDxsjcUfOKCNBp75Z5lGrW/KplRDsjTZYT2g=", - "h1:b8LORLOKMOOl+nK1M2UhCjELSjjziClJuAv6hYuySHs=", - "h1:bUfTX1giRLOyfDbBvsDbwR3tJmsTFRWcOTQdj2npDWA=", - "h1:dzs4kwx+itVGAH7yEOyeoWcE3LNRMnWtlt4ROgyAa0M=", - "h1:lnjou+SiwpYJ+j9PXWozXPHSPlhxIZb0RqpsSEBzfGw=", - "h1:pqzUeHAQj9NctgkwaynaF2aB+3QiZXcoslzMGjT743w=", - "h1:qTXEWOWxA6sfUpC29UXrsbHnNzWH7+j1RTUVG4YCm+U=", - "h1:qdHKOKt/ISn9RLjUe22OZBpN3F7H2DFeHJL/CSc2x8E=", - "h1:tpNzIZBzzUW7/kLU3BhYf3jhdO5uNwYfNmgC9B8kvMM=", - "h1:uVVlFgjg6GyxJLbCsTO1+R5fTNbZ73mLpVpSd0mMrFk=", - "h1:xGJsV5IFf7c11cXzJrsY40hiJCghp4odT0eJyTyAUYY=", - "zh:039a03e920e55f14a691feb67216a2d142bfee603128e15f9c5138f9ecd85016", - "zh:14e060b7f46ca7b0fa009b91aef419c58cbdff854de96e9a1d853166f8d902fd", - "zh:18803e8fe2c291c8db5526c71b3287ff7c81453f10ca6d8e69cdf9c535b00783", - "zh:1b83fce6e31a6095e932d80a7c3f47ac04252653a2de2b98ec6204563310fcba", - "zh:2add7bc976ceebb1a94d84598762c9b9cf281ca52ec83deeb4e95e90aa200a12", - "zh:2f22cd5372408f11937fa5513a7b960d3cebc334c5ec65fc5322c3bac1c1f664", - "zh:41c5e857dacfd83b7ca12a435204957ff6ca8830b9efefd0d381ad4d63b19779", - "zh:4eace6246e46999782d219bc4f50f83d19ef9156bacf5ca1528da12da4918015", - "zh:5e1c1281c3f929399e2ed3dbdce03426fd57a9ec55cd36e04acf1712aa5954ba", - "zh:608272b1f5d75ead123c9d933aa1fed7dc832cedd1506019046b4c8fdcc91dce", - "zh:6b3680f8a2f7be2c171953aba89d639fb2624b9cf52ec304e16434874566601d", - "zh:99aa1006f2141f3341a02020e1c91abfb02280e57c77e0415c98b8d900353d88", - "zh:9ad235bef34a89a8dd9943f9fa9f05cc729bb52a4e0dc926a31bb13cb0ae2418", - "zh:e0e3ac361e04748a4ca0c1cdbb6abab2aa817f4ad67e1692817d16e370161d59", - "zh:f60962c982a41fde956e796425e7194b4311741c179c060c1c8b5e16a557d635", - ] -} - -provider "registry.opentofu.org/hashicorp/local" { - version = "2.9.0" - constraints = "~> 2.0" - hashes = [ - "h1:1dtKYW/5a1qob3yneL6WzOlnSGfYtJ6a2XeejCk9yb4=", - "h1:5NseXq5wU8O20ersTtV4ocrLYFFtgFr7n0pRLO1W2Rw=", - "h1:5d22ZPPK4iiygPbwRz/PJF5Es/0axVpMlPRpCR0Padw=", - "h1:AnwyolirmIlBMjH6+tV8bKkvT+5axJNYxi2y2IguiX4=", - "h1:PBp+HeseY021Fw3sLznCG27idgwPoff4cBuNmKgPL2w=", - "h1:VDxIhe4GbzdOCdmt7mQaqdwERQW6GSI7Roonts42Gr0=", - "h1:ZO6eWWnf8LjjV1q/JNeL9WLtZ6fwIttOnyN5LjCNSEo=", - "h1:dPIAf8oUAz+vW2E0iZunMvpuPddRZIztRsPSY1u+VnY=", - "h1:fwTDVG9AhFVKQZIb1EXkHv4FqzsZNlLWgkyPGDmZZEE=", - "h1:kDc465XPC7/6XFCjrMC4mTqhA9ef0FHKuJ3ZgfGNfeg=", - "h1:kGbjxrI2P8MHeyVtE1U3Q1TbyF71ExnHxtkrE+Aj6UU=", - "h1:kcoK6Afbsj54u9zaEqpecWAFKytqjBijtguCNwV3d4M=", - "h1:rxomJjDwOo+YZ+WIPc25FqEgsz9orh/2MCyUcZmFjvw=", - "h1:t0CMn/Rkwquw8l2yQ+O4ApzbMZfY2UazbsDnZygzACA=", - "h1:tJwgm2BS4xCGlElCDQEFXQoefY9Y4t0JdSKTtsPBbBo=", - "zh:13ef7ecd1e397ec5b20ea588508dd3e3b8d6c50d809ae76b079abf9dd8d02e4b", - "zh:2190c9325980076489ce02b0f5dd2c0b91fc8711cefa99e714d8619a32827ad1", - "zh:2a0cfc5600730093705071707e4a4e4e953e7d9091859e0f66b46daa1060dd5d", - "zh:2ff53eac1af43ab9a2248a0e53c963d46e19cf04bc4c3f323591cfcebb218252", - "zh:4ebc3dee700f60af9da29970052fd02fa947813162b224716862dc9d7f1f7542", - "zh:5fe6dab84ceeaa8eb3f1567c5f05578333370c472240ca5c5bfc25e92d4d5586", - "zh:66bbec16367bbf440045502c9779b11f4ac5b022c8d8d17afe12d431950838b5", - "zh:7641e5c2e4b529e869cde29ab5b1de2fd1091489eb745b19ac2709bd7f4dfd84", - "zh:855bfba0756d17ce07595ff57d7cf664443d1495127cb88fb063362734b8b22a", - "zh:aaec10f237921d60c581d1b7a66f0a8a8019d9802dc04af11b5b981f6682e01d", - "zh:e460835a38ffa1e74f6929904bfd14ef473d217fd537b7ce834abe5ce5e2ce07", - "zh:ecc4295215db0e4aea3c9329611c31e09a853e1ae207d56742403bd4f5516703", - "zh:ee6d9fae63a612072e00402894e14826af7a3351c235b9c5b423b7629a77ca29", - "zh:f2b5c8db74aa7ebcf7cd423672358437d42401675069ef67b01ff910054e49d5", - "zh:f5aff74d3eb96d4592c7bca5cd3ea89b469e84efbf382944bd0f844a57059c09", - ] -} - -provider "registry.opentofu.org/hashicorp/null" { - version = "3.3.1" - constraints = "~> 3.0, ~> 3.2" - hashes = [ - "h1:2wld81FnmHW0WVgy081sIfokCr2+NuatS8yjeLEet7Y=", - "h1:AClQjJ6X22V4qcRgcYSxiXCMmp2pz0G8WVQC7wAx66o=", - "h1:AY3XQbuviNd2X5VhHYEbhNta1m/CG3JD2BKFKhCt1Y4=", - "h1:CUOZUd7H11lsU+4tISlnYIiP5BqnX8IDwFCVqfLJyAg=", - "h1:JIfV0nA/pLWnIFGscvTfuavQCn2NeHxJBeb6UUg/joA=", - "h1:RejAh+nyCwqDGExGln2Kb4Ro5LyHak0eJe0P9g8CHPc=", - "h1:SHOuTZjYymsmy4asuRq6NC3yW+zdVZOOt4f5nrb+EPM=", - "h1:WwPat/gT4gO8GvvKNdSkkXWVD65JppLJfqKOt9HhOqQ=", - "h1:Z3hXVLrOyaRiiLmmL5UCOdcRMguwjN1x5TYNdmBDgls=", - "h1:dd78Ad5HdfPzPts7A9qIxfitXhAriV/qza38fr2ukjk=", - "h1:dyVb++KwDdybzLTE6bf7GZiVQ31iWsgKPWmhTQ8G42k=", - "h1:gD8ZH6WWe+5gg5+y8SpLWGPUDzSxcQ3HKP8IDM/wW3I=", - "h1:juXCww0zRQKFTDZoKqYR0+Sn1lu99oeL6pr0Jh6LWx0=", - "h1:kFAySmtsshyNV7IhIrEdASzVcvwy68eeZCVC66P7yNk=", - "h1:nS5azDopRisB2NInwDx3Hrfg2FdVt8Gw0gTQzC0rd70=", - "zh:164eb061d84e01759f391265865fb31828083d0a06b25f7af7e094cbdb18c799", - "zh:1bb9b669a82b52c0cba2860c71e9ee6699ef302f28cb8ed06f572d39bc6c7c4f", - "zh:1ea9b31a8f29302122c1e8d673693f3ac270336dae560af803cd1117265a469a", - "zh:238bd463cb0154fb935dc331da40c0a9cbe5db9cee615ae5f35ccad5eed7dc41", - "zh:30ef2b7384cf7e20f33fe75754b54cf669d59816f3ad4fc73bfb2b26fb6735e9", - "zh:35b5cded16e4b57c207d03ee0979b14baf486fa520e6edb7a2eecf18f1b85471", - "zh:3dc840d13a50cd215c7540573f27e2b61f739ba90aee5b7c3846079aa0ab5534", - "zh:3f9309a18db608f975d5691fcb47a6e14d77199156a52e9c39dcafe3737f2b07", - "zh:44263a219f7dbd1848b545d080110b4f7d0495e77b71cd3c7a0b5ec52a09accb", - "zh:4dec54aa5f445eeea035bbd4839bcded5e47ecd07cba0e70c5a09e9272cb592f", - "zh:5e8fb319d7c6d6c4566a18b9d0c91580b4901a96acd7fdc476bfc79f074368e2", - "zh:b0e8b6d41834b57fcfbb5ca00da52ccb757e1a95b6a2d546c0dae8bfbeca1cdf", - "zh:bbde4c3a1dcc1718027a61a4cdf661619d17af1b58df1038fe27bcf43c3dc29b", - "zh:c4140fff9f692baf29236557f706f9515f93229413438527d764023a82301da3", - "zh:f8e9d83184e4bbeb97c6f0d569833007c48ba5a7ff334def201df4991d03a962", - ] -} - -provider "registry.opentofu.org/hashicorp/random" { - version = "3.9.0" - constraints = "~> 3.0" - hashes = [ - "h1:8EQU5KSxezcjo/phRSe69rDOI0lk4pSaggj7FsskYp8=", - "h1:Lw9im2VBBJQ3RyAbHPQ0rcvcmmcZWm3x+kIOpN+Tv9s=", - "h1:U8KXqGCoNI9/guYbTvzgdtVk3fRthoG0UXwm1JoEpIs=", - "h1:YXaVd4p6qXPPVaxIBaIDNXmBwT02ZqDn0qD+tYpw8sA=", - "h1:cOpc03fphEt/G9Rfc4jLL/fW0D7tgvlXqiDKPF4vuww=", - "h1:g09RR7T1xWkeGrZwWvWMT9ncJrFGr1k3CBD585UmO7w=", - "h1:gGDdPPibmw2EWROx+sh1RGLjR5+nPwZyrf6/N9jXfeM=", - "h1:haE7/nXCOhXKP4oXeEnER3t5CaVQWqujz4nBnpeTUv4=", - "h1:ieSVpfZS2lKuMr05ph0QsOVpCzg7uk3cgKBaXR+Ikug=", - "h1:ig2s1IS9IzehorRjvVAnKIsUUj8fkgyxct1L/kswcc4=", - "h1:j3lS+ZEERFnoab8t1ppDrScGVP/cgWbzlCrEYKTCXYw=", - "h1:lxezrKmOiQIySHAM+os8qLVq7hqufDr8h3Hpzvsk+78=", - "h1:lzRqBJAG+NETxHbEZUJ/YP3RMEjZBinTX7VmgH3lw60=", - "h1:tdSNWK5ApqUsgbdYieyeYLTu6nIZUV3hR1oFqUfAuGo=", - "h1:xedet8yH/zI2CfdxsGlK0nlFWc/Bp61yrWsEa3fHB8g=", - "zh:03f1114cc20b8913523735ab76e0f0a2b16ce13c92923a53304bf85f07fc0dbc", - "zh:105b678ee72322a3067f105d7e05e940f6143238f377f6e87ff4ec909246ac2a", - "zh:55f3bbf13ea18cbace61a706566a80f25f33fe2b1780b6f3d7b582af2a05b6d2", - "zh:63adf996db48f082f7a6351eb485e219cd88795fc71e6ec60a837263ab0d2cb1", - "zh:7e99550738a4e3cc68b8a467714b0d69371025fe95e3326d5323d026d55653e9", - "zh:8342b54af3a18a37e075eeae61be57f4de2ba71b35d95c5075d402dd2c1f289d", - "zh:83ee18e32ac9dd5fc91298554b7c4cfa4c3a1db50f4c797945637cc93c0844ae", - "zh:993ecc0adbf6bd535a59fbc9b735d8c33950e6f6eb5e621d750da9b71d65d80a", - "zh:ad722bc59d4edbf1415e827fc007c0efe6e0e9462d5568bae20b34be1058a261", - "zh:ae9448e1f87b2f9a6c5197a0e9862162ec6b137cb3a3835e11522995d8939e7c", - "zh:bc9cdd3aac784f759125c6627f6f6416e8726a1c184eb9cf3e55b9edbc94c627", - "zh:c8e35b89572ba1c40a9b20022e033a3395fb8d42e7604d50c900f193ba10382e", - "zh:e2deaa8a9975ef81d9f62baed12c41286918b0a10908e0e031f13f69a3b730a1", - "zh:ee39707557210a0ab1098aa357d2cdfe502e5a312d0dbdffb09d08facc4d3fc5", - "zh:f81afe4eb63e8aa9e0ea71be6c990f0dc69cb360e7191c0742a991f4a5081b64", - ] -} diff --git a/examples/multi-runner-scale-set/README.md b/examples/multi-runner-scale-set/README.md deleted file mode 100644 index a2ca69cff4..0000000000 --- a/examples/multi-runner-scale-set/README.md +++ /dev/null @@ -1,86 +0,0 @@ -# Multi-runner scale-set example - -This example demonstrates the experimental multi-runner v2 interface. Shared -defaults are configured with `global_config*` variables, while -each runner lane uses `multi_runner_config` for its matcher, -runner lifecycle, and compute-provider settings. - -The example creates four lanes from one deployment: - -- Linux ARM64 Amazon Linux runners. -- Ephemeral Linux x64 Amazon Linux runners with job retry enabled. -- Linux x64 runners managed by a GitHub Actions scale set. -- Windows x64 Server Core 2022 runners. - -The v2 interface keeps provider-owned settings inside the selected provider -configuration. For example, VPC and subnet settings are under -`global_config_compute_provider.aws.ec2`, while the per-lane -instance types and AMI filter are under each lane's compute provider block. - -The scale-set lane uses `orchestration_provider.scale_set`. Its controller -network is configured under the global scale-set block and its GitHub -installation ID is provided by `var.github_app`. - -Configure the GitHub App variables before applying: - -```bash -terraform init -terraform apply \ - -var='github_app={id="123456",key_base64="...",installation_id="123456789"}' \ - -var='github={runner_owner="example",registration_level="organization"}' \ - -var='scale_set={name="linux-scale-set",container={image="ghcr.io/github-aws-runners/terraform-aws-github-runner-scale-set-service@sha256:"}}' -``` - -The `github_app` value is sensitive and should be supplied through a secure -variable source in real deployments rather than committed to configuration. -The GitHub App must be installed for the configured GitHub account. - - -## Requirements - -| Name | Version | -|------|---------| -| [terraform](#requirement\_terraform) | >= 1.5.6 | -| [aws](#requirement\_aws) | >= 6.33 | -| [local](#requirement\_local) | ~> 2.0 | -| [random](#requirement\_random) | ~> 3.0 | - -## Providers - -| Name | Version | -|------|---------| -| [random](#provider\_random) | 3.9.0 | - -## Modules - -| Name | Source | Version | -|------|--------|---------| -| [base](#module\_base) | ../base | n/a | -| [runners](#module\_runners) | ../../modules/multi-runner | n/a | -| [webhook\_github\_app](#module\_webhook\_github\_app) | ../../modules/webhook-github-app | n/a | - -## Resources - -| Name | Type | -|------|------| -| [random_id.random](https://registry.terraform.io/providers/hashicorp/random/latest/docs/resources/id) | resource | - -## Inputs - -| Name | Description | Type | Default | Required | -|------|-------------|------|---------|:--------:| -| [ami](#input\_ami) | Optional AMI configuration keyed by runner lane. |
map(object({
filter = optional(map(list(string)), { state = ["available"] })
owners = optional(list(string), ["amazon"])
id_ssm_parameter = optional(object({
arn = string
}), null)
kms_key = optional(object({
arn = string
}), null)
}))
| `{}` | no | -| [aws\_region](#input\_aws\_region) | AWS region to deploy to. | `string` | `"eu-west-1"` | no | -| [environment](#input\_environment) | Environment name, used as prefix. | `string` | n/a | yes | -| [github](#input\_github) | Optional GitHub endpoint and scale-set ownership settings. |
object({
url = optional(string, null)
ssl_verify = optional(bool, true)
runner_owner = optional(string, null)
registration_level = optional(string, "organization")
})
| `{}` | no | -| [github\_app](#input\_github\_app) | GitHub App ID, base64-encoded private key, and installation ID. |
object({
id = string
key_base64 = string
installation_id = optional(string, null)
})
| n/a | yes | -| [runner\_binaries\_enabled](#input\_runner\_binaries\_enabled) | Whether runner binary synchronization is enabled. | `bool` | `true` | no | -| [scale\_set](#input\_scale\_set) | GitHub Actions scale-set configuration. |
object({
name = string
runner_group_name = optional(string, "Default")
min_runners = optional(number, 0)
container = optional(object({
image = optional(string, null)
}), {})
})
| n/a | yes | - -## Outputs - -| Name | Description | -|------|-------------| -| [webhook\_endpoint](#output\_webhook\_endpoint) | n/a | -| [webhook\_secret](#output\_webhook\_secret) | n/a | - diff --git a/examples/multi-runner-scale-set/main.tf b/examples/multi-runner-scale-set/main.tf deleted file mode 100644 index 30d70a3a9b..0000000000 --- a/examples/multi-runner-scale-set/main.tf +++ /dev/null @@ -1,215 +0,0 @@ -locals { - environment = var.environment - aws_region = var.aws_region -} - -resource "random_id" "random" { - byte_length = 20 -} - -module "base" { - source = "../base" - - prefix = local.environment - aws_region = local.aws_region -} - -module "runners" { - source = "../../modules/multi-runner" - - prefix = local.environment - aws_region = local.aws_region - - experimental_features = ["multi-runner-v2"] - - global_config = { - tags = { - Example = local.environment - Project = "ProjectX" - } - runner = { - os = "linux" - architecture = "x64" - extra_labels = ["v2"] - } - } - - global_config_github = { - app = { - key_base64 = var.github_app.key_base64 - id = var.github_app.id - installation_id = var.github_app.installation_id - webhook_secret = random_id.random.hex - } - enterprise_server = { - url = var.github.url - ssl_verify = var.github.ssl_verify - } - runner_owner = var.github.runner_owner - runner_registration_level = var.github.registration_level - } - - global_config_lambda = { - architecture = "arm64" - } - - global_config_orchestration_provider = { - webhook = { - eventbridge = { - enabled = true - accept_events = ["workflow_job"] - } - } - scale_set = { - grouping = { - strategy = "runner_config" - } - container = var.scale_set.container - network = { - vpc_id = module.base.vpc.vpc_id - subnet_ids = module.base.vpc.private_subnets - } - } - } - - global_config_compute_provider = { - aws = { - ec2 = { - vpc_id = module.base.vpc.vpc_id - subnet_ids = module.base.vpc.private_subnets - ssm_enabled = true - runner_binaries = { - enabled = var.runner_binaries_enabled - } - } - } - } - - multi_runner_config = { - linux-arm64 = { - runner = { - architecture = "arm64" - name_prefix = "amazon-arm64-" - extra_labels = ["amazon"] - } - orchestration_provider = { - webhook = { - runner = { - maximum_count = 1 - } - matcherConfig = { - exactMatch = true - labelMatchers = [["self-hosted", "linux", "arm64", "amazon"]] - } - } - } - compute_provider = { - aws = { - ec2 = { - instance_types = ["t4g.large", "c6g.large"] - ami = lookup(var.ami, "linux-arm64", null) - } - } - } - } - - linux-x64 = { - runner = { - name_prefix = "amazon-x64-" - extra_labels = ["amazon"] - } - orchestration_provider = { - webhook = { - runner = { - ephemeral = true - maximum_count = 1 - } - matcherConfig = { - labelMatchers = [["self-hosted", "linux", "x64", "amazon"]] - exactMatch = false - priority = 1 - } - queue = { - delay_webhook_event = 0 - } - job_retry = { - enabled = true - } - } - } - compute_provider = { - aws = { - ec2 = { - instance_types = ["m5a.large", "m5ad.large"] - ami = lookup(var.ami, "linux-x64", null) - } - } - } - } - - linux-scale-set = { - runner = { - name_prefix = "scale-set-" - extra_labels = ["scale-set"] - group_name = var.scale_set.runner_group_name - } - orchestration_provider = { - scale_set = { - name = var.scale_set.name - runner = { - min_runners = var.scale_set.min_runners - max_runners = 10 - boot_time_in_minutes = 10 - } - } - } - compute_provider = { - aws = { - ec2 = { - instance_types = ["m5.large"] - ami = lookup(var.ami, "linux-scale-set", null) - } - } - } - } - - windows-x64 = { - runner = { - os = "windows" - name_prefix = "windows-x64-" - } - orchestration_provider = { - webhook = { - runner = { - boot_time_in_minutes = 20 - maximum_count = 1 - } - matcherConfig = { - exactMatch = true - labelMatchers = [["self-hosted", "windows", "x64", "servercore-2022"]] - } - } - } - compute_provider = { - aws = { - ec2 = { - instance_types = ["m5.large", "c5.large"] - ami = lookup(var.ami, "windows-x64", null) - } - } - } - } - } -} - -module "webhook_github_app" { - source = "../../modules/webhook-github-app" - depends_on = [module.runners] - - github_app = { - key_base64 = var.github_app.key_base64 - id = var.github_app.id - webhook_secret = random_id.random.hex - } - webhook_endpoint = module.runners.webhook.endpoint -} \ No newline at end of file diff --git a/examples/multi-runner-scale-set/providers.tf b/examples/multi-runner-scale-set/providers.tf deleted file mode 100644 index eca2fe96a7..0000000000 --- a/examples/multi-runner-scale-set/providers.tf +++ /dev/null @@ -1,9 +0,0 @@ -provider "aws" { - region = local.aws_region - - default_tags { - tags = { - Example = local.environment - } - } -} diff --git a/examples/multi-runner-scale-set/variables.tf b/examples/multi-runner-scale-set/variables.tf deleted file mode 100644 index f0f9f66c4d..0000000000 --- a/examples/multi-runner-scale-set/variables.tf +++ /dev/null @@ -1,72 +0,0 @@ -variable "github_app" { - description = "GitHub App ID, base64-encoded private key, and installation ID." - - type = object({ - id = string - key_base64 = string - installation_id = optional(string, null) - }) - sensitive = true -} - -variable "github" { - description = "Optional GitHub endpoint and scale-set ownership settings." - - type = object({ - url = optional(string, null) - ssl_verify = optional(bool, true) - runner_owner = optional(string, null) - registration_level = optional(string, "organization") - }) - - default = {} -} - -variable "scale_set" { - description = "GitHub Actions scale-set configuration." - - type = object({ - name = string - runner_group_name = optional(string, "Default") - min_runners = optional(number, 0) - container = optional(object({ - image = optional(string, null) - }), {}) - }) -} - -variable "environment" { - description = "Environment name, used as prefix." - - type = string -} - -variable "aws_region" { - description = "AWS region to deploy to." - - type = string - default = "eu-west-1" -} - -variable "runner_binaries_enabled" { - description = "Whether runner binary synchronization is enabled." - - type = bool - default = true -} - -variable "ami" { - description = "Optional AMI configuration keyed by runner lane." - - type = map(object({ - filter = optional(map(list(string)), { state = ["available"] }) - owners = optional(list(string), ["amazon"]) - id_ssm_parameter = optional(object({ - arn = string - }), null) - kms_key = optional(object({ - arn = string - }), null) - })) - default = {} -} \ No newline at end of file diff --git a/examples/multi-runner-v2/.terraform.lock.hcl b/examples/multi-runner-v2/.terraform.lock.hcl deleted file mode 100644 index 62c535d49e..0000000000 --- a/examples/multi-runner-v2/.terraform.lock.hcl +++ /dev/null @@ -1,93 +0,0 @@ -# This file is maintained automatically by "terraform init". -# Manual edits may be lost in future updates. - -provider "registry.terraform.io/hashicorp/aws" { - version = "6.64.0" - constraints = ">= 5.0.0, >= 6.21.0, >= 6.33.0" - hashes = [ - "h1:2fTLxzUDmp/KVIHbIeLTB4bIzWHx8E6Dw+1ALLUi+Yw=", - "h1:wXARLY+IeQ7ufYxCLTPCwToWGMRvOpiOTfJS97iwUzI=", - "zh:07172315d67bc9781240272759cdfc7bd32b7e72384a56862c2c1da3cca99a81", - "zh:154ce7d2659de9a59ddfe96d7cab41a9ddc2cb267a7d4bcdf4e737ff2ffdec06", - "zh:17324d4335a7a7ac01cc23eded530775606680ff53b47cb74a3cb95d1121f836", - "zh:307ab92324ec5a61b124881ab8cac1d9e316f4527dfd0e1b59794c229407eb4e", - "zh:31e25f1903661332e36a95283042dd3ec50b47c186db00663fbd976a11e6a6b2", - "zh:3311d9f3bd12a24886027dbe73859dcd1e67bd0e3046227a338cf2c7ca04d18e", - "zh:37916156a3aac3b29be3acebd15d53145ea4ab5d4aaa825eaebe75481fa00500", - "zh:4158cb8c38b3ac6aa98eb15935ec6bd7c30838d85d2b00acc9812df8382ae908", - "zh:5bfb9499c66d9db5b34dc5c60f426a1ab1baa5457ce2aefebca826a9c3f92fb0", - "zh:6eb29ead5a4aca3b1f35812e7e8c75419180e1928e479b458f206861277736db", - "zh:7a82b6dd0c0cdef8045a4adfbddd36acb86b6b23fcbed8e189c2d71f7dc4a502", - "zh:9556bd792032c3f7e73ea4dd08cec88dc1327f5a4a57d79c30ba844ae2b9a3c0", - "zh:9b12af85486a96aedd8d7984b0ff811a4b42e3d88dad1a3fb4c0b580d04fa425", - "zh:c5234180464cb800c83a41f57462742b802c150ad7d4417626fcd9cb511c01d2", - "zh:cd776b83b1f7b36635957350afe7ce28ba4e4ea3a5e2deb00d13dbd3b35d9d40", - "zh:fb583a7b791c6f915b86573d04f05ddbf7f1a5e4120c5d8a7450a3086c1225c4", - ] -} - -provider "registry.terraform.io/hashicorp/local" { - version = "2.9.0" - constraints = "~> 2.0" - hashes = [ - "h1:9rBZCMNpxKwMlRbWH2QpwD3kqUCAejdOZQ/aiiDObXQ=", - "h1:m24fjcInWvTVZ1XSo2MaNuKPe+X/gfG8SIi09rA7a7M=", - "zh:0baa4566cf77f1ff52f4293d1c8536202dd23edc197c3196413a28343c3ac3a0", - "zh:16b5559c3c07088ddad11a9bb9e9c0799999363c2958e9a5be2bcbbf2cd9ca64", - "zh:197c79015a10d1cce904a8ea722cbc750c42aeae2da53f44a6a0751d9fd1aa90", - "zh:29d0b03e5343a80677ebfeb2e2c31cbe4b1f65e736e53417454a4277fec2544c", - "zh:4896bfa6cf1d2fd562b47ef2e87f47862ae92a04f8ad5d764380f0c6653473b8", - "zh:531f8529cbca49f681883e57761a05a8398afaef6d1ab0d205d26bf12f4428e8", - "zh:6aaf5011d83161c86d2bfb80c0923ec934e578288758da2f37acb7aec129004b", - "zh:7430275253d3d3c40aa6179e0ec0d63212874dbbc06c5a51b9d07ec590f9756c", - "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3", - "zh:be17dc611e95e26cdf6cad79dfccf1064f0e32032a2efeb939a9bbe7fb1cbfe9", - "zh:f0e3b0aa644202e1d79d2000dca91f6019425da71e9800fa23f27e51c034f195", - "zh:f62bae4519e4ead49182ddc8afe8cf61e2a4c3ba3973b0fbba967736a2696aa3", - "zh:fcafa360a5b0b96244f26f4e3a6d642b716a376557142c2442ff2fb12d11da18", - ] -} - -provider "registry.terraform.io/hashicorp/null" { - version = "3.3.1" - constraints = "~> 3.0, ~> 3.2" - hashes = [ - "h1:TuxJq10DVnRP7c5HBZPyyvQGcckNVfijyU1eXEu5e4M=", - "h1:m5FqidbIgh+E9OigiZh8/xbkvpUQFSj3hZo/jqNLCLQ=", - "zh:08c59776542ea16e5a8545752787b17ff412922182b4cfabe16139197be8ac44", - "zh:123109cc7e5ed6d515787fbc212f2a3fd5e75647bb24ab7c801ccd4d4ed42451", - "zh:14b3fa4372754b54844b41d5dbd4671a292d8d6828b90169061feb4d7b15dd05", - "zh:56a4daaa3212f57b764bf3d1f333141c6610c5f21abb240e0111221f7c7fa4d4", - "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3", - "zh:7e888a026dbacd2474a42264227ae35f639780f0f0c613529d10a95cd61988b3", - "zh:85a53646267e87d600df7124e4767ffde9bba3b6356d45d961618bdd68131cc7", - "zh:8ffa0e9c7c39b2ab0905b472465d6e35ef0b776b3f6273bb34c150340b61bff1", - "zh:9846510a1841530d4403f4818e233f91e3b3bade7441047599fbf800742f65be", - "zh:afa98d44860875f037c6def0a7e6ff208e042712ba771f620482b143cd336891", - "zh:bdca130d9ef27488ae0b13bc8fd8019e8bbdd4f2ceff29da066bd333165d68c5", - "zh:cb3b94cbca88210dd0d1f11e2b8a89333f48c3857faf8f70f589072ce7c28610", - "zh:f0c0ba87925fe32f84b80f7513b1efb1b0866f51f899ba825e95ad59ff09b018", - ] -} - -provider "registry.terraform.io/hashicorp/random" { - version = "3.9.0" - constraints = "~> 3.0" - hashes = [ - "h1:OO+IuvQJSPmWdN8AyyIEvPJbLvDQpgX/zbktoa9KsJE=", - "h1:UlBuNVuCGJ39tTv2c5gz2NRZnQbXfbIWbTzWcth5o74=", - "zh:161ad0bd9a75768c82f53fb6e7172a9d8be2d4889b012645a34795031aaf1bf1", - "zh:19dc9a5b17729725ccfc4f45b0500af0ee5bc6b6b160c7adb8f2bf617d2c80ea", - "zh:269eda8fe42daa7974d5a34d166c3ba9defe80cde86c01e4dadcfdf2e1f05e5f", - "zh:373f7c65566f8f2cc7f45d698654feb9d988996957e1266a69ca00c52d6d16d0", - "zh:5599d16804c41c83009ec621b6d6b6f74e102f5827678a4750f8809055546b61", - "zh:583be0440469a22bff70dcfa56593b01566860b29607437264adb51060cf46fc", - "zh:5f211d8ec3f2e1f414870d9584bfe26e6995560ef81c748f8447a48164767398", - "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3", - "zh:7b547fd16216761ef86efc3ed516ac5ac0c5c42b7c7eb24a08cef2d93f69ed5e", - "zh:7e7c0679daf2a382151d05068c8c3f0dae6b7b7dccf818827b73dd08638df2ef", - "zh:8089dec888a8038b9b4fb23b3df7e1057293dbc5b60b42cc47ff690d69d4b61b", - "zh:c51f15a031edfd6f23ce8ced3446ca7f8d8d647e2499890d7d5d10d5016d7257", - "zh:c94784f005708890dc6895afd53636ec00ec1e430b15d41e5aebfb1d4b39bd04", - ] -} diff --git a/examples/multi-runner-v2/.terraform.lock.hcl.tofu b/examples/multi-runner-v2/.terraform.lock.hcl.tofu deleted file mode 100644 index e8fb9a5cf8..0000000000 --- a/examples/multi-runner-v2/.terraform.lock.hcl.tofu +++ /dev/null @@ -1,150 +0,0 @@ -# This file is maintained automatically by "tofu init". -# Manual edits may be lost in future updates. - -provider "registry.opentofu.org/hashicorp/aws" { - version = "6.64.0" - constraints = ">= 5.0.0, >= 6.21.0, >= 6.33.0" - hashes = [ - "h1:/G38+XhC1mBVkmeWdtk/wk7lX2BxviJ2XZ70dpoaKKQ=", - "h1:7BzHdGCBG5usqOIhfBq89dkdUopnSo+qe9qRCKDSRHc=", - "h1:8AgY9Hc5/R5j97WgCcDSlbuKk0pjk3vkp7oz4mtGVY8=", - "h1:DKdOy/0RfYLxpzAXBPWTO5Eusvqx5UoGxiq2J0E6DY4=", - "h1:KSwetpR4S2eUsKmHftt73Cbx72lPWYET4V+Ej05rnkI=", - "h1:MEi5Ecge1Uwx/DRGfdVDzV5Q/soRxKh6dBHxUjGdaDQ=", - "h1:VqjWicgPZW32+YnSe0Lo78qq8/24I8XNV+E9d/lBz/4=", - "h1:WBgbFHdg/3ekWoAH6UeKiwfk6iqLr1f7TX9R/mJUK8M=", - "h1:YisB3zMV5Kh6p5/eVuPAAPEmudD/UqGN4C/V3zRtAq4=", - "h1:bG5dXqR4mSlcebUG+anerOWYDyeaScZJeLSJk0cYBfE=", - "h1:iosW/imG2pc4La7qdeM/rK6ldMXhcU6YVW7tjqwNXtI=", - "h1:nKE1gnLZxIoqukQ1YI9EUdmrQIUeAN4PWb5ecN8U9K8=", - "h1:x0hJO5+On8FaKExr4p2cNJhWsNWFZq1EiDD6CfVwy2E=", - "h1:yPH75sRH+f3aJlJAloOL/BikeZV6/0GP8VQvnJoMRKM=", - "h1:zCWB5ZD98/ZC0a50HTGoC/fTAseh189xxCFEL5Mt7r4=", - "zh:06e09ced9480ae12578122f7a25758a15d8fe684da0f6a0a61b9bc2f4a4918ad", - "zh:2035805f0ed8bf81d493e7a52f22965b3d5d402687a95d1caa8c4b1b348c1264", - "zh:25fe72a3d6a330eab6c8957f9e6bdf297ffdce95fa059fef30b80da764bee6b2", - "zh:49df644d19e39b9947e84609260028687057191ddd941783c0211386ade53040", - "zh:4d8438a5d25f18eb376c8375c70f81afb79d0fc1e63ebb6df1d0e02287964dde", - "zh:5cd9717e819506132126a896e959cd4cf1bb213c033c37777c9d01a593937e2c", - "zh:6955caa4f435373ae870de31bdda85e51c60b68c51a4206df5a21b853bcefe21", - "zh:82a413500c35241745e097797610d2bff57c26e29f34ca711182fdde5c265d13", - "zh:831f78acce42a759a977769b0409387ec13ff64b4f46c24eb7e7662e0f352525", - "zh:88648a159119a0435bf86c6cd1f7482dc43dfa2eb742f9b29053da1ee9fdabd8", - "zh:9fc745d71a2e36dbdae0ee69be70675509e5a9dec1a3c5a9be6007e568d78c07", - "zh:bf6d11d6ed1655f61f70eb2906e5d1f7ff5e78b6539dcfb3116ed6f8960c9e20", - "zh:ca17a6ca363afe930ad3474966d39cb549b7f1e5efdca909972dd26f90eefc89", - "zh:d7e9cc87ada1314e6d8ecc5849385a8f8f45757bd2c145b8657f015c65e5078d", - "zh:eddb4d6d86700788d132ba2a83d306646ccb2a3a0cec0a0ab3e215307c61d8f2", - ] -} - -provider "registry.opentofu.org/hashicorp/local" { - version = "2.9.0" - constraints = "~> 2.0" - hashes = [ - "h1:1dtKYW/5a1qob3yneL6WzOlnSGfYtJ6a2XeejCk9yb4=", - "h1:5NseXq5wU8O20ersTtV4ocrLYFFtgFr7n0pRLO1W2Rw=", - "h1:5d22ZPPK4iiygPbwRz/PJF5Es/0axVpMlPRpCR0Padw=", - "h1:AnwyolirmIlBMjH6+tV8bKkvT+5axJNYxi2y2IguiX4=", - "h1:PBp+HeseY021Fw3sLznCG27idgwPoff4cBuNmKgPL2w=", - "h1:VDxIhe4GbzdOCdmt7mQaqdwERQW6GSI7Roonts42Gr0=", - "h1:ZO6eWWnf8LjjV1q/JNeL9WLtZ6fwIttOnyN5LjCNSEo=", - "h1:dPIAf8oUAz+vW2E0iZunMvpuPddRZIztRsPSY1u+VnY=", - "h1:fwTDVG9AhFVKQZIb1EXkHv4FqzsZNlLWgkyPGDmZZEE=", - "h1:kDc465XPC7/6XFCjrMC4mTqhA9ef0FHKuJ3ZgfGNfeg=", - "h1:kGbjxrI2P8MHeyVtE1U3Q1TbyF71ExnHxtkrE+Aj6UU=", - "h1:kcoK6Afbsj54u9zaEqpecWAFKytqjBijtguCNwV3d4M=", - "h1:rxomJjDwOo+YZ+WIPc25FqEgsz9orh/2MCyUcZmFjvw=", - "h1:t0CMn/Rkwquw8l2yQ+O4ApzbMZfY2UazbsDnZygzACA=", - "h1:tJwgm2BS4xCGlElCDQEFXQoefY9Y4t0JdSKTtsPBbBo=", - "zh:13ef7ecd1e397ec5b20ea588508dd3e3b8d6c50d809ae76b079abf9dd8d02e4b", - "zh:2190c9325980076489ce02b0f5dd2c0b91fc8711cefa99e714d8619a32827ad1", - "zh:2a0cfc5600730093705071707e4a4e4e953e7d9091859e0f66b46daa1060dd5d", - "zh:2ff53eac1af43ab9a2248a0e53c963d46e19cf04bc4c3f323591cfcebb218252", - "zh:4ebc3dee700f60af9da29970052fd02fa947813162b224716862dc9d7f1f7542", - "zh:5fe6dab84ceeaa8eb3f1567c5f05578333370c472240ca5c5bfc25e92d4d5586", - "zh:66bbec16367bbf440045502c9779b11f4ac5b022c8d8d17afe12d431950838b5", - "zh:7641e5c2e4b529e869cde29ab5b1de2fd1091489eb745b19ac2709bd7f4dfd84", - "zh:855bfba0756d17ce07595ff57d7cf664443d1495127cb88fb063362734b8b22a", - "zh:aaec10f237921d60c581d1b7a66f0a8a8019d9802dc04af11b5b981f6682e01d", - "zh:e460835a38ffa1e74f6929904bfd14ef473d217fd537b7ce834abe5ce5e2ce07", - "zh:ecc4295215db0e4aea3c9329611c31e09a853e1ae207d56742403bd4f5516703", - "zh:ee6d9fae63a612072e00402894e14826af7a3351c235b9c5b423b7629a77ca29", - "zh:f2b5c8db74aa7ebcf7cd423672358437d42401675069ef67b01ff910054e49d5", - "zh:f5aff74d3eb96d4592c7bca5cd3ea89b469e84efbf382944bd0f844a57059c09", - ] -} - -provider "registry.opentofu.org/hashicorp/null" { - version = "3.3.2" - constraints = "~> 3.0, ~> 3.2" - hashes = [ - "h1:1T+00cjQNmRAHAz9xjEBFpf5wRRb0IBuXS/W8ke5BWs=", - "h1:46gmIYe+klib6TlHKSqEkMLjvnzVWiCB2NYA2zR8MX8=", - "h1:7WQ3wjfaeqnXxq+a8cYiYeWUnMTgY1JcuX+z7sZd72s=", - "h1:MVM+vkVtW/YyKfn111pyho0y87I4TekaNMbBLkn0/C8=", - "h1:QBcIbI2Dp4v6Iui37pn4qmw8YeiFLbSWcJuzZVl/65Y=", - "h1:SsVKTUR+vgLaC1YnoDa2fnYpzREcgNgWRcu5x+vwjHA=", - "h1:WUaeuTNn9w6UXZ9cMq4+qZy4ZAr71B9NcUDEXjqfdKs=", - "h1:WtEaA7alasNwEQ4L3+KyQtbkSOPsexzJ4LUZ7PKaycI=", - "h1:WxS7rjYIZ1WQc4GkICch8XrbxoSY8TjUfLbPDo6oEcQ=", - "h1:Ysvc/FPvcwk+iMg7IcLkqZhT/KhtZTYji+UBqMlcTs4=", - "h1:ZLjbXnfVcRvS/DAN3BcNebOsnOcs3Nx6mJpFCj4dZ2c=", - "h1:fAmvQjIGyqdGMc+v/fUEINCyuU4iaKSzsV8PWsOnmAc=", - "h1:jwkbEtf3S7W+Bl4soynNkUHFfK/4I/H74urHY758XVw=", - "h1:qY1sKzlxNTp/dqZR23bM4egmVMRlaQudLlBYraMt1pw=", - "h1:t8H1KNwJQwKE/GqpHeRxOWgMk0Yv35qbBTBzqB/rhr0=", - "zh:09e94b0b7dfc0c6450c247517b5410546039c758e513d89b588af6df70c3d57d", - "zh:0b72497b6fd79a2b04785b64890a565a8cc7b06ded95da05e6dab2f3b8a02d58", - "zh:1c0ee6f81f7bcdec8d568a145a450eb57a6f1cfe5e48943375d1af22ed54151e", - "zh:1c6899b475f035d352af1e7f33dc30beab8b8e3784f8cb55a2cc4a11997fbd66", - "zh:43e57a2a56e9874604501bdebe431bb573fb77d2c5f4d7598ab30727dc0e90ea", - "zh:49cf2f36298a5ac3ac8d80ceb87466e6a99d2c021005bcd9e3a79f2314fd0a13", - "zh:665be40d2c7f3d768b8f39a041371526d4b7b396b4c11e162a1886212da176c9", - "zh:6bb1583d88ddb38b1c6b4624e25ad414ddd8bf65b0dd9c074580847311f83924", - "zh:71d64453bdc795667e9841d7c90e3fef6ff157e0598d51dac4bb1e4583b85407", - "zh:73ac02bc3b680e1ea75aab24ec2a359c8f0a021f73d43b2feae0a899e75a93ad", - "zh:b2b777ee07b910e7345df85321fab6c9a25c33ecb56129758dda8fadaba09fe4", - "zh:bb984d52880749a49e509e3b243804869e1af40ee2d34322a30f5f340f8d8dbd", - "zh:e0724bc083527343b4a4099fd4f95511e49a0e113416cdba58a64446742b68b1", - "zh:e391c14e367cd64d986ddc8d81f2db76d49600ce0521720618ff1ecc0decde7f", - "zh:e95c1af8e8e9967d678cfa7c77ca229c863a68f7c9a85318bf08f26628afe338", - ] -} - -provider "registry.opentofu.org/hashicorp/random" { - version = "3.9.0" - constraints = "~> 3.0" - hashes = [ - "h1:8EQU5KSxezcjo/phRSe69rDOI0lk4pSaggj7FsskYp8=", - "h1:Lw9im2VBBJQ3RyAbHPQ0rcvcmmcZWm3x+kIOpN+Tv9s=", - "h1:U8KXqGCoNI9/guYbTvzgdtVk3fRthoG0UXwm1JoEpIs=", - "h1:YXaVd4p6qXPPVaxIBaIDNXmBwT02ZqDn0qD+tYpw8sA=", - "h1:cOpc03fphEt/G9Rfc4jLL/fW0D7tgvlXqiDKPF4vuww=", - "h1:g09RR7T1xWkeGrZwWvWMT9ncJrFGr1k3CBD585UmO7w=", - "h1:gGDdPPibmw2EWROx+sh1RGLjR5+nPwZyrf6/N9jXfeM=", - "h1:haE7/nXCOhXKP4oXeEnER3t5CaVQWqujz4nBnpeTUv4=", - "h1:ieSVpfZS2lKuMr05ph0QsOVpCzg7uk3cgKBaXR+Ikug=", - "h1:ig2s1IS9IzehorRjvVAnKIsUUj8fkgyxct1L/kswcc4=", - "h1:j3lS+ZEERFnoab8t1ppDrScGVP/cgWbzlCrEYKTCXYw=", - "h1:lxezrKmOiQIySHAM+os8qLVq7hqufDr8h3Hpzvsk+78=", - "h1:lzRqBJAG+NETxHbEZUJ/YP3RMEjZBinTX7VmgH3lw60=", - "h1:tdSNWK5ApqUsgbdYieyeYLTu6nIZUV3hR1oFqUfAuGo=", - "h1:xedet8yH/zI2CfdxsGlK0nlFWc/Bp61yrWsEa3fHB8g=", - "zh:03f1114cc20b8913523735ab76e0f0a2b16ce13c92923a53304bf85f07fc0dbc", - "zh:105b678ee72322a3067f105d7e05e940f6143238f377f6e87ff4ec909246ac2a", - "zh:55f3bbf13ea18cbace61a706566a80f25f33fe2b1780b6f3d7b582af2a05b6d2", - "zh:63adf996db48f082f7a6351eb485e219cd88795fc71e6ec60a837263ab0d2cb1", - "zh:7e99550738a4e3cc68b8a467714b0d69371025fe95e3326d5323d026d55653e9", - "zh:8342b54af3a18a37e075eeae61be57f4de2ba71b35d95c5075d402dd2c1f289d", - "zh:83ee18e32ac9dd5fc91298554b7c4cfa4c3a1db50f4c797945637cc93c0844ae", - "zh:993ecc0adbf6bd535a59fbc9b735d8c33950e6f6eb5e621d750da9b71d65d80a", - "zh:ad722bc59d4edbf1415e827fc007c0efe6e0e9462d5568bae20b34be1058a261", - "zh:ae9448e1f87b2f9a6c5197a0e9862162ec6b137cb3a3835e11522995d8939e7c", - "zh:bc9cdd3aac784f759125c6627f6f6416e8726a1c184eb9cf3e55b9edbc94c627", - "zh:c8e35b89572ba1c40a9b20022e033a3395fb8d42e7604d50c900f193ba10382e", - "zh:e2deaa8a9975ef81d9f62baed12c41286918b0a10908e0e031f13f69a3b730a1", - "zh:ee39707557210a0ab1098aa357d2cdfe502e5a312d0dbdffb09d08facc4d3fc5", - "zh:f81afe4eb63e8aa9e0ea71be6c990f0dc69cb360e7191c0742a991f4a5081b64", - ] -} diff --git a/examples/multi-runner-v2/README.md b/examples/multi-runner-v2/README.md deleted file mode 100644 index 2755c8fcf5..0000000000 --- a/examples/multi-runner-v2/README.md +++ /dev/null @@ -1,77 +0,0 @@ -# Multi-runner v2 example - -This example demonstrates the experimental multi-runner v2 interface. Shared -defaults are configured with `global_config*` variables, while -each runner lane uses `multi_runner_config` for its matcher, -runner lifecycle, and compute-provider settings. - -The example creates three lanes from one deployment: - -- Linux ARM64 Amazon Linux runners. -- Ephemeral Linux x64 Amazon Linux runners with job retry enabled. -- Windows x64 Server Core 2022 runners. - -The v2 interface keeps provider-owned settings inside the selected provider -configuration. For example, VPC and subnet settings are under -`global_config_compute_provider.aws.ec2`, while the per-lane -instance types and AMI configuration are under each lane's compute provider -block. The optional `ami` variable can provide per-lane AMI filters and owners, -which is useful for test environments with locally registered images. - -Configure the GitHub App variables before applying: - -```bash -terraform init -terraform apply \ - -var='github_app={id="123456",key_base64="..."}' -``` - -The `github_app` value is sensitive and should be supplied through a secure -variable source in real deployments rather than committed to configuration. - - -## Requirements - -| Name | Version | -|------|---------| -| [terraform](#requirement\_terraform) | >= 1.5.6 | -| [aws](#requirement\_aws) | >= 6.33 | -| [local](#requirement\_local) | ~> 2.0 | -| [random](#requirement\_random) | ~> 3.0 | - -## Providers - -| Name | Version | -|------|---------| -| [random](#provider\_random) | 3.9.0 | - -## Modules - -| Name | Source | Version | -|------|--------|---------| -| [base](#module\_base) | ../base | n/a | -| [runners](#module\_runners) | ../../modules/multi-runner | n/a | -| [webhook\_github\_app](#module\_webhook\_github\_app) | ../../modules/webhook-github-app | n/a | - -## Resources - -| Name | Type | -|------|------| -| [random_id.random](https://registry.terraform.io/providers/hashicorp/random/latest/docs/resources/id) | resource | - -## Inputs - -| Name | Description | Type | Default | Required | -|------|-------------|------|---------|:--------:| -| [ami](#input\_ami) | Optional AMI configuration keyed by runner lane. |
map(object({
filter = optional(map(list(string)), { state = ["available"] })
owners = optional(list(string), ["amazon"])
id_ssm_parameter = optional(object({
arn = string
}), null)
kms_key = optional(object({
arn = string
}), null)
}))
| `{}` | no | -| [aws\_region](#input\_aws\_region) | AWS region to deploy to. | `string` | `"eu-west-1"` | no | -| [environment](#input\_environment) | Environment name, used as prefix. | `string` | `null` | no | -| [github\_app](#input\_github\_app) | GitHub App ID and base64-encoded private key. |
object({
id = string
key_base64 = string
})
| n/a | yes | - -## Outputs - -| Name | Description | -|------|-------------| -| [webhook\_endpoint](#output\_webhook\_endpoint) | n/a | -| [webhook\_secret](#output\_webhook\_secret) | n/a | - diff --git a/examples/multi-runner-v2/main.tf b/examples/multi-runner-v2/main.tf deleted file mode 100644 index 4f63974f11..0000000000 --- a/examples/multi-runner-v2/main.tf +++ /dev/null @@ -1,180 +0,0 @@ -locals { - environment = var.environment != null ? var.environment : "multi-runner-v2" - aws_region = var.aws_region -} - -resource "random_id" "random" { - byte_length = 20 -} - -module "base" { - source = "../base" - - prefix = local.environment - aws_region = local.aws_region -} - -module "runners" { - source = "../../modules/multi-runner" - - prefix = local.environment - aws_region = local.aws_region - - experimental_features = ["multi-runner-v2"] - - global_config = { - tags = { - Example = local.environment - Project = "ProjectX" - } - runner = { - os = "linux" - architecture = "x64" - extra_labels = ["v2"] - } - } - - global_config_github = { - app = { - key_base64 = var.github_app.key_base64 - id = var.github_app.id - webhook_secret = random_id.random.hex - } - } - - global_config_lambda = { - architecture = "arm64" - } - - global_config_orchestration_provider = { - webhook = { - eventbridge = { - enabled = true - accept_events = ["workflow_job"] - } - } - } - - global_config_compute_provider = { - aws = { - ec2 = { - vpc_id = module.base.vpc.vpc_id - subnet_ids = module.base.vpc.private_subnets - ssm_enabled = true - runner_binaries = { - enabled = true - } - } - } - } - - multi_runner_config = { - linux-arm64 = { - runner = { - architecture = "arm64" - name_prefix = "amazon-arm64-" - extra_labels = ["amazon"] - } - orchestration_provider = { - webhook = { - runner = { - maximum_count = 1 - } - matcherConfig = { - exactMatch = true - labelMatchers = [["self-hosted", "linux", "arm64", "amazon"]] - } - } - } - compute_provider = { - aws = { - ec2 = { - instance_types = ["t4g.large", "c6g.large"] - ami = lookup(var.ami, "linux-arm64", null) - } - } - } - } - - linux-x64 = { - runner = { - name_prefix = "amazon-x64-" - extra_labels = ["amazon"] - } - orchestration_provider = { - webhook = { - runner = { - ephemeral = true - maximum_count = 1 - } - matcherConfig = { - labelMatchers = [["self-hosted", "linux", "x64", "amazon"]] - exactMatch = false - priority = 1 - } - queue = { - delay_webhook_event = 0 - } - job_retry = { - enabled = true - } - } - } - compute_provider = { - aws = { - ec2 = { - instance_types = ["m5a.large", "m5ad.large"] - ami = lookup(var.ami, "linux-x64", null) - } - } - } - } - - windows-x64 = { - runner = { - os = "windows" - name_prefix = "windows-x64-" - } - orchestration_provider = { - webhook = { - runner = { - boot_time_in_minutes = 20 - maximum_count = 1 - } - matcherConfig = { - exactMatch = true - labelMatchers = [["self-hosted", "windows", "x64", "servercore-2022"]] - } - } - } - compute_provider = { - aws = { - ec2 = { - instance_types = ["m5.large", "c5.large"] - ami = lookup(var.ami, "windows-x64", { - filter = { - name = ["Windows_Server-2022-English-Full-ECS_Optimized-*"] - state = ["available"] - } - owners = ["amazon"] - id_ssm_parameter = null - kms_key = null - }) - } - } - } - } - } -} - -module "webhook_github_app" { - source = "../../modules/webhook-github-app" - depends_on = [module.runners] - - github_app = { - key_base64 = var.github_app.key_base64 - id = var.github_app.id - webhook_secret = random_id.random.hex - } - webhook_endpoint = module.runners.webhook.endpoint -} diff --git a/examples/multi-runner-v2/outputs.tf b/examples/multi-runner-v2/outputs.tf deleted file mode 100644 index 1feaf2e671..0000000000 --- a/examples/multi-runner-v2/outputs.tf +++ /dev/null @@ -1,8 +0,0 @@ -output "webhook_endpoint" { - value = module.runners.webhook.endpoint -} - -output "webhook_secret" { - sensitive = true - value = random_id.random.hex -} diff --git a/examples/multi-runner-v2/providers.tf b/examples/multi-runner-v2/providers.tf deleted file mode 100644 index eca2fe96a7..0000000000 --- a/examples/multi-runner-v2/providers.tf +++ /dev/null @@ -1,9 +0,0 @@ -provider "aws" { - region = local.aws_region - - default_tags { - tags = { - Example = local.environment - } - } -} diff --git a/examples/multi-runner-v2/variables.tf b/examples/multi-runner-v2/variables.tf deleted file mode 100644 index fe104758b9..0000000000 --- a/examples/multi-runner-v2/variables.tf +++ /dev/null @@ -1,39 +0,0 @@ -variable "github_app" { - description = "GitHub App ID and base64-encoded private key." - - type = object({ - id = string - key_base64 = string - }) - sensitive = true -} - -variable "environment" { - description = "Environment name, used as prefix." - - type = string - default = null -} - -variable "aws_region" { - description = "AWS region to deploy to." - - type = string - default = "eu-west-1" -} - -variable "ami" { - description = "Optional AMI configuration keyed by runner lane." - - type = map(object({ - filter = optional(map(list(string)), { state = ["available"] }) - owners = optional(list(string), ["amazon"]) - id_ssm_parameter = optional(object({ - arn = string - }), null) - kms_key = optional(object({ - arn = string - }), null) - })) - default = {} -} diff --git a/examples/multi-runner-v2/versions.tf b/examples/multi-runner-v2/versions.tf deleted file mode 100644 index 6af69ab915..0000000000 --- a/examples/multi-runner-v2/versions.tf +++ /dev/null @@ -1,17 +0,0 @@ -terraform { - required_providers { - aws = { - source = "hashicorp/aws" - version = ">= 6.33" - } - local = { - source = "hashicorp/local" - version = "~> 2.0" - } - random = { - source = "hashicorp/random" - version = "~> 3.0" - } - } - required_version = ">= 1.5.6" -} diff --git a/tests/ministack/README.md b/tests/ministack/README.md index 1a00a7e32c..201cc0fda9 100644 --- a/tests/ministack/README.md +++ b/tests/ministack/README.md @@ -1,8 +1,7 @@ # MiniStack example tests The MiniStack workflow runs the `base`, `prebuilt`, `default`, `ephemeral`, -`multi-runner`, `multi-runner-v2`, `multi-runner-scale-set`, and -`termination-watcher` examples directly +`multi-runner`, and `termination-watcher` examples directly with Terraform 1.5.6 and the latest Terraform release, and with OpenTofu 1.11 and the latest OpenTofu release. The examples with input variables get their inputs from their own tfvars files @@ -11,7 +10,7 @@ and uses the configuration checked into the example itself. No override files, setup module, or Terraform fixture configuration is checked in. The helper creates and removes a temporary AMI override for `default` and `ephemeral`, temporary SSM parameters for `multi-runner`, and temporary AMI -fixtures for `multi-runner-v2` and `multi-runner-scale-set`. The migration test +fixtures as needed. The migration test uses its dedicated `run-migration-test.sh` lifecycle script. Start MiniStack, set the AWS endpoint and test credentials, then run: @@ -27,10 +26,6 @@ tests/ministack/run-example.sh apply ephemeral # or tests/ministack/run-example.sh apply multi-runner # or -tests/ministack/run-example.sh apply multi-runner-v2 -# or -tests/ministack/run-example.sh apply multi-runner-scale-set -# or tests/ministack/run-example.sh apply termination-watcher ``` @@ -39,59 +34,5 @@ ZIP fixtures in the paths expected by the modules when they are absent, and removes only the files it created. For `prebuilt`, it seeds AMI metadata through MiniStack's AWS-compatible EC2 API, then removes only the resources it created during cleanup. MiniStack v1.5.11 provides the EC2 image behavior needed by the -`default`, `ephemeral`, `multi-runner`, and `multi-runner-scale-set` examples, -so they are included in the same lifecycle matrix. - -## Webhook and runner lifecycle smoke test - -The smoke test covers two independent lifecycle chains. The webhook chain -sends signed `workflow_job` webhooks through the API Gateway endpoint and -verifies the asynchronous path through EventBridge, the dispatcher Lambda, SQS, -and the scale-up Lambda. It runs scale-up once without a dynamic label and once -with `ghr-ec2-instance-type:m5.large`, checking that the first launch uses a -configured default instance type and the second launch uses exactly `m5.large`. -The scale-up Lambda calls a pinned `mockserver/mockserver` container initialized -from `github-api-expectations.json`; the test uses MockServer's verification API -to confirm the expected GitHub API calls for both jobs. It also checks the -webhook, dispatcher, and scale-up Lambda log groups for each smoke job ID, then -confirms that both MiniStack EC2 runner instances are removed and terminated. - -The second, pool chain then invokes the pool Lambda with a pool size of one and verifies every -expected GitHub API route for pool reconciliation, including the installation, -token, runner-list, and registration-token calls, before confirming that it -creates a second EC2 runner. Installation lookup is mocked for configurations -that do not provide a stored installation ID, but is conditional and is not a -required assertion. The test also verifies the `ghr:Application`, -`ghr:created_by`, `ghr:Type`, and `ghr:Owner` tags used to discover managed -instances. MiniStack v1.5.10 propagates the Terraform launch-template tags to -instances, allowing the scale-down Lambda to discover and remove each runner. -The smoke test invokes scale-down for the webhook and pool-created runners and -verifies the GitHub API calls and EC2 termination. -The pool schedule is configured for a far-future date because the test invokes -the Lambda directly. - -Build the two real Lambda distributions, start MiniStack, and run: - -```sh -(cd lambdas && yarn install --frozen-lockfile) -(cd lambdas && yarn workspace @aws-github-runner/webhook dist) -(cd lambdas && yarn workspace @aws-github-runner/control-plane dist) -sh tests/ministack/run-smoke.sh -``` - -The smoke script generates a temporary RSA key and Terraform variables file, -starts the MockServer container on a temporary port, and removes all temporary -state during cleanup. In CI, the pinned MockServer setup action starts the -server and waits for readiness; the expectations are loaded after checkout. -MiniStack must be able to reach -`host.docker.internal`; -override the hostname with `MINISTACK_GITHUB_MOCK_HOST` when using a different -container runtime. When MiniStack is exposed on a non-default local port, use a -host address reachable from its container for `AWS_ENDPOINT_URL`, for example -`AWS_ENDPOINT_URL=http://:14568`, instead of `127.0.0.1`. - -The workflow also runs `run-scale-set-integration.sh`. It applies the -`multi-runner-scale-set` example and verifies the managed ECS controller, -Fargate task hardening, scale-set environment contract, and reconciler SSM -parameter through MiniStack's AWS-compatible APIs. It does not send webhook -events or exercise webhook scale-up, scale-down, or pool handlers. +`default`, `ephemeral`, and `multi-runner` examples, so they are included in the +same lifecycle matrix. diff --git a/tests/ministack/github-api-expectations.json b/tests/ministack/github-api-expectations.json deleted file mode 100644 index a85e727bd2..0000000000 --- a/tests/ministack/github-api-expectations.json +++ /dev/null @@ -1,77 +0,0 @@ -[ - { - "httpRequest": { - "method": "GET", - "path": "/api/v3/repos/test-owner/test-repo/actions/jobs/123456" - }, - "httpResponse": { - "statusCode": 200, - "headers": { - "Content-Type": ["application/json"], - "X-RateLimit-Limit": ["5000"], - "X-RateLimit-Remaining": ["4999"] - }, - "body": "{\"id\":123456,\"status\":\"queued\",\"name\":\"ministack-smoke\"}" - } - }, - { - "httpRequest": { - "method": "GET", - "path": "/api/v3/repos/test-owner/test-repo/actions/jobs/123457" - }, - "httpResponse": { - "statusCode": 200, - "headers": { - "Content-Type": ["application/json"], - "X-RateLimit-Limit": ["5000"], - "X-RateLimit-Remaining": ["4999"] - }, - "body": "{\"id\":123457,\"status\":\"queued\",\"name\":\"ministack-smoke-dynamic\"}" - } - }, - { - "httpRequest": { - "method": "POST", - "path": "/api/v3/app/installations/123/access_tokens" - }, - "httpResponse": { - "statusCode": 201, - "headers": { - "Content-Type": ["application/json"], - "X-RateLimit-Limit": ["5000"], - "X-RateLimit-Remaining": ["4999"] - }, - "body": "{\"token\":\"ministack-installation-token\",\"expires_at\":\"2099-01-01T00:00:00Z\"}" - } - }, - { - "httpRequest": { - "method": "GET", - "path": "/api/v3/orgs/test-owner/installation" - }, - "httpResponse": { - "statusCode": 200, - "headers": { - "Content-Type": ["application/json"], - "X-RateLimit-Limit": ["5000"], - "X-RateLimit-Remaining": ["4999"] - }, - "body": "{\"id\":123}" - } - }, - { - "httpRequest": { - "method": "POST", - "path": "/api/v3/orgs/test-owner/actions/runners/registration-token" - }, - "httpResponse": { - "statusCode": 201, - "headers": { - "Content-Type": ["application/json"], - "X-RateLimit-Limit": ["5000"], - "X-RateLimit-Remaining": ["4999"] - }, - "body": "{\"token\":\"ministack-registration-token\",\"expires_at\":\"2099-01-01T00:00:00Z\"}" - } - } -] diff --git a/tests/ministack/initializerJson.json b/tests/ministack/initializerJson.json deleted file mode 100644 index 443ddb6201..0000000000 --- a/tests/ministack/initializerJson.json +++ /dev/null @@ -1,138 +0,0 @@ -[ - { - "httpRequest": { - "method": "POST", - "path": "/api/v3/app/installations/456/access_tokens" - }, - "httpResponse": { - "statusCode": 201, - "headers": { "Content-Type": ["application/json"] }, - "body": "{\"token\":\"fake-installation-token\",\"expires_at\":\"2099-01-01T00:00:00Z\"}" - } - }, - { - "httpRequest": { - "method": "POST", - "path": "/api/v3/orgs/example/actions/runners/registration-token" - }, - "httpResponse": { - "statusCode": 201, - "headers": { "Content-Type": ["application/json"] }, - "body": "{\"token\":\"fake-registration-token\"}" - } - }, - { - "httpRequest": { - "method": "POST", - "path": "/api/v3/actions/runner-registration" - }, - "httpResponse": { - "statusCode": 200, - "headers": { "Content-Type": ["application/json"] }, - "body": "{\"url\":\"https://mockserver:1080/tenant/123\",\"token\":\"header.eyJleHAiOjQwNzA5MDg4MDB9.signature\"}" - } - }, - { - "httpRequest": { - "method": "GET", - "path": "/tenant/123/_apis/runtime/runnergroups/" - }, - "httpResponse": { - "statusCode": 200, - "headers": { "Content-Type": ["application/json"] }, - "body": "{\"count\":1,\"value\":[{\"id\":48,\"name\":\"experimental-euw1-sl-cicd-forge-emu\"}]}" - } - }, - { - "httpRequest": { - "method": "GET", - "path": "/tenant/123/_apis/runtime/runnerscalesets" - }, - "httpResponse": { - "statusCode": 200, - "headers": { "Content-Type": ["application/json"] }, - "body": "{\"count\":1,\"value\":[{\"id\":223,\"name\":\"medium\",\"runnerGroupId\":48,\"labels\":[{\"name\":\"medium\",\"type\":\"system\"}],\"runnerSetting\":{}}]}" - } - }, - { - "httpRequest": { - "method": "GET", - "path": "/tenant/123/_apis/runtime/runnerscalesets/223" - }, - "httpResponse": { - "statusCode": 200, - "headers": { "Content-Type": ["application/json"] }, - "body": "{\"id\":223,\"name\":\"medium\",\"runnerGroupId\":48,\"labels\":[{\"name\":\"medium\",\"type\":\"system\"}],\"runnerSetting\":{}}" - } - }, - { - "httpRequest": { - "method": "PATCH", - "path": "/tenant/123/_apis/runtime/runnerscalesets/223" - }, - "httpResponse": { - "statusCode": 200, - "headers": { "Content-Type": ["application/json"] }, - "body": "{\"id\":223,\"name\":\"medium\",\"runnerGroupId\":48,\"labels\":[{\"name\":\"medium\",\"type\":\"system\"},{\"name\":\"linux\",\"type\":\"user\"},{\"name\":\"scale-set\",\"type\":\"user\"},{\"name\":\"self-hosted\",\"type\":\"user\"},{\"name\":\"x64\",\"type\":\"user\"}],\"runnerSetting\":{}}" - } - }, - { - "httpRequest": { - "method": "POST", - "path": "/tenant/123/_apis/runtime/runnerscalesets/223/generatejitconfig" - }, - "httpResponseTemplate": { - "templateType": "MUSTACHE", - "template": "{\"statusCode\":200,\"headers\":{\"Content-Type\":[\"application/json\"]},\"body\":\"{\\\"runner\\\":{\\\"id\\\":321,\\\"name\\\":\\\"{{#jsonPath}}$.name{{/jsonPath}}{{jsonPathResult}}\\\",\\\"runnerScaleSetId\\\":223},\\\"encodedJITConfig\\\":\\\"test-only-jit-configuration\\\"}\"}" - } - }, - { - "httpRequest": { - "method": "POST", - "path": "/tenant/123/_apis/runtime/runnerscalesets/223/sessions" - }, - "httpResponse": { - "statusCode": 200, - "headers": { "Content-Type": ["application/json"] }, - "body": "{\"sessionId\":\"11111111-1111-1111-1111-111111111111\",\"ownerName\":\"local.medium\",\"runnerScaleSet\":{\"id\":223,\"name\":\"medium\",\"runnerGroupId\":48},\"messageQueueUrl\":\"https://mockserver:1080/messages?sessionId=11111111-1111-1111-1111-111111111111&api-version=6.0-preview\",\"messageQueueAccessToken\":\"fake-queue-token\",\"statistics\":{\"totalAvailableJobs\":0,\"totalAcquiredJobs\":0,\"totalAssignedJobs\":0,\"totalRunningJobs\":0,\"totalRegisteredRunners\":0,\"totalBusyRunners\":0,\"totalIdleRunners\":0}}" - } - }, - { - "httpRequest": { - "method": "GET", - "path": "/messages" - }, - "httpResponse": { - "statusCode": 202, - "delay": { "timeUnit": "MILLISECONDS", "value": 1000 } - } - }, - { - "httpRequest": { - "method": "GET", - "path": "/tenant/123/_apis/distributedtask/pools/0/agents" - }, - "httpResponseTemplate": { - "templateType": "MUSTACHE", - "template": "{\"statusCode\":200,\"headers\":{\"Content-Type\":[\"application/json\"]},\"body\":\"{\\\"count\\\":1,\\\"value\\\":[{\\\"id\\\":321,\\\"name\\\":\\\"{{ request.queryStringParameters.agentName.0 }}\\\",\\\"runnerScaleSetId\\\":223}]}\"}" - } - }, - { - "httpRequest": { - "method": "DELETE", - "path": "/tenant/123/_apis/distributedtask/pools/0/agents/321" - }, - "httpResponse": { - "statusCode": 204 - } - }, - { - "httpRequest": { - "method": "DELETE", - "path": "/tenant/123/_apis/runtime/runnerscalesets/223/sessions/11111111-1111-1111-1111-111111111111" - }, - "httpResponse": { - "statusCode": 204 - } - } -] diff --git a/tests/ministack/multi-runner-scale-set.tfvars b/tests/ministack/multi-runner-scale-set.tfvars deleted file mode 100644 index 7dd1a0ad39..0000000000 --- a/tests/ministack/multi-runner-scale-set.tfvars +++ /dev/null @@ -1,57 +0,0 @@ -environment = "ministack-scale-set" -aws_region = "eu-west-1" - -github = { - url = "https://mockserver:1080" - ssl_verify = false - runner_owner = "example" - registration_level = "organization" -} - -github_app = { - id = "123" - key_base64 = "ministack-invalid-key" - installation_id = "456" -} - -runner_binaries_enabled = false - -ami = { - "linux-arm64" = { - filter = { - name = ["ministack-scale-set-linux-arm64"] - state = ["available"] - } - owners = ["self"] - } - "linux-x64" = { - filter = { - name = ["ministack-scale-set-linux-x64"] - state = ["available"] - } - owners = ["self"] - } - "linux-scale-set" = { - filter = { - name = ["ministack-scale-set-linux-x64"] - state = ["available"] - } - owners = ["self"] - } - "windows-x64" = { - filter = { - name = ["ministack-scale-set-windows-x64"] - state = ["available"] - } - owners = ["self"] - } -} - -scale_set = { - name = "medium" - runner_group_name = "experimental-euw1-sl-cicd-forge-emu" - min_runners = 1 - container = { - image = "localhost:4566/scale-set-controller:smoke" - } -} diff --git a/tests/ministack/multi-runner-v2.tfvars b/tests/ministack/multi-runner-v2.tfvars deleted file mode 100644 index de54e291cf..0000000000 --- a/tests/ministack/multi-runner-v2.tfvars +++ /dev/null @@ -1,33 +0,0 @@ -environment = "ministack-v2" -aws_region = "eu-west-1" - -github_app = { - id = "0" - key_base64 = "ministack-invalid-key" -} - -runner_binaries_enabled = false - -ami = { - "linux-arm64" = { - filter = { - name = ["ministack-v2-linux-arm64"] - state = ["available"] - } - owners = ["self"] - } - "linux-x64" = { - filter = { - name = ["ministack-v2-linux-x64"] - state = ["available"] - } - owners = ["self"] - } - "windows-x64" = { - filter = { - name = ["ministack-v2-windows-x64"] - state = ["available"] - } - owners = ["self"] - } -} diff --git a/tests/ministack/run-scale-set-integration.sh b/tests/ministack/run-scale-set-integration.sh deleted file mode 100755 index 08e0d156de..0000000000 --- a/tests/ministack/run-scale-set-integration.sh +++ /dev/null @@ -1,600 +0,0 @@ -#!/bin/sh - -set -eu - -export AWS_ACCESS_KEY_ID="${AWS_ACCESS_KEY_ID:-000000000000}" -export AWS_SECRET_ACCESS_KEY="${AWS_SECRET_ACCESS_KEY:-test-only}" -export AWS_DEFAULT_REGION="${AWS_DEFAULT_REGION:-eu-west-1}" -export AWS_REGION="${AWS_REGION:-eu-west-1}" -export AWS_ENDPOINT_URL="${AWS_ENDPOINT_URL:-http://127.0.0.1:4566}" -export AWS_EC2_METADATA_DISABLED="${AWS_EC2_METADATA_DISABLED:-true}" - -script_dir=$(CDPATH='' cd -- "$(dirname -- "$0")" && pwd) -source_root=$(CDPATH='' cd -- "$script_dir/../.." && pwd) -example="multi-runner-scale-set" -base_tfvars="$script_dir/$example.tfvars" -tfvars_file=$(mktemp "${TMPDIR:-/tmp}/terraform-aws-github-runner-scale-set.XXXXXX") -app_key_file=$(mktemp "${TMPDIR:-/tmp}/terraform-aws-github-runner-scale-set-key.XXXXXX") -log_file=$(mktemp "${TMPDIR:-/tmp}/terraform-aws-github-runner-scale-set-logs.XXXXXX") -controller_log_file=$(mktemp "${TMPDIR:-/tmp}/terraform-aws-github-runner-scale-set-controller-logs.XXXXXX") -config_file=$(mktemp "${TMPDIR:-/tmp}/terraform-aws-github-runner-scale-set-config.XXXXXX") -task_definition_file=$(mktemp "${TMPDIR:-/tmp}/terraform-aws-github-runner-scale-set-task-definition.XXXXXX") -instance_file=$(mktemp "${TMPDIR:-/tmp}/terraform-aws-github-runner-scale-set-instances.XXXXXX") -scale_down_config_file=$(mktemp "${TMPDIR:-/tmp}/terraform-aws-github-runner-scale-set-scale-down-config.XXXXXX") -initializer_file=$(mktemp "${TMPDIR:-/tmp}/terraform-aws-github-runner-scale-set-mockserver.XXXXXX") -repository_name="scale-set-controller" -image_reference="localhost:4566/${repository_name}:smoke" -mockserver_host="${MINISTACK_GITHUB_MOCK_HOST:-127.0.0.1}" -mockserver_port="${MINISTACK_GITHUB_MOCK_PORT:-1080}" -mockserver_url="${MINISTACK_GITHUB_MOCK_URL:-http://127.0.0.1:${mockserver_port}}" -controller_mock_url="https://${mockserver_host}:${mockserver_port}" -terraform_state_exists=false - -cleanup() { - cleanup_status=$? - set +e - - if [ "$terraform_state_exists" = true ]; then - "$source_root/tests/ministack/run-example.sh" destroy "$example" "$tfvars_file" >/dev/null 2>&1 - fi - - rm -f "$tfvars_file" "$app_key_file" "$log_file" "$controller_log_file" "$config_file" "$task_definition_file" "$instance_file" "$scale_down_config_file" "$initializer_file" - exit "$cleanup_status" -} -trap cleanup EXIT INT TERM - -require_command() { - if ! command -v "$1" >/dev/null 2>&1; then - echo "$1 is required to run the scale-set MiniStack smoke test." >&2 - exit 69 - fi -} - -for command in aws curl docker openssl python3 terraform; do - require_command "$command" -done - -ministack_aws() { - aws --endpoint-url "$AWS_ENDPOINT_URL" --region "$AWS_DEFAULT_REGION" "$@" -} - -wait_for_http() { - url="$1" - attempts=90 - while ! curl -fsS --max-time 2 "$url" >/dev/null 2>&1; do - attempts=$((attempts - 1)) - if [ "$attempts" -le 0 ]; then - echo "Timed out waiting for $url." >&2 - exit 70 - fi - sleep 1 - done -} - -wait_for_scale_set_runner() { - attempts=90 - while :; do - ministack_aws ec2 describe-instances --output json > "$instance_file" - instance_id=$(python3 - "$instance_file" <<'PY' -import json -import sys - -with open(sys.argv[1], encoding="utf-8") as instance_file: - response = json.load(instance_file) - -required_tags = { - "ghr:Application": "github-action-runner", - "ghr:created_by": "scale-set-service", - "ghr:environment": "ministack-scale-set-linux-scale-set", - "ghr:Type": "Org", - "ghr:Owner": "example", - "ghr:scale_set_state": "config-published", - "ghr:github_runner_id": "321", -} -active_states = {"pending", "running", "stopping", "stopped", "shutting-down"} -matches = [] -for reservation in response.get("Reservations", []): - for instance in reservation.get("Instances", []): - if instance.get("State", {}).get("Name") not in active_states: - continue - tags = {tag.get("Key"): tag.get("Value") for tag in instance.get("Tags", [])} - if all(tags.get(key) == value for key, value in required_tags.items()) and tags.get("ghr:runner_name", "").startswith("scale-set-"): - matches.append(instance["InstanceId"]) - -if len(matches) == 1: - print(matches[0]) -PY -) - if [ -n "$instance_id" ]; then - printf ' [PASS] MiniStack created and registered scale-set EC2 runner %s\n' "$instance_id" - return 0 - fi - attempts=$((attempts - 1)) - if [ "$attempts" -le 0 ]; then - echo "Timed out waiting for the scale-set EC2 runner to reach config-published state." >&2 - cat "$instance_file" >&2 || true - exit 1 - fi - sleep 2 - done -} - -wait_for_no_scale_set_runners() { - attempts=90 - while :; do - ministack_aws ec2 describe-instances --output json > "$instance_file" - active_count=$(python3 - "$instance_file" <<'PY' -import json -import sys - -with open(sys.argv[1], encoding="utf-8") as instance_file: - response = json.load(instance_file) - -required_tags = { - "ghr:Application": "github-action-runner", - "ghr:created_by": "scale-set-service", - "ghr:environment": "ministack-scale-set-linux-scale-set", - "ghr:Type": "Org", - "ghr:Owner": "example", -} -active_states = {"pending", "running", "stopping", "stopped", "shutting-down"} -count = 0 -for reservation in response.get("Reservations", []): - for instance in reservation.get("Instances", []): - if instance.get("State", {}).get("Name") not in active_states: - continue - tags = {tag.get("Key"): tag.get("Value") for tag in instance.get("Tags", [])} - if all(tags.get(key) == value for key, value in required_tags.items()): - count += 1 -print(count) -PY -) - if [ "$active_count" = "0" ]; then - printf '%s\n' ' [PASS] scale-set EC2 runner was terminated after the minimum changed to zero' - return 0 - fi - attempts=$((attempts - 1)) - if [ "$attempts" -le 0 ]; then - echo "Timed out waiting for the scale-set EC2 runner to terminate; active count is $active_count." >&2 - cat "$instance_file" >&2 || true - exit 1 - fi - sleep 2 - done -} - -wait_for_http "$AWS_ENDPOINT_URL/_ministack/health" -attempts=90 -while ! curl -fsS --max-time 2 -X PUT "$mockserver_url/mockserver/status" >/dev/null 2>&1; do - attempts=$((attempts - 1)) - if [ "$attempts" -le 0 ]; then - echo "Timed out waiting for $mockserver_url/mockserver/status." >&2 - exit 70 - fi - sleep 1 -done -curl -fsS -X PUT "$mockserver_url/mockserver/reset" >/dev/null -CONTROLLER_MOCK_URL="$controller_mock_url" python3 - "$source_root/tests/ministack/initializerJson.json" "$initializer_file" <<'PY' -import os -import sys - -source, destination = sys.argv[1:] -with open(source, encoding="utf-8") as source_file: - fixture = source_file.read() -fixture = fixture.replace("https://mockserver:1080", os.environ["CONTROLLER_MOCK_URL"]) -with open(destination, "w", encoding="utf-8") as destination_file: - destination_file.write(fixture) -PY -curl -fsS -X PUT \ - "$mockserver_url/mockserver/expectation" \ - -H 'Content-Type: application/json' \ - --data-binary "@$initializer_file" \ - >/dev/null - -repository_policy=$(python3 - <<'PY' -import json - -print(json.dumps({ - "Version": "2012-10-17", - "Statement": [{ - "Sid": "AllowAccountPull", - "Effect": "Allow", - "Principal": {"AWS": "arn:aws:iam::000000000000:root"}, - "Action": [ - "ecr:BatchCheckLayerAvailability", - "ecr:BatchGetImage", - "ecr:GetDownloadUrlForLayer", - ], - }], -})) -PY -) - -ministack_aws ecr create-repository \ - --repository-name "$repository_name" \ - --image-tag-mutability IMMUTABLE \ - --image-scanning-configuration scanOnPush=false \ - >/dev/null -ministack_aws ecr set-repository-policy \ - --repository-name "$repository_name" \ - --policy-text "$repository_policy" \ - >/dev/null - -docker build \ - --target runtime \ - --file "$source_root/lambdas/services/scale-set/Dockerfile" \ - --tag "$image_reference" \ - "$source_root" - -ministack_aws ecr get-login-password | docker login \ - --username AWS \ - --password-stdin localhost:4566 >/dev/null -docker push "$image_reference" - -ministack_aws ecr describe-images \ - --repository-name "$repository_name" \ - --image-ids imageTag=smoke \ - >/dev/null - -openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 -out "$app_key_file" 2>/dev/null -app_key_base64=$(base64 < "$app_key_file" | tr -d '\n') -APP_KEY_BASE64="$app_key_base64" GITHUB_CONFIG_URL="$controller_mock_url" python3 - "$base_tfvars" "$tfvars_file" <<'PY' -import os -import sys - -source, destination = sys.argv[1:] -replacement = os.environ["APP_KEY_BASE64"] -github_config_url = os.environ["GITHUB_CONFIG_URL"] -with open(source, encoding="utf-8") as source_file: - lines = source_file.readlines() -with open(destination, "w", encoding="utf-8") as destination_file: - for line in lines: - if line.lstrip().startswith("key_base64"): - destination_file.write(f' key_base64 = "{replacement}"\n') - elif line.lstrip().startswith("config_url"): - destination_file.write(f' config_url = "{github_config_url}"\n') - else: - destination_file.write(line) -PY -unset app_key_base64 APP_KEY_BASE64 - -terraform_state_exists=true -"$source_root/tests/ministack/run-example.sh" apply "$example" "$tfvars_file" - -config_path="/ministack-scale-set/scale-set-controller/linux-scale-set/linux-scale-set" -ministack_aws ssm get-parameter \ - --name "$config_path" \ - --query 'Parameter.Value' \ - --output text > "$config_file" -EXPECTED_GITHUB_CONFIG_URL="$controller_mock_url/example" python3 - "$config_file" <<'PY' -import json -import os -import sys - -with open(sys.argv[1], encoding="utf-8") as config_file: - config = json.load(config_file) - -assert config["githubConfigUrl"] == os.environ["EXPECTED_GITHUB_CONFIG_URL"] -assert config["forceGhes"] is True -assert config["sslVerify"] is False -assert config["minRunners"] == 1 -assert config["githubApp"]["appIdParameterName"] -assert config["githubApp"]["installationIdParameterName"] -assert config["githubApp"]["privateKeyParameterName"] -print(" [PASS] SSM manifest has the expected MockServer and GitHub App settings") -PY - -task_definition=$(ministack_aws ecs list-task-definitions \ - --family-prefix ministack-scale-set-ss-linux-scale-se- \ - --sort DESC \ - --query 'taskDefinitionArns[0]' \ - --output text) -if [ -z "$task_definition" ] || [ "$task_definition" = "None" ]; then - echo "The scale-set task definition was not registered." >&2 - exit 1 -fi - -actual_image=$(ministack_aws ecs describe-task-definition \ - --task-definition "$task_definition" \ - --query 'taskDefinition.containerDefinitions[?name==`scale-set-controller`].image | [0]' \ - --output text) -if [ "$actual_image" != "$image_reference" ]; then - echo "Expected the ECS task to use $image_reference, got $actual_image." >&2 - exit 1 -fi -printf '%s\n' ' [PASS] ECS task definition uses the image pushed to MiniStack ECR' - -log_driver=$(ministack_aws ecs describe-task-definition \ - --task-definition "$task_definition" \ - --query 'taskDefinition.containerDefinitions[?name==`scale-set-controller`].logConfiguration.logDriver | [0]' \ - --output text) -if [ "$log_driver" != "awslogs" ]; then - echo "Expected the ECS task to request the awslogs driver, got $log_driver." >&2 - exit 1 -fi -printf '%s\n' ' [PASS] ECS task definition requests the awslogs driver' - -log_group=$(ministack_aws logs describe-log-groups \ - --log-group-name-prefix "/aws/ecs/ministack-scale-set-ss-linux-scale-se-" \ - --query 'logGroups[0].logGroupName' \ - --output text) -if [ -z "$log_group" ] || [ "$log_group" = "None" ]; then - echo "The scale-set CloudWatch log group was not created." >&2 - exit 1 -fi -printf '%s\n' ' [PASS] CloudWatch log group was created for the ECS controller' - -task_family=$(ministack_aws ecs describe-task-definition \ - --task-definition "$task_definition" \ - --query 'taskDefinition.family' \ - --output text) - -# MiniStack exposes ECS credentials through the gateway's container IP. The -# Node.js AWS SDK intentionally rejects that address in -# AWS_CONTAINER_CREDENTIALS_FULL_URI because non-HTTPS full URIs are limited -# to loopback and the real ECS metadata address. Use only synthetic, -# test-scoped credentials in a temporary task-definition revision so the -# smoke test still exercises the pushed image, ECS service, and controller -# lifecycle without changing the production task definition or application. -ministack_aws ecs describe-task-definition \ - --task-definition "$task_definition" \ - --query 'taskDefinition' \ - --output json > "$task_definition_file" -TASK_DEFINITION_FILE="$task_definition_file" python3 - <<'PY' -import json -import os - -path = os.environ["TASK_DEFINITION_FILE"] -with open(path, encoding="utf-8") as task_definition_file: - task_definition = json.load(task_definition_file) - -for field in ( - "taskDefinitionArn", - "revision", - "status", - "requiresAttributes", - "compatibilities", - "registeredAt", - "registeredBy", -): - task_definition.pop(field, None) - -for container in task_definition["containerDefinitions"]: - if container["name"] != "scale-set-controller": - continue - environment = container.setdefault("environment", []) - environment.extend([ - {"name": "AWS_ACCESS_KEY_ID", "value": "000000000000"}, - {"name": "AWS_SECRET_ACCESS_KEY", "value": "test-only"}, - ]) - -with open(path, "w", encoding="utf-8") as task_definition_file: - json.dump(task_definition, task_definition_file) -PY - -environment_name=$(sed -n 's/^environment[[:space:]]*=[[:space:]]*"\([^"]*\)".*/\1/p' "$base_tfvars") -cluster_name="${environment_name}-scale-set" -task_definition=$(ministack_aws ecs register-task-definition \ - --cli-input-json "file://$task_definition_file" \ - --query 'taskDefinition.taskDefinitionArn' \ - --output text) -ministack_aws ecs update-service \ - --cluster "$cluster_name" \ - --service "$task_family" \ - --task-definition "$task_definition" \ - >/dev/null - -task_revision=$(ministack_aws ecs describe-task-definition \ - --task-definition "$task_definition" \ - --query 'taskDefinition.revision' \ - --output text) -controller_container="" -attempts=90 -while [ -z "$controller_container" ]; do - controller_container=$(docker ps -a \ - --filter "label=com.amazonaws.ecs.task-definition-family=$task_family" \ - --filter "label=com.amazonaws.ecs.task-definition-version=$task_revision" \ - --filter 'name=scale-set-controller' \ - --format '{{.ID}}' | sed -n '1p') - if [ -n "$controller_container" ]; then - break - fi - attempts=$((attempts - 1)) - if [ "$attempts" -le 0 ]; then - echo "Timed out waiting for the MiniStack ECS controller container." >&2 - docker ps -a --format '{{.ID}} {{.Image}} {{.Status}} {{.Names}}' >&2 || true - exit 1 - fi - sleep 2 -done -printf ' [PASS] MiniStack started ECS controller container %s\n' "$controller_container" - -wait_for_controller_log_event() { - marker="$1" - required_text="${2:-}" - attempts=90 - while :; do - docker logs "$controller_container" > "$controller_log_file" 2>&1 || true - if python3 - "$controller_log_file" "$marker" "$required_text" <<'PY' -import sys - -marker, required = sys.argv[2:] -with open(sys.argv[1], encoding="utf-8") as log_file: - messages = log_file.read().splitlines() -if any(marker in message and (not required or required in message) for message in messages): - raise SystemExit(0) -raise SystemExit(1) -PY - then - printf ' [PASS] CloudWatch logs contain %s\n' "$marker" - return 0 - fi - attempts=$((attempts - 1)) - if [ "$attempts" -le 0 ]; then - echo "Timed out waiting for controller log marker '$marker'." >&2 - cat "$controller_log_file" >&2 || true - echo "Controller AWS/ECS metadata environment:" >&2 - docker inspect --format '{{range .Config.Env}}{{println .}}{{end}}' "$controller_container" \ - | sed -E 's/^(AWS_CONTAINER_AUTHORIZATION_TOKEN|AWS_CONTAINER_CREDENTIALS_FULL_URI)=.*/\1=/' \ - | grep -E '^(AWS_|ECS_)' >&2 || true - echo "Controller network attachments:" >&2 - docker inspect --format '{{json .NetworkSettings.Networks}}' "$controller_container" >&2 || true - echo "Controller credential endpoint probe:" >&2 - docker exec "$controller_container" node -e \ - 'fetch(process.env.AWS_CONTAINER_CREDENTIALS_FULL_URI, {headers: {Authorization: process.env.AWS_CONTAINER_AUTHORIZATION_TOKEN}}).then((response) => { console.error(`status=${response.status}`); process.exit(response.ok ? 0 : 1); }).catch((error) => { console.error(`${error.name}:${error.message}`); process.exit(1); })' \ - >&2 || true - exit 1 - fi - sleep 2 - done -} - -printf '%s\n' ' [INFO] MiniStack 1.5.12 does not emit ECS awslogs streams; validating controller runtime logs instead' -wait_for_controller_log_event 'scale_set_controller_started' -wait_for_controller_log_event 'scale_set_session_created' -wait_for_controller_log_event 'scale_set_reconciled' '"desiredRunners":1' -wait_for_controller_log_event 'scale_set_reconciled' '"status":"converged"' -wait_for_scale_set_runner - -wait_for_mock_route() { - method="$1" - route="$2" - body=$(REQUEST_METHOD="$method" REQUEST_PATH="$route" python3 - <<'PY' -import json -import os - -print(json.dumps({ - "httpRequest": { - "method": os.environ["REQUEST_METHOD"], - "path": os.environ["REQUEST_PATH"], - }, - "times": {"atLeast": 1}, -})) -PY - ) - attempts=45 - while ! curl -fsS --max-time 5 -X PUT \ - http://127.0.0.1:1080/mockserver/verify \ - -H 'Content-Type: application/json' \ - --data "$body" >/dev/null 2>&1; do - attempts=$((attempts - 1)) - if [ "$attempts" -le 0 ]; then - echo "Timed out waiting for MockServer route: $method $route" >&2 - curl -sS --max-time 5 \ - 'http://127.0.0.1:1080/mockserver/retrieve?type=REQUESTS&format=JSON' >&2 || true - exit 1 - fi - sleep 2 - done - printf ' [PASS] MockServer received %s %s\n' "$method" "$route" -} - -wait_for_mock_route POST '/api/v3/app/installations/456/access_tokens' -wait_for_mock_route POST '/api/v3/orgs/example/actions/runners/registration-token' -wait_for_mock_route POST '/api/v3/actions/runner-registration' -wait_for_mock_route GET '/tenant/123/_apis/runtime/runnergroups/' -wait_for_mock_route GET '/tenant/123/_apis/runtime/runnerscalesets' -wait_for_mock_route GET '/tenant/123/_apis/runtime/runnerscalesets/223' -wait_for_mock_route PATCH '/tenant/123/_apis/runtime/runnerscalesets/223' -wait_for_mock_route POST '/tenant/123/_apis/runtime/runnerscalesets/223/generatejitconfig' -wait_for_mock_route POST '/tenant/123/_apis/runtime/runnerscalesets/223/sessions' -wait_for_mock_route GET '/messages' - -python3 - "$config_file" "$scale_down_config_file" <<'PY' -import json -import sys - -with open(sys.argv[1], encoding="utf-8") as config_file: - reconciler = json.load(config_file) - -assert reconciler["minRunners"] == 1 -reconciler["minRunners"] = 0 -with open(sys.argv[2], "w", encoding="utf-8") as config_file: - json.dump(reconciler, config_file) -PY -ministack_aws ssm put-parameter \ - --name "$config_path" \ - --type String \ - --value "$(cat "$scale_down_config_file")" \ - --overwrite \ - >/dev/null -printf '%s\n' ' [PASS] SSM manifest minimum changed from one runner to zero' - -SCALE_DOWN_CONFIG_FILE="$scale_down_config_file" TASK_DEFINITION_FILE="$task_definition_file" python3 - <<'PY' -import json -import os - -with open(os.environ["SCALE_DOWN_CONFIG_FILE"], encoding="utf-8") as config_file: - reconciler = json.load(config_file) -with open(os.environ["TASK_DEFINITION_FILE"], encoding="utf-8") as task_definition_file: - task_definition = json.load(task_definition_file) - -for container in task_definition["containerDefinitions"]: - if container["name"] != "scale-set-controller": - continue - for environment in container.get("environment", []): - if environment["name"] == "SCALE_SET_CONTROLLER_MANIFEST": - manifest = json.loads(environment["value"]) - assert len(manifest.get("reconcilers", [])) == 1 - manifest["reconcilers"][0]["minRunners"] = reconciler["minRunners"] - environment["value"] = json.dumps(manifest, separators=(",", ":")) - break - else: - raise RuntimeError("scale-set controller task definition has no inline manifest") - -with open(os.environ["TASK_DEFINITION_FILE"], "w", encoding="utf-8") as task_definition_file: - json.dump(task_definition, task_definition_file) -PY -task_definition=$(ministack_aws ecs register-task-definition \ - --cli-input-json "file://$task_definition_file" \ - --query 'taskDefinition.taskDefinitionArn' \ - --output text) -task_revision=$(ministack_aws ecs describe-task-definition \ - --task-definition "$task_definition" \ - --query 'taskDefinition.revision' \ - --output text) -ministack_aws ecs update-service \ - --cluster "$cluster_name" \ - --service "$task_family" \ - --task-definition "$task_definition" \ - --force-new-deployment \ - >/dev/null - -old_controller_container="$controller_container" -new_controller_container="" -attempts=90 -while [ -z "$new_controller_container" ]; do - for candidate in $(docker ps -a \ - --filter "label=com.amazonaws.ecs.task-definition-family=$task_family" \ - --filter "label=com.amazonaws.ecs.task-definition-version=$task_revision" \ - --filter 'name=scale-set-controller' \ - --format '{{.ID}}'); do - if [ "$candidate" != "$old_controller_container" ]; then - new_controller_container="$candidate" - break - fi - done - if [ -n "$new_controller_container" ]; then - break - fi - attempts=$((attempts - 1)) - if [ "$attempts" -le 0 ]; then - echo 'Timed out waiting for the ECS service to deploy the scale-down task.' >&2 - docker ps -a --format '{{.ID}} {{.Image}} {{.Status}} {{.Names}}' >&2 || true - exit 1 - fi - sleep 2 -done -controller_container="$new_controller_container" -printf ' [PASS] ECS service deployed a fresh controller container %s for scale-down\n' "$controller_container" -wait_for_controller_log_event 'scale_set_controller_started' -wait_for_controller_log_event 'scale_set_session_created' -wait_for_controller_log_event 'scale_set_reconciled' '"desiredRunners":0' -wait_for_controller_log_event 'scale_set_reconciled' '"status":"converged"' -wait_for_no_scale_set_runners - -"$source_root/tests/ministack/run-example.sh" destroy "$example" "$tfvars_file" -terraform_state_exists=false -wait_for_mock_route DELETE '/tenant/123/_apis/runtime/runnerscalesets/223/sessions/11111111-1111-1111-1111-111111111111' - -echo 'Scale-set MiniStack ECS/MockServer smoke test passed.' diff --git a/tests/ministack/run-smoke.sh b/tests/ministack/run-smoke.sh deleted file mode 100644 index d4d9c7ab3e..0000000000 --- a/tests/ministack/run-smoke.sh +++ /dev/null @@ -1,721 +0,0 @@ -#!/bin/sh - -set -eu - -export AWS_ACCESS_KEY_ID="${AWS_ACCESS_KEY_ID:-000000000000}" -export AWS_SECRET_ACCESS_KEY="${AWS_SECRET_ACCESS_KEY:-test-only}" -export AWS_DEFAULT_REGION="${AWS_DEFAULT_REGION:-eu-west-1}" -export AWS_REGION="${AWS_REGION:-eu-west-1}" -export AWS_ENDPOINT_URL="${AWS_ENDPOINT_URL:-http://127.0.0.1:4566}" -export AWS_EC2_METADATA_DISABLED="${AWS_EC2_METADATA_DISABLED:-true}" - -script_dir=$(CDPATH='' cd -- "$(dirname -- "$0")" && pwd) -source_root=$(CDPATH='' cd -- "$script_dir/../.." && pwd) -example_root="$source_root/examples/default" -mock_expectations="$script_dir/github-api-expectations.json" -fixture="$script_dir/workflow_job_event.json" -dynamic_fixture=$(mktemp "${TMPDIR:-/tmp}/terraform-aws-github-runner-dynamic-workflow-job.XXXXXX") -mock_host="${MINISTACK_GITHUB_MOCK_HOST:-host.docker.internal}" -mock_port="${MINISTACK_GITHUB_MOCK_PORT:-}" -mock_service_url="${MINISTACK_GITHUB_MOCK_URL:-}" -mock_image="${MINISTACK_GITHUB_MOCK_IMAGE:-mockserver/mockserver:7.6.0@sha256:80b3b1a26f3553d0c81a3f3896b5b7274c17b2a2e52f0fd2b28e246bc9efa290}" -mock_container="" -tfvars_file=$(mktemp "${TMPDIR:-/tmp}/terraform-aws-github-runner-smoke.XXXXXX") -app_key_file=$(mktemp "${TMPDIR:-/tmp}/terraform-aws-github-runner-github-app.XXXXXX") -response_file=$(mktemp "${TMPDIR:-/tmp}/terraform-aws-github-runner-smoke-response.XXXXXX") -lambda_response_file=$(mktemp "${TMPDIR:-/tmp}/terraform-aws-github-runner-lambda-response.XXXXXX") -override_file="$example_root/zz_ministack_smoke_override.tf" -terraform_initialized=false -discovered_instance_ids="" - -cleanup() { - set +e - for instance_id in $discovered_instance_ids; do - aws --endpoint-url "$AWS_ENDPOINT_URL" ec2 terminate-instances \ - --instance-ids "$instance_id" >/dev/null 2>&1 - done - if [ "$terraform_initialized" = true ]; then - "$source_root/tests/ministack/run-example.sh" destroy default "$tfvars_file" >/dev/null 2>&1 - fi - if [ -n "$mock_container" ]; then - docker rm -f "$mock_container" >/dev/null 2>&1 - fi - rm -f "$override_file" "$tfvars_file" "$app_key_file" "$response_file" "$lambda_response_file" "$dynamic_fixture" -} -trap cleanup EXIT INT TERM - -require_command() { - if ! command -v "$1" >/dev/null 2>&1; then - echo "$1 is required to run the MiniStack smoke test." >&2 - exit 69 - fi -} - -for command in aws curl openssl python3 terraform; do - require_command "$command" -done -if [ -z "$mock_service_url" ]; then - require_command docker -fi - -for lambda_zip in \ - "$source_root/lambdas/functions/webhook/webhook.zip" \ - "$source_root/lambdas/functions/control-plane/runners.zip"; do - if [ ! -f "$lambda_zip" ]; then - echo "Missing $lambda_zip. Build the webhook and control-plane distributions first." >&2 - exit 66 - fi -done - -if [ -z "$mock_port" ]; then - if [ -n "$mock_service_url" ]; then - mock_port=1080 - else - mock_port=$(python3 -c 'import socket; s = socket.socket(); s.bind(("", 0)); print(s.getsockname()[1]); s.close()') - fi -fi - -if [ -z "$mock_service_url" ]; then - mock_container="terraform-aws-github-runner-github-api-mock-$$" - mock_service_url="http://127.0.0.1:${mock_port}" -fi - -openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 -out "$app_key_file" 2>/dev/null -app_key_base64=$(base64 < "$app_key_file" | tr -d '\n') -APP_KEY_BASE64="$app_key_base64" python3 - "$script_dir/default.tfvars" "$tfvars_file" <<'PY' -import os -import sys - -source, destination = sys.argv[1:] -replacement = os.environ["APP_KEY_BASE64"] -with open(source, encoding="utf-8") as source_file: - lines = source_file.readlines() -with open(destination, "w", encoding="utf-8") as destination_file: - for line in lines: - if line.lstrip().startswith("key_base64 ="): - destination_file.write(f' key_base64 = "{replacement}"\n') - elif line.lstrip().startswith('id') and '=' in line: - destination_file.write(' id = "123"\n') - else: - destination_file.write(line) -PY -unset app_key_base64 APP_KEY_BASE64 - -printf '%s\n' \ - 'module "runners" {' \ - " ghes_url = \"http://${mock_host}:${mock_port}\"" \ - ' ghes_ssl_verify = false' \ - ' eventbridge = {' \ - ' enable = true' \ - ' accept_events = ["workflow_job"]' \ - ' }' \ - ' delay_webhook_event = 0' \ - ' runners_maximum_count = 1' \ - ' instance_types = ["m7a.large"]' \ - ' enable_dynamic_labels = true' \ - ' minimum_running_time_in_minutes = 0' \ - ' pool_runner_owner = "test-owner"' \ - ' pool_config = [{ schedule_expression = "cron(0 0 1 1 ? 2099)", size = 1 }]' \ - ' scale_down_schedule_expression = "cron(0 0 1 1 ? 2099)"' \ - ' enable_job_queued_check = true' \ - ' enable_jit_config = false' \ - ' enable_runner_binaries_syncer = false' \ - ' log_level = "debug"' \ - '}' \ - '' \ - 'module "webhook_github_app" {' \ - ' count = 0' \ - '}' > "$override_file" - -if [ -n "$mock_container" ]; then - docker run --detach --name "$mock_container" --publish "${mock_port}:1080" \ - --volume "$mock_expectations:/config/github-api-expectations.json:ro" \ - --env MOCKSERVER_INITIALIZATION_JSON_PATH=/config/github-api-expectations.json \ - "$mock_image" >/dev/null -fi - -attempts=30 -while ! curl -fsS --max-time 2 -X PUT "${mock_service_url}/mockserver/status" >/dev/null 2>&1; do - attempts=$((attempts - 1)) - if [ "$attempts" -le 0 ]; then - echo "MockServer did not become ready." >&2 - if [ -n "$mock_container" ]; then - docker logs "$mock_container" >&2 - fi - exit 70 - fi - sleep 1 -done - -if [ -z "$mock_container" ]; then - MOCKSERVER_URL="$mock_service_url" python3 - "$mock_expectations" <<'PY' -import json -import os -import sys -import urllib.request - -with open(sys.argv[1], encoding="utf-8") as expectations_file: - expectations = json.load(expectations_file) - -for expectation in expectations: - request = urllib.request.Request( - f'{os.environ["MOCKSERVER_URL"]}/mockserver/expectation', - data=json.dumps(expectation).encode("utf-8"), - headers={"Content-Type": "application/json"}, - method="PUT", - ) - with urllib.request.urlopen(request, timeout=10) as response: - if response.status not in (200, 201): - raise RuntimeError(f"MockServer expectation rejected with HTTP {response.status}") -PY -fi - -python3 - "$fixture" "$dynamic_fixture" <<'PY' -import json -import sys - -source, destination = sys.argv[1:] -with open(source, encoding="utf-8") as source_file: - event = json.load(source_file) - -job = event["workflow_job"] -job["id"] = 123457 -job["run_id"] = 654322 -job["run_url"] = job["run_url"].replace("654321", "654322") -job["url"] = job["url"].replace("123456", "123457") -job["html_url"] = job["html_url"].replace("123456", "123457") -job["name"] = "ministack-smoke-dynamic" -job["labels"].append("ghr-ec2-instance-type:m5.large") - -with open(destination, "w", encoding="utf-8") as destination_file: - json.dump(event, destination_file) -PY - -terraform_initialized=true -"$source_root/tests/ministack/run-example.sh" apply default "$tfvars_file" - -printf '%s\n' \ - 'MiniStack smoke chain evidence checklist:' \ - ' [ ] API Gateway accepted the signed workflow_job webhook (HTTP 201)' \ - ' [ ] Webhook Lambda log contains workflow job 123456' \ - ' [ ] EventBridge invoked the dispatcher Lambda (dispatcher log contains 123456)' \ - ' [ ] Dispatcher delivered the job through SQS (scale-up log contains 123456)' \ - ' [ ] Scale-up without a dynamic label called each expected GitHub API route in MockServer' \ - ' [ ] MiniStack EC2 API reports a standard scale-up instance with default EC2 configuration' \ - ' [ ] Scale-up with ghr-ec2-instance-type:m5.large called each expected GitHub API route in MockServer' \ - ' [ ] Dynamic label selected EC2 instance type m5.large' \ - ' [ ] Scale-up EC2 instance has the expected runner discovery tags' \ - ' [ ] Pool called every expected GitHub API route in MockServer' \ - ' [ ] Pool Lambda created a runner instance' \ - ' [ ] Pool EC2 instance has the expected runner discovery tags' \ - ' [ ] Scale-down removed each runner from GitHub and terminated its EC2 instance' - -webhook_endpoint=$(terraform -chdir="$example_root" output -raw webhook_endpoint) -endpoint_host_port=${AWS_ENDPOINT_URL#*://} -endpoint_port=${endpoint_host_port##*:} -api_host_port=${webhook_endpoint#*://} -api_host_port=${api_host_port%%/*} -api_host=${api_host_port%:*} -webhook_secret=$(terraform -chdir="$example_root" output -raw webhook_secret) - -send_webhook() { - fixture_file="$1" - delivery_id="$2" - signature=$(openssl dgst -sha256 -hmac "$webhook_secret" "$fixture_file" | awk '{print $NF}') - status_code=$(curl -sS --max-time 15 -o "$response_file" -w '%{http_code}' \ - --connect-to "${api_host}:4566:127.0.0.1:${endpoint_port}" \ - -X POST "$webhook_endpoint" \ - -H 'Content-Type: application/json' \ - -H 'X-GitHub-Event: workflow_job' \ - -H "X-GitHub-Delivery: ${delivery_id}" \ - -H 'X-GitHub-Hook-Installation-Target-ID: 123' \ - -H "X-Hub-Signature-256: sha256=${signature}" \ - --data-binary "@${fixture_file}") - - if [ "$status_code" != 201 ]; then - echo "Webhook smoke request failed with HTTP $status_code." >&2 - sed -n '1,80p' "$response_file" >&2 - exit 1 - fi - echo " [PASS] API Gateway accepted the signed workflow_job webhook ${delivery_id} (HTTP 201)" -} - -send_webhook "$fixture" "ministack-smoke-123456" - -wait_for_log_event() { - log_group="$1" - marker="$2" - description="$3" - attempts=60 - while ! aws --endpoint-url "$AWS_ENDPOINT_URL" logs filter-log-events \ - --log-group-name "$log_group" --filter-pattern "$marker" --limit 1 --output text 2>/dev/null | grep -Fq "$marker"; do - attempts=$((attempts - 1)) - if [ "$attempts" -le 0 ]; then - echo "Timed out waiting for MiniStack log marker '$marker' in $log_group." >&2 - exit 1 - fi - sleep 2 - done - printf ' [PASS] %s (log group %s contains %s)\n' "$description" "$log_group" "$marker" -} - -wait_for_optional_log_event() { - log_group="$1" - marker="$2" - description="$3" - attempts=60 - while ! aws --endpoint-url "$AWS_ENDPOINT_URL" logs filter-log-events \ - --log-group-name "$log_group" --filter-pattern "$marker" --limit 1 --output text 2>/dev/null | grep -Fq "$marker"; do - attempts=$((attempts - 1)) - if [ "$attempts" -le 0 ]; then - printf ' [WARN] %s (log marker %s was not observed in %s)\n' \ - "$description" "$marker" "$log_group" - return 0 - fi - sleep 2 - done - printf ' [PASS] %s (log group %s contains %s)\n' "$description" "$log_group" "$marker" -} - -wait_for_log_event "/aws/lambda/ministack-default-webhook" "123456" \ - "Webhook Lambda received workflow job 123456" -wait_for_log_event "/aws/lambda/ministack-default-dispatch-to-runner" "123456" \ - "EventBridge invoked the dispatcher Lambda" -wait_for_log_event "/aws/lambda/ministack-default-scale-up" "123456" \ - "Dispatcher delivered workflow job 123456 through SQS to scale-up" - -wait_for_mock_route() { - method="$1" - route="$2" - description="$3" - verification_body=$(printf '{"httpRequest":{"method":"%s","path":"%s"},"times":{"atLeast":1}}' "$method" "$route") - attempts=60 - while ! curl -fsS --max-time 5 -X PUT "${mock_service_url}/mockserver/verify" \ - -H 'Content-Type: application/json' \ - --data-binary "$verification_body" >/dev/null 2>&1; do - attempts=$((attempts - 1)) - if [ "$attempts" -le 0 ]; then - echo "Timed out waiting for MockServer route: $method $route" >&2 - curl -sS --max-time 5 -X PUT \ - "${mock_service_url}/mockserver/retrieve?type=REQUEST_RESPONSES&format=JSON" >&2 || true - exit 1 - fi - sleep 2 - done - printf ' [PASS] %s (MockServer verified %s %s)\n' "$description" "$method" "$route" -} - -clear_mock_request_log() { - if ! curl -fsS --max-time 5 -X PUT \ - "${mock_service_url}/mockserver/clear?type=log" >/dev/null 2>&1; then - echo "Failed to clear MockServer request history before the next lifecycle phase." >&2 - exit 1 - fi -} - -assert_scale_down_github_routes() { - wait_for_mock_route POST "/api/v3/app/installations/123/access_tokens" \ - "Scale-down requested a GitHub App installation token" - wait_for_mock_route GET "/api/v3/orgs/test-owner/actions/runners" \ - "Scale-down listed organization runners" - wait_for_mock_route GET "/api/v3/orgs/test-owner/actions/runners/${1}" \ - "Scale-down checked the runner busy state" - wait_for_mock_route DELETE "/api/v3/orgs/test-owner/actions/runners/${1}" \ - "Scale-down deleted the runner from GitHub" -} - -assert_pool_github_routes() { - wait_for_mock_route GET "/api/v3/orgs/test-owner/installation" \ - "Pool looked up the GitHub App installation" - wait_for_mock_route POST "/api/v3/app/installations/123/access_tokens" \ - "Pool requested a GitHub App installation token" - wait_for_mock_route GET "/api/v3/orgs/test-owner/actions/runners" \ - "Pool listed organization runners" - wait_for_mock_route POST "/api/v3/orgs/test-owner/actions/runners/registration-token" \ - "Pool requested a GitHub runner registration token" -} - -assert_scale_up_github_routes() { - job_id="$1" - wait_for_mock_route POST "/api/v3/app/installations/123/access_tokens" \ - "Scale-up requested a GitHub App installation token for job ${job_id}" - wait_for_mock_route GET "/api/v3/repos/test-owner/test-repo/actions/jobs/${job_id}" \ - "Scale-up checked the queued GitHub job ${job_id}" - wait_for_mock_route POST "/api/v3/orgs/test-owner/actions/runners/registration-token" \ - "Scale-up requested a GitHub runner registration token for job ${job_id}" -} - -assert_scale_up_github_routes 123456 - -wait_for_ec2_instance() { - source="$1" - description="$2" - attempts=60 - while :; do - found_instance_id=$(aws --endpoint-url "$AWS_ENDPOINT_URL" ec2 describe-instances \ - --filters \ - "Name=instance-state-name,Values=running,pending" \ - "Name=tag:ghr:Application,Values=github-action-runner" \ - "Name=tag:ghr:created_by,Values=$source" \ - --query 'Reservations[].Instances[].InstanceId | [0]' \ - --output text 2>/dev/null || true) - if [ -n "$found_instance_id" ] && [ "$found_instance_id" != "None" ]; then - case " $discovered_instance_ids " in - *" $found_instance_id "*) ;; - *) discovered_instance_ids="$discovered_instance_ids $found_instance_id" ;; - esac - printf ' [PASS] MiniStack EC2 API reports %s: %s\n' "$description" "$found_instance_id" - return - fi - - attempts=$((attempts - 1)) - if [ "$attempts" -le 0 ]; then - echo "Timed out waiting for $description in the MiniStack EC2 API." >&2 - aws --endpoint-url "$AWS_ENDPOINT_URL" ec2 describe-instances \ - --filters \ - "Name=instance-state-name,Values=running,pending" \ - "Name=tag:ghr:Application,Values=github-action-runner" \ - "Name=tag:ghr:created_by,Values=$source" \ - --output json >&2 || true - exit 1 - fi - sleep 2 - done -} - -wait_for_ec2_instance "scale-up-lambda" "a scale-up instance" -scale_up_instance_id="$found_instance_id" - -assert_ec2_tag() { - instance_id="$1" - key="$2" - expected_value="$3" - description="$4" - actual_value=$(aws --endpoint-url "$AWS_ENDPOINT_URL" ec2 describe-instances \ - --instance-ids "$instance_id" \ - --query "Reservations[].Instances[].Tags[?Key=='${key}'].Value | [0]" \ - --output text 2>/dev/null || true) - if [ "$actual_value" != "$expected_value" ]; then - echo "Expected $description tag $key=$expected_value on $instance_id, got $actual_value." >&2 - exit 1 - fi -} - -assert_ec2_runner_tags() { - instance_id="$1" - source="$2" - description="$3" - assert_ec2_tag "$instance_id" "ghr:Application" "github-action-runner" "$description" - assert_ec2_tag "$instance_id" "ghr:created_by" "$source" "$description" - assert_ec2_tag "$instance_id" "ghr:Type" "Org" "$description" - assert_ec2_tag "$instance_id" "ghr:Owner" "test-owner" "$description" - printf ' [PASS] MiniStack EC2 API reports correct runner tags on %s\n' "$instance_id" -} - -assert_ec2_runner_tags "$scale_up_instance_id" "scale-up-lambda" "the scale-up runner" - -assert_ec2_default_instance_type() { - instance_id="$1" - actual_type=$(aws --endpoint-url "$AWS_ENDPOINT_URL" ec2 describe-instances \ - --instance-ids "$instance_id" \ - --query 'Reservations[0].Instances[0].InstanceType' \ - --output text 2>/dev/null || true) - if [ "$actual_type" != "m7a.large" ]; then - echo "Expected standard scale-up to use the configured default m7a.large, got $actual_type." >&2 - exit 1 - fi - printf ' [PASS] Standard scale-up used the configured default EC2 instance type: %s\n' "$actual_type" -} - -assert_ec2_instance_type() { - instance_id="$1" - expected_type="$2" - actual_type=$(aws --endpoint-url "$AWS_ENDPOINT_URL" ec2 describe-instances \ - --instance-ids "$instance_id" \ - --query 'Reservations[0].Instances[0].InstanceType' \ - --output text 2>/dev/null || true) - if [ "$actual_type" != "$expected_type" ]; then - echo "Expected $instance_id to use EC2 instance type $expected_type, got $actual_type." >&2 - exit 1 - fi - printf ' [PASS] EC2 dynamic label selected instance type %s on %s\n' "$expected_type" "$instance_id" -} - -assert_ec2_default_instance_type "$scale_up_instance_id" - -configure_mock_runner_state() { - instance_id="$1" - runner_id="$2" - MOCKSERVER_URL="$mock_service_url" python3 - "$instance_id" "$runner_id" <<'PY' -import json -import os -import sys -import urllib.request - -instance_id, runner_id = sys.argv[1:] -runner_id = int(runner_id) -base = "/api/v3/orgs/test-owner/actions/runners" - -def control(path, method, payload): - request = urllib.request.Request( - f'{os.environ["MOCKSERVER_URL"]}{path}', - data=json.dumps(payload).encode("utf-8"), - headers={"Content-Type": "application/json"}, - method=method, - ) - with urllib.request.urlopen(request, timeout=10) as response: - if response.status not in (200, 201, 202): - raise RuntimeError(f'MockServer API rejected {method} {path} with HTTP {response.status}') - -def clear(method, path): - control("/mockserver/clear", "PUT", {"httpRequest": {"method": method, "path": path}}) - -def expect(method, path, status, body=None): - response = {"statusCode": status} - if body is not None: - response["headers"] = {"Content-Type": ["application/json"]} - response["body"] = json.dumps(body) - control( - "/mockserver/expectation", - "PUT", - {"httpRequest": {"method": method, "path": path}, "httpResponse": response}, - ) - -state_path = f"{base}/{runner_id}" -clear("GET", base) -clear("GET", state_path) -clear("DELETE", state_path) -expect( - "GET", - base, - 200, - { - "total_count": 1, - "runners": [ - { - "id": runner_id, - "name": f"ministack-smoke-{instance_id}", - "os": "linux", - "status": "offline", - "busy": False, - "labels": [], - } - ], - }, -) -expect( - "GET", - state_path, - 200, - { - "id": runner_id, - "name": f"ministack-smoke-{instance_id}", - "os": "linux", - "status": "offline", - "busy": False, - "labels": [], - }, -) -expect("DELETE", state_path, 204) -PY -} - -configure_mock_runner_removed() { - runner_id="$1" - MOCKSERVER_URL="$mock_service_url" python3 - "$runner_id" <<'PY' -import json -import os -import sys -import urllib.request - -runner_id = sys.argv[1] -path = f"/api/v3/orgs/test-owner/actions/runners/{runner_id}" - -def control(path, method, payload): - request = urllib.request.Request( - f'{os.environ["MOCKSERVER_URL"]}{path}', - data=json.dumps(payload).encode("utf-8"), - headers={"Content-Type": "application/json"}, - method=method, - ) - with urllib.request.urlopen(request, timeout=10) as response: - if response.status not in (200, 201, 202): - raise RuntimeError(f'MockServer API rejected {method} {path} with HTTP {response.status}') - -control("/mockserver/clear", "PUT", {"httpRequest": {"method": "GET", "path": path}}) -control( - "/mockserver/expectation", - "PUT", - { - "httpRequest": {"method": "GET", "path": path}, - "httpResponse": { - "statusCode": 404, - "headers": {"Content-Type": ["application/json"]}, - "body": '{"message":"Not Found"}', - }, - }, -) -PY -} - -configure_empty_mock_runner_list() { - MOCKSERVER_URL="$mock_service_url" python3 - <<'PY' -import json -import os -import urllib.request - -path = "/api/v3/orgs/test-owner/actions/runners" - -def control(path, method, payload): - request = urllib.request.Request( - f'{os.environ["MOCKSERVER_URL"]}{path}', - data=json.dumps(payload).encode("utf-8"), - headers={"Content-Type": "application/json"}, - method=method, - ) - with urllib.request.urlopen(request, timeout=10) as response: - if response.status not in (200, 201, 202): - raise RuntimeError(f'MockServer API rejected {method} {path} with HTTP {response.status}') - -control("/mockserver/clear", "PUT", {"httpRequest": {"method": "GET", "path": path}}) -control( - "/mockserver/expectation", - "PUT", - { - "httpRequest": {"method": "GET", "path": path}, - "httpResponse": { - "statusCode": 200, - "headers": {"Content-Type": ["application/json"]}, - "body": '{"total_count":0,"runners":[]}', - }, - }, -) -PY -} - -assert_mock_runner_removed() { - runner_id="$1" - status_code=$(curl -sS --max-time 5 -o "$response_file" -w '%{http_code}' \ - "${mock_service_url}/api/v3/orgs/test-owner/actions/runners/${runner_id}") - if [ "$status_code" != 404 ]; then - echo "Expected GitHub API mock to return 404 for removed runner $runner_id, got HTTP $status_code." >&2 - sed -n '1,80p' "$response_file" >&2 - exit 1 - fi - printf ' [PASS] GitHub API mock reports runner %s removed (HTTP 404)\n' "$runner_id" -} - -wait_for_ec2_termination() { - instance_id="$1" - description="$2" - attempts=60 - while :; do - if state=$(aws --endpoint-url "$AWS_ENDPOINT_URL" ec2 describe-instances \ - --instance-ids "$instance_id" \ - --query 'Reservations[].Instances[].State.Name | [0]' \ - --output text 2>/dev/null); then - if [ -z "$state" ] || [ "$state" = "None" ] || [ "$state" = "terminated" ]; then - printf ' [PASS] MiniStack EC2 API reports %s terminated\n' "$description" - return - fi - else - state="describe-instances failed" - fi - attempts=$((attempts - 1)) - if [ "$attempts" -le 0 ]; then - echo "Timed out waiting for $description to terminate; current state: $state." >&2 - exit 1 - fi - sleep 2 - done -} - -invoke_lambda() { - function_name="$1" - payload="$2" - description="$3" - invocation_result=$(aws --endpoint-url "$AWS_ENDPOINT_URL" lambda invoke \ - --cli-binary-format raw-in-base64-out \ - --invocation-type RequestResponse \ - --function-name "$function_name" \ - --payload "$payload" \ - "$lambda_response_file" --output json) - if printf '%s' "$invocation_result" | grep -Fq '"FunctionError"'; then - echo "Lambda invocation returned FunctionError for $function_name." >&2 - exit 1 - fi - printf ' [PASS] %s (Lambda API accepted the request)\n' "$description" -} - -scale_up_runner_id=987654321 -configure_mock_runner_state "$scale_up_instance_id" "$scale_up_runner_id" -clear_mock_request_log -invoke_lambda "ministack-default-scale-down" '{"smokeMarker":"ministack-scale-up-scale-down"}' \ - "Scale-down Lambda invoked for the scale-up runner" -wait_for_log_event "/aws/lambda/ministack-default-scale-down" "ministack-scale-up-scale-down" \ - "Scale-down Lambda started processing the scale-up runner" -assert_scale_down_github_routes "$scale_up_runner_id" -configure_mock_runner_removed "$scale_up_runner_id" -assert_mock_runner_removed "$scale_up_runner_id" -wait_for_ec2_termination "$scale_up_instance_id" "the scale-up instance" -wait_for_optional_log_event "/aws/lambda/ministack-default-scale-down" "$scale_up_instance_id" \ - "Scale-down log recorded termination of the scale-up EC2 runner" - -clear_mock_request_log -send_webhook "$dynamic_fixture" "ministack-smoke-123457" -wait_for_log_event "/aws/lambda/ministack-default-webhook" "123457" \ - "Webhook Lambda received dynamic-label workflow job 123457" -wait_for_log_event "/aws/lambda/ministack-default-dispatch-to-runner" "123457" \ - "EventBridge invoked the dispatcher for dynamic-label workflow job 123457" -wait_for_log_event "/aws/lambda/ministack-default-scale-up" "123457" \ - "Dispatcher delivered dynamic-label workflow job 123457 through SQS to scale-up" -assert_scale_up_github_routes 123457 -wait_for_ec2_instance "scale-up-lambda" "a dynamic-label scale-up instance" -dynamic_scale_up_instance_id="$found_instance_id" -assert_ec2_runner_tags "$dynamic_scale_up_instance_id" "scale-up-lambda" \ - "the dynamic-label scale-up runner" -assert_ec2_instance_type "$dynamic_scale_up_instance_id" "m5.large" - -dynamic_scale_up_runner_id=987654323 -configure_mock_runner_state "$dynamic_scale_up_instance_id" "$dynamic_scale_up_runner_id" -clear_mock_request_log -invoke_lambda "ministack-default-scale-down" '{"smokeMarker":"ministack-dynamic-scale-up-scale-down"}' \ - "Scale-down Lambda invoked for the dynamic-label scale-up runner" -wait_for_log_event "/aws/lambda/ministack-default-scale-down" "ministack-dynamic-scale-up-scale-down" \ - "Scale-down Lambda started processing the dynamic-label scale-up runner" -assert_scale_down_github_routes "$dynamic_scale_up_runner_id" -configure_mock_runner_removed "$dynamic_scale_up_runner_id" -assert_mock_runner_removed "$dynamic_scale_up_runner_id" -wait_for_ec2_termination "$dynamic_scale_up_instance_id" "the dynamic-label scale-up instance" -wait_for_optional_log_event "/aws/lambda/ministack-default-scale-down" "$dynamic_scale_up_instance_id" \ - "Scale-down log recorded termination of the dynamic-label scale-up EC2 runner" - -echo "MiniStack smoke chain 1 passed: API Gateway -> webhook -> EventBridge -> dispatcher -> SQS -> scale-up without and with EC2 dynamic label -> GitHub API mock." - -configure_empty_mock_runner_list -clear_mock_request_log -invoke_lambda "ministack-default-pool" '{"poolSize":1,"type":"ec2"}' \ - "Pool Lambda invoked to maintain one runner" -assert_pool_github_routes -wait_for_log_event "/aws/lambda/ministack-default-pool" "topped up with 1 runners" \ - "Pool Lambda requested one runner" -wait_for_ec2_instance "pool-lambda" "a pool instance" -pool_instance_id="$found_instance_id" -assert_ec2_runner_tags "$pool_instance_id" "pool-lambda" "the pool runner" - -pool_runner_id=987654322 -configure_mock_runner_state "$pool_instance_id" "$pool_runner_id" -clear_mock_request_log -invoke_lambda "ministack-default-scale-down" '{"smokeMarker":"ministack-pool-scale-down"}' \ - "Scale-down Lambda invoked for the pool runner" -wait_for_log_event "/aws/lambda/ministack-default-scale-down" "ministack-pool-scale-down" \ - "Scale-down Lambda started processing the pool runner" -assert_scale_down_github_routes "$pool_runner_id" -configure_mock_runner_removed "$pool_runner_id" -assert_mock_runner_removed "$pool_runner_id" -wait_for_ec2_termination "$pool_instance_id" "the pool instance" -wait_for_optional_log_event "/aws/lambda/ministack-default-scale-down" "$pool_instance_id" \ - "Scale-down log recorded termination of the pool EC2 runner" - -echo "MiniStack smoke chain 2 passed: pool -> GitHub API mock -> EC2 runner creation -> scale-down -> GitHub API mock -> EC2 termination." -echo "MiniStack smoke tests passed: both lifecycle chains completed." diff --git a/tests/ministack/workflow_job_event.json b/tests/ministack/workflow_job_event.json deleted file mode 100644 index 6200b40fb1..0000000000 --- a/tests/ministack/workflow_job_event.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "action": "queued", - "workflow_job": { - "id": 123456, - "run_id": 654321, - "run_url": "https://github.example.invalid/test-owner/test-repo/actions/runs/654321", - "url": "https://github.example.invalid/test-owner/test-repo/actions/jobs/123456", - "html_url": "https://github.example.invalid/test-owner/test-repo/actions/jobs/123456", - "status": "queued", - "conclusion": null, - "name": "ministack-smoke", - "labels": ["self-hosted", "linux", "x64", "default", "example"] - }, - "repository": { - "id": 1, - "name": "test-repo", - "full_name": "test-owner/test-repo", - "owner": { - "login": "test-owner", - "type": "Organization" - } - }, - "installation": { - "id": 123 - } -} diff --git a/tests/multi-runner-orchestration/Dockerfile b/tests/multi-runner-orchestration/Dockerfile new file mode 100644 index 0000000000..0e1b6f82da --- /dev/null +++ b/tests/multi-runner-orchestration/Dockerfile @@ -0,0 +1,18 @@ +FROM eclipse-temurin:21-jdk AS certificate +RUN keytool -genkeypair \ + -alias wiremock \ + -keyalg RSA \ + -keysize 2048 \ + -validity 3650 \ + -storetype PKCS12 \ + -keystore /tmp/wiremock.p12 \ + -storepass changeit \ + -keypass changeit \ + -dname "CN=wiremock" \ + -ext "SAN=dns:wiremock,dns:localhost,ip:127.0.0.1" \ + -noprompt + +FROM wiremock/wiremock:3.13.2 +COPY --from=certificate /tmp/wiremock.p12 /home/wiremock/wiremock.p12 +COPY mappings /home/wiremock/mappings +ENTRYPOINT ["/docker-entrypoint.sh", "--https-port", "8443", "--https-keystore", "/home/wiremock/wiremock.p12", "--keystore-password", "changeit", "--key-manager-password", "changeit", "--keystore-type", "PKCS12", "--global-response-templating"] diff --git a/tests/multi-runner-orchestration/README.md b/tests/multi-runner-orchestration/README.md new file mode 100644 index 0000000000..42f7b4a5e2 --- /dev/null +++ b/tests/multi-runner-orchestration/README.md @@ -0,0 +1,101 @@ +# WireMock for `terraform-aws-github-runner` Lambdas + +This standalone WireMock instance contains happy-path stubs for the GitHub REST and GitHub Actions scale-set calls found under `lambdas/`. It accepts both the GitHub Cloud path form (`/repos/...`) and the GHES form (`/api/v3/repos/...`). It is a deterministic test double, not a complete GitHub emulator. + +## Start it + +From the repository root: + +```sh +docker compose -f tests/multi-runner-orchestration/docker-compose.yml up --build --detach +curl --fail http://localhost:8080/__admin/mappings +``` + +The server listens on HTTP `8080` and HTTPS `8443`. The self-signed certificate is only for local/CI testing. Scale-set protocol responses contain the same HTTPS base URL used for its registration handshake. The scale-set test config must set `sslVerify: false` for this test certificate. + +To stop it: + +```sh +docker compose -f tests/multi-runner-orchestration/docker-compose.yml down +``` + +## MiniStack fixtures + +`docker compose -f tests/multi-runner-orchestration/docker-compose.yml up --build` runs `.ci/build.sh` to build the Lambda distributions, starts MiniStack, seeds AMIs and the `scale-set-controller` ECR repository/policy, builds the controller for `linux/amd64` and `linux/arm64`, and pushes it to MiniStack. The Compose services use `linux/amd64`; CI enables QEMU to build the ARM64 image on its AMD64 runner. + +The controller image build is configured directly in the `docker-buildx` Compose service; it is not a fixture in `fixtures/ministack-fixtures.json`. Compose creates the `multi-runner-orchestration` network and attaches MiniStack's nested ECS containers to it. The AWS CLI uses `ministack:4566` to reach MiniStack. The job resolves MiniStack's network IP for BuildKit's registry push, since BuildKit does not resolve Compose service DNS in this setup; it configures that registry as plain HTTP. + +## Programmatic MiniStack smoke test + +Compose owns the environment lifecycle: `up` starts WireMock and MiniStack, seeds AWS fixtures, builds the Lambda archives and scale-set controller image, applies `examples/multi-runner-orchestration`, then runs the `smoke-tests` job container. That job first registers a scale-set task-definition revision with dummy MiniStack credentials, updates the ECS service, then runs the Node.js smoke suite: + +```sh +docker compose -f tests/multi-runner-orchestration/docker-compose.yml up --build --detach +docker compose -f tests/multi-runner-orchestration/docker-compose.yml wait aws-fixtures lambda-builder docker-buildx terraform-apply smoke-tests +``` + +The suite contains four named Node.js test cases: webhook scale-up and scale-down with standard labels, webhook scale-up and scale-down with a dynamic EC2 instance-type label, pool runner creation and scale-down, and scale-set scale-up and scale-down. The webhook and pool cases assert the EC2 runner count before launch, after launch, and after cleanup; the dynamic-label case also verifies the requested `m5.large` override in the scale-up logs. The scale-set case checks zero runners at baseline, one runner after scale-up, and a termination returning the count to zero. All cases assert the corresponding Lambda or controller CloudWatch logs. WireMock API responses are test fixtures, so the tests do not assert those routes. Run the smoke cases with the concise reporter: + +MiniStack does not start the EC2 runner agent, so the webhook and pool tests invoke scale-down twice: the first pass marks the unregistered instance orphaned, and the next pass confirms it is absent from GitHub and terminates it. + +```sh +node --test --test-concurrency=1 \ + --test-reporter=./tests/multi-runner-orchestration/smoke/reporter.mjs \ + tests/multi-runner-orchestration/smoke/*.test.mjs +``` + +The scale-set check registers temporary ECS task-definition revisions with the requested minimum runner count and dummy MiniStack credentials, then updates the local ECS service to exercise reconciliation. The Compose MiniStack container owns the ephemeral AWS state. + +## Route the Lambdas to the mock + +The Compose stack uses the named `multi-runner-orchestration_default` network. MiniStack and its nested ECS containers join that network, so use service DNS names (`ministack` and `github-api`) from containers. For the control-plane and termination-watcher Octokit clients, set `GHES_URL=http://github-api:8080`. Their existing GHES URL handling appends `/api/v3`, which these mappings accept. The webhook itself is inbound to the webhook Lambda; send your signed `workflow_job` fixture to that Lambda's local/API Gateway endpoint. The subsequent GitHub API calls are handled by this mock. + +For the scale-set controller, use a test config with `githubConfigUrl: "https://localhost:8443/acme"` for a host-run test (or `https://github-api:8443/acme` from the Compose network), `forceGhes: true`, and `sslVerify: false`. The mock supplies the scale-set admin handshake, scale-set/session responses, and an empty long-poll response. The controller should then remain healthy and poll until your test stops it. + +Scale-set stubs follow `lambdas/libs/github-actions-scale-set/src/endpoints.ts`, `client.ts`, and `message-session-client.ts`. They accept the tenant-scoped `/{tenant}/_apis/...` paths returned by the registration handshake and return the `/messages?sessionId=...` queue URL used by the MiniStack initializer fixture. The fixture currently uses tenant `123`, runner group `48`, scale set `223`, and runner `321`. + +The mock does not validate GitHub App JWTs, installation permissions, runner token validity, or GitHub's authorization policy. It accepts any request matching a mapping. It also does not implement the runner agent's actual Actions service protocol; the scale-set routes here support the Lambda controller's HTTP client only. + +## Covered GitHub API requests + +| Area | Requests represented in `mappings/` | +|---|---| +| GitHub App auth | `GET /app/installations`; `GET /orgs/{org}/installation`; `GET /repos/{owner}/{repo}/installation`; `POST /app/installations/{id}/access_tokens` | +| Registration and deregistration | Org/repo/enterprise registration and remove-token requests; list/get/delete runner requests | +| Workflow jobs | `GET /repos/{owner}/{repo}/actions/jobs/{job_id}` | +| Runner groups and JIT | `GET /orgs/{org}/actions/runner-groups`; org/repo/enterprise JIT config requests | +| Runner release syncer | `GET /repos/actions/runner/releases/latest`; mock asset download under `/downloads/` | +| Actions scale-set API | Runner registration handshake; runner group and scale-set get/list/create/update/delete; JIT config; runner list/get/remove | +| Scale-set sessions | Session create/refresh/close, queue poll/acknowledge, and acquire-jobs | + +The enterprise routes are included as fixtures for forward-looking coverage. The current control-plane's classic runner path primarily uses organization/repository routes. The enterprise runner registration APIs have different token-auth requirements in GitHub, so the mock does not prove that an app token will be accepted by a real enterprise account. + +## CI usage + +Build and run this image before the Lambda integration step, then set the API URL/environment above in that step. For example, from a GitHub Actions job running on the host: + +```yaml +- name: Start GitHub API mock + run: | + docker build -t local/github-api-mock ./tests/multi-runner-orchestration + docker run -d --name github-api-mock \ + -p 8080:8080 -p 8443:8443 \ + local/github-api-mock + +- name: Run Lambda smoke test + env: + GHES_URL: http://127.0.0.1:8080 + run: + +- name: Stop GitHub API mock + if: always() + run: docker rm -f github-api-mock +``` + +If the Lambdas run in job containers instead of on the host, put them and the mock on the same user-defined Docker network. Use `GHES_URL=http://github-api-mock:8080` for Octokit and `githubConfigUrl=https://github-api-mock:8443/acme` for the scale-set controller. The handshake/session mappings derive the queue URL from the incoming request base URL. + +## Limits and current code routing + +Most Lambdas already support a configurable GHES API base (`GHES_URL` or the scale-set `githubConfigUrl`), so the mock can be used without changing their request code. `gh-agent-syncer` currently constructs a default `Octokit` client, so its release API request still targets `api.github.com` unless that Lambda is given an API base URL override or the test adds a host-level redirect. The mock includes the endpoint and download fixture, but it cannot intercept that hard-coded client by itself. The release fixture is the Linux x64 archive used by the default syncer settings; adjust its mapping for a different OS/architecture. + +Unmatched requests return WireMock's normal 404 with request-match diagnostics. Inspect received calls at `http://localhost:8080/__admin/requests`. diff --git a/tests/multi-runner-orchestration/buildkitd.toml b/tests/multi-runner-orchestration/buildkitd.toml new file mode 100644 index 0000000000..52d6d3b0ba --- /dev/null +++ b/tests/multi-runner-orchestration/buildkitd.toml @@ -0,0 +1,5 @@ +insecure-entitlements = ["network.host"] + +[worker.oci] + max-parallelism = 1 + networkMode = "host" diff --git a/tests/multi-runner-orchestration/docker-compose.yml b/tests/multi-runner-orchestration/docker-compose.yml new file mode 100644 index 0000000000..e2a7329b71 --- /dev/null +++ b/tests/multi-runner-orchestration/docker-compose.yml @@ -0,0 +1,175 @@ +services: + github-api: + build: . + pull_policy: build + platform: linux/amd64 + ports: + - "8080:8080" + - "8443:8443" + ministack: + image: ghcr.io/ministackorg/ministack:v1.5.30@sha256:9019a8ab2531163fc6c3119a277a181faadcd2cde8fd20272dc8ec0ccda154b9 + platform: linux/amd64 + ports: + - "4566:4566" + extra_hosts: + - "host.docker.internal:host-gateway" + volumes: + - /var/run/docker.sock:/var/run/docker.sock + environment: + MINISTACK_ACCOUNT_ID: "000000000000" + MINISTACK_REGION: eu-west-1 + DOCKER_NETWORK: multi-runner-orchestration_default + aws-fixtures: + image: amazon/aws-cli:2.27.41@sha256:bc6b7bba44ce38f9604ede49c584824af919047ea03fbcc7c7610671fdef95d8 + platform: linux/amd64 + depends_on: + ministack: + condition: service_started + entrypoint: ["/usr/bin/python"] + command: ["/workspace/tests/multi-runner-orchestration/fixtures/seed_ministack_fixtures.py", "--manifest", "/workspace/tests/multi-runner-orchestration/fixtures/ministack-fixtures.json", "--type", "ami", "--type", "ecr-repository"] + volumes: + - ../..:/workspace:ro + environment: + AWS_ACCESS_KEY_ID: "000000000000" + AWS_SECRET_ACCESS_KEY: test-only + AWS_DEFAULT_REGION: eu-west-1 + AWS_EC2_METADATA_DISABLED: "true" + MINISTACK_ENDPOINT_URL: http://ministack:4566 + lambda-builder: + image: docker:27-cli + platform: linux/amd64 + working_dir: /workspace + entrypoint: ["/bin/sh", "-ec"] + command: + - | + apk add --no-cache bash git + cd /workspace + OUTPUT_DIR=/workspace/lambda_output bash .ci/build.sh + volumes: + - ../..:/workspace + - /var/run/docker.sock:/var/run/docker.sock + environment: + DOCKER_HOST: unix:///var/run/docker.sock + docker-buildx: + image: docker:27-cli + platform: linux/amd64 + depends_on: + ministack: + condition: service_started + aws-fixtures: + condition: service_completed_successfully + working_dir: /workspace + entrypoint: ["/bin/sh", "-ec"] + command: + - | + apk add --no-cache aws-cli + ecr_password="$$(aws ecr get-login-password --endpoint-url "$${AWS_ENDPOINT_URL}" --region "$${AWS_DEFAULT_REGION}")" + if [ -z "$$ecr_password" ]; then + echo "MiniStack returned an empty ECR login password" >&2 + exit 1 + fi + ministack_ip="$$(getent hosts ministack | awk 'NR == 1 { print $1 }')" + if [ -z "$$ministack_ip" ]; then + echo "Could not resolve MiniStack on the Compose network" >&2 + exit 1 + fi + export MINISTACK_REGISTRY="$$ministack_ip:4566" + docker_config="$$(mktemp -d)" + export DOCKER_CONFIG="$$docker_config" + cp /workspace/tests/multi-runner-orchestration/buildkitd.toml "$$DOCKER_CONFIG/buildkitd.toml" + cat >> "$$DOCKER_CONFIG/buildkitd.toml" < "$$DOCKER_CONFIG/config.json" + docker buildx rm runner-smoke-builder >/dev/null 2>&1 || true + trap 'docker buildx rm runner-smoke-builder >/dev/null 2>&1 || true; rm -rf "$$DOCKER_CONFIG"' EXIT + docker buildx create \ + --name runner-smoke-builder \ + --driver docker-container \ + --driver-opt network=multi-runner-orchestration_default \ + --buildkitd-config "$$DOCKER_CONFIG/buildkitd.toml" \ + --use --bootstrap + docker buildx build --builder runner-smoke-builder \ + --network host --allow network.host \ + --platform linux/amd64,linux/arm64 \ + --target runtime \ + --file /workspace/lambdas/services/scale-set/Dockerfile \ + --tag "$${MINISTACK_REGISTRY}/scale-set-controller:latest" \ + --provenance=false --push /workspace + volumes: + - ../..:/workspace:ro + - /var/run/docker.sock:/var/run/docker.sock + environment: + DOCKER_HOST: unix:///var/run/docker.sock + AWS_ACCESS_KEY_ID: "000000000000" + AWS_SECRET_ACCESS_KEY: test-only + AWS_DEFAULT_REGION: eu-west-1 + AWS_EC2_METADATA_DISABLED: "true" + AWS_ENDPOINT_URL: http://ministack:4566 + DOCKER_BUILDKIT: 1 + terraform-apply: + image: hashicorp/terraform:1.17 + platform: linux/amd64 + depends_on: + github-api: + condition: service_started + aws-fixtures: + condition: service_completed_successfully + lambda-builder: + condition: service_completed_successfully + docker-buildx: + condition: service_completed_successfully + working_dir: /workspace + entrypoint: ["/bin/sh", "-ec"] + command: + - >- + terraform -chdir=/workspace/examples/multi-runner-orchestration init -input=false; + terraform -chdir=/workspace/examples/multi-runner-orchestration apply -auto-approve -input=false + -var-file=/workspace/tests/multi-runner-orchestration/fixtures/multi-runner-orchestration.tfvars + extra_hosts: + - "host.docker.internal:host-gateway" + volumes: + - ../..:/workspace + environment: + AWS_ACCESS_KEY_ID: "000000000000" + AWS_SECRET_ACCESS_KEY: test-only + AWS_DEFAULT_REGION: eu-west-1 + AWS_REGION: eu-west-1 + AWS_EC2_METADATA_DISABLED: "true" + AWS_ENDPOINT_URL: http://ministack:4566 + TF_IN_AUTOMATION: "true" + TF_INPUT: "false" + + smoke-tests: + image: hashicorp/terraform:1.17 + platform: linux/amd64 + depends_on: + terraform-apply: + condition: service_completed_successfully + working_dir: /workspace + entrypoint: ["/bin/sh", "-ec"] + command: + - | + apk add --no-cache aws-cli nodejs + node tests/multi-runner-orchestration/smoke/prepare-scale-set-credentials.mjs + node --test --test-concurrency=1 \ + --test-reporter=./tests/multi-runner-orchestration/smoke/reporter.mjs \ + tests/multi-runner-orchestration/smoke/*.test.mjs + volumes: + - ../..:/workspace + environment: + AWS_ACCESS_KEY_ID: "000000000000" + AWS_SECRET_ACCESS_KEY: test-only + AWS_DEFAULT_REGION: eu-west-1 + AWS_REGION: eu-west-1 + AWS_EC2_METADATA_DISABLED: "true" + AWS_ENDPOINT_URL: http://ministack:4566 + TF_IN_AUTOMATION: "true" + TF_INPUT: "false" + +networks: + default: + name: multi-runner-orchestration_default diff --git a/tests/multi-runner-orchestration/fixtures/ministack-fixtures.json b/tests/multi-runner-orchestration/fixtures/ministack-fixtures.json new file mode 100644 index 0000000000..7bf7476e55 --- /dev/null +++ b/tests/multi-runner-orchestration/fixtures/ministack-fixtures.json @@ -0,0 +1,38 @@ +{ + "fixtures": [ + { + "type": "ami", + "name": "multi-runner-orchestration", + "description": "MiniStack test-only AMI", + "architecture": "x86_64", + "root_device_name": "/dev/xvda", + "virtualization_type": "hvm", + "image_location": "alpine:3.20" + }, + { + "type": "ecr-repository", + "name": "scale-set-controller", + "image_tag_mutability": "MUTABLE", + "image_scanning_configuration": { + "scanOnPush": false + }, + "policy": { + "Version": "2012-10-17", + "Statement": [ + { + "Sid": "AllowAccountPull", + "Effect": "Allow", + "Principal": { + "AWS": "arn:aws:iam::000000000000:root" + }, + "Action": [ + "ecr:BatchCheckLayerAvailability", + "ecr:BatchGetImage", + "ecr:GetDownloadUrlForLayer" + ] + } + ] + } + } + ] +} diff --git a/tests/multi-runner-orchestration/fixtures/multi-runner-orchestration.tfvars b/tests/multi-runner-orchestration/fixtures/multi-runner-orchestration.tfvars new file mode 100644 index 0000000000..2046fb1448 --- /dev/null +++ b/tests/multi-runner-orchestration/fixtures/multi-runner-orchestration.tfvars @@ -0,0 +1,48 @@ +aws_region = "eu-west-1" +environment = "multi-runner-webhook" + +runners_lambda_zip = "../../lambda_output/runners.zip" +webhook_lambda_zip = "../../lambda_output/webhook.zip" + +github = { + url = "https://host.docker.internal:8443" + ssl_verify = false + runner_owner = "example" + registration_level = "organization" +} + +scale_set = { + name = "linux-scale-set" + runner_group_name = "experimental-euw1-sl-cicd-forge-emu" + min_runners = 1 + container = { + image = "localhost:4566/scale-set-controller:latest" + } + ecs = { + task = { + architecture = "ARM64" + } + } +} + +github_app = { + id = "123" + installation_id = "123" + key_base64 = "LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCk1JSUV2UUlCQURBTkJna3Foa2lHOXcwQkFRRUZBQVNDQktjd2dnU2pBZ0VBQW9JQkFRRFM3OUJ1dU9rVTJ2V2EKT3J2aHBWNENON1g3dm5qdXBuWlhSZFNvWGVQNWxKMEs4dHhrcnpIaEVyT3JobGJZd1JoUEJjaFFBVzJtemQ4RgpqNytTVGliL0VFd1ZXZVFWUWZMbTFwMDRyR3FNc1B0WkcvcVFWelNwZTkyRWhYTnZzWFRmS0c1UjNYTE5OdS9uCm9PNmRablJvM2U0TENYd1BaMGdMR3Q3K1hrZk9QQU5LZDZnSW1pS1hiQXAxV1VzK0JrdXI1TTBXNjgvWjd0WlgKYnBTcHV3MWVwc2RwM2FpYzM2V2FmVWloZDNMQUR5UFE5c1JkZ2E2VXBaRFJTUEE3VWlUd3FwTlluY2ZEODFkKwpFWnI3ZlNEcjRqb2s5S0F1SzRQWTBML3lMVGlBVEg1aWFKcWhxT252UDN4MFBOSzd4aEw4SHZpVThON0dqNHlhClQ2eDdheC90QWdNQkFBRUNnZ0VBTGZ0SmdpMXNYOWU0Vk1uTFpZMnRIOG9oc1ZKTTNlY3JJbXl1UmhLTGpQODAKdWF1RjJDdnBzV0RvaWpKRGNjWXNCS1BqemU3MXhuZ1h6d2hPTURnNW4zM2Z2QUxIcWN1aHhlalFSb2V0UldTYQpNZ3RibEVYeVRLVGIvZFN5ZkkxZ1J2b3pERVQ4RHRpVU5QZTd0Rk9lZWpLd3ZyYXZWTk5Fbk9rYlpiTkxMMTAzCndtZS9ObDBjTDhVN1lHaGtLRm0wMmZjTVpaWENiT3puYzQ1UFFXNmVJaUhJWW1sZ056Q2lIenRvZS9UZ3JKekoKS1dsMzU5WUM4VGZZd3AydDQ5ZTVSS3BKN0lRY3A5NUEvVFBHVXVHaEMxRkZEZlh0eUgySlYxRGJOQ3RhVjNRcwp3M3g1LzErZDRoN0dxUjFzTW01V0NMUmduSjhYOU0xRm5IMlAvYzAxK1FLQmdRRDBBcWFPQkJ0ay81WFl4YnowCmM1R25iOXdDa0xGaVp1WElGbnV0d0JTaEprU055Q3NFbDB5UHBrV0NnTk1zQ0xtaUNldGpSUm0zMFZXSC9jeUMKekxxb0FXNU5VTmorK3dmZHcrM1hHODlkVjEzRWxLL1VKNDE0RkR4NFZ4NE4rTWtHaHVkZno2dXdUbFVJYXFDVAo0czIrSko4NDBraUIxRUUycFBoYmd4b2c5UUtCZ1FEZFRTT0QxcEl1b29YR0pBQTZxWUVoMWtOb1g1Q0IzOE9TCk92TThwTFV1djN6cmp1QzdQbm5CZEF5aEdGeG9qcFlQbG1pNFIrWDJraGVPd0ZscTM2V3hDOERHeEN2L0h2ODMKV0lLdTc4c25MQmlLMlBzdk9vdmNZZ1BRVVBkZWV4ODkwcU92Tjh1MzV0NjZuNnliOVlCaWNSd01tVFlJS2Irdwp4ZmEzenBWSUdRS0JnRTZaOS9yeFdiMjhFeElFQ0cwYU9VWWRZMXEwTDEvS1U2U1RrWDJUelZGbjJWZHVybVRKCk5CRmw5eWdseFdCKzR3Vk9lck84azU5QjRISnhFQXJtVmc0K2Y4Q0tGWXgxWXNCT0YvUmQ4ZnRZWkUwcHRFWDgKQTdjMWxaRVNSdzBySWQ1bllHcmRvNm1rWWdIWER5ajBPMkJQdmJaNURvQTcxbEg2N0VIV1JubHRBb0dBVUlTeAppRlY0eUpqL2ZWRTUrQjRXU3Q1dmxqazNGZG41aVdvRGVDd2hkQW5lQk1MdWg1STZzS1Z0dHV0cksrdTlNNXl4CksyT1Zpem9xY2hrUUw2K0IxTUlHK1FzRlN3VFhMU2I2SFhaSC8wK3UrazNxVG92SnVYeThFSDUxRXA1UGhxbnUKUG1GWnNPYkNZVHFaamdFc0hpVXpaRzRXVjdBQSsrdDdVZk5MSTNrQ2dZRUFra1VsV3JxdG1rTFV2TlZ4d0tMZwo5eStWcTRrRGRSR2hta3VhQmNFK0NkOW14bVpDbzFMeHU2bWxJRHlvQzV5TVpLVjlNQndOeW5kQTc4UkIxNEtaCkc4aHRCazROd2FPUWlZYmRLbzJMQ2FVSWdyVlorTk1SNHZvU0NNeThHNG5TeUhRWmJ1b1lXdWU1c3JUSFdEcW8KNElaOVJyRXd5ODR4SHl5RE0xVGN1VG89Ci0tLS0tRU5EIFBSSVZBVEUgS0VZLS0tLS0K" + webhook_secret = "ministack-webhook-secret" +} + +compute_provider = { + aws = { + ec2 = { + instance_types = ["m7a.large", "m5.large"] + ami = { + filter = { + name = ["multi-runner-orchestration"] + state = ["available"] + } + owners = ["self"] + } + } + } +} diff --git a/tests/multi-runner-orchestration/fixtures/seed_ministack_fixtures.py b/tests/multi-runner-orchestration/fixtures/seed_ministack_fixtures.py new file mode 100644 index 0000000000..e61c118362 --- /dev/null +++ b/tests/multi-runner-orchestration/fixtures/seed_ministack_fixtures.py @@ -0,0 +1,198 @@ +#!/usr/bin/env python +"""Seed declared fixtures into a running MiniStack instance.""" + +from __future__ import print_function + +import argparse +import json +import os +import subprocess +import sys +import time + +try: + from urllib.request import urlopen +except ImportError: # Python 2.7 in the AWS CLI container image. + from urllib2 import URLError, urlopen +else: + from urllib.error import URLError + +def aws(*args): + endpoint = os.environ.get( + "MINISTACK_ENDPOINT_URL", + os.environ.get("AWS_ENDPOINT_URL", "http://localhost:4566"), + ) + region = os.environ.get("AWS_DEFAULT_REGION", "eu-west-1") + command = ["aws", "--endpoint-url", endpoint, "--region", region] + list(args) + process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + stdout, stderr = process.communicate() + if process.returncode: + output = (stdout or b"") + (stderr or b"") + if hasattr(output, "decode"): + output = output.decode("utf-8") + sys.stderr.write(output) + raise subprocess.CalledProcessError(process.returncode, command, output) + if hasattr(stdout, "decode"): + stdout = stdout.decode("utf-8") + return stdout.strip() + + +def wait_for_ministack(attempts=60): + endpoint = os.environ.get( + "MINISTACK_ENDPOINT_URL", + os.environ.get("AWS_ENDPOINT_URL", "http://localhost:4566"), + ) + health_url = endpoint.rstrip("/") + "/_ministack/health" + for unused in range(attempts): + try: + response = urlopen(health_url, timeout=2) + response.close() + print("MiniStack is ready at {0}".format(endpoint), file=sys.stderr) + return + except (URLError, IOError): + time.sleep(1) + raise RuntimeError("MiniStack did not become ready at {0}".format(endpoint)) + + +def seed_ami(fixture): + name = fixture["name"] + ami_id = aws( + "ec2", + "describe-images", + "--owners", + "self", + "--filters", + "Name=name,Values={0}".format(name), + "Name=state,Values=available", + "--query", + "Images[0].ImageId", + "--output", + "text", + ) + if ami_id != "None": + print("Fixture AMI already exists: {0} ({1})".format(name, ami_id), file=sys.stderr) + return None + + ami_id = aws( + "ec2", + "register-image", + "--name", + name, + "--description", + fixture.get("description", "MiniStack test-only AMI"), + "--architecture", + fixture["architecture"], + "--root-device-name", + fixture.get("root_device_name", "/dev/xvda"), + "--virtualization-type", + fixture.get("virtualization_type", "hvm"), + "--image-location", + fixture["image_location"], + "--query", + "ImageId", + "--output", + "text", + ) + print("Created fixture AMI: {0} ({1})".format(name, ami_id), file=sys.stderr) + return ami_id + + +def seed_ecr_repository(fixture): + name = fixture["name"] + try: + aws( + "ecr", + "create-repository", + "--repository-name", + name, + "--image-tag-mutability", + fixture.get("image_tag_mutability", "IMMUTABLE"), + "--image-scanning-configuration", + "scanOnPush={0}".format(str(fixture.get("image_scanning_configuration", {}).get("scanOnPush", False)).lower()), + "--output", + "json", + ) + print("Created fixture ECR repository: {0}".format(name), file=sys.stderr) + except subprocess.CalledProcessError as error: + output = error.output or "" + if hasattr(output, "decode"): + output = output.decode("utf-8") + if "RepositoryAlreadyExistsException" not in output: + raise + print("Fixture ECR repository already exists: {0}".format(name), file=sys.stderr) + + policy = fixture.get("policy") + if policy: + aws( + "ecr", + "set-repository-policy", + "--repository-name", + name, + "--policy-text", + json.dumps(policy, separators=(",", ":")), + ) + print("Applied fixture ECR repository policy: {0}".format(name), file=sys.stderr) + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + default_manifest = os.path.join(os.path.dirname(os.path.abspath(__file__)), "ministack-fixtures.json") + parser.add_argument("--manifest", default=default_manifest) + parser.add_argument("--endpoint-url", help="MiniStack endpoint override") + parser.add_argument("--region", help="AWS region override") + parser.add_argument( + "--type", + dest="fixture_types", + action="append", + help="Seed only fixtures of this type; may be repeated", + ) + parser.add_argument( + "--name", + dest="fixture_names", + action="append", + help="Seed only fixtures with this name; may be repeated", + ) + parser.add_argument( + "--created-ids", + action="store_true", + help="Print IDs of fixtures created during this run, for caller cleanup", + ) + args = parser.parse_args() + + if args.endpoint_url: + os.environ["MINISTACK_ENDPOINT_URL"] = args.endpoint_url + if args.region: + os.environ["AWS_DEFAULT_REGION"] = args.region + + wait_for_ministack() + with open(args.manifest, "r") as manifest_file: + manifest = json.load(manifest_file) + fixtures = manifest.get("fixtures", []) + selected = [] + for fixture in fixtures: + fixture_type = fixture.get("type") + if args.fixture_types and fixture_type not in args.fixture_types: + continue + if args.fixture_names and fixture.get("name") not in args.fixture_names: + continue + selected.append(fixture) + + created_ids = [] + for fixture in selected: + fixture_type = fixture.get("type") + if fixture_type == "ami": + created_id = seed_ami(fixture) + if created_id: + created_ids.append(created_id) + elif fixture_type == "ecr-repository": + seed_ecr_repository(fixture) + else: + raise ValueError("Unsupported MiniStack fixture type: {0!r}".format(fixture_type)) + + if args.created_ids: + print("\n".join(created_ids)) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tests/multi-runner-orchestration/mappings/01-github-app---list-installations.json b/tests/multi-runner-orchestration/mappings/01-github-app---list-installations.json new file mode 100644 index 0000000000..00b817393f --- /dev/null +++ b/tests/multi-runner-orchestration/mappings/01-github-app---list-installations.json @@ -0,0 +1,26 @@ +{ + "name": "GitHub App - list installations", + "request": { + "method": "GET", + "urlPathPattern": "(?:/api/v3)?/app/installations" + }, + "response": { + "status": 200, + "jsonBody": [ + { + "id": 1111, + "app_id": 999, + "target_id": 2222, + "target_type": "Organization", + "account": { + "login": "acme", + "id": 2222, + "type": "Organization" + } + } + ], + "headers": { + "Content-Type": "application/json" + } + } +} diff --git a/tests/multi-runner-orchestration/mappings/02-github-app---organization-installation.json b/tests/multi-runner-orchestration/mappings/02-github-app---organization-installation.json new file mode 100644 index 0000000000..02ce6141f9 --- /dev/null +++ b/tests/multi-runner-orchestration/mappings/02-github-app---organization-installation.json @@ -0,0 +1,24 @@ +{ + "name": "GitHub App - organization installation", + "request": { + "method": "GET", + "urlPathPattern": "(?:/api/v3)?/orgs/[^/]+/installation" + }, + "response": { + "status": 200, + "jsonBody": { + "id": 123, + "app_id": 999, + "target_id": 123, + "target_type": "Organization", + "account": { + "login": "acme", + "id": 123, + "type": "Organization" + } + }, + "headers": { + "Content-Type": "application/json" + } + } +} diff --git a/tests/multi-runner-orchestration/mappings/03-github-app---repository-installation.json b/tests/multi-runner-orchestration/mappings/03-github-app---repository-installation.json new file mode 100644 index 0000000000..1ba1540601 --- /dev/null +++ b/tests/multi-runner-orchestration/mappings/03-github-app---repository-installation.json @@ -0,0 +1,24 @@ +{ + "name": "GitHub App - repository installation", + "request": { + "method": "GET", + "urlPathPattern": "(?:/api/v3)?/repos/[^/]+/[^/]+/installation" + }, + "response": { + "status": 200, + "jsonBody": { + "id": 1111, + "app_id": 999, + "target_id": 2222, + "target_type": "Organization", + "account": { + "login": "acme", + "id": 2222, + "type": "Organization" + } + }, + "headers": { + "Content-Type": "application/json" + } + } +} diff --git a/tests/multi-runner-orchestration/mappings/04-github-app---create-installation-access-token.json b/tests/multi-runner-orchestration/mappings/04-github-app---create-installation-access-token.json new file mode 100644 index 0000000000..606fdc125d --- /dev/null +++ b/tests/multi-runner-orchestration/mappings/04-github-app---create-installation-access-token.json @@ -0,0 +1,22 @@ +{ + "name": "GitHub App - create installation access token", + "request": { + "method": "POST", + "urlPathPattern": "(?:/api/v3)?/app/installations/\\d+/access_tokens" + }, + "response": { + "status": 201, + "jsonBody": { + "token": "ghs_wiremock_installation_token", + "expires_at": "2099-01-01T00:00:00Z", + "permissions": { + "actions": "write", + "contents": "read", + "metadata": "read" + } + }, + "headers": { + "Content-Type": "application/json" + } + } +} diff --git a/tests/multi-runner-orchestration/mappings/05-runner---org-registration-token.json b/tests/multi-runner-orchestration/mappings/05-runner---org-registration-token.json new file mode 100644 index 0000000000..2f74c75c32 --- /dev/null +++ b/tests/multi-runner-orchestration/mappings/05-runner---org-registration-token.json @@ -0,0 +1,17 @@ +{ + "name": "Runner - org registration token", + "request": { + "method": "POST", + "urlPathPattern": "(?:/api/v3)?/orgs/[^/]+/actions/runners/registration-token" + }, + "response": { + "status": 201, + "jsonBody": { + "token": "runner-registration-token-mock", + "expires_at": "2099-01-01T00:00:00Z" + }, + "headers": { + "Content-Type": "application/json" + } + } +} diff --git a/tests/multi-runner-orchestration/mappings/06-runner---repo-registration-token.json b/tests/multi-runner-orchestration/mappings/06-runner---repo-registration-token.json new file mode 100644 index 0000000000..ead84d0ba5 --- /dev/null +++ b/tests/multi-runner-orchestration/mappings/06-runner---repo-registration-token.json @@ -0,0 +1,17 @@ +{ + "name": "Runner - repo registration token", + "request": { + "method": "POST", + "urlPathPattern": "(?:/api/v3)?/repos/[^/]+/[^/]+/actions/runners/registration-token" + }, + "response": { + "status": 201, + "jsonBody": { + "token": "runner-registration-token-mock", + "expires_at": "2099-01-01T00:00:00Z" + }, + "headers": { + "Content-Type": "application/json" + } + } +} diff --git a/tests/multi-runner-orchestration/mappings/07-runner---enterprise-registration-token.json b/tests/multi-runner-orchestration/mappings/07-runner---enterprise-registration-token.json new file mode 100644 index 0000000000..12246d5572 --- /dev/null +++ b/tests/multi-runner-orchestration/mappings/07-runner---enterprise-registration-token.json @@ -0,0 +1,17 @@ +{ + "name": "Runner - enterprise registration token", + "request": { + "method": "POST", + "urlPathPattern": "(?:/api/v3)?/enterprises/[^/]+/actions/runners/registration-token" + }, + "response": { + "status": 201, + "jsonBody": { + "token": "runner-registration-token-mock", + "expires_at": "2099-01-01T00:00:00Z" + }, + "headers": { + "Content-Type": "application/json" + } + } +} diff --git a/tests/multi-runner-orchestration/mappings/08-runner---org-remove-token.json b/tests/multi-runner-orchestration/mappings/08-runner---org-remove-token.json new file mode 100644 index 0000000000..76f8a6eee1 --- /dev/null +++ b/tests/multi-runner-orchestration/mappings/08-runner---org-remove-token.json @@ -0,0 +1,17 @@ +{ + "name": "Runner - org remove token", + "request": { + "method": "POST", + "urlPathPattern": "(?:/api/v3)?/orgs/[^/]+/actions/runners/remove-token" + }, + "response": { + "status": 201, + "jsonBody": { + "token": "runner-removal-token-mock", + "expires_at": "2099-01-01T00:00:00Z" + }, + "headers": { + "Content-Type": "application/json" + } + } +} diff --git a/tests/multi-runner-orchestration/mappings/09-runner---repo-remove-token.json b/tests/multi-runner-orchestration/mappings/09-runner---repo-remove-token.json new file mode 100644 index 0000000000..912bea1a2a --- /dev/null +++ b/tests/multi-runner-orchestration/mappings/09-runner---repo-remove-token.json @@ -0,0 +1,17 @@ +{ + "name": "Runner - repo remove token", + "request": { + "method": "POST", + "urlPathPattern": "(?:/api/v3)?/repos/[^/]+/[^/]+/actions/runners/remove-token" + }, + "response": { + "status": 201, + "jsonBody": { + "token": "runner-removal-token-mock", + "expires_at": "2099-01-01T00:00:00Z" + }, + "headers": { + "Content-Type": "application/json" + } + } +} diff --git a/tests/multi-runner-orchestration/mappings/10-runner---enterprise-remove-token.json b/tests/multi-runner-orchestration/mappings/10-runner---enterprise-remove-token.json new file mode 100644 index 0000000000..ceacec88e4 --- /dev/null +++ b/tests/multi-runner-orchestration/mappings/10-runner---enterprise-remove-token.json @@ -0,0 +1,17 @@ +{ + "name": "Runner - enterprise remove token", + "request": { + "method": "POST", + "urlPathPattern": "(?:/api/v3)?/enterprises/[^/]+/actions/runners/remove-token" + }, + "response": { + "status": 201, + "jsonBody": { + "token": "runner-removal-token-mock", + "expires_at": "2099-01-01T00:00:00Z" + }, + "headers": { + "Content-Type": "application/json" + } + } +} diff --git a/tests/multi-runner-orchestration/mappings/11-runner---list-org-runners.json b/tests/multi-runner-orchestration/mappings/11-runner---list-org-runners.json new file mode 100644 index 0000000000..3c75201834 --- /dev/null +++ b/tests/multi-runner-orchestration/mappings/11-runner---list-org-runners.json @@ -0,0 +1,37 @@ +{ + "name": "Runner - list org runners", + "request": { + "method": "GET", + "urlPathPattern": "(?:/api/v3)?/orgs/[^/]+/actions/runners" + }, + "response": { + "status": 200, + "jsonBody": { + "total_count": 1, + "runners": [ + { + "id": 4242, + "name": "wiremock-runner-001", + "os": "Linux", + "status": "online", + "busy": false, + "labels": [ + { + "id": 1, + "name": "self-hosted", + "type": "read-only" + }, + { + "id": 2, + "name": "Linux", + "type": "read-only" + } + ] + } + ] + }, + "headers": { + "Content-Type": "application/json" + } + } +} diff --git a/tests/multi-runner-orchestration/mappings/12-runner---list-repository-runners.json b/tests/multi-runner-orchestration/mappings/12-runner---list-repository-runners.json new file mode 100644 index 0000000000..cfdaff60d7 --- /dev/null +++ b/tests/multi-runner-orchestration/mappings/12-runner---list-repository-runners.json @@ -0,0 +1,37 @@ +{ + "name": "Runner - list repository runners", + "request": { + "method": "GET", + "urlPathPattern": "(?:/api/v3)?/repos/[^/]+/[^/]+/actions/runners" + }, + "response": { + "status": 200, + "jsonBody": { + "total_count": 1, + "runners": [ + { + "id": 4242, + "name": "wiremock-runner-001", + "os": "Linux", + "status": "online", + "busy": false, + "labels": [ + { + "id": 1, + "name": "self-hosted", + "type": "read-only" + }, + { + "id": 2, + "name": "Linux", + "type": "read-only" + } + ] + } + ] + }, + "headers": { + "Content-Type": "application/json" + } + } +} diff --git a/tests/multi-runner-orchestration/mappings/13-runner---list-enterprise-runners.json b/tests/multi-runner-orchestration/mappings/13-runner---list-enterprise-runners.json new file mode 100644 index 0000000000..34d9e37f75 --- /dev/null +++ b/tests/multi-runner-orchestration/mappings/13-runner---list-enterprise-runners.json @@ -0,0 +1,37 @@ +{ + "name": "Runner - list enterprise runners", + "request": { + "method": "GET", + "urlPathPattern": "(?:/api/v3)?/enterprises/[^/]+/actions/runners" + }, + "response": { + "status": 200, + "jsonBody": { + "total_count": 1, + "runners": [ + { + "id": 4242, + "name": "wiremock-runner-001", + "os": "Linux", + "status": "online", + "busy": false, + "labels": [ + { + "id": 1, + "name": "self-hosted", + "type": "read-only" + }, + { + "id": 2, + "name": "Linux", + "type": "read-only" + } + ] + } + ] + }, + "headers": { + "Content-Type": "application/json" + } + } +} diff --git a/tests/multi-runner-orchestration/mappings/14-runner---delete-org-runner.json b/tests/multi-runner-orchestration/mappings/14-runner---delete-org-runner.json new file mode 100644 index 0000000000..7d16c49cf0 --- /dev/null +++ b/tests/multi-runner-orchestration/mappings/14-runner---delete-org-runner.json @@ -0,0 +1,10 @@ +{ + "name": "Runner - delete org runner", + "request": { + "method": "DELETE", + "urlPathPattern": "(?:/api/v3)?/orgs/[^/]+/actions/runners/\\d+" + }, + "response": { + "status": 204 + } +} diff --git a/tests/multi-runner-orchestration/mappings/15-runner---delete-repository-runner.json b/tests/multi-runner-orchestration/mappings/15-runner---delete-repository-runner.json new file mode 100644 index 0000000000..e1c40703f7 --- /dev/null +++ b/tests/multi-runner-orchestration/mappings/15-runner---delete-repository-runner.json @@ -0,0 +1,10 @@ +{ + "name": "Runner - delete repository runner", + "request": { + "method": "DELETE", + "urlPathPattern": "(?:/api/v3)?/repos/[^/]+/[^/]+/actions/runners/\\d+" + }, + "response": { + "status": 204 + } +} diff --git a/tests/multi-runner-orchestration/mappings/16-runner---delete-enterprise-runner.json b/tests/multi-runner-orchestration/mappings/16-runner---delete-enterprise-runner.json new file mode 100644 index 0000000000..e3759d98ff --- /dev/null +++ b/tests/multi-runner-orchestration/mappings/16-runner---delete-enterprise-runner.json @@ -0,0 +1,10 @@ +{ + "name": "Runner - delete enterprise runner", + "request": { + "method": "DELETE", + "urlPathPattern": "(?:/api/v3)?/enterprises/[^/]+/actions/runners/\\d+" + }, + "response": { + "status": 204 + } +} diff --git a/tests/multi-runner-orchestration/mappings/17-runner---org-runner-groups.json b/tests/multi-runner-orchestration/mappings/17-runner---org-runner-groups.json new file mode 100644 index 0000000000..24f03af319 --- /dev/null +++ b/tests/multi-runner-orchestration/mappings/17-runner---org-runner-groups.json @@ -0,0 +1,24 @@ +{ + "name": "Runner - org runner groups", + "request": { + "method": "GET", + "urlPathPattern": "(?:/api/v3)?/orgs/[^/]+/actions/runner-groups" + }, + "response": { + "status": 200, + "jsonBody": { + "total_count": 1, + "runner_groups": [ + { + "id": 1, + "name": "Default", + "size": 100, + "isDefaultGroup": true + } + ] + }, + "headers": { + "Content-Type": "application/json" + } + } +} diff --git a/tests/multi-runner-orchestration/mappings/18-runner---repo-workflow-job.json b/tests/multi-runner-orchestration/mappings/18-runner---repo-workflow-job.json new file mode 100644 index 0000000000..b031a2ef78 --- /dev/null +++ b/tests/multi-runner-orchestration/mappings/18-runner---repo-workflow-job.json @@ -0,0 +1,27 @@ +{ + "name": "Runner - repo workflow job", + "request": { + "method": "GET", + "urlPathPattern": "(?:/api/v3)?/repos/[^/]+/[^/]+/actions/jobs/\\d+" + }, + "response": { + "status": 200, + "jsonBody": { + "id": 5150, + "run_id": 8080, + "status": "queued", + "conclusion": null, + "name": "wiremock-job", + "started_at": null, + "completed_at": null, + "html_url": "https://github.com/acme/repo/actions/runs/8080/job/5150", + "labels": [ + "self-hosted", + "Linux" + ] + }, + "headers": { + "Content-Type": "application/json" + } + } +} diff --git a/tests/multi-runner-orchestration/mappings/19-runner---org-jit-config.json b/tests/multi-runner-orchestration/mappings/19-runner---org-jit-config.json new file mode 100644 index 0000000000..6b08f8fda5 --- /dev/null +++ b/tests/multi-runner-orchestration/mappings/19-runner---org-jit-config.json @@ -0,0 +1,20 @@ +{ + "name": "Runner - org JIT config", + "request": { + "method": "POST", + "urlPathPattern": "(?:/api/v3)?/orgs/[^/]+/actions/runners/generate-jitconfig" + }, + "response": { + "status": 200, + "jsonBody": { + "runner": { + "id": 4242, + "name": "wiremock-runner-001" + }, + "encoded_jit_config": "eyJtb2NrIjoiaml0LWNvbmZpZyJ9" + }, + "headers": { + "Content-Type": "application/json" + } + } +} diff --git a/tests/multi-runner-orchestration/mappings/20-runner---repo-jit-config.json b/tests/multi-runner-orchestration/mappings/20-runner---repo-jit-config.json new file mode 100644 index 0000000000..8de6c79074 --- /dev/null +++ b/tests/multi-runner-orchestration/mappings/20-runner---repo-jit-config.json @@ -0,0 +1,20 @@ +{ + "name": "Runner - repo JIT config", + "request": { + "method": "POST", + "urlPathPattern": "(?:/api/v3)?/repos/[^/]+/[^/]+/actions/runners/generate-jitconfig" + }, + "response": { + "status": 200, + "jsonBody": { + "runner": { + "id": 4242, + "name": "wiremock-runner-001" + }, + "encoded_jit_config": "eyJtb2NrIjoiaml0LWNvbmZpZyJ9" + }, + "headers": { + "Content-Type": "application/json" + } + } +} diff --git a/tests/multi-runner-orchestration/mappings/21-runner---enterprise-jit-config.json b/tests/multi-runner-orchestration/mappings/21-runner---enterprise-jit-config.json new file mode 100644 index 0000000000..69edbfa0c7 --- /dev/null +++ b/tests/multi-runner-orchestration/mappings/21-runner---enterprise-jit-config.json @@ -0,0 +1,20 @@ +{ + "name": "Runner - enterprise JIT config", + "request": { + "method": "POST", + "urlPathPattern": "(?:/api/v3)?/enterprises/[^/]+/actions/runners/generate-jitconfig" + }, + "response": { + "status": 200, + "jsonBody": { + "runner": { + "id": 4242, + "name": "wiremock-runner-001" + }, + "encoded_jit_config": "eyJtb2NrIjoiaml0LWNvbmZpZyJ9" + }, + "headers": { + "Content-Type": "application/json" + } + } +} diff --git a/tests/multi-runner-orchestration/mappings/22-runner-release---latest-runner-release.json b/tests/multi-runner-orchestration/mappings/22-runner-release---latest-runner-release.json new file mode 100644 index 0000000000..4ead33471a --- /dev/null +++ b/tests/multi-runner-orchestration/mappings/22-runner-release---latest-runner-release.json @@ -0,0 +1,14 @@ +{ + "name": "Runner release - latest runner release", + "request": { + "method": "GET", + "urlPathPattern": "(?:/api/v3)?/repos/actions/runner/releases/latest" + }, + "response": { + "status": 200, + "headers": { + "Content-Type": "application/json" + }, + "body": "{\"id\":123456,\"tag_name\":\"v2.321.0\",\"name\":\"actions/runner v2.321.0\",\"draft\":false,\"prerelease\":false,\"assets\":[{\"name\":\"actions-runner-linux-x64-2.321.0.tar.gz\",\"browser_download_url\":\"{{request.baseUrl}}/downloads/actions-runner-linux-x64-2.321.0.tar.gz\"}]}" + } +} diff --git a/tests/multi-runner-orchestration/mappings/23-runner-release---runner-archive.json b/tests/multi-runner-orchestration/mappings/23-runner-release---runner-archive.json new file mode 100644 index 0000000000..67259ba3d2 --- /dev/null +++ b/tests/multi-runner-orchestration/mappings/23-runner-release---runner-archive.json @@ -0,0 +1,14 @@ +{ + "name": "Runner release - runner archive", + "request": { + "method": "GET", + "urlPathPattern": "/downloads/actions-runner-linux-x64-2\\.321\\.0\\.tar\\.gz" + }, + "response": { + "status": 200, + "headers": { + "Content-Type": "application/octet-stream" + }, + "bodyFileName": "actions-runner-linux-x64-2.321.0.tar.gz" + } +} diff --git a/tests/multi-runner-orchestration/mappings/24-scale-set---actions-registration-handshake.json b/tests/multi-runner-orchestration/mappings/24-scale-set---actions-registration-handshake.json new file mode 100644 index 0000000000..5db902bf74 --- /dev/null +++ b/tests/multi-runner-orchestration/mappings/24-scale-set---actions-registration-handshake.json @@ -0,0 +1,14 @@ +{ + "name": "Scale set - Actions registration handshake", + "request": { + "method": "POST", + "urlPathPattern": "(?:/api/v3)?/actions/runner-registration" + }, + "response": { + "status": 200, + "headers": { + "Content-Type": "application/json" + }, + "body": "{\"url\":\"{{request.baseUrl}}/tenant/123\",\"token\":\"eyJhbGciOiJub25lIn0.eyJleHAiOjQxMDI0NDgwMDB9.mock-signature\"}" + } +} diff --git a/tests/multi-runner-orchestration/mappings/25-scale-set---list-runner-groups.json b/tests/multi-runner-orchestration/mappings/25-scale-set---list-runner-groups.json new file mode 100644 index 0000000000..c6d288c70c --- /dev/null +++ b/tests/multi-runner-orchestration/mappings/25-scale-set---list-runner-groups.json @@ -0,0 +1,14 @@ +{ + "name": "Scale set - list runner groups", + "request": { + "method": "GET", + "urlPathPattern": "(?:/api/v3)?(?:/tenant/[^/]+)?/_apis/runtime/runnergroups/?" + }, + "response": { + "status": 200, + "headers": { + "Content-Type": "application/json" + }, + "body": "{\"count\":1,\"value\":[{\"id\":48,\"name\":\"experimental-euw1-sl-cicd-forge-emu\",\"size\":100,\"isDefaultGroup\":false}]}" + } +} diff --git a/tests/multi-runner-orchestration/mappings/26-scale-set---list-scale-sets.json b/tests/multi-runner-orchestration/mappings/26-scale-set---list-scale-sets.json new file mode 100644 index 0000000000..5e2ab4c110 --- /dev/null +++ b/tests/multi-runner-orchestration/mappings/26-scale-set---list-scale-sets.json @@ -0,0 +1,40 @@ +{ + "name": "Scale set - list scale sets", + "request": { + "method": "GET", + "urlPathPattern": "(?:/api/v3)?(?:/tenant/[^/]+)?/_apis/runtime/runnerscalesets" + }, + "response": { + "status": 200, + "jsonBody": { + "count": 1, + "value": [ + { + "id": 223, + "name": "linux-scale-set", + "runnerGroupId": 48, + "runnerGroupName": "experimental-euw1-sl-cicd-forge-emu", + "labels": [ + { + "name": "linux-scale-set", + "type": "system" + } + ], + "runnerSetting": {}, + "statistics": { + "totalAvailableJobs": 0, + "totalAcquiredJobs": 0, + "totalAssignedJobs": 0, + "totalRunningJobs": 0, + "totalRegisteredRunners": 0, + "totalBusyRunners": 0, + "totalIdleRunners": 0 + } + } + ] + }, + "headers": { + "Content-Type": "application/json" + } + } +} diff --git a/tests/multi-runner-orchestration/mappings/27-scale-set---create-scale-set.json b/tests/multi-runner-orchestration/mappings/27-scale-set---create-scale-set.json new file mode 100644 index 0000000000..1d39c7c384 --- /dev/null +++ b/tests/multi-runner-orchestration/mappings/27-scale-set---create-scale-set.json @@ -0,0 +1,35 @@ +{ + "name": "Scale set - create scale set", + "request": { + "method": "POST", + "urlPathPattern": "(?:/api/v3)?(?:/tenant/[^/]+)?/_apis/runtime/runnerscalesets" + }, + "response": { + "status": 200, + "jsonBody": { + "id": 223, + "name": "linux-scale-set", + "runnerGroupId": 48, + "runnerGroupName": "experimental-euw1-sl-cicd-forge-emu", + "labels": [ + { + "name": "linux-scale-set", + "type": "system" + } + ], + "runnerSetting": {}, + "statistics": { + "totalAvailableJobs": 0, + "totalAcquiredJobs": 0, + "totalAssignedJobs": 0, + "totalRunningJobs": 0, + "totalRegisteredRunners": 0, + "totalBusyRunners": 0, + "totalIdleRunners": 0 + } + }, + "headers": { + "Content-Type": "application/json" + } + } +} diff --git a/tests/multi-runner-orchestration/mappings/28-scale-set---get-scale-set-by-id.json b/tests/multi-runner-orchestration/mappings/28-scale-set---get-scale-set-by-id.json new file mode 100644 index 0000000000..93505b73d9 --- /dev/null +++ b/tests/multi-runner-orchestration/mappings/28-scale-set---get-scale-set-by-id.json @@ -0,0 +1,35 @@ +{ + "name": "Scale set - get scale set by id", + "request": { + "method": "GET", + "urlPathPattern": "(?:/api/v3)?(?:/tenant/[^/]+)?/_apis/runtime/runnerscalesets/\\d+" + }, + "response": { + "status": 200, + "jsonBody": { + "id": 223, + "name": "linux-scale-set", + "runnerGroupId": 48, + "runnerGroupName": "experimental-euw1-sl-cicd-forge-emu", + "labels": [ + { + "name": "linux-scale-set", + "type": "system" + } + ], + "runnerSetting": {}, + "statistics": { + "totalAvailableJobs": 0, + "totalAcquiredJobs": 0, + "totalAssignedJobs": 0, + "totalRunningJobs": 0, + "totalRegisteredRunners": 0, + "totalBusyRunners": 0, + "totalIdleRunners": 0 + } + }, + "headers": { + "Content-Type": "application/json" + } + } +} diff --git a/tests/multi-runner-orchestration/mappings/29-scale-set---update-scale-set.json b/tests/multi-runner-orchestration/mappings/29-scale-set---update-scale-set.json new file mode 100644 index 0000000000..2a1afdb1ff --- /dev/null +++ b/tests/multi-runner-orchestration/mappings/29-scale-set---update-scale-set.json @@ -0,0 +1,35 @@ +{ + "name": "Scale set - update scale set", + "request": { + "method": "PATCH", + "urlPathPattern": "(?:/api/v3)?(?:/tenant/[^/]+)?/_apis/runtime/runnerscalesets/\\d+" + }, + "response": { + "status": 200, + "jsonBody": { + "id": 223, + "name": "linux-scale-set", + "runnerGroupId": 48, + "runnerGroupName": "experimental-euw1-sl-cicd-forge-emu", + "labels": [ + { + "name": "linux-scale-set", + "type": "system" + } + ], + "runnerSetting": {}, + "statistics": { + "totalAvailableJobs": 0, + "totalAcquiredJobs": 0, + "totalAssignedJobs": 0, + "totalRunningJobs": 0, + "totalRegisteredRunners": 0, + "totalBusyRunners": 0, + "totalIdleRunners": 0 + } + }, + "headers": { + "Content-Type": "application/json" + } + } +} diff --git a/tests/multi-runner-orchestration/mappings/30-scale-set---delete-scale-set.json b/tests/multi-runner-orchestration/mappings/30-scale-set---delete-scale-set.json new file mode 100644 index 0000000000..d29b5b4dd8 --- /dev/null +++ b/tests/multi-runner-orchestration/mappings/30-scale-set---delete-scale-set.json @@ -0,0 +1,10 @@ +{ + "name": "Scale set - delete scale set", + "request": { + "method": "DELETE", + "urlPathPattern": "(?:/api/v3)?(?:/tenant/[^/]+)?/_apis/runtime/runnerscalesets/\\d+" + }, + "response": { + "status": 204 + } +} diff --git a/tests/multi-runner-orchestration/mappings/31-scale-set---generate-jit-runner-config.json b/tests/multi-runner-orchestration/mappings/31-scale-set---generate-jit-runner-config.json new file mode 100644 index 0000000000..92c1972253 --- /dev/null +++ b/tests/multi-runner-orchestration/mappings/31-scale-set---generate-jit-runner-config.json @@ -0,0 +1,14 @@ +{ + "name": "Scale set - generate JIT runner config", + "request": { + "method": "POST", + "urlPathPattern": "(?:/api/v3)?(?:/tenant/[^/]+)?/_apis/runtime/runnerscalesets/\\d+/generatejitconfig" + }, + "response": { + "status": 200, + "headers": { + "Content-Type": "application/json" + }, + "body": "{\"runner\":{\"id\":321,\"name\":\"{{jsonPath request.body '$.name'}}\",\"runnerScaleSetId\":223},\"encodedJITConfig\":\"test-only-jit-configuration\"}" + } +} diff --git a/tests/multi-runner-orchestration/mappings/32-scale-set---create-message-session.json b/tests/multi-runner-orchestration/mappings/32-scale-set---create-message-session.json new file mode 100644 index 0000000000..43c4dee6e5 --- /dev/null +++ b/tests/multi-runner-orchestration/mappings/32-scale-set---create-message-session.json @@ -0,0 +1,14 @@ +{ + "name": "Scale set - create message session", + "request": { + "method": "POST", + "urlPathPattern": "(?:/api/v3)?(?:/tenant/[^/]+)?/_apis/runtime/runnerscalesets/\\d+/sessions" + }, + "response": { + "status": 200, + "headers": { + "Content-Type": "application/json" + }, + "body": "{\"sessionId\":\"11111111-1111-1111-1111-111111111111\",\"ownerName\":\"local.linux-scale-set\",\"runnerScaleSet\":{\"id\":223,\"name\":\"linux-scale-set\",\"runnerGroupId\":48},\"messageQueueUrl\":\"{{request.baseUrl}}/messages?sessionId=11111111-1111-1111-1111-111111111111&api-version=6.0-preview\",\"messageQueueAccessToken\":\"fake-queue-token\",\"statistics\":{\"totalAvailableJobs\":0,\"totalAcquiredJobs\":0,\"totalAssignedJobs\":0,\"totalRunningJobs\":0,\"totalRegisteredRunners\":0,\"totalBusyRunners\":0,\"totalIdleRunners\":0}}" + } +} diff --git a/tests/multi-runner-orchestration/mappings/33-scale-set---refresh-message-session.json b/tests/multi-runner-orchestration/mappings/33-scale-set---refresh-message-session.json new file mode 100644 index 0000000000..244d01138c --- /dev/null +++ b/tests/multi-runner-orchestration/mappings/33-scale-set---refresh-message-session.json @@ -0,0 +1,14 @@ +{ + "name": "Scale set - refresh message session", + "request": { + "method": "PATCH", + "urlPathPattern": "(?:/api/v3)?(?:/tenant/[^/]+)?/_apis/runtime/runnerscalesets/\\d+/sessions/[^/]+" + }, + "response": { + "status": 200, + "headers": { + "Content-Type": "application/json" + }, + "body": "{\"sessionId\":\"11111111-1111-1111-1111-111111111111\",\"ownerName\":\"local.linux-scale-set\",\"runnerScaleSet\":{\"id\":223,\"name\":\"linux-scale-set\",\"runnerGroupId\":48},\"messageQueueUrl\":\"{{request.baseUrl}}/messages?sessionId=11111111-1111-1111-1111-111111111111&api-version=6.0-preview\",\"messageQueueAccessToken\":\"fake-queue-token\",\"statistics\":{\"totalAvailableJobs\":0,\"totalAcquiredJobs\":0,\"totalAssignedJobs\":0,\"totalRunningJobs\":0,\"totalRegisteredRunners\":0,\"totalBusyRunners\":0,\"totalIdleRunners\":0}}" + } +} diff --git a/tests/multi-runner-orchestration/mappings/34-scale-set---close-message-session.json b/tests/multi-runner-orchestration/mappings/34-scale-set---close-message-session.json new file mode 100644 index 0000000000..3f1ad50f49 --- /dev/null +++ b/tests/multi-runner-orchestration/mappings/34-scale-set---close-message-session.json @@ -0,0 +1,10 @@ +{ + "name": "Scale set - close message session", + "request": { + "method": "DELETE", + "urlPathPattern": "(?:/api/v3)?(?:/tenant/[^/]+)?/_apis/runtime/runnerscalesets/\\d+/sessions/[^/]+" + }, + "response": { + "status": 204 + } +} diff --git a/tests/multi-runner-orchestration/mappings/35-scale-set---poll-message-queue-empty.json b/tests/multi-runner-orchestration/mappings/35-scale-set---poll-message-queue-empty.json new file mode 100644 index 0000000000..c47d8ae6a1 --- /dev/null +++ b/tests/multi-runner-orchestration/mappings/35-scale-set---poll-message-queue-empty.json @@ -0,0 +1,11 @@ +{ + "name": "Scale set - poll message queue empty", + "request": { + "method": "GET", + "urlPathPattern": "(?:/queue/sessions/[^/]+/messages|/messages)" + }, + "response": { + "status": 202, + "fixedDelayMilliseconds": 1000 + } +} diff --git a/tests/multi-runner-orchestration/mappings/36-scale-set---acknowledge-message.json b/tests/multi-runner-orchestration/mappings/36-scale-set---acknowledge-message.json new file mode 100644 index 0000000000..dc4e16ce80 --- /dev/null +++ b/tests/multi-runner-orchestration/mappings/36-scale-set---acknowledge-message.json @@ -0,0 +1,10 @@ +{ + "name": "Scale set - acknowledge message", + "request": { + "method": "DELETE", + "urlPathPattern": "(?:/queue/sessions/[^/]+/messages/\\d+|/messages/\\d+)" + }, + "response": { + "status": 204 + } +} diff --git a/tests/multi-runner-orchestration/mappings/37-scale-set---acquire-jobs.json b/tests/multi-runner-orchestration/mappings/37-scale-set---acquire-jobs.json new file mode 100644 index 0000000000..b11c4779e1 --- /dev/null +++ b/tests/multi-runner-orchestration/mappings/37-scale-set---acquire-jobs.json @@ -0,0 +1,16 @@ +{ + "name": "Scale set - acquire jobs", + "request": { + "method": "POST", + "urlPathPattern": "(?:/api/v3)?(?:/tenant/[^/]+)?/_apis/runtime/runnerscalesets/\\d+/acquirejobs" + }, + "response": { + "status": 200, + "jsonBody": { + "value": [] + }, + "headers": { + "Content-Type": "application/json" + } + } +} diff --git a/tests/multi-runner-orchestration/mappings/38-scale-set---get-runner-by-id.json b/tests/multi-runner-orchestration/mappings/38-scale-set---get-runner-by-id.json new file mode 100644 index 0000000000..844212da3f --- /dev/null +++ b/tests/multi-runner-orchestration/mappings/38-scale-set---get-runner-by-id.json @@ -0,0 +1,14 @@ +{ + "name": "Scale set - get runner by id", + "request": { + "method": "GET", + "urlPathPattern": "(?:/api/v3)?(?:/tenant/[^/]+)?/_apis/distributedtask/pools/0/agents/\\d+" + }, + "response": { + "status": 200, + "headers": { + "Content-Type": "application/json" + }, + "body": "{\"id\":321,\"name\":\"linux-scale-set\",\"runnerScaleSetId\":223}" + } +} diff --git a/tests/multi-runner-orchestration/mappings/39-scale-set---list-runners-by-name.json b/tests/multi-runner-orchestration/mappings/39-scale-set---list-runners-by-name.json new file mode 100644 index 0000000000..0e02ccbe7e --- /dev/null +++ b/tests/multi-runner-orchestration/mappings/39-scale-set---list-runners-by-name.json @@ -0,0 +1,14 @@ +{ + "name": "Scale set - list runners by name", + "request": { + "method": "GET", + "urlPathPattern": "(?:/api/v3)?(?:/tenant/[^/]+)?/_apis/distributedtask/pools/0/agents" + }, + "response": { + "status": 200, + "headers": { + "Content-Type": "application/json" + }, + "body": "{\"count\":1,\"value\":[{\"id\":321,\"name\":\"{{request.query.agentName}}\",\"runnerScaleSetId\":223}]}" + } +} diff --git a/tests/multi-runner-orchestration/mappings/40-scale-set---remove-runner.json b/tests/multi-runner-orchestration/mappings/40-scale-set---remove-runner.json new file mode 100644 index 0000000000..85b4222a37 --- /dev/null +++ b/tests/multi-runner-orchestration/mappings/40-scale-set---remove-runner.json @@ -0,0 +1,10 @@ +{ + "name": "Scale set - remove runner", + "request": { + "method": "DELETE", + "urlPathPattern": "(?:/api/v3)?(?:/tenant/[^/]+)?/_apis/distributedtask/pools/0/agents/\\d+" + }, + "response": { + "status": 204 + } +} diff --git a/tests/multi-runner-orchestration/mappings/41-runner---get-org-runner.json b/tests/multi-runner-orchestration/mappings/41-runner---get-org-runner.json new file mode 100644 index 0000000000..8eb38eef9b --- /dev/null +++ b/tests/multi-runner-orchestration/mappings/41-runner---get-org-runner.json @@ -0,0 +1,13 @@ +{ + "name": "runner get org runner", + "request": { + "method": "GET", + "urlPathPattern": "(?:/api/v3)?/orgs/[^/]+/actions/runners/\\d+" + }, + "response": { + "status": 404, + "jsonBody": { + "message": "Runner not found" + } + } +} diff --git a/tests/multi-runner-orchestration/mappings/42-runner---get-repository-runner.json b/tests/multi-runner-orchestration/mappings/42-runner---get-repository-runner.json new file mode 100644 index 0000000000..61a2f27b6d --- /dev/null +++ b/tests/multi-runner-orchestration/mappings/42-runner---get-repository-runner.json @@ -0,0 +1,32 @@ +{ + "name": "runner get repository runner", + "request": { + "method": "GET", + "urlPathPattern": "(?:/api/v3)?/repos/[^/]+/[^/]+/actions/runners/\\d+" + }, + "response": { + "status": 200, + "headers": { + "Content-Type": "application/json" + }, + "jsonBody": { + "id": 4242, + "name": "wiremock-runner-001", + "os": "Linux", + "status": "online", + "busy": false, + "labels": [ + { + "id": 1, + "name": "self-hosted", + "type": "read-only" + }, + { + "id": 2, + "name": "Linux", + "type": "read-only" + } + ] + } + } +} diff --git a/tests/multi-runner-orchestration/smoke/prepare-scale-set-credentials.mjs b/tests/multi-runner-orchestration/smoke/prepare-scale-set-credentials.mjs new file mode 100644 index 0000000000..bee29333b4 --- /dev/null +++ b/tests/multi-runner-orchestration/smoke/prepare-scale-set-credentials.mjs @@ -0,0 +1,75 @@ +import assert from 'node:assert/strict'; +import { mkdtemp, rm, writeFile } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import path from 'node:path'; +import { aws } from './support.mjs'; + +const cluster = process.env.MINISTACK_SCALE_SET_CLUSTER + ?? 'multi-runner-webhook-scale-set'; +const serviceArns = (await aws('ecs', 'list-services', '--cluster', cluster)).serviceArns ?? []; +assert.ok(serviceArns.length > 0, `No ECS service found in cluster ${cluster}`); + +const serviceResponse = await aws( + 'ecs', 'describe-services', + '--cluster', cluster, + '--services', serviceArns[0], +); +const service = serviceResponse.services?.[0]; +assert.ok(service?.taskDefinition, `No task definition found for service ${serviceArns[0]}`); + +const taskDefinitionResponse = await aws( + 'ecs', 'describe-task-definition', + '--task-definition', service.taskDefinition, +); +const taskDefinition = taskDefinitionResponse.taskDefinition; +assert.ok(taskDefinition, `Could not describe task definition ${service.taskDefinition}`); + +for (const field of [ + 'taskDefinitionArn', + 'revision', + 'status', + 'requiresAttributes', + 'compatibilities', + 'registeredAt', + 'registeredBy', +]) { + delete taskDefinition[field]; +} + +const controller = taskDefinition.containerDefinitions?.find( + (definition) => definition.name === 'scale-set-controller', +); +assert.ok(controller, 'Task definition has no scale-set-controller container'); + +const environment = new Map( + (controller.environment ?? []) + .filter(({ name }) => name !== 'AWS_ACCESS_KEY_ID' && name !== 'AWS_SECRET_ACCESS_KEY') + .map(({ name, value }) => [name, value]), +); +environment.set('AWS_ACCESS_KEY_ID', '000000000000'); +environment.set('AWS_SECRET_ACCESS_KEY', 'test-only'); +controller.environment = [...environment].map(([name, value]) => ({ name, value })); + +const temporaryDirectory = await mkdtemp(path.join(tmpdir(), 'scale-set-credentials-')); +try { + const inputFile = path.join(temporaryDirectory, 'task-definition.json'); + await writeFile(inputFile, JSON.stringify(taskDefinition)); + + const registration = await aws( + 'ecs', 'register-task-definition', + '--cli-input-json', `file://${inputFile}`, + ); + const taskDefinitionArn = registration.taskDefinition?.taskDefinitionArn; + assert.ok(taskDefinitionArn, 'MiniStack did not register the credential-enabled task definition'); + + await aws( + 'ecs', 'update-service', + '--cluster', cluster, + '--service', service.serviceName, + '--task-definition', taskDefinitionArn, + '--force-new-deployment', + ); + +} finally { + await rm(temporaryDirectory, { recursive: true, force: true }); +} diff --git a/tests/multi-runner-orchestration/smoke/reporter.mjs b/tests/multi-runner-orchestration/smoke/reporter.mjs new file mode 100644 index 0000000000..2571787980 --- /dev/null +++ b/tests/multi-runner-orchestration/smoke/reporter.mjs @@ -0,0 +1,15 @@ +export default async function* smokeReporter(events) { + for await (const event of events) { + const { name, details, error } = event.data ?? {}; + if (details?.type !== 'test') continue; + + if (event.type === 'test:pass') { + yield `PASS ${name}\n`; + } else if (event.type === 'test:fail') { + yield `FAIL ${name}\n`; + if (error?.message) { + yield ` ${error.message.split('\n').join('\n ')}\n`; + } + } + } +} diff --git a/tests/multi-runner-orchestration/smoke/scale-set.test.mjs b/tests/multi-runner-orchestration/smoke/scale-set.test.mjs new file mode 100644 index 0000000000..0774cee705 --- /dev/null +++ b/tests/multi-runner-orchestration/smoke/scale-set.test.mjs @@ -0,0 +1,218 @@ +import assert from 'node:assert/strict'; +import { mkdtemp, rm, writeFile } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import path from 'node:path'; +import { test } from 'node:test'; +import { + assertCloudWatchEvent, + aws, + waitFor, +} from './support.mjs'; + +const testTimeout = Number(process.env.MINISTACK_SMOKE_TIMEOUT_MS ?? 7200000); + +async function findScaleSetControllerService() { + const clusters = (await aws('ecs', 'list-clusters')).clusterArns ?? []; + const clusterArn = clusters.find((arn) => arn.includes('multi-runner-webhook-scale-set')); + assert.ok(clusterArn, 'Scale-set ECS cluster is missing; ensure Compose terraform-apply completed'); + + const serviceArns = (await aws('ecs', 'list-services', '--cluster', clusterArn)).serviceArns ?? []; + for (const serviceArn of serviceArns) { + const response = await aws( + 'ecs', 'describe-services', + '--cluster', clusterArn, + '--services', serviceArn, + ); + const service = response.services?.[0]; + if (!service?.taskDefinition) continue; + + const taskResponse = await aws('ecs', 'describe-task-definition', '--task-definition', service.taskDefinition); + const taskDefinition = taskResponse.taskDefinition; + const container = taskDefinition.containerDefinitions?.find( + (definition) => definition.name === 'scale-set-controller', + ); + + if (container) { + return { + clusterArn, + service, + taskDefinition, + container, + logGroup: container.logConfiguration?.options?.['awslogs-group'], + }; + } + } + + throw new Error('Scale-set controller ECS service/task definition not found'); +} + +function prepareTaskDefinition(controller, minimumRunners) { + const taskDefinition = structuredClone(controller.taskDefinition); + const registeredFields = [ + 'taskDefinitionArn', + 'revision', + 'status', + 'requiresAttributes', + 'compatibilities', + 'registeredAt', + 'registeredBy', + ]; + + for (const field of registeredFields) delete taskDefinition[field]; + + const container = taskDefinition.containerDefinitions.find( + (definition) => definition.name === 'scale-set-controller', + ); + const environment = new Map( + (container.environment ?? []).map(({ name, value }) => [name, value]), + ); + environment.set('AWS_ACCESS_KEY_ID', '000000000000'); + environment.set('AWS_SECRET_ACCESS_KEY', 'test-only'); + + const manifest = JSON.parse(environment.get('SCALE_SET_CONTROLLER_MANIFEST')); + assert.equal(manifest.reconcilers?.length, 1, 'Expected one scale-set reconciler'); + manifest.reconcilers[0].minRunners = minimumRunners; + environment.set('SCALE_SET_CONTROLLER_MANIFEST', JSON.stringify(manifest)); + + container.environment = [...environment].map(([name, value]) => ({ name, value })); + return taskDefinition; +} + +async function waitForTaskRevision(controller, taskDefinitionArn, minimumRunners) { + return waitFor(`controller deployment with minimum ${minimumRunners}`, async () => { + const response = await aws( + 'ecs', 'list-tasks', + '--cluster', controller.clusterArn, + '--service-name', controller.service.serviceName, + ).catch(() => ({})); + const taskArns = response.taskArns ?? []; + if (taskArns.length === 0) return false; + + const taskResponse = await aws( + 'ecs', 'describe-tasks', + '--cluster', controller.clusterArn, + '--tasks', ...taskArns, + ).catch(() => ({})); + + return taskResponse.tasks?.find((task) => ( + task.taskDefinitionArn === taskDefinitionArn && task.lastStatus === 'RUNNING' + )); + }); +} + +async function deployMinimumRunners(controller, minimumRunners, temporaryDirectory) { + const taskDefinition = prepareTaskDefinition(controller, minimumRunners); + const inputFile = path.join(temporaryDirectory, `task-${minimumRunners}.json`); + await writeFile(inputFile, JSON.stringify(taskDefinition)); + + const registration = await aws( + 'ecs', 'register-task-definition', + '--cli-input-json', `file://${inputFile}`, + ); + const taskDefinitionArn = registration.taskDefinition?.taskDefinitionArn; + assert.ok(taskDefinitionArn, 'MiniStack did not register the scale-set task revision'); + const startTime = Date.now() - 1000; + + await aws( + 'ecs', 'update-service', + '--cluster', controller.clusterArn, + '--service', controller.service.serviceName, + '--task-definition', taskDefinitionArn, + '--force-new-deployment', + ); + const task = await waitForTaskRevision(controller, taskDefinitionArn, minimumRunners); + const streamPrefix = controller.container.logConfiguration?.options?.['awslogs-stream-prefix']; + const taskId = task.taskArn?.split('/').at(-1); + assert.ok(streamPrefix && taskId, 'MiniStack did not report the controller task log stream identity'); + + return { + startTime, + logStreamName: `${streamPrefix}/${controller.container.name}/${taskId}`, + }; +} + +async function assertReconciliation(controller, minimumRunners, deployment) { + await assertCloudWatchEvent( + controller.logGroup, + 'scale_set_compute_provider_reconcile_started', + { desiredRunners: minimumRunners }, + deployment.startTime, + deployment.logStreamName, + ); + + const message = await assertCloudWatchEvent( + controller.logGroup, + 'scale_set_reconciled', + { desiredRunners: minimumRunners }, + deployment.startTime, + deployment.logStreamName, + ); + + return JSON.parse(message); +} + +test('Scale set: baseline at 0, scale up to 1, and scale down to 0 runners', { + timeout: testTimeout, +}, async () => { + const controller = await findScaleSetControllerService(); + assert.ok(controller.logGroup, 'Scale-set controller task has no CloudWatch log group'); + + const temporaryDirectory = await mkdtemp(path.join(tmpdir(), 'scale-set-smoke-')); + const scaleSteps = [ + { + minimumRunners: 0, + verify: (event) => assert.equal( + event.currentRunners, + 0, + 'Scale-set baseline should have no runners', + ), + }, + { + minimumRunners: 1, + verify: (event) => { + assert.ok( + event.actions?.launched > 0, + 'Scale-set scale-up did not launch a runner', + ); + assert.equal( + event.currentRunners, + 1, + 'Scale-set scale-up did not reach one runner', + ); + }, + }, + { + minimumRunners: 0, + verify: (event) => { + assert.ok( + event.actions?.terminated > 0, + 'Scale-set scale-down did not terminate a runner', + ); + assert.equal( + event.currentRunners, + 0, + 'Scale-set scale-down did not return to zero runners', + ); + }, + }, + ]; + + try { + for (const step of scaleSteps) { + const deployment = await deployMinimumRunners( + controller, + step.minimumRunners, + temporaryDirectory, + ); + const reconciliation = await assertReconciliation( + controller, + step.minimumRunners, + deployment, + ); + + step.verify?.(reconciliation); + } + } finally { + await rm(temporaryDirectory, { recursive: true, force: true }); + } +}); diff --git a/tests/multi-runner-orchestration/smoke/support.mjs b/tests/multi-runner-orchestration/smoke/support.mjs new file mode 100644 index 0000000000..04a3bd48d7 --- /dev/null +++ b/tests/multi-runner-orchestration/smoke/support.mjs @@ -0,0 +1,226 @@ +import { spawn } from 'node:child_process'; +import { mkdtemp, rm } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { setTimeout as delay } from 'node:timers/promises'; +import { fileURLToPath } from 'node:url'; +import path from 'node:path'; +import { request as httpRequest } from 'node:http'; + +const supportDirectory = path.dirname(fileURLToPath(import.meta.url)); +const repositoryRoot = path.resolve(supportDirectory, '../../..'); +const exampleDirectory = path.join(repositoryRoot, 'examples/multi-runner-orchestration'); + +export const awsEndpoint = process.env.AWS_ENDPOINT_URL ?? 'http://localhost:4566'; +export const awsRegion = process.env.AWS_DEFAULT_REGION ?? 'eu-west-1'; + +const awsEnvironment = { + ...process.env, + AWS_ACCESS_KEY_ID: process.env.AWS_ACCESS_KEY_ID ?? '000000000000', + AWS_SECRET_ACCESS_KEY: process.env.AWS_SECRET_ACCESS_KEY ?? 'test-only', + AWS_DEFAULT_REGION: awsRegion, + AWS_REGION: awsRegion, + AWS_ENDPOINT_URL: awsEndpoint, + AWS_EC2_METADATA_DISABLED: 'true', +}; + +export function run(command, args, { cwd = repositoryRoot, silent = false, check = true } = {}) { + return new Promise((resolve, reject) => { + const child = spawn(command, args, { + cwd, + env: awsEnvironment, + stdio: ['ignore', 'pipe', 'pipe'], + }); + const stdout = []; + const stderr = []; + + child.stdout.on('data', (chunk) => { + stdout.push(chunk); + if (!silent) process.stdout.write(chunk); + }); + child.stderr.on('data', (chunk) => { + stderr.push(chunk); + if (!silent) process.stderr.write(chunk); + }); + child.on('error', reject); + child.on('close', (code) => { + const result = { + code: code ?? 1, + stdout: Buffer.concat(stdout).toString().trim(), + stderr: Buffer.concat(stderr).toString().trim(), + }; + + if (code === 0 || !check) { + resolve(result); + return; + } + + reject(new Error(`${command} ${args.join(' ')} failed (${code}): ${result.stderr}`)); + }); + }); +} + +export async function aws(...args) { + const result = await run( + 'aws', + ['--endpoint-url', awsEndpoint, '--region', awsRegion, ...args, '--output', 'json'], + { silent: true }, + ); + + return result.stdout ? JSON.parse(result.stdout) : {}; +} + +export async function invokeLambda(functionName, payload = {}) { + const temporaryDirectory = await mkdtemp(path.join(tmpdir(), 'runner-smoke-lambda-')); + const responseFile = path.join(temporaryDirectory, 'response.json'); + + try { + const response = await aws( + 'lambda', 'invoke', + '--function-name', functionName, + '--cli-binary-format', 'raw-in-base64-out', + '--payload', JSON.stringify(payload), + responseFile, + ); + + if (response.FunctionError) { + throw new Error(`${functionName} invocation failed: ${response.FunctionError}`); + } + + return response; + } finally { + await rm(temporaryDirectory, { recursive: true, force: true }); + } +} + +export async function assertEc2RunnerCount(expectedCount) { + const environment = process.env.MINISTACK_RUNNER_ENVIRONMENT ?? 'multi-runner-webhook-ec2'; + const description = `${expectedCount} active EC2 runner(s) in ${environment}`; + + await waitFor(description, async () => { + const response = await aws( + 'ec2', 'describe-instances', + '--filters', + `Name=tag:ghr:environment,Values=${environment}`, + 'Name=instance-state-name,Values=pending,running', + ); + const instances = (response.Reservations ?? []).flatMap((reservation) => reservation.Instances ?? []); + return instances.length === expectedCount; + }); + +} + +export async function waitFor(description, predicate, attempts = 90) { + for (let attempt = 1; attempt <= attempts; attempt += 1) { + const result = await predicate(); + if (result) return result; + + await delay(2000); + } + + throw new Error(`Timed out waiting for ${description}`); +} + +export async function terraformOutput(name) { + const result = await run( + 'terraform', + [`-chdir=${exampleDirectory}`, 'output', '-raw', name], + { silent: true }, + ); + + return result.stdout; +} + +export function postWebhook(host, pathname, headers, body) { + return new Promise((resolve, reject) => { + const endpoint = new URL(awsEndpoint); + const request = httpRequest( + { + hostname: endpoint.hostname, + port: endpoint.port || (endpoint.protocol === 'https:' ? 443 : 80), + path: pathname, + method: 'POST', + headers: { ...headers, host }, + }, + (response) => { + const responseBody = []; + response.setEncoding('utf8'); + response.on('data', (chunk) => responseBody.push(chunk)); + response.on('end', () => { + resolve({ + status: response.statusCode ?? 0, + body: responseBody.join(''), + }); + }); + }, + ); + + request.setTimeout(15000, () => request.destroy(new Error('Timed out sending webhook'))); + request.on('error', reject); + request.end(body); + }); +} + +export async function logMessages(logGroup, startTime, logStreamName) { + const args = [ + '--endpoint-url', awsEndpoint, + '--region', awsRegion, + 'logs', 'filter-log-events', + '--log-group-name', logGroup, + '--start-time', String(startTime), + '--limit', '100', + '--output', 'json', + ]; + if (logStreamName) args.push('--log-stream-names', logStreamName); + + const result = await run( + 'aws', + args, + { silent: true, check: false }, + ); + + if (result.code !== 0) return []; + + try { + const response = JSON.parse(result.stdout); + return response.events?.map((event) => event.message) ?? []; + } catch { + return []; + } +} + +export async function assertCloudWatchLogContains(logGroup, description, text, startTime) { + await waitFor(description, async () => { + const messages = await logMessages(logGroup, startTime); + return messages.find((message) => message.includes(String(text))); + }); + +} + +function eventMatches(message, eventName, expectedFields) { + try { + const event = JSON.parse(message); + return event.event === eventName + && Object.entries(expectedFields).every(([key, value]) => event[key] === value); + } catch { + return message.includes(eventName) + && Object.entries(expectedFields).every(([key, value]) => ( + message.includes(`"${key}":${JSON.stringify(value)}`) + )); + } +} + +export async function assertCloudWatchEvent( + logGroup, + eventName, + expectedFields, + startTime = Date.now() - 60_000, + logStreamName, +) { + const description = `CloudWatch ${eventName} (${JSON.stringify(expectedFields)}) in ${logGroup}`; + const message = await waitFor(description, async () => { + const messages = await logMessages(logGroup, startTime, logStreamName); + return messages.find((entry) => eventMatches(entry, eventName, expectedFields)); + }); + + return message; +} diff --git a/tests/multi-runner-orchestration/smoke/webhook.test.mjs b/tests/multi-runner-orchestration/smoke/webhook.test.mjs new file mode 100644 index 0000000000..dbffbd6065 --- /dev/null +++ b/tests/multi-runner-orchestration/smoke/webhook.test.mjs @@ -0,0 +1,160 @@ +import assert from 'node:assert/strict'; +import { createHmac, randomUUID } from 'node:crypto'; +import { test } from 'node:test'; +import { + assertEc2RunnerCount, + assertCloudWatchLogContains, + invokeLambda, + postWebhook, + terraformOutput, +} from './support.mjs'; + +const testTimeout = Number(process.env.MINISTACK_SMOKE_TIMEOUT_MS ?? 7200000); +const scaleDownFunction = 'multi-runner-webhook-ec2-scale-down'; +const scaleDownLogGroup = '/aws/lambda/multi-runner-webhook-ec2-scale-down'; +let nextJobId = 0; + +function createWorkflowJobEvent(jobId, labels) { + const repositoryName = 'test-owner/test-repo'; + const jobUrl = `https://github.example.invalid/${repositoryName}/actions/jobs/${jobId}`; + const workflowRunUrl = `https://github.example.invalid/${repositoryName}/actions/runs/654321`; + + return { + action: 'queued', + installation: { id: 123 }, + repository: { + full_name: repositoryName, + id: 1, + name: 'test-repo', + owner: { login: 'test-owner', type: 'Organization' }, + }, + workflow_job: { + conclusion: null, + html_url: jobUrl, + id: jobId, + labels, + name: `node-smoke-${jobId}`, + run_id: 654321, + run_url: workflowRunUrl, + status: 'queued', + url: jobUrl, + }, + }; +} + +async function sendWorkflowJob(labels) { + const endpoint = process.env.MINISTACK_WEBHOOK_ENDPOINT + ?? await terraformOutput('webhook_endpoint'); + const secret = process.env.MINISTACK_WEBHOOK_SECRET + ?? await terraformOutput('webhook_secret'); + const jobId = Number(process.env.MINISTACK_SMOKE_JOB_ID ?? Date.now().toString().slice(-8)) + + nextJobId; + nextJobId += 1; + + const eventBody = Buffer.from(JSON.stringify(createWorkflowJobEvent(jobId, labels))); + const signature = createHmac('sha256', secret).update(eventBody).digest('hex'); + const webhookUrl = new URL(endpoint); + const startTime = Date.now() - 1000; + const response = await postWebhook( + webhookUrl.host, + `${webhookUrl.pathname}${webhookUrl.search}`, + { + 'content-type': 'application/json', + 'x-github-event': 'workflow_job', + 'x-github-delivery': `node-smoke-${randomUUID()}`, + 'x-github-hook-installation-target-id': '123', + 'x-hub-signature-256': `sha256=${signature}`, + }, + eventBody, + ); + + assert.ok( + [200, 201].includes(response.status), + `Webhook failed with HTTP ${response.status}: ${response.body}`, + ); + + const handlers = [ + ['/aws/lambda/multi-runner-webhook-webhook', 'webhook'], + ['/aws/lambda/multi-runner-webhook-dispatch-to-runner', 'dispatch-to-runner'], + ['/aws/lambda/multi-runner-webhook-ec2-scale-up', 'scale-up'], + ]; + + for (const [logGroup, handler] of handlers) { + await assertCloudWatchLogContains( + logGroup, + `${handler} handler logged job ${jobId}`, + jobId, + startTime, + ); + } + + await assertCloudWatchLogContains( + '/aws/lambda/multi-runner-webhook-ec2-scale-up', + `scale-up handler successfully created a runner for job ${jobId}`, + '"successfulRunnerCount":1,"retryableErrorCount":0', + startTime, + ); + await assertEc2RunnerCount(1); + + return { jobId, startTime }; +} + +async function scaleDown(description, startTime) { + // MiniStack does not boot the EC2 runner agent to register it with GitHub. + // The first scheduled cleanup marks it orphaned; the next confirms GitHub + // has no such runner and terminates the EC2 instance. + await invokeLambda(scaleDownFunction); + await invokeLambda(scaleDownFunction); + await assertCloudWatchLogContains( + scaleDownLogGroup, + `${description} runner instance was terminated`, + 'has been terminated.', + startTime, + ); + await assertEc2RunnerCount(0); +} + +test('Webhook: standard labels create and terminate a runner', { + timeout: testTimeout, +}, async () => { + await assertEc2RunnerCount(0); + const { jobId } = await sendWorkflowJob(['self-hosted', 'linux', 'x64', 'ec2']); + const scaleDownStartTime = Date.now() - 1000; + await scaleDown(`standard-label job ${jobId}`, scaleDownStartTime); +}); + +test('Webhook: dynamic labels apply m5.large and terminate the runner', { + timeout: testTimeout, +}, async () => { + await assertEc2RunnerCount(0); + const dynamicLabel = 'ghr-ec2-instance-type:m5.large'; + const { jobId, startTime } = await sendWorkflowJob([ + 'self-hosted', 'linux', 'x64', 'ec2', dynamicLabel, + ]); + await assertCloudWatchLogContains( + '/aws/lambda/multi-runner-webhook-ec2-scale-up', + `scale-up applied dynamic label ${dynamicLabel} for job ${jobId}`, + '"ec2OverrideConfig":{"InstanceType":"m5.large"}', + startTime, + ); + const scaleDownStartTime = Date.now() - 1000; + await scaleDown(`dynamic-label job ${jobId}`, scaleDownStartTime); +}); + +test('Pool: create and terminate one runner', { + timeout: testTimeout, +}, async () => { + await assertEc2RunnerCount(0); + const startTime = Date.now() - 1000; + await invokeLambda('multi-runner-webhook-ec2-pool', { poolSize: 1, type: 'ec2' }); + await assertCloudWatchLogContains( + '/aws/lambda/multi-runner-webhook-ec2-pool', + 'pool handler requested one runner', + 'The pool will be topped up with 1 runners.', + startTime, + ); + await assertEc2RunnerCount(1); + + const scaleDownStartTime = Date.now() - 1000; + await scaleDown('pool', scaleDownStartTime); +}); From 5f6a4ee13281d6e3e368b146606632e7c1cdded0 Mon Sep 17 00:00:00 2001 From: edersonbrilhante Date: Thu, 22 Oct 2026 15:35:23 +0200 Subject: [PATCH 2/7] test: remove unused fixtures --- .../.terraform.lock.hcl | 3 + .../.terraform.lock.hcl.tofu | 150 ++++++++++++++++ tests/multi-runner-orchestration/README.md | 21 +-- .../docker-compose.yml | 18 +- .../01-github-app---list-installations.json | 26 --- ...ithub-app---organization-installation.json | 24 --- ...-github-app---repository-installation.json | 24 --- .../06-runner---repo-registration-token.json | 17 -- ...unner---enterprise-registration-token.json | 17 -- .../08-runner---org-remove-token.json | 17 -- .../09-runner---repo-remove-token.json | 17 -- .../10-runner---enterprise-remove-token.json | 17 -- .../12-runner---list-repository-runners.json | 37 ---- .../13-runner---list-enterprise-runners.json | 37 ---- .../14-runner---delete-org-runner.json | 10 -- .../15-runner---delete-repository-runner.json | 10 -- .../16-runner---delete-enterprise-runner.json | 10 -- .../mappings/20-runner---repo-jit-config.json | 20 --- .../21-runner---enterprise-jit-config.json | 20 --- ...unner-release---latest-runner-release.json | 14 -- .../23-runner-release---runner-archive.json | 14 -- .../27-scale-set---create-scale-set.json | 35 ---- .../30-scale-set---delete-scale-set.json | 10 -- ...3-scale-set---refresh-message-session.json | 14 -- .../36-scale-set---acknowledge-message.json | 10 -- .../mappings/37-scale-set---acquire-jobs.json | 16 -- .../38-scale-set---get-runner-by-id.json | 14 -- .../42-runner---get-repository-runner.json | 32 ---- ...t.ec2.actions-registration-handshake.json} | 0 ... scale-set.ec2.close-message-session.json} | 0 ...scale-set.ec2.create-message-session.json} | 0 ...e-set.ec2.generate-jit-runner-config.json} | 0 ...=> scale-set.ec2.get-scale-set-by-id.json} | 0 ... => scale-set.ec2.list-runner-groups.json} | 0 ...> scale-set.ec2.list-runners-by-name.json} | 0 ...son => scale-set.ec2.list-scale-sets.json} | 0 ...ale-set.ec2.poll-empty-message-queue.json} | 0 ....json => scale-set.ec2.remove-runner.json} | 0 ...on => scale-set.ec2.update-scale-set.json} | 0 ...k.ec2.generate-org-jit-runner-config.json} | 0 ... webhook.ec2.get-organization-runner.json} | 0 ...ok.ec2.github-app-installation-token.json} | 0 ....ec2.list-organization-runner-groups.json} | 0 ...ebhook.ec2.list-organization-runners.json} | 0 ....ec2.organization-registration-token.json} | 0 ... => webhook.ec2.workflow-job-details.json} | 0 .../smoke/reporter.mjs | 9 +- ...ale-set.test.mjs => scale-set-support.mjs} | 117 ++++++------- .../smoke/scale-set.ec2.test.mjs | 19 +++ .../smoke/support.mjs | 64 +++++++ .../smoke/webhook.ec2.test.mjs | 72 ++++++++ .../smoke/webhook.test.mjs | 160 ------------------ 52 files changed, 385 insertions(+), 710 deletions(-) create mode 100644 examples/multi-runner-orchestration/.terraform.lock.hcl.tofu delete mode 100644 tests/multi-runner-orchestration/mappings/01-github-app---list-installations.json delete mode 100644 tests/multi-runner-orchestration/mappings/02-github-app---organization-installation.json delete mode 100644 tests/multi-runner-orchestration/mappings/03-github-app---repository-installation.json delete mode 100644 tests/multi-runner-orchestration/mappings/06-runner---repo-registration-token.json delete mode 100644 tests/multi-runner-orchestration/mappings/07-runner---enterprise-registration-token.json delete mode 100644 tests/multi-runner-orchestration/mappings/08-runner---org-remove-token.json delete mode 100644 tests/multi-runner-orchestration/mappings/09-runner---repo-remove-token.json delete mode 100644 tests/multi-runner-orchestration/mappings/10-runner---enterprise-remove-token.json delete mode 100644 tests/multi-runner-orchestration/mappings/12-runner---list-repository-runners.json delete mode 100644 tests/multi-runner-orchestration/mappings/13-runner---list-enterprise-runners.json delete mode 100644 tests/multi-runner-orchestration/mappings/14-runner---delete-org-runner.json delete mode 100644 tests/multi-runner-orchestration/mappings/15-runner---delete-repository-runner.json delete mode 100644 tests/multi-runner-orchestration/mappings/16-runner---delete-enterprise-runner.json delete mode 100644 tests/multi-runner-orchestration/mappings/20-runner---repo-jit-config.json delete mode 100644 tests/multi-runner-orchestration/mappings/21-runner---enterprise-jit-config.json delete mode 100644 tests/multi-runner-orchestration/mappings/22-runner-release---latest-runner-release.json delete mode 100644 tests/multi-runner-orchestration/mappings/23-runner-release---runner-archive.json delete mode 100644 tests/multi-runner-orchestration/mappings/27-scale-set---create-scale-set.json delete mode 100644 tests/multi-runner-orchestration/mappings/30-scale-set---delete-scale-set.json delete mode 100644 tests/multi-runner-orchestration/mappings/33-scale-set---refresh-message-session.json delete mode 100644 tests/multi-runner-orchestration/mappings/36-scale-set---acknowledge-message.json delete mode 100644 tests/multi-runner-orchestration/mappings/37-scale-set---acquire-jobs.json delete mode 100644 tests/multi-runner-orchestration/mappings/38-scale-set---get-runner-by-id.json delete mode 100644 tests/multi-runner-orchestration/mappings/42-runner---get-repository-runner.json rename tests/multi-runner-orchestration/mappings/{24-scale-set---actions-registration-handshake.json => scale-set.ec2.actions-registration-handshake.json} (100%) rename tests/multi-runner-orchestration/mappings/{34-scale-set---close-message-session.json => scale-set.ec2.close-message-session.json} (100%) rename tests/multi-runner-orchestration/mappings/{32-scale-set---create-message-session.json => scale-set.ec2.create-message-session.json} (100%) rename tests/multi-runner-orchestration/mappings/{31-scale-set---generate-jit-runner-config.json => scale-set.ec2.generate-jit-runner-config.json} (100%) rename tests/multi-runner-orchestration/mappings/{28-scale-set---get-scale-set-by-id.json => scale-set.ec2.get-scale-set-by-id.json} (100%) rename tests/multi-runner-orchestration/mappings/{25-scale-set---list-runner-groups.json => scale-set.ec2.list-runner-groups.json} (100%) rename tests/multi-runner-orchestration/mappings/{39-scale-set---list-runners-by-name.json => scale-set.ec2.list-runners-by-name.json} (100%) rename tests/multi-runner-orchestration/mappings/{26-scale-set---list-scale-sets.json => scale-set.ec2.list-scale-sets.json} (100%) rename tests/multi-runner-orchestration/mappings/{35-scale-set---poll-message-queue-empty.json => scale-set.ec2.poll-empty-message-queue.json} (100%) rename tests/multi-runner-orchestration/mappings/{40-scale-set---remove-runner.json => scale-set.ec2.remove-runner.json} (100%) rename tests/multi-runner-orchestration/mappings/{29-scale-set---update-scale-set.json => scale-set.ec2.update-scale-set.json} (100%) rename tests/multi-runner-orchestration/mappings/{19-runner---org-jit-config.json => webhook.ec2.generate-org-jit-runner-config.json} (100%) rename tests/multi-runner-orchestration/mappings/{41-runner---get-org-runner.json => webhook.ec2.get-organization-runner.json} (100%) rename tests/multi-runner-orchestration/mappings/{04-github-app---create-installation-access-token.json => webhook.ec2.github-app-installation-token.json} (100%) rename tests/multi-runner-orchestration/mappings/{17-runner---org-runner-groups.json => webhook.ec2.list-organization-runner-groups.json} (100%) rename tests/multi-runner-orchestration/mappings/{11-runner---list-org-runners.json => webhook.ec2.list-organization-runners.json} (100%) rename tests/multi-runner-orchestration/mappings/{05-runner---org-registration-token.json => webhook.ec2.organization-registration-token.json} (100%) rename tests/multi-runner-orchestration/mappings/{18-runner---repo-workflow-job.json => webhook.ec2.workflow-job-details.json} (100%) rename tests/multi-runner-orchestration/smoke/{scale-set.test.mjs => scale-set-support.mjs} (70%) create mode 100644 tests/multi-runner-orchestration/smoke/scale-set.ec2.test.mjs create mode 100644 tests/multi-runner-orchestration/smoke/webhook.ec2.test.mjs delete mode 100644 tests/multi-runner-orchestration/smoke/webhook.test.mjs diff --git a/examples/multi-runner-orchestration/.terraform.lock.hcl b/examples/multi-runner-orchestration/.terraform.lock.hcl index e393f93ab1..effbe5314b 100644 --- a/examples/multi-runner-orchestration/.terraform.lock.hcl +++ b/examples/multi-runner-orchestration/.terraform.lock.hcl @@ -6,6 +6,7 @@ provider "registry.terraform.io/hashicorp/aws" { constraints = ">= 5.0.0, >= 6.21.0, >= 6.33.0" hashes = [ "h1:VCLG26frd0ZRApDQ0l4pqjMCBUi1VPm0/M6c3pnUDgg=", + "h1:fdynuf4Y8WZ95RtQXa4tJpzcRwTnOio8XJiA/F8x7ms=", "h1:j8OcftKysEw0hTrFA5FczfGrXOFGMyorSTEiQihUw4o=", "zh:0ad1319a9fea21dacf084152e236cfd6c150ccc91854ad547c5ab9bc1cddad9a", "zh:113d7ceaca21f0e4b84f45e69369263311512f858a402955f9e9a8a5bb7f3151", @@ -32,6 +33,7 @@ provider "registry.terraform.io/hashicorp/null" { hashes = [ "h1:/Wbz7DHFO92KAqyDBMxGA8nwOJYECM8iLGhR7+wVWhQ=", "h1:IQ1qrkht1sC1nibUR+AJ3ulryyhVDHfCHZhoJi0sg2Y=", + "h1:vpRtoWIdmRPuUP+2DgvgeWNkhA9HB7h6w4xVnaS2J/U=", "zh:10ec43b8b7b18d5639238c7fb9e111f6a4b038523dd66c7a426bf27b25fa4c08", "zh:60beb9cc2ad5b871c710860cee75b42850cc6acd43db0d77cb5e00fda7288b55", "zh:62538582d0a4a2f10ad8a8d9a6c3cd3f05af6c6d91c6641ffc78d4f0e8e69b27", @@ -54,6 +56,7 @@ provider "registry.terraform.io/hashicorp/random" { hashes = [ "h1:PlW+UZ4EElQF3NQwf41KQwavFujab3Czc51zu9dyVM8=", "h1:g40qr7yDmIpaur4SsK5BcOda3HSo1RJ6zHVMqN4EJ+0=", + "h1:nozfr4CZq73d4HjubKJundX/8A+Mj282oS/hyNfjUPU=", "zh:05f4734c1f0be840b711b3eff259ebc5fca436784c728955b1678078466f48d7", "zh:0b91bf19371d012434eba1deeb6aab77158def9b39601dcbd94450b3974a2a26", "zh:0ee6eacd47ec00183d55d726a4b6c4ce951a199f944bf22f1aa58392ebdfa7a2", diff --git a/examples/multi-runner-orchestration/.terraform.lock.hcl.tofu b/examples/multi-runner-orchestration/.terraform.lock.hcl.tofu new file mode 100644 index 0000000000..e8fb9a5cf8 --- /dev/null +++ b/examples/multi-runner-orchestration/.terraform.lock.hcl.tofu @@ -0,0 +1,150 @@ +# This file is maintained automatically by "tofu init". +# Manual edits may be lost in future updates. + +provider "registry.opentofu.org/hashicorp/aws" { + version = "6.64.0" + constraints = ">= 5.0.0, >= 6.21.0, >= 6.33.0" + hashes = [ + "h1:/G38+XhC1mBVkmeWdtk/wk7lX2BxviJ2XZ70dpoaKKQ=", + "h1:7BzHdGCBG5usqOIhfBq89dkdUopnSo+qe9qRCKDSRHc=", + "h1:8AgY9Hc5/R5j97WgCcDSlbuKk0pjk3vkp7oz4mtGVY8=", + "h1:DKdOy/0RfYLxpzAXBPWTO5Eusvqx5UoGxiq2J0E6DY4=", + "h1:KSwetpR4S2eUsKmHftt73Cbx72lPWYET4V+Ej05rnkI=", + "h1:MEi5Ecge1Uwx/DRGfdVDzV5Q/soRxKh6dBHxUjGdaDQ=", + "h1:VqjWicgPZW32+YnSe0Lo78qq8/24I8XNV+E9d/lBz/4=", + "h1:WBgbFHdg/3ekWoAH6UeKiwfk6iqLr1f7TX9R/mJUK8M=", + "h1:YisB3zMV5Kh6p5/eVuPAAPEmudD/UqGN4C/V3zRtAq4=", + "h1:bG5dXqR4mSlcebUG+anerOWYDyeaScZJeLSJk0cYBfE=", + "h1:iosW/imG2pc4La7qdeM/rK6ldMXhcU6YVW7tjqwNXtI=", + "h1:nKE1gnLZxIoqukQ1YI9EUdmrQIUeAN4PWb5ecN8U9K8=", + "h1:x0hJO5+On8FaKExr4p2cNJhWsNWFZq1EiDD6CfVwy2E=", + "h1:yPH75sRH+f3aJlJAloOL/BikeZV6/0GP8VQvnJoMRKM=", + "h1:zCWB5ZD98/ZC0a50HTGoC/fTAseh189xxCFEL5Mt7r4=", + "zh:06e09ced9480ae12578122f7a25758a15d8fe684da0f6a0a61b9bc2f4a4918ad", + "zh:2035805f0ed8bf81d493e7a52f22965b3d5d402687a95d1caa8c4b1b348c1264", + "zh:25fe72a3d6a330eab6c8957f9e6bdf297ffdce95fa059fef30b80da764bee6b2", + "zh:49df644d19e39b9947e84609260028687057191ddd941783c0211386ade53040", + "zh:4d8438a5d25f18eb376c8375c70f81afb79d0fc1e63ebb6df1d0e02287964dde", + "zh:5cd9717e819506132126a896e959cd4cf1bb213c033c37777c9d01a593937e2c", + "zh:6955caa4f435373ae870de31bdda85e51c60b68c51a4206df5a21b853bcefe21", + "zh:82a413500c35241745e097797610d2bff57c26e29f34ca711182fdde5c265d13", + "zh:831f78acce42a759a977769b0409387ec13ff64b4f46c24eb7e7662e0f352525", + "zh:88648a159119a0435bf86c6cd1f7482dc43dfa2eb742f9b29053da1ee9fdabd8", + "zh:9fc745d71a2e36dbdae0ee69be70675509e5a9dec1a3c5a9be6007e568d78c07", + "zh:bf6d11d6ed1655f61f70eb2906e5d1f7ff5e78b6539dcfb3116ed6f8960c9e20", + "zh:ca17a6ca363afe930ad3474966d39cb549b7f1e5efdca909972dd26f90eefc89", + "zh:d7e9cc87ada1314e6d8ecc5849385a8f8f45757bd2c145b8657f015c65e5078d", + "zh:eddb4d6d86700788d132ba2a83d306646ccb2a3a0cec0a0ab3e215307c61d8f2", + ] +} + +provider "registry.opentofu.org/hashicorp/local" { + version = "2.9.0" + constraints = "~> 2.0" + hashes = [ + "h1:1dtKYW/5a1qob3yneL6WzOlnSGfYtJ6a2XeejCk9yb4=", + "h1:5NseXq5wU8O20ersTtV4ocrLYFFtgFr7n0pRLO1W2Rw=", + "h1:5d22ZPPK4iiygPbwRz/PJF5Es/0axVpMlPRpCR0Padw=", + "h1:AnwyolirmIlBMjH6+tV8bKkvT+5axJNYxi2y2IguiX4=", + "h1:PBp+HeseY021Fw3sLznCG27idgwPoff4cBuNmKgPL2w=", + "h1:VDxIhe4GbzdOCdmt7mQaqdwERQW6GSI7Roonts42Gr0=", + "h1:ZO6eWWnf8LjjV1q/JNeL9WLtZ6fwIttOnyN5LjCNSEo=", + "h1:dPIAf8oUAz+vW2E0iZunMvpuPddRZIztRsPSY1u+VnY=", + "h1:fwTDVG9AhFVKQZIb1EXkHv4FqzsZNlLWgkyPGDmZZEE=", + "h1:kDc465XPC7/6XFCjrMC4mTqhA9ef0FHKuJ3ZgfGNfeg=", + "h1:kGbjxrI2P8MHeyVtE1U3Q1TbyF71ExnHxtkrE+Aj6UU=", + "h1:kcoK6Afbsj54u9zaEqpecWAFKytqjBijtguCNwV3d4M=", + "h1:rxomJjDwOo+YZ+WIPc25FqEgsz9orh/2MCyUcZmFjvw=", + "h1:t0CMn/Rkwquw8l2yQ+O4ApzbMZfY2UazbsDnZygzACA=", + "h1:tJwgm2BS4xCGlElCDQEFXQoefY9Y4t0JdSKTtsPBbBo=", + "zh:13ef7ecd1e397ec5b20ea588508dd3e3b8d6c50d809ae76b079abf9dd8d02e4b", + "zh:2190c9325980076489ce02b0f5dd2c0b91fc8711cefa99e714d8619a32827ad1", + "zh:2a0cfc5600730093705071707e4a4e4e953e7d9091859e0f66b46daa1060dd5d", + "zh:2ff53eac1af43ab9a2248a0e53c963d46e19cf04bc4c3f323591cfcebb218252", + "zh:4ebc3dee700f60af9da29970052fd02fa947813162b224716862dc9d7f1f7542", + "zh:5fe6dab84ceeaa8eb3f1567c5f05578333370c472240ca5c5bfc25e92d4d5586", + "zh:66bbec16367bbf440045502c9779b11f4ac5b022c8d8d17afe12d431950838b5", + "zh:7641e5c2e4b529e869cde29ab5b1de2fd1091489eb745b19ac2709bd7f4dfd84", + "zh:855bfba0756d17ce07595ff57d7cf664443d1495127cb88fb063362734b8b22a", + "zh:aaec10f237921d60c581d1b7a66f0a8a8019d9802dc04af11b5b981f6682e01d", + "zh:e460835a38ffa1e74f6929904bfd14ef473d217fd537b7ce834abe5ce5e2ce07", + "zh:ecc4295215db0e4aea3c9329611c31e09a853e1ae207d56742403bd4f5516703", + "zh:ee6d9fae63a612072e00402894e14826af7a3351c235b9c5b423b7629a77ca29", + "zh:f2b5c8db74aa7ebcf7cd423672358437d42401675069ef67b01ff910054e49d5", + "zh:f5aff74d3eb96d4592c7bca5cd3ea89b469e84efbf382944bd0f844a57059c09", + ] +} + +provider "registry.opentofu.org/hashicorp/null" { + version = "3.3.2" + constraints = "~> 3.0, ~> 3.2" + hashes = [ + "h1:1T+00cjQNmRAHAz9xjEBFpf5wRRb0IBuXS/W8ke5BWs=", + "h1:46gmIYe+klib6TlHKSqEkMLjvnzVWiCB2NYA2zR8MX8=", + "h1:7WQ3wjfaeqnXxq+a8cYiYeWUnMTgY1JcuX+z7sZd72s=", + "h1:MVM+vkVtW/YyKfn111pyho0y87I4TekaNMbBLkn0/C8=", + "h1:QBcIbI2Dp4v6Iui37pn4qmw8YeiFLbSWcJuzZVl/65Y=", + "h1:SsVKTUR+vgLaC1YnoDa2fnYpzREcgNgWRcu5x+vwjHA=", + "h1:WUaeuTNn9w6UXZ9cMq4+qZy4ZAr71B9NcUDEXjqfdKs=", + "h1:WtEaA7alasNwEQ4L3+KyQtbkSOPsexzJ4LUZ7PKaycI=", + "h1:WxS7rjYIZ1WQc4GkICch8XrbxoSY8TjUfLbPDo6oEcQ=", + "h1:Ysvc/FPvcwk+iMg7IcLkqZhT/KhtZTYji+UBqMlcTs4=", + "h1:ZLjbXnfVcRvS/DAN3BcNebOsnOcs3Nx6mJpFCj4dZ2c=", + "h1:fAmvQjIGyqdGMc+v/fUEINCyuU4iaKSzsV8PWsOnmAc=", + "h1:jwkbEtf3S7W+Bl4soynNkUHFfK/4I/H74urHY758XVw=", + "h1:qY1sKzlxNTp/dqZR23bM4egmVMRlaQudLlBYraMt1pw=", + "h1:t8H1KNwJQwKE/GqpHeRxOWgMk0Yv35qbBTBzqB/rhr0=", + "zh:09e94b0b7dfc0c6450c247517b5410546039c758e513d89b588af6df70c3d57d", + "zh:0b72497b6fd79a2b04785b64890a565a8cc7b06ded95da05e6dab2f3b8a02d58", + "zh:1c0ee6f81f7bcdec8d568a145a450eb57a6f1cfe5e48943375d1af22ed54151e", + "zh:1c6899b475f035d352af1e7f33dc30beab8b8e3784f8cb55a2cc4a11997fbd66", + "zh:43e57a2a56e9874604501bdebe431bb573fb77d2c5f4d7598ab30727dc0e90ea", + "zh:49cf2f36298a5ac3ac8d80ceb87466e6a99d2c021005bcd9e3a79f2314fd0a13", + "zh:665be40d2c7f3d768b8f39a041371526d4b7b396b4c11e162a1886212da176c9", + "zh:6bb1583d88ddb38b1c6b4624e25ad414ddd8bf65b0dd9c074580847311f83924", + "zh:71d64453bdc795667e9841d7c90e3fef6ff157e0598d51dac4bb1e4583b85407", + "zh:73ac02bc3b680e1ea75aab24ec2a359c8f0a021f73d43b2feae0a899e75a93ad", + "zh:b2b777ee07b910e7345df85321fab6c9a25c33ecb56129758dda8fadaba09fe4", + "zh:bb984d52880749a49e509e3b243804869e1af40ee2d34322a30f5f340f8d8dbd", + "zh:e0724bc083527343b4a4099fd4f95511e49a0e113416cdba58a64446742b68b1", + "zh:e391c14e367cd64d986ddc8d81f2db76d49600ce0521720618ff1ecc0decde7f", + "zh:e95c1af8e8e9967d678cfa7c77ca229c863a68f7c9a85318bf08f26628afe338", + ] +} + +provider "registry.opentofu.org/hashicorp/random" { + version = "3.9.0" + constraints = "~> 3.0" + hashes = [ + "h1:8EQU5KSxezcjo/phRSe69rDOI0lk4pSaggj7FsskYp8=", + "h1:Lw9im2VBBJQ3RyAbHPQ0rcvcmmcZWm3x+kIOpN+Tv9s=", + "h1:U8KXqGCoNI9/guYbTvzgdtVk3fRthoG0UXwm1JoEpIs=", + "h1:YXaVd4p6qXPPVaxIBaIDNXmBwT02ZqDn0qD+tYpw8sA=", + "h1:cOpc03fphEt/G9Rfc4jLL/fW0D7tgvlXqiDKPF4vuww=", + "h1:g09RR7T1xWkeGrZwWvWMT9ncJrFGr1k3CBD585UmO7w=", + "h1:gGDdPPibmw2EWROx+sh1RGLjR5+nPwZyrf6/N9jXfeM=", + "h1:haE7/nXCOhXKP4oXeEnER3t5CaVQWqujz4nBnpeTUv4=", + "h1:ieSVpfZS2lKuMr05ph0QsOVpCzg7uk3cgKBaXR+Ikug=", + "h1:ig2s1IS9IzehorRjvVAnKIsUUj8fkgyxct1L/kswcc4=", + "h1:j3lS+ZEERFnoab8t1ppDrScGVP/cgWbzlCrEYKTCXYw=", + "h1:lxezrKmOiQIySHAM+os8qLVq7hqufDr8h3Hpzvsk+78=", + "h1:lzRqBJAG+NETxHbEZUJ/YP3RMEjZBinTX7VmgH3lw60=", + "h1:tdSNWK5ApqUsgbdYieyeYLTu6nIZUV3hR1oFqUfAuGo=", + "h1:xedet8yH/zI2CfdxsGlK0nlFWc/Bp61yrWsEa3fHB8g=", + "zh:03f1114cc20b8913523735ab76e0f0a2b16ce13c92923a53304bf85f07fc0dbc", + "zh:105b678ee72322a3067f105d7e05e940f6143238f377f6e87ff4ec909246ac2a", + "zh:55f3bbf13ea18cbace61a706566a80f25f33fe2b1780b6f3d7b582af2a05b6d2", + "zh:63adf996db48f082f7a6351eb485e219cd88795fc71e6ec60a837263ab0d2cb1", + "zh:7e99550738a4e3cc68b8a467714b0d69371025fe95e3326d5323d026d55653e9", + "zh:8342b54af3a18a37e075eeae61be57f4de2ba71b35d95c5075d402dd2c1f289d", + "zh:83ee18e32ac9dd5fc91298554b7c4cfa4c3a1db50f4c797945637cc93c0844ae", + "zh:993ecc0adbf6bd535a59fbc9b735d8c33950e6f6eb5e621d750da9b71d65d80a", + "zh:ad722bc59d4edbf1415e827fc007c0efe6e0e9462d5568bae20b34be1058a261", + "zh:ae9448e1f87b2f9a6c5197a0e9862162ec6b137cb3a3835e11522995d8939e7c", + "zh:bc9cdd3aac784f759125c6627f6f6416e8726a1c184eb9cf3e55b9edbc94c627", + "zh:c8e35b89572ba1c40a9b20022e033a3395fb8d42e7604d50c900f193ba10382e", + "zh:e2deaa8a9975ef81d9f62baed12c41286918b0a10908e0e031f13f69a3b730a1", + "zh:ee39707557210a0ab1098aa357d2cdfe502e5a312d0dbdffb09d08facc4d3fc5", + "zh:f81afe4eb63e8aa9e0ea71be6c990f0dc69cb360e7191c0742a991f4a5081b64", + ] +} diff --git a/tests/multi-runner-orchestration/README.md b/tests/multi-runner-orchestration/README.md index 42f7b4a5e2..a07cb9eab1 100644 --- a/tests/multi-runner-orchestration/README.md +++ b/tests/multi-runner-orchestration/README.md @@ -34,7 +34,7 @@ docker compose -f tests/multi-runner-orchestration/docker-compose.yml up --build docker compose -f tests/multi-runner-orchestration/docker-compose.yml wait aws-fixtures lambda-builder docker-buildx terraform-apply smoke-tests ``` -The suite contains four named Node.js test cases: webhook scale-up and scale-down with standard labels, webhook scale-up and scale-down with a dynamic EC2 instance-type label, pool runner creation and scale-down, and scale-set scale-up and scale-down. The webhook and pool cases assert the EC2 runner count before launch, after launch, and after cleanup; the dynamic-label case also verifies the requested `m5.large` override in the scale-up logs. The scale-set case checks zero runners at baseline, one runner after scale-up, and a termination returning the count to zero. All cases assert the corresponding Lambda or controller CloudWatch logs. WireMock API responses are test fixtures, so the tests do not assert those routes. Run the smoke cases with the concise reporter: +Name smoke suites `..test.mjs`; the current suites are `webhook.ec2.test.mjs` and `scale-set.ec2.test.mjs`. To cover MicroVM, add `webhook.microvm.test.mjs` and/or `scale-set.microvm.test.mjs`. Webhook suites can reuse `sendWorkflowJob` and shared AWS/CloudWatch helpers from `smoke/support.mjs`; scale-set suites can register the common lifecycle with `registerScaleSetLifecycleTest` from `smoke/scale-set-support.mjs` and provide provider-specific reconciliation assertions. Keep resource checks and cleanup provider-specific. The `*.test.mjs` command discovers new suites automatically. The suite contains four named Node.js test cases: EC2 webhook scale-up and scale-down with standard labels, EC2 webhook scale-up and scale-down with a dynamic instance-type label, EC2 pool runner creation and scale-down, and EC2 scale-set scale-up and scale-down. The webhook and pool cases assert the EC2 runner count before launch, after launch, and after cleanup; the dynamic-label case also verifies the requested `m5.large` override in the scale-up logs. The scale-set case checks zero runners at baseline, one runner after scale-up, and a termination returning the count to zero. All cases assert the corresponding Lambda or controller CloudWatch logs. WireMock API responses are test fixtures, so the tests do not assert those routes. Run the smoke cases with the concise reporter: MiniStack does not start the EC2 runner agent, so the webhook and pool tests invoke scale-down twice: the first pass marks the unregistered instance orphaned, and the next pass confirms it is absent from GitHub and terminates it. @@ -56,19 +56,16 @@ Scale-set stubs follow `lambdas/libs/github-actions-scale-set/src/endpoints.ts`, The mock does not validate GitHub App JWTs, installation permissions, runner token validity, or GitHub's authorization policy. It accepts any request matching a mapping. It also does not implement the runner agent's actual Actions service protocol; the scale-set routes here support the Lambda controller's HTTP client only. -## Covered GitHub API requests +## GitHub API mappings used by this smoke suite -| Area | Requests represented in `mappings/` | +The mappings directory contains only the requests exercised by the current MiniStack smoke paths: + +| Flow | Requests covered | |---|---| -| GitHub App auth | `GET /app/installations`; `GET /orgs/{org}/installation`; `GET /repos/{owner}/{repo}/installation`; `POST /app/installations/{id}/access_tokens` | -| Registration and deregistration | Org/repo/enterprise registration and remove-token requests; list/get/delete runner requests | -| Workflow jobs | `GET /repos/{owner}/{repo}/actions/jobs/{job_id}` | -| Runner groups and JIT | `GET /orgs/{org}/actions/runner-groups`; org/repo/enterprise JIT config requests | -| Runner release syncer | `GET /repos/actions/runner/releases/latest`; mock asset download under `/downloads/` | -| Actions scale-set API | Runner registration handshake; runner group and scale-set get/list/create/update/delete; JIT config; runner list/get/remove | -| Scale-set sessions | Session create/refresh/close, queue poll/acknowledge, and acquire-jobs | - -The enterprise routes are included as fixtures for forward-looking coverage. The current control-plane's classic runner path primarily uses organization/repository routes. The enterprise runner registration APIs have different token-auth requirements in GitHub, so the mock does not prove that an app token will be accepted by a real enterprise account. +| Webhook and EC2 pool | `POST /app/installations/{id}/access_tokens`; `POST /orgs/{org}/actions/runners/registration-token`; `GET /orgs/{org}/actions/runners`; `GET /orgs/{org}/actions/runner-groups`; `POST /orgs/{org}/actions/runners/generate-jitconfig`; `GET /orgs/{org}/actions/runners/{runner_id}` during orphan cleanup; `GET /repos/{owner}/{repo}/actions/jobs/{job_id}` | +| EC2 scale set | Registration handshake; runner-group and scale-set lookup/update; JIT runner config; message-session create/close and empty queue polling; runner lookup by name and removal | + +Mapping filenames use `...json`; add provider-specific files alongside the matching smoke suite when new paths are exercised. Fixtures for other GitHub API paths can be added in a follow-up. ## CI usage diff --git a/tests/multi-runner-orchestration/docker-compose.yml b/tests/multi-runner-orchestration/docker-compose.yml index e2a7329b71..f7d125b221 100644 --- a/tests/multi-runner-orchestration/docker-compose.yml +++ b/tests/multi-runner-orchestration/docker-compose.yml @@ -112,7 +112,6 @@ services: DOCKER_BUILDKIT: 1 terraform-apply: image: hashicorp/terraform:1.17 - platform: linux/amd64 depends_on: github-api: condition: service_started @@ -125,10 +124,19 @@ services: working_dir: /workspace entrypoint: ["/bin/sh", "-ec"] command: - - >- - terraform -chdir=/workspace/examples/multi-runner-orchestration init -input=false; - terraform -chdir=/workspace/examples/multi-runner-orchestration apply -auto-approve -input=false - -var-file=/workspace/tests/multi-runner-orchestration/fixtures/multi-runner-orchestration.tfvars + - | + host_arch=$$(uname -m) + case "$$host_arch" in + aarch64|arm64) scale_set_arch=ARM64 ;; + x86_64|amd64) scale_set_arch=X86_64 ;; + *) echo "Unsupported host architecture: $$host_arch" >&2; exit 1 ;; + esac + sed "s/architecture = \".*\"/architecture = \"$$scale_set_arch\"/" \ + /workspace/tests/multi-runner-orchestration/fixtures/multi-runner-orchestration.tfvars \ + > /tmp/multi-runner-orchestration.tfvars + terraform -chdir=/workspace/examples/multi-runner-orchestration init -input=false + terraform -chdir=/workspace/examples/multi-runner-orchestration apply -auto-approve -input=false \ + -var-file=/tmp/multi-runner-orchestration.tfvars extra_hosts: - "host.docker.internal:host-gateway" volumes: diff --git a/tests/multi-runner-orchestration/mappings/01-github-app---list-installations.json b/tests/multi-runner-orchestration/mappings/01-github-app---list-installations.json deleted file mode 100644 index 00b817393f..0000000000 --- a/tests/multi-runner-orchestration/mappings/01-github-app---list-installations.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "name": "GitHub App - list installations", - "request": { - "method": "GET", - "urlPathPattern": "(?:/api/v3)?/app/installations" - }, - "response": { - "status": 200, - "jsonBody": [ - { - "id": 1111, - "app_id": 999, - "target_id": 2222, - "target_type": "Organization", - "account": { - "login": "acme", - "id": 2222, - "type": "Organization" - } - } - ], - "headers": { - "Content-Type": "application/json" - } - } -} diff --git a/tests/multi-runner-orchestration/mappings/02-github-app---organization-installation.json b/tests/multi-runner-orchestration/mappings/02-github-app---organization-installation.json deleted file mode 100644 index 02ce6141f9..0000000000 --- a/tests/multi-runner-orchestration/mappings/02-github-app---organization-installation.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "name": "GitHub App - organization installation", - "request": { - "method": "GET", - "urlPathPattern": "(?:/api/v3)?/orgs/[^/]+/installation" - }, - "response": { - "status": 200, - "jsonBody": { - "id": 123, - "app_id": 999, - "target_id": 123, - "target_type": "Organization", - "account": { - "login": "acme", - "id": 123, - "type": "Organization" - } - }, - "headers": { - "Content-Type": "application/json" - } - } -} diff --git a/tests/multi-runner-orchestration/mappings/03-github-app---repository-installation.json b/tests/multi-runner-orchestration/mappings/03-github-app---repository-installation.json deleted file mode 100644 index 1ba1540601..0000000000 --- a/tests/multi-runner-orchestration/mappings/03-github-app---repository-installation.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "name": "GitHub App - repository installation", - "request": { - "method": "GET", - "urlPathPattern": "(?:/api/v3)?/repos/[^/]+/[^/]+/installation" - }, - "response": { - "status": 200, - "jsonBody": { - "id": 1111, - "app_id": 999, - "target_id": 2222, - "target_type": "Organization", - "account": { - "login": "acme", - "id": 2222, - "type": "Organization" - } - }, - "headers": { - "Content-Type": "application/json" - } - } -} diff --git a/tests/multi-runner-orchestration/mappings/06-runner---repo-registration-token.json b/tests/multi-runner-orchestration/mappings/06-runner---repo-registration-token.json deleted file mode 100644 index ead84d0ba5..0000000000 --- a/tests/multi-runner-orchestration/mappings/06-runner---repo-registration-token.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "name": "Runner - repo registration token", - "request": { - "method": "POST", - "urlPathPattern": "(?:/api/v3)?/repos/[^/]+/[^/]+/actions/runners/registration-token" - }, - "response": { - "status": 201, - "jsonBody": { - "token": "runner-registration-token-mock", - "expires_at": "2099-01-01T00:00:00Z" - }, - "headers": { - "Content-Type": "application/json" - } - } -} diff --git a/tests/multi-runner-orchestration/mappings/07-runner---enterprise-registration-token.json b/tests/multi-runner-orchestration/mappings/07-runner---enterprise-registration-token.json deleted file mode 100644 index 12246d5572..0000000000 --- a/tests/multi-runner-orchestration/mappings/07-runner---enterprise-registration-token.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "name": "Runner - enterprise registration token", - "request": { - "method": "POST", - "urlPathPattern": "(?:/api/v3)?/enterprises/[^/]+/actions/runners/registration-token" - }, - "response": { - "status": 201, - "jsonBody": { - "token": "runner-registration-token-mock", - "expires_at": "2099-01-01T00:00:00Z" - }, - "headers": { - "Content-Type": "application/json" - } - } -} diff --git a/tests/multi-runner-orchestration/mappings/08-runner---org-remove-token.json b/tests/multi-runner-orchestration/mappings/08-runner---org-remove-token.json deleted file mode 100644 index 76f8a6eee1..0000000000 --- a/tests/multi-runner-orchestration/mappings/08-runner---org-remove-token.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "name": "Runner - org remove token", - "request": { - "method": "POST", - "urlPathPattern": "(?:/api/v3)?/orgs/[^/]+/actions/runners/remove-token" - }, - "response": { - "status": 201, - "jsonBody": { - "token": "runner-removal-token-mock", - "expires_at": "2099-01-01T00:00:00Z" - }, - "headers": { - "Content-Type": "application/json" - } - } -} diff --git a/tests/multi-runner-orchestration/mappings/09-runner---repo-remove-token.json b/tests/multi-runner-orchestration/mappings/09-runner---repo-remove-token.json deleted file mode 100644 index 912bea1a2a..0000000000 --- a/tests/multi-runner-orchestration/mappings/09-runner---repo-remove-token.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "name": "Runner - repo remove token", - "request": { - "method": "POST", - "urlPathPattern": "(?:/api/v3)?/repos/[^/]+/[^/]+/actions/runners/remove-token" - }, - "response": { - "status": 201, - "jsonBody": { - "token": "runner-removal-token-mock", - "expires_at": "2099-01-01T00:00:00Z" - }, - "headers": { - "Content-Type": "application/json" - } - } -} diff --git a/tests/multi-runner-orchestration/mappings/10-runner---enterprise-remove-token.json b/tests/multi-runner-orchestration/mappings/10-runner---enterprise-remove-token.json deleted file mode 100644 index ceacec88e4..0000000000 --- a/tests/multi-runner-orchestration/mappings/10-runner---enterprise-remove-token.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "name": "Runner - enterprise remove token", - "request": { - "method": "POST", - "urlPathPattern": "(?:/api/v3)?/enterprises/[^/]+/actions/runners/remove-token" - }, - "response": { - "status": 201, - "jsonBody": { - "token": "runner-removal-token-mock", - "expires_at": "2099-01-01T00:00:00Z" - }, - "headers": { - "Content-Type": "application/json" - } - } -} diff --git a/tests/multi-runner-orchestration/mappings/12-runner---list-repository-runners.json b/tests/multi-runner-orchestration/mappings/12-runner---list-repository-runners.json deleted file mode 100644 index cfdaff60d7..0000000000 --- a/tests/multi-runner-orchestration/mappings/12-runner---list-repository-runners.json +++ /dev/null @@ -1,37 +0,0 @@ -{ - "name": "Runner - list repository runners", - "request": { - "method": "GET", - "urlPathPattern": "(?:/api/v3)?/repos/[^/]+/[^/]+/actions/runners" - }, - "response": { - "status": 200, - "jsonBody": { - "total_count": 1, - "runners": [ - { - "id": 4242, - "name": "wiremock-runner-001", - "os": "Linux", - "status": "online", - "busy": false, - "labels": [ - { - "id": 1, - "name": "self-hosted", - "type": "read-only" - }, - { - "id": 2, - "name": "Linux", - "type": "read-only" - } - ] - } - ] - }, - "headers": { - "Content-Type": "application/json" - } - } -} diff --git a/tests/multi-runner-orchestration/mappings/13-runner---list-enterprise-runners.json b/tests/multi-runner-orchestration/mappings/13-runner---list-enterprise-runners.json deleted file mode 100644 index 34d9e37f75..0000000000 --- a/tests/multi-runner-orchestration/mappings/13-runner---list-enterprise-runners.json +++ /dev/null @@ -1,37 +0,0 @@ -{ - "name": "Runner - list enterprise runners", - "request": { - "method": "GET", - "urlPathPattern": "(?:/api/v3)?/enterprises/[^/]+/actions/runners" - }, - "response": { - "status": 200, - "jsonBody": { - "total_count": 1, - "runners": [ - { - "id": 4242, - "name": "wiremock-runner-001", - "os": "Linux", - "status": "online", - "busy": false, - "labels": [ - { - "id": 1, - "name": "self-hosted", - "type": "read-only" - }, - { - "id": 2, - "name": "Linux", - "type": "read-only" - } - ] - } - ] - }, - "headers": { - "Content-Type": "application/json" - } - } -} diff --git a/tests/multi-runner-orchestration/mappings/14-runner---delete-org-runner.json b/tests/multi-runner-orchestration/mappings/14-runner---delete-org-runner.json deleted file mode 100644 index 7d16c49cf0..0000000000 --- a/tests/multi-runner-orchestration/mappings/14-runner---delete-org-runner.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "name": "Runner - delete org runner", - "request": { - "method": "DELETE", - "urlPathPattern": "(?:/api/v3)?/orgs/[^/]+/actions/runners/\\d+" - }, - "response": { - "status": 204 - } -} diff --git a/tests/multi-runner-orchestration/mappings/15-runner---delete-repository-runner.json b/tests/multi-runner-orchestration/mappings/15-runner---delete-repository-runner.json deleted file mode 100644 index e1c40703f7..0000000000 --- a/tests/multi-runner-orchestration/mappings/15-runner---delete-repository-runner.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "name": "Runner - delete repository runner", - "request": { - "method": "DELETE", - "urlPathPattern": "(?:/api/v3)?/repos/[^/]+/[^/]+/actions/runners/\\d+" - }, - "response": { - "status": 204 - } -} diff --git a/tests/multi-runner-orchestration/mappings/16-runner---delete-enterprise-runner.json b/tests/multi-runner-orchestration/mappings/16-runner---delete-enterprise-runner.json deleted file mode 100644 index e3759d98ff..0000000000 --- a/tests/multi-runner-orchestration/mappings/16-runner---delete-enterprise-runner.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "name": "Runner - delete enterprise runner", - "request": { - "method": "DELETE", - "urlPathPattern": "(?:/api/v3)?/enterprises/[^/]+/actions/runners/\\d+" - }, - "response": { - "status": 204 - } -} diff --git a/tests/multi-runner-orchestration/mappings/20-runner---repo-jit-config.json b/tests/multi-runner-orchestration/mappings/20-runner---repo-jit-config.json deleted file mode 100644 index 8de6c79074..0000000000 --- a/tests/multi-runner-orchestration/mappings/20-runner---repo-jit-config.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "name": "Runner - repo JIT config", - "request": { - "method": "POST", - "urlPathPattern": "(?:/api/v3)?/repos/[^/]+/[^/]+/actions/runners/generate-jitconfig" - }, - "response": { - "status": 200, - "jsonBody": { - "runner": { - "id": 4242, - "name": "wiremock-runner-001" - }, - "encoded_jit_config": "eyJtb2NrIjoiaml0LWNvbmZpZyJ9" - }, - "headers": { - "Content-Type": "application/json" - } - } -} diff --git a/tests/multi-runner-orchestration/mappings/21-runner---enterprise-jit-config.json b/tests/multi-runner-orchestration/mappings/21-runner---enterprise-jit-config.json deleted file mode 100644 index 69edbfa0c7..0000000000 --- a/tests/multi-runner-orchestration/mappings/21-runner---enterprise-jit-config.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "name": "Runner - enterprise JIT config", - "request": { - "method": "POST", - "urlPathPattern": "(?:/api/v3)?/enterprises/[^/]+/actions/runners/generate-jitconfig" - }, - "response": { - "status": 200, - "jsonBody": { - "runner": { - "id": 4242, - "name": "wiremock-runner-001" - }, - "encoded_jit_config": "eyJtb2NrIjoiaml0LWNvbmZpZyJ9" - }, - "headers": { - "Content-Type": "application/json" - } - } -} diff --git a/tests/multi-runner-orchestration/mappings/22-runner-release---latest-runner-release.json b/tests/multi-runner-orchestration/mappings/22-runner-release---latest-runner-release.json deleted file mode 100644 index 4ead33471a..0000000000 --- a/tests/multi-runner-orchestration/mappings/22-runner-release---latest-runner-release.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "name": "Runner release - latest runner release", - "request": { - "method": "GET", - "urlPathPattern": "(?:/api/v3)?/repos/actions/runner/releases/latest" - }, - "response": { - "status": 200, - "headers": { - "Content-Type": "application/json" - }, - "body": "{\"id\":123456,\"tag_name\":\"v2.321.0\",\"name\":\"actions/runner v2.321.0\",\"draft\":false,\"prerelease\":false,\"assets\":[{\"name\":\"actions-runner-linux-x64-2.321.0.tar.gz\",\"browser_download_url\":\"{{request.baseUrl}}/downloads/actions-runner-linux-x64-2.321.0.tar.gz\"}]}" - } -} diff --git a/tests/multi-runner-orchestration/mappings/23-runner-release---runner-archive.json b/tests/multi-runner-orchestration/mappings/23-runner-release---runner-archive.json deleted file mode 100644 index 67259ba3d2..0000000000 --- a/tests/multi-runner-orchestration/mappings/23-runner-release---runner-archive.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "name": "Runner release - runner archive", - "request": { - "method": "GET", - "urlPathPattern": "/downloads/actions-runner-linux-x64-2\\.321\\.0\\.tar\\.gz" - }, - "response": { - "status": 200, - "headers": { - "Content-Type": "application/octet-stream" - }, - "bodyFileName": "actions-runner-linux-x64-2.321.0.tar.gz" - } -} diff --git a/tests/multi-runner-orchestration/mappings/27-scale-set---create-scale-set.json b/tests/multi-runner-orchestration/mappings/27-scale-set---create-scale-set.json deleted file mode 100644 index 1d39c7c384..0000000000 --- a/tests/multi-runner-orchestration/mappings/27-scale-set---create-scale-set.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Scale set - create scale set", - "request": { - "method": "POST", - "urlPathPattern": "(?:/api/v3)?(?:/tenant/[^/]+)?/_apis/runtime/runnerscalesets" - }, - "response": { - "status": 200, - "jsonBody": { - "id": 223, - "name": "linux-scale-set", - "runnerGroupId": 48, - "runnerGroupName": "experimental-euw1-sl-cicd-forge-emu", - "labels": [ - { - "name": "linux-scale-set", - "type": "system" - } - ], - "runnerSetting": {}, - "statistics": { - "totalAvailableJobs": 0, - "totalAcquiredJobs": 0, - "totalAssignedJobs": 0, - "totalRunningJobs": 0, - "totalRegisteredRunners": 0, - "totalBusyRunners": 0, - "totalIdleRunners": 0 - } - }, - "headers": { - "Content-Type": "application/json" - } - } -} diff --git a/tests/multi-runner-orchestration/mappings/30-scale-set---delete-scale-set.json b/tests/multi-runner-orchestration/mappings/30-scale-set---delete-scale-set.json deleted file mode 100644 index d29b5b4dd8..0000000000 --- a/tests/multi-runner-orchestration/mappings/30-scale-set---delete-scale-set.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "name": "Scale set - delete scale set", - "request": { - "method": "DELETE", - "urlPathPattern": "(?:/api/v3)?(?:/tenant/[^/]+)?/_apis/runtime/runnerscalesets/\\d+" - }, - "response": { - "status": 204 - } -} diff --git a/tests/multi-runner-orchestration/mappings/33-scale-set---refresh-message-session.json b/tests/multi-runner-orchestration/mappings/33-scale-set---refresh-message-session.json deleted file mode 100644 index 244d01138c..0000000000 --- a/tests/multi-runner-orchestration/mappings/33-scale-set---refresh-message-session.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "name": "Scale set - refresh message session", - "request": { - "method": "PATCH", - "urlPathPattern": "(?:/api/v3)?(?:/tenant/[^/]+)?/_apis/runtime/runnerscalesets/\\d+/sessions/[^/]+" - }, - "response": { - "status": 200, - "headers": { - "Content-Type": "application/json" - }, - "body": "{\"sessionId\":\"11111111-1111-1111-1111-111111111111\",\"ownerName\":\"local.linux-scale-set\",\"runnerScaleSet\":{\"id\":223,\"name\":\"linux-scale-set\",\"runnerGroupId\":48},\"messageQueueUrl\":\"{{request.baseUrl}}/messages?sessionId=11111111-1111-1111-1111-111111111111&api-version=6.0-preview\",\"messageQueueAccessToken\":\"fake-queue-token\",\"statistics\":{\"totalAvailableJobs\":0,\"totalAcquiredJobs\":0,\"totalAssignedJobs\":0,\"totalRunningJobs\":0,\"totalRegisteredRunners\":0,\"totalBusyRunners\":0,\"totalIdleRunners\":0}}" - } -} diff --git a/tests/multi-runner-orchestration/mappings/36-scale-set---acknowledge-message.json b/tests/multi-runner-orchestration/mappings/36-scale-set---acknowledge-message.json deleted file mode 100644 index dc4e16ce80..0000000000 --- a/tests/multi-runner-orchestration/mappings/36-scale-set---acknowledge-message.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "name": "Scale set - acknowledge message", - "request": { - "method": "DELETE", - "urlPathPattern": "(?:/queue/sessions/[^/]+/messages/\\d+|/messages/\\d+)" - }, - "response": { - "status": 204 - } -} diff --git a/tests/multi-runner-orchestration/mappings/37-scale-set---acquire-jobs.json b/tests/multi-runner-orchestration/mappings/37-scale-set---acquire-jobs.json deleted file mode 100644 index b11c4779e1..0000000000 --- a/tests/multi-runner-orchestration/mappings/37-scale-set---acquire-jobs.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "name": "Scale set - acquire jobs", - "request": { - "method": "POST", - "urlPathPattern": "(?:/api/v3)?(?:/tenant/[^/]+)?/_apis/runtime/runnerscalesets/\\d+/acquirejobs" - }, - "response": { - "status": 200, - "jsonBody": { - "value": [] - }, - "headers": { - "Content-Type": "application/json" - } - } -} diff --git a/tests/multi-runner-orchestration/mappings/38-scale-set---get-runner-by-id.json b/tests/multi-runner-orchestration/mappings/38-scale-set---get-runner-by-id.json deleted file mode 100644 index 844212da3f..0000000000 --- a/tests/multi-runner-orchestration/mappings/38-scale-set---get-runner-by-id.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "name": "Scale set - get runner by id", - "request": { - "method": "GET", - "urlPathPattern": "(?:/api/v3)?(?:/tenant/[^/]+)?/_apis/distributedtask/pools/0/agents/\\d+" - }, - "response": { - "status": 200, - "headers": { - "Content-Type": "application/json" - }, - "body": "{\"id\":321,\"name\":\"linux-scale-set\",\"runnerScaleSetId\":223}" - } -} diff --git a/tests/multi-runner-orchestration/mappings/42-runner---get-repository-runner.json b/tests/multi-runner-orchestration/mappings/42-runner---get-repository-runner.json deleted file mode 100644 index 61a2f27b6d..0000000000 --- a/tests/multi-runner-orchestration/mappings/42-runner---get-repository-runner.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "name": "runner get repository runner", - "request": { - "method": "GET", - "urlPathPattern": "(?:/api/v3)?/repos/[^/]+/[^/]+/actions/runners/\\d+" - }, - "response": { - "status": 200, - "headers": { - "Content-Type": "application/json" - }, - "jsonBody": { - "id": 4242, - "name": "wiremock-runner-001", - "os": "Linux", - "status": "online", - "busy": false, - "labels": [ - { - "id": 1, - "name": "self-hosted", - "type": "read-only" - }, - { - "id": 2, - "name": "Linux", - "type": "read-only" - } - ] - } - } -} diff --git a/tests/multi-runner-orchestration/mappings/24-scale-set---actions-registration-handshake.json b/tests/multi-runner-orchestration/mappings/scale-set.ec2.actions-registration-handshake.json similarity index 100% rename from tests/multi-runner-orchestration/mappings/24-scale-set---actions-registration-handshake.json rename to tests/multi-runner-orchestration/mappings/scale-set.ec2.actions-registration-handshake.json diff --git a/tests/multi-runner-orchestration/mappings/34-scale-set---close-message-session.json b/tests/multi-runner-orchestration/mappings/scale-set.ec2.close-message-session.json similarity index 100% rename from tests/multi-runner-orchestration/mappings/34-scale-set---close-message-session.json rename to tests/multi-runner-orchestration/mappings/scale-set.ec2.close-message-session.json diff --git a/tests/multi-runner-orchestration/mappings/32-scale-set---create-message-session.json b/tests/multi-runner-orchestration/mappings/scale-set.ec2.create-message-session.json similarity index 100% rename from tests/multi-runner-orchestration/mappings/32-scale-set---create-message-session.json rename to tests/multi-runner-orchestration/mappings/scale-set.ec2.create-message-session.json diff --git a/tests/multi-runner-orchestration/mappings/31-scale-set---generate-jit-runner-config.json b/tests/multi-runner-orchestration/mappings/scale-set.ec2.generate-jit-runner-config.json similarity index 100% rename from tests/multi-runner-orchestration/mappings/31-scale-set---generate-jit-runner-config.json rename to tests/multi-runner-orchestration/mappings/scale-set.ec2.generate-jit-runner-config.json diff --git a/tests/multi-runner-orchestration/mappings/28-scale-set---get-scale-set-by-id.json b/tests/multi-runner-orchestration/mappings/scale-set.ec2.get-scale-set-by-id.json similarity index 100% rename from tests/multi-runner-orchestration/mappings/28-scale-set---get-scale-set-by-id.json rename to tests/multi-runner-orchestration/mappings/scale-set.ec2.get-scale-set-by-id.json diff --git a/tests/multi-runner-orchestration/mappings/25-scale-set---list-runner-groups.json b/tests/multi-runner-orchestration/mappings/scale-set.ec2.list-runner-groups.json similarity index 100% rename from tests/multi-runner-orchestration/mappings/25-scale-set---list-runner-groups.json rename to tests/multi-runner-orchestration/mappings/scale-set.ec2.list-runner-groups.json diff --git a/tests/multi-runner-orchestration/mappings/39-scale-set---list-runners-by-name.json b/tests/multi-runner-orchestration/mappings/scale-set.ec2.list-runners-by-name.json similarity index 100% rename from tests/multi-runner-orchestration/mappings/39-scale-set---list-runners-by-name.json rename to tests/multi-runner-orchestration/mappings/scale-set.ec2.list-runners-by-name.json diff --git a/tests/multi-runner-orchestration/mappings/26-scale-set---list-scale-sets.json b/tests/multi-runner-orchestration/mappings/scale-set.ec2.list-scale-sets.json similarity index 100% rename from tests/multi-runner-orchestration/mappings/26-scale-set---list-scale-sets.json rename to tests/multi-runner-orchestration/mappings/scale-set.ec2.list-scale-sets.json diff --git a/tests/multi-runner-orchestration/mappings/35-scale-set---poll-message-queue-empty.json b/tests/multi-runner-orchestration/mappings/scale-set.ec2.poll-empty-message-queue.json similarity index 100% rename from tests/multi-runner-orchestration/mappings/35-scale-set---poll-message-queue-empty.json rename to tests/multi-runner-orchestration/mappings/scale-set.ec2.poll-empty-message-queue.json diff --git a/tests/multi-runner-orchestration/mappings/40-scale-set---remove-runner.json b/tests/multi-runner-orchestration/mappings/scale-set.ec2.remove-runner.json similarity index 100% rename from tests/multi-runner-orchestration/mappings/40-scale-set---remove-runner.json rename to tests/multi-runner-orchestration/mappings/scale-set.ec2.remove-runner.json diff --git a/tests/multi-runner-orchestration/mappings/29-scale-set---update-scale-set.json b/tests/multi-runner-orchestration/mappings/scale-set.ec2.update-scale-set.json similarity index 100% rename from tests/multi-runner-orchestration/mappings/29-scale-set---update-scale-set.json rename to tests/multi-runner-orchestration/mappings/scale-set.ec2.update-scale-set.json diff --git a/tests/multi-runner-orchestration/mappings/19-runner---org-jit-config.json b/tests/multi-runner-orchestration/mappings/webhook.ec2.generate-org-jit-runner-config.json similarity index 100% rename from tests/multi-runner-orchestration/mappings/19-runner---org-jit-config.json rename to tests/multi-runner-orchestration/mappings/webhook.ec2.generate-org-jit-runner-config.json diff --git a/tests/multi-runner-orchestration/mappings/41-runner---get-org-runner.json b/tests/multi-runner-orchestration/mappings/webhook.ec2.get-organization-runner.json similarity index 100% rename from tests/multi-runner-orchestration/mappings/41-runner---get-org-runner.json rename to tests/multi-runner-orchestration/mappings/webhook.ec2.get-organization-runner.json diff --git a/tests/multi-runner-orchestration/mappings/04-github-app---create-installation-access-token.json b/tests/multi-runner-orchestration/mappings/webhook.ec2.github-app-installation-token.json similarity index 100% rename from tests/multi-runner-orchestration/mappings/04-github-app---create-installation-access-token.json rename to tests/multi-runner-orchestration/mappings/webhook.ec2.github-app-installation-token.json diff --git a/tests/multi-runner-orchestration/mappings/17-runner---org-runner-groups.json b/tests/multi-runner-orchestration/mappings/webhook.ec2.list-organization-runner-groups.json similarity index 100% rename from tests/multi-runner-orchestration/mappings/17-runner---org-runner-groups.json rename to tests/multi-runner-orchestration/mappings/webhook.ec2.list-organization-runner-groups.json diff --git a/tests/multi-runner-orchestration/mappings/11-runner---list-org-runners.json b/tests/multi-runner-orchestration/mappings/webhook.ec2.list-organization-runners.json similarity index 100% rename from tests/multi-runner-orchestration/mappings/11-runner---list-org-runners.json rename to tests/multi-runner-orchestration/mappings/webhook.ec2.list-organization-runners.json diff --git a/tests/multi-runner-orchestration/mappings/05-runner---org-registration-token.json b/tests/multi-runner-orchestration/mappings/webhook.ec2.organization-registration-token.json similarity index 100% rename from tests/multi-runner-orchestration/mappings/05-runner---org-registration-token.json rename to tests/multi-runner-orchestration/mappings/webhook.ec2.organization-registration-token.json diff --git a/tests/multi-runner-orchestration/mappings/18-runner---repo-workflow-job.json b/tests/multi-runner-orchestration/mappings/webhook.ec2.workflow-job-details.json similarity index 100% rename from tests/multi-runner-orchestration/mappings/18-runner---repo-workflow-job.json rename to tests/multi-runner-orchestration/mappings/webhook.ec2.workflow-job-details.json diff --git a/tests/multi-runner-orchestration/smoke/reporter.mjs b/tests/multi-runner-orchestration/smoke/reporter.mjs index 2571787980..a83a0d18ea 100644 --- a/tests/multi-runner-orchestration/smoke/reporter.mjs +++ b/tests/multi-runner-orchestration/smoke/reporter.mjs @@ -1,15 +1,16 @@ export default async function* smokeReporter(events) { for await (const event of events) { - const { name, details, error } = event.data ?? {}; + const { name, details } = event.data ?? {}; + const error = details?.error ?? event.data?.error; + const failure = error?.cause ?? error; + const failureText = failure?.stack ?? failure?.message ?? (failure ? String(failure) : 'No error details in test event'); if (details?.type !== 'test') continue; if (event.type === 'test:pass') { yield `PASS ${name}\n`; } else if (event.type === 'test:fail') { yield `FAIL ${name}\n`; - if (error?.message) { - yield ` ${error.message.split('\n').join('\n ')}\n`; - } + yield ` ${failureText.split('\n').join('\n ')}\n`; } } } diff --git a/tests/multi-runner-orchestration/smoke/scale-set.test.mjs b/tests/multi-runner-orchestration/smoke/scale-set-support.mjs similarity index 70% rename from tests/multi-runner-orchestration/smoke/scale-set.test.mjs rename to tests/multi-runner-orchestration/smoke/scale-set-support.mjs index 0774cee705..8e4749b2eb 100644 --- a/tests/multi-runner-orchestration/smoke/scale-set.test.mjs +++ b/tests/multi-runner-orchestration/smoke/scale-set-support.mjs @@ -11,9 +11,11 @@ import { const testTimeout = Number(process.env.MINISTACK_SMOKE_TIMEOUT_MS ?? 7200000); -async function findScaleSetControllerService() { +export async function findScaleSetControllerService( + clusterName = process.env.MINISTACK_SCALE_SET_CLUSTER ?? 'multi-runner-webhook-scale-set', +) { const clusters = (await aws('ecs', 'list-clusters')).clusterArns ?? []; - const clusterArn = clusters.find((arn) => arn.includes('multi-runner-webhook-scale-set')); + const clusterArn = clusters.find((arn) => arn.includes(clusterName)); assert.ok(clusterArn, 'Scale-set ECS cluster is missing; ensure Compose terraform-apply completed'); const serviceArns = (await aws('ecs', 'list-services', '--cluster', clusterArn)).serviceArns ?? []; @@ -46,7 +48,7 @@ async function findScaleSetControllerService() { throw new Error('Scale-set controller ECS service/task definition not found'); } -function prepareTaskDefinition(controller, minimumRunners) { +export function prepareTaskDefinition(controller, minimumRunners) { const taskDefinition = structuredClone(controller.taskDefinition); const registeredFields = [ 'taskDefinitionArn', @@ -78,7 +80,7 @@ function prepareTaskDefinition(controller, minimumRunners) { return taskDefinition; } -async function waitForTaskRevision(controller, taskDefinitionArn, minimumRunners) { +export async function waitForTaskRevision(controller, taskDefinitionArn, minimumRunners) { return waitFor(`controller deployment with minimum ${minimumRunners}`, async () => { const response = await aws( 'ecs', 'list-tasks', @@ -100,7 +102,7 @@ async function waitForTaskRevision(controller, taskDefinitionArn, minimumRunners }); } -async function deployMinimumRunners(controller, minimumRunners, temporaryDirectory) { +export async function deployMinimumRunners(controller, minimumRunners, temporaryDirectory) { const taskDefinition = prepareTaskDefinition(controller, minimumRunners); const inputFile = path.join(temporaryDirectory, `task-${minimumRunners}.json`); await writeFile(inputFile, JSON.stringify(taskDefinition)); @@ -131,7 +133,7 @@ async function deployMinimumRunners(controller, minimumRunners, temporaryDirecto }; } -async function assertReconciliation(controller, minimumRunners, deployment) { +export async function assertReconciliation(controller, minimumRunners, deployment) { await assertCloudWatchEvent( controller.logGroup, 'scale_set_compute_provider_reconcile_started', @@ -151,68 +153,49 @@ async function assertReconciliation(controller, minimumRunners, deployment) { return JSON.parse(message); } -test('Scale set: baseline at 0, scale up to 1, and scale down to 0 runners', { - timeout: testTimeout, -}, async () => { - const controller = await findScaleSetControllerService(); - assert.ok(controller.logGroup, 'Scale-set controller task has no CloudWatch log group'); - - const temporaryDirectory = await mkdtemp(path.join(tmpdir(), 'scale-set-smoke-')); - const scaleSteps = [ - { - minimumRunners: 0, - verify: (event) => assert.equal( - event.currentRunners, - 0, - 'Scale-set baseline should have no runners', - ), - }, - { - minimumRunners: 1, - verify: (event) => { - assert.ok( - event.actions?.launched > 0, - 'Scale-set scale-up did not launch a runner', - ); - assert.equal( - event.currentRunners, - 1, - 'Scale-set scale-up did not reach one runner', - ); - }, - }, - { - minimumRunners: 0, - verify: (event) => { - assert.ok( - event.actions?.terminated > 0, - 'Scale-set scale-down did not terminate a runner', +export function registerScaleSetLifecycleTest({ + providerName, + clusterName = process.env.MINISTACK_SCALE_SET_CLUSTER ?? 'multi-runner-webhook-scale-set', + verifyBaseline, + verifyScaleUp, + verifyScaleDown, +}) { + for (const [name, verifier] of Object.entries({ verifyBaseline, verifyScaleUp, verifyScaleDown })) { + if (typeof verifier !== 'function') { + throw new TypeError(`${name} must be a function for ${providerName} scale-set smoke coverage`); + } + } + + test(`${providerName} Scale set: baseline at 0, scale up to 1, and scale down to 0 runners`, { + timeout: testTimeout, + }, async () => { + const controller = await findScaleSetControllerService(clusterName); + assert.ok(controller.logGroup, 'Scale-set controller task has no CloudWatch log group'); + + const temporaryDirectory = await mkdtemp(path.join(tmpdir(), 'scale-set-smoke-')); + const scaleSteps = [ + { minimumRunners: 0, verify: verifyBaseline }, + { minimumRunners: 1, verify: verifyScaleUp }, + { minimumRunners: 0, verify: verifyScaleDown }, + ]; + + try { + for (const step of scaleSteps) { + const deployment = await deployMinimumRunners( + controller, + step.minimumRunners, + temporaryDirectory, ); - assert.equal( - event.currentRunners, - 0, - 'Scale-set scale-down did not return to zero runners', + const reconciliation = await assertReconciliation( + controller, + step.minimumRunners, + deployment, ); - }, - }, - ]; - try { - for (const step of scaleSteps) { - const deployment = await deployMinimumRunners( - controller, - step.minimumRunners, - temporaryDirectory, - ); - const reconciliation = await assertReconciliation( - controller, - step.minimumRunners, - deployment, - ); - - step.verify?.(reconciliation); + step.verify(reconciliation); + } + } finally { + await rm(temporaryDirectory, { recursive: true, force: true }); } - } finally { - await rm(temporaryDirectory, { recursive: true, force: true }); - } -}); + }); +} diff --git a/tests/multi-runner-orchestration/smoke/scale-set.ec2.test.mjs b/tests/multi-runner-orchestration/smoke/scale-set.ec2.test.mjs new file mode 100644 index 0000000000..1e21ba1e28 --- /dev/null +++ b/tests/multi-runner-orchestration/smoke/scale-set.ec2.test.mjs @@ -0,0 +1,19 @@ +import assert from 'node:assert/strict'; +import { registerScaleSetLifecycleTest } from './scale-set-support.mjs'; + +registerScaleSetLifecycleTest({ + providerName: 'EC2', + verifyBaseline: (event) => assert.equal( + event.currentRunners, + 0, + 'Scale-set baseline should have no runners', + ), + verifyScaleUp: (event) => { + assert.ok(event.actions?.launched > 0, 'EC2 scale-set scale-up did not launch a runner'); + assert.equal(event.currentRunners, 1, 'EC2 scale-set scale-up did not reach one runner'); + }, + verifyScaleDown: (event) => { + assert.ok(event.actions?.terminated > 0, 'EC2 scale-set scale-down did not terminate a runner'); + assert.equal(event.currentRunners, 0, 'EC2 scale-set scale-down did not return to zero runners'); + }, +}); diff --git a/tests/multi-runner-orchestration/smoke/support.mjs b/tests/multi-runner-orchestration/smoke/support.mjs index 04a3bd48d7..86edcf7791 100644 --- a/tests/multi-runner-orchestration/smoke/support.mjs +++ b/tests/multi-runner-orchestration/smoke/support.mjs @@ -1,4 +1,5 @@ import { spawn } from 'node:child_process'; +import { createHmac, randomUUID } from 'node:crypto'; import { mkdtemp, rm } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { setTimeout as delay } from 'node:timers/promises'; @@ -130,6 +131,69 @@ export async function terraformOutput(name) { return result.stdout; } +let nextJobId = 0; + +function createWorkflowJobEvent(jobId, labels) { + const repositoryName = 'test-owner/test-repo'; + const jobUrl = `https://github.example.invalid/${repositoryName}/actions/jobs/${jobId}`; + const workflowRunUrl = `https://github.example.invalid/${repositoryName}/actions/runs/654321`; + + return { + action: 'queued', + installation: { id: 123 }, + repository: { + full_name: repositoryName, + id: 1, + name: 'test-repo', + owner: { login: 'test-owner', type: 'Organization' }, + }, + workflow_job: { + conclusion: null, + html_url: jobUrl, + id: jobId, + labels, + name: `node-smoke-${jobId}`, + run_id: 654321, + run_url: workflowRunUrl, + status: 'queued', + url: jobUrl, + }, + }; +} + +export async function sendWorkflowJob(labels) { + const endpoint = process.env.MINISTACK_WEBHOOK_ENDPOINT + ?? await terraformOutput('webhook_endpoint'); + const secret = process.env.MINISTACK_WEBHOOK_SECRET + ?? await terraformOutput('webhook_secret'); + const jobId = Number(process.env.MINISTACK_SMOKE_JOB_ID ?? Date.now().toString().slice(-8)) + + nextJobId; + nextJobId += 1; + + const eventBody = Buffer.from(JSON.stringify(createWorkflowJobEvent(jobId, labels))); + const signature = createHmac('sha256', secret).update(eventBody).digest('hex'); + const webhookUrl = new URL(endpoint); + const startTime = Date.now() - 1000; + const response = await postWebhook( + webhookUrl.host, + `${webhookUrl.pathname}${webhookUrl.search}`, + { + 'content-type': 'application/json', + 'x-github-event': 'workflow_job', + 'x-github-delivery': `node-smoke-${randomUUID()}`, + 'x-github-hook-installation-target-id': '123', + 'x-hub-signature-256': `sha256=${signature}`, + }, + eventBody, + ); + + if (![200, 201].includes(response.status)) { + throw new Error(`Webhook failed with HTTP ${response.status}: ${response.body}`); + } + + return { jobId, startTime }; +} + export function postWebhook(host, pathname, headers, body) { return new Promise((resolve, reject) => { const endpoint = new URL(awsEndpoint); diff --git a/tests/multi-runner-orchestration/smoke/webhook.ec2.test.mjs b/tests/multi-runner-orchestration/smoke/webhook.ec2.test.mjs new file mode 100644 index 0000000000..3f3c18663c --- /dev/null +++ b/tests/multi-runner-orchestration/smoke/webhook.ec2.test.mjs @@ -0,0 +1,72 @@ +import { test } from 'node:test'; +import { + assertEc2RunnerCount, + assertCloudWatchLogContains, + invokeLambda, + sendWorkflowJob, +} from './support.mjs'; + +const testTimeout = Number(process.env.MINISTACK_SMOKE_TIMEOUT_MS ?? 7200000); +const scaleDownFunction = 'multi-runner-webhook-ec2-scale-down'; +const scaleDownLogGroup = '/aws/lambda/multi-runner-webhook-ec2-scale-down'; + +async function scaleDown(description, startTime) { + // MiniStack does not boot the EC2 runner agent to register it with GitHub. + // The first scheduled cleanup marks it orphaned; the next confirms GitHub + // has no such runner and terminates the EC2 instance. + await invokeLambda(scaleDownFunction); + await invokeLambda(scaleDownFunction); + await assertCloudWatchLogContains( + scaleDownLogGroup, + `${description} runner instance was terminated`, + 'has been terminated.', + startTime, + ); + await assertEc2RunnerCount(0); +} + +test('EC2 Webhook: standard labels create and terminate a runner', { + timeout: testTimeout, +}, async () => { + await assertEc2RunnerCount(0); + const { jobId } = await sendWorkflowJob(['self-hosted', 'linux', 'x64', 'ec2']); + await assertEc2RunnerCount(1); + const scaleDownStartTime = Date.now() - 1000; + await scaleDown(`standard-label job ${jobId}`, scaleDownStartTime); +}); + +test('EC2 Webhook: dynamic labels apply m5.large and terminate the runner', { + timeout: testTimeout, +}, async () => { + await assertEc2RunnerCount(0); + const dynamicLabel = 'ghr-ec2-instance-type:m5.large'; + const { jobId, startTime } = await sendWorkflowJob([ + 'self-hosted', 'linux', 'x64', 'ec2', dynamicLabel, + ]); + await assertCloudWatchLogContains( + '/aws/lambda/multi-runner-webhook-ec2-scale-up', + `scale-up applied dynamic label ${dynamicLabel} for job ${jobId}`, + '"ec2OverrideConfig":{"InstanceType":"m5.large"}', + startTime, + ); + const scaleDownStartTime = Date.now() - 1000; + await scaleDown(`dynamic-label job ${jobId}`, scaleDownStartTime); +}); + +test('EC2 Pool: create and terminate one runner', { + timeout: testTimeout, +}, async () => { + await assertEc2RunnerCount(0); + const startTime = Date.now() - 1000; + await invokeLambda('multi-runner-webhook-ec2-pool', { poolSize: 1, type: 'ec2' }); + await assertCloudWatchLogContains( + '/aws/lambda/multi-runner-webhook-ec2-pool', + 'pool handler requested one runner', + 'The pool will be topped up with 1 runners.', + startTime, + ); + await assertEc2RunnerCount(1); + + const scaleDownStartTime = Date.now() - 1000; + await scaleDown('pool', scaleDownStartTime); +}); diff --git a/tests/multi-runner-orchestration/smoke/webhook.test.mjs b/tests/multi-runner-orchestration/smoke/webhook.test.mjs deleted file mode 100644 index dbffbd6065..0000000000 --- a/tests/multi-runner-orchestration/smoke/webhook.test.mjs +++ /dev/null @@ -1,160 +0,0 @@ -import assert from 'node:assert/strict'; -import { createHmac, randomUUID } from 'node:crypto'; -import { test } from 'node:test'; -import { - assertEc2RunnerCount, - assertCloudWatchLogContains, - invokeLambda, - postWebhook, - terraformOutput, -} from './support.mjs'; - -const testTimeout = Number(process.env.MINISTACK_SMOKE_TIMEOUT_MS ?? 7200000); -const scaleDownFunction = 'multi-runner-webhook-ec2-scale-down'; -const scaleDownLogGroup = '/aws/lambda/multi-runner-webhook-ec2-scale-down'; -let nextJobId = 0; - -function createWorkflowJobEvent(jobId, labels) { - const repositoryName = 'test-owner/test-repo'; - const jobUrl = `https://github.example.invalid/${repositoryName}/actions/jobs/${jobId}`; - const workflowRunUrl = `https://github.example.invalid/${repositoryName}/actions/runs/654321`; - - return { - action: 'queued', - installation: { id: 123 }, - repository: { - full_name: repositoryName, - id: 1, - name: 'test-repo', - owner: { login: 'test-owner', type: 'Organization' }, - }, - workflow_job: { - conclusion: null, - html_url: jobUrl, - id: jobId, - labels, - name: `node-smoke-${jobId}`, - run_id: 654321, - run_url: workflowRunUrl, - status: 'queued', - url: jobUrl, - }, - }; -} - -async function sendWorkflowJob(labels) { - const endpoint = process.env.MINISTACK_WEBHOOK_ENDPOINT - ?? await terraformOutput('webhook_endpoint'); - const secret = process.env.MINISTACK_WEBHOOK_SECRET - ?? await terraformOutput('webhook_secret'); - const jobId = Number(process.env.MINISTACK_SMOKE_JOB_ID ?? Date.now().toString().slice(-8)) - + nextJobId; - nextJobId += 1; - - const eventBody = Buffer.from(JSON.stringify(createWorkflowJobEvent(jobId, labels))); - const signature = createHmac('sha256', secret).update(eventBody).digest('hex'); - const webhookUrl = new URL(endpoint); - const startTime = Date.now() - 1000; - const response = await postWebhook( - webhookUrl.host, - `${webhookUrl.pathname}${webhookUrl.search}`, - { - 'content-type': 'application/json', - 'x-github-event': 'workflow_job', - 'x-github-delivery': `node-smoke-${randomUUID()}`, - 'x-github-hook-installation-target-id': '123', - 'x-hub-signature-256': `sha256=${signature}`, - }, - eventBody, - ); - - assert.ok( - [200, 201].includes(response.status), - `Webhook failed with HTTP ${response.status}: ${response.body}`, - ); - - const handlers = [ - ['/aws/lambda/multi-runner-webhook-webhook', 'webhook'], - ['/aws/lambda/multi-runner-webhook-dispatch-to-runner', 'dispatch-to-runner'], - ['/aws/lambda/multi-runner-webhook-ec2-scale-up', 'scale-up'], - ]; - - for (const [logGroup, handler] of handlers) { - await assertCloudWatchLogContains( - logGroup, - `${handler} handler logged job ${jobId}`, - jobId, - startTime, - ); - } - - await assertCloudWatchLogContains( - '/aws/lambda/multi-runner-webhook-ec2-scale-up', - `scale-up handler successfully created a runner for job ${jobId}`, - '"successfulRunnerCount":1,"retryableErrorCount":0', - startTime, - ); - await assertEc2RunnerCount(1); - - return { jobId, startTime }; -} - -async function scaleDown(description, startTime) { - // MiniStack does not boot the EC2 runner agent to register it with GitHub. - // The first scheduled cleanup marks it orphaned; the next confirms GitHub - // has no such runner and terminates the EC2 instance. - await invokeLambda(scaleDownFunction); - await invokeLambda(scaleDownFunction); - await assertCloudWatchLogContains( - scaleDownLogGroup, - `${description} runner instance was terminated`, - 'has been terminated.', - startTime, - ); - await assertEc2RunnerCount(0); -} - -test('Webhook: standard labels create and terminate a runner', { - timeout: testTimeout, -}, async () => { - await assertEc2RunnerCount(0); - const { jobId } = await sendWorkflowJob(['self-hosted', 'linux', 'x64', 'ec2']); - const scaleDownStartTime = Date.now() - 1000; - await scaleDown(`standard-label job ${jobId}`, scaleDownStartTime); -}); - -test('Webhook: dynamic labels apply m5.large and terminate the runner', { - timeout: testTimeout, -}, async () => { - await assertEc2RunnerCount(0); - const dynamicLabel = 'ghr-ec2-instance-type:m5.large'; - const { jobId, startTime } = await sendWorkflowJob([ - 'self-hosted', 'linux', 'x64', 'ec2', dynamicLabel, - ]); - await assertCloudWatchLogContains( - '/aws/lambda/multi-runner-webhook-ec2-scale-up', - `scale-up applied dynamic label ${dynamicLabel} for job ${jobId}`, - '"ec2OverrideConfig":{"InstanceType":"m5.large"}', - startTime, - ); - const scaleDownStartTime = Date.now() - 1000; - await scaleDown(`dynamic-label job ${jobId}`, scaleDownStartTime); -}); - -test('Pool: create and terminate one runner', { - timeout: testTimeout, -}, async () => { - await assertEc2RunnerCount(0); - const startTime = Date.now() - 1000; - await invokeLambda('multi-runner-webhook-ec2-pool', { poolSize: 1, type: 'ec2' }); - await assertCloudWatchLogContains( - '/aws/lambda/multi-runner-webhook-ec2-pool', - 'pool handler requested one runner', - 'The pool will be topped up with 1 runners.', - startTime, - ); - await assertEc2RunnerCount(1); - - const scaleDownStartTime = Date.now() - 1000; - await scaleDown('pool', scaleDownStartTime); -}); From 03fd67d257287a515dda7e340d660c667c057533 Mon Sep 17 00:00:00 2001 From: edersonbrilhante Date: Thu, 22 Oct 2026 16:04:07 +0200 Subject: [PATCH 3/7] ci: fix ci --- .github/workflows/smoke-tests.yml | 10 ++++++++-- tests/multi-runner-orchestration/README.md | 12 +++++++++--- .../docker-compose.yml | 16 +++++++++++++++- .../multi-runner-orchestration/smoke/support.mjs | 16 ++++++++++++---- 4 files changed, 44 insertions(+), 10 deletions(-) diff --git a/.github/workflows/smoke-tests.yml b/.github/workflows/smoke-tests.yml index 4f947d717c..ad9a9d23c6 100644 --- a/.github/workflows/smoke-tests.yml +++ b/.github/workflows/smoke-tests.yml @@ -34,6 +34,12 @@ jobs: with: persist-credentials: false - - name: Build and start the complete Compose stack + - name: Build and run MiniStack smoke tests run: | - docker compose -f tests/multi-runner-orchestration/docker-compose.yml up --build --force-recreate + compose_file=tests/multi-runner-orchestration/docker-compose.yml + docker compose -f "$compose_file" build + docker compose -f "$compose_file" run --no-TTY --rm smoke-tests + + - name: Stop MiniStack Compose services + if: always() + run: docker compose -f tests/multi-runner-orchestration/docker-compose.yml down --remove-orphans diff --git a/tests/multi-runner-orchestration/README.md b/tests/multi-runner-orchestration/README.md index a07cb9eab1..385649f4cc 100644 --- a/tests/multi-runner-orchestration/README.md +++ b/tests/multi-runner-orchestration/README.md @@ -27,11 +27,17 @@ The controller image build is configured directly in the `docker-buildx` Compose ## Programmatic MiniStack smoke test -Compose owns the environment lifecycle: `up` starts WireMock and MiniStack, seeds AWS fixtures, builds the Lambda archives and scale-set controller image, applies `examples/multi-runner-orchestration`, then runs the `smoke-tests` job container. That job first registers a scale-set task-definition revision with dummy MiniStack credentials, updates the ECS service, then runs the Node.js smoke suite: +Compose owns the environment lifecycle: `run smoke-tests` starts the required WireMock and MiniStack dependencies, seeds AWS fixtures, builds the Lambda archives and scale-set controller image, applies `examples/multi-runner-orchestration`, then runs the smoke-test container. The command streams the Node.js reporter output and returns the smoke container's exit code. The job first registers a scale-set task-definition revision with dummy MiniStack credentials and updates the ECS service: ```sh -docker compose -f tests/multi-runner-orchestration/docker-compose.yml up --build --detach -docker compose -f tests/multi-runner-orchestration/docker-compose.yml wait aws-fixtures lambda-builder docker-buildx terraform-apply smoke-tests +docker compose -f tests/multi-runner-orchestration/docker-compose.yml build +docker compose -f tests/multi-runner-orchestration/docker-compose.yml run --no-TTY --rm smoke-tests +``` + +Stop the long-running MiniStack and WireMock services after the command finishes: + +```sh +docker compose -f tests/multi-runner-orchestration/docker-compose.yml down --remove-orphans ``` Name smoke suites `..test.mjs`; the current suites are `webhook.ec2.test.mjs` and `scale-set.ec2.test.mjs`. To cover MicroVM, add `webhook.microvm.test.mjs` and/or `scale-set.microvm.test.mjs`. Webhook suites can reuse `sendWorkflowJob` and shared AWS/CloudWatch helpers from `smoke/support.mjs`; scale-set suites can register the common lifecycle with `registerScaleSetLifecycleTest` from `smoke/scale-set-support.mjs` and provide provider-specific reconciliation assertions. Keep resource checks and cleanup provider-specific. The `*.test.mjs` command discovers new suites automatically. The suite contains four named Node.js test cases: EC2 webhook scale-up and scale-down with standard labels, EC2 webhook scale-up and scale-down with a dynamic instance-type label, EC2 pool runner creation and scale-down, and EC2 scale-set scale-up and scale-down. The webhook and pool cases assert the EC2 runner count before launch, after launch, and after cleanup; the dynamic-label case also verifies the requested `m5.large` override in the scale-up logs. The scale-set case checks zero runners at baseline, one runner after scale-up, and a termination returning the count to zero. All cases assert the corresponding Lambda or controller CloudWatch logs. WireMock API responses are test fixtures, so the tests do not assert those routes. Run the smoke cases with the concise reporter: diff --git a/tests/multi-runner-orchestration/docker-compose.yml b/tests/multi-runner-orchestration/docker-compose.yml index f7d125b221..7c3abb1fdf 100644 --- a/tests/multi-runner-orchestration/docker-compose.yml +++ b/tests/multi-runner-orchestration/docker-compose.yml @@ -44,6 +44,7 @@ services: - | apk add --no-cache bash git cd /workspace + git config --global --add safe.directory /workspace OUTPUT_DIR=/workspace/lambda_output bash .ci/build.sh volumes: - ../..:/workspace @@ -73,6 +74,12 @@ services: echo "Could not resolve MiniStack on the Compose network" >&2 exit 1 fi + docker_arch="$$(docker info --format '{{.Architecture}}')" + case "$$docker_arch" in + aarch64|arm64) scale_set_platform=linux/arm64 ;; + x86_64|amd64) scale_set_platform=linux/amd64 ;; + *) echo "Unsupported Docker host architecture: $$docker_arch" >&2; exit 1 ;; + esac export MINISTACK_REGISTRY="$$ministack_ip:4566" docker_config="$$(mktemp -d)" export DOCKER_CONFIG="$$docker_config" @@ -94,7 +101,7 @@ services: --use --bootstrap docker buildx build --builder runner-smoke-builder \ --network host --allow network.host \ - --platform linux/amd64,linux/arm64 \ + --platform "$$scale_set_platform" \ --target runtime \ --file /workspace/lambdas/services/scale-set/Dockerfile \ --tag "$${MINISTACK_REGISTRY}/scale-set-controller:latest" \ @@ -141,6 +148,7 @@ services: - "host.docker.internal:host-gateway" volumes: - ../..:/workspace + - terraform-data:/terraform-data environment: AWS_ACCESS_KEY_ID: "000000000000" AWS_SECRET_ACCESS_KEY: test-only @@ -150,6 +158,7 @@ services: AWS_ENDPOINT_URL: http://ministack:4566 TF_IN_AUTOMATION: "true" TF_INPUT: "false" + TF_DATA_DIR: /terraform-data smoke-tests: image: hashicorp/terraform:1.17 @@ -168,6 +177,7 @@ services: tests/multi-runner-orchestration/smoke/*.test.mjs volumes: - ../..:/workspace + - terraform-data:/terraform-data environment: AWS_ACCESS_KEY_ID: "000000000000" AWS_SECRET_ACCESS_KEY: test-only @@ -177,6 +187,10 @@ services: AWS_ENDPOINT_URL: http://ministack:4566 TF_IN_AUTOMATION: "true" TF_INPUT: "false" + TF_DATA_DIR: /terraform-data + +volumes: + terraform-data: networks: default: diff --git a/tests/multi-runner-orchestration/smoke/support.mjs b/tests/multi-runner-orchestration/smoke/support.mjs index 86edcf7791..4cc2f0d729 100644 --- a/tests/multi-runner-orchestration/smoke/support.mjs +++ b/tests/multi-runner-orchestration/smoke/support.mjs @@ -253,10 +253,18 @@ export async function logMessages(logGroup, startTime, logStreamName) { } export async function assertCloudWatchLogContains(logGroup, description, text, startTime) { - await waitFor(description, async () => { - const messages = await logMessages(logGroup, startTime); - return messages.find((message) => message.includes(String(text))); - }); + let recentMessages = []; + try { + await waitFor(description, async () => { + recentMessages = await logMessages(logGroup, startTime); + return recentMessages.find((message) => message.includes(String(text))); + }); + } catch (error) { + const logContext = recentMessages.length + ? recentMessages.slice(-10).join('\n') + : `No CloudWatch messages found in ${logGroup} after ${new Date(startTime).toISOString()}`; + throw new Error(`${error.message}\nRecent CloudWatch messages:\n${logContext}`, { cause: error }); + } } From 710598bf2122706289222cd244d4edfe828bf766 Mon Sep 17 00:00:00 2001 From: edersonbrilhante Date: Thu, 22 Oct 2026 16:54:11 +0200 Subject: [PATCH 4/7] docs: update docs --- docs/configuration.md | 4 ---- docs/examples/index.md | 2 -- mkdocs.yaml | 2 -- 3 files changed, 8 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index d207f23c70..74eb855661 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -373,10 +373,6 @@ The scale-set lane itself defaults to `runner.group_name = "Default"`, name, runner owner, and GitHub App SSM references in the lane; credential values are not placed in the controller manifest. -The [multi-runner scale-set example](multi-runner-scale-set.md) shows how these -provider-specific settings coexist with webhook lanes in the same v2 -`multi_runner_config` map. - ### macOS Runners This feature is in early stage and should be considered experimental. The module supports macOS-based GitHub Actions self-hosted runners on AWS EC2 Mac instances (`mac1.metal`, `mac2.metal`, `mac2-m2.metal`). macOS runners require dedicated hosts due to Apple's licensing requirements and have longer boot times (6–20 minutes). Set `runner_os = "osx"` and `use_dedicated_host = true` to enable. See the full [macOS Runners documentation](mac-runners.md) for details. diff --git a/docs/examples/index.md b/docs/examples/index.md index 50aff55389..aee1d868b0 100644 --- a/docs/examples/index.md +++ b/docs/examples/index.md @@ -5,8 +5,6 @@ Examples are located in the [examples](https://github.com/github-aws-runners/ter - _[Default](default.md)_: The default example of the module - _[Ephemeral](ephemeral.md)_: Example usages of ephemeral runners based on the default example. - _[Multi Runner](multi-runner.md)_ : Example usage of creating a multi runner which creates multiple runners/ configurations with a single deployment. The examples including: "arm64", "windows", and "ubuntu" runners. -- _[Multi Runner v2](multi-runner-v2.md)_ : Example usage of the experimental v2 multi-runner configuration interface with shared defaults and per-lane overrides. -- _[Multi Runner scale-set](multi-runner-scale-set.md)_ : Example usage of a v2 deployment combining webhook lanes with an experimental GitHub Actions scale-set lane. - _[Permissions boundary](permissions-boundary.md)_: Example usages of permissions boundaries. - _[Prebuilt Images](prebuilt.md)_: Example usages of deploying runners with a custom prebuilt image. - _[Termination watcher](termination-watcher.md)_: Example usages of termination watcher. diff --git a/mkdocs.yaml b/mkdocs.yaml index 4553b6f71f..ba38664afb 100644 --- a/mkdocs.yaml +++ b/mkdocs.yaml @@ -81,8 +81,6 @@ nav: - Overview: examples/index.md - Default: examples/default.md - Multi Runner: examples/multi-runner.md - - Multi Runner v2: examples/multi-runner-v2.md - - Multi Runner scale-set: examples/multi-runner-scale-set.md - Ephemeral: examples/ephemeral.md - External managed secrets: examples/external-managed-ssm-secrets.md - Custom AMI: examples/prebuilt.md From c1d87d32ef8677b45dfde0f1037a45cf70244ee3 Mon Sep 17 00:00:00 2001 From: edersonbrilhante Date: Thu, 22 Oct 2026 16:56:34 +0200 Subject: [PATCH 5/7] test: share tf files with services --- .github/workflows/smoke-tests.yml | 12 +++++++---- tests/multi-runner-orchestration/README.md | 2 +- .../docker-compose.yml | 6 ++---- .../multi-runner-orchestration.tfvars | 2 +- ...ook.ec2.get-organization-installation.json | 21 +++++++++++++++++++ .../smoke/support.mjs | 20 +++++++++++------- 6 files changed, 46 insertions(+), 17 deletions(-) create mode 100644 tests/multi-runner-orchestration/mappings/webhook.ec2.get-organization-installation.json diff --git a/.github/workflows/smoke-tests.yml b/.github/workflows/smoke-tests.yml index ad9a9d23c6..5e632fec88 100644 --- a/.github/workflows/smoke-tests.yml +++ b/.github/workflows/smoke-tests.yml @@ -34,12 +34,16 @@ jobs: with: persist-credentials: false - - name: Build and run MiniStack smoke tests + - name: Build run: | compose_file=tests/multi-runner-orchestration/docker-compose.yml docker compose -f "$compose_file" build + + - name: Run MiniStack smoke tests + run: | + compose_file=tests/multi-runner-orchestration/docker-compose.yml docker compose -f "$compose_file" run --no-TTY --rm smoke-tests - - name: Stop MiniStack Compose services - if: always() - run: docker compose -f tests/multi-runner-orchestration/docker-compose.yml down --remove-orphans + - name: Print MiniStack logs on failure + if: failure() + run: docker compose -f tests/multi-runner-orchestration/docker-compose.yml logs --no-color --timestamps --tail=all ministack || true diff --git a/tests/multi-runner-orchestration/README.md b/tests/multi-runner-orchestration/README.md index 385649f4cc..c6914102e9 100644 --- a/tests/multi-runner-orchestration/README.md +++ b/tests/multi-runner-orchestration/README.md @@ -68,7 +68,7 @@ The mappings directory contains only the requests exercised by the current MiniS | Flow | Requests covered | |---|---| -| Webhook and EC2 pool | `POST /app/installations/{id}/access_tokens`; `POST /orgs/{org}/actions/runners/registration-token`; `GET /orgs/{org}/actions/runners`; `GET /orgs/{org}/actions/runner-groups`; `POST /orgs/{org}/actions/runners/generate-jitconfig`; `GET /orgs/{org}/actions/runners/{runner_id}` during orphan cleanup; `GET /repos/{owner}/{repo}/actions/jobs/{job_id}` | +| Webhook and EC2 pool | `POST /app/installations/{id}/access_tokens`; `GET /orgs/{org}/installation` (pool installation lookup); `POST /orgs/{org}/actions/runners/registration-token`; `GET /orgs/{org}/actions/runners`; `GET /orgs/{org}/actions/runner-groups`; `POST /orgs/{org}/actions/runners/generate-jitconfig`; `GET /orgs/{org}/actions/runners/{runner_id}` during orphan cleanup; `GET /repos/{owner}/{repo}/actions/jobs/{job_id}` | | EC2 scale set | Registration handshake; runner-group and scale-set lookup/update; JIT runner config; message-session create/close and empty queue polling; runner lookup by name and removal | Mapping filenames use `...json`; add provider-specific files alongside the matching smoke suite when new paths are exercised. Fixtures for other GitHub API paths can be added in a follow-up. diff --git a/tests/multi-runner-orchestration/docker-compose.yml b/tests/multi-runner-orchestration/docker-compose.yml index 7c3abb1fdf..cde5c8d25e 100644 --- a/tests/multi-runner-orchestration/docker-compose.yml +++ b/tests/multi-runner-orchestration/docker-compose.yml @@ -148,7 +148,7 @@ services: - "host.docker.internal:host-gateway" volumes: - ../..:/workspace - - terraform-data:/terraform-data + - terraform-data:/workspace/examples/multi-runner-orchestration/.terraform environment: AWS_ACCESS_KEY_ID: "000000000000" AWS_SECRET_ACCESS_KEY: test-only @@ -158,7 +158,6 @@ services: AWS_ENDPOINT_URL: http://ministack:4566 TF_IN_AUTOMATION: "true" TF_INPUT: "false" - TF_DATA_DIR: /terraform-data smoke-tests: image: hashicorp/terraform:1.17 @@ -177,7 +176,7 @@ services: tests/multi-runner-orchestration/smoke/*.test.mjs volumes: - ../..:/workspace - - terraform-data:/terraform-data + - terraform-data:/workspace/examples/multi-runner-orchestration/.terraform environment: AWS_ACCESS_KEY_ID: "000000000000" AWS_SECRET_ACCESS_KEY: test-only @@ -187,7 +186,6 @@ services: AWS_ENDPOINT_URL: http://ministack:4566 TF_IN_AUTOMATION: "true" TF_INPUT: "false" - TF_DATA_DIR: /terraform-data volumes: terraform-data: diff --git a/tests/multi-runner-orchestration/fixtures/multi-runner-orchestration.tfvars b/tests/multi-runner-orchestration/fixtures/multi-runner-orchestration.tfvars index 2046fb1448..e5025654ad 100644 --- a/tests/multi-runner-orchestration/fixtures/multi-runner-orchestration.tfvars +++ b/tests/multi-runner-orchestration/fixtures/multi-runner-orchestration.tfvars @@ -5,7 +5,7 @@ runners_lambda_zip = "../../lambda_output/runners.zip" webhook_lambda_zip = "../../lambda_output/webhook.zip" github = { - url = "https://host.docker.internal:8443" + url = "https://github-api:8443" ssl_verify = false runner_owner = "example" registration_level = "organization" diff --git a/tests/multi-runner-orchestration/mappings/webhook.ec2.get-organization-installation.json b/tests/multi-runner-orchestration/mappings/webhook.ec2.get-organization-installation.json new file mode 100644 index 0000000000..c11b2ce83b --- /dev/null +++ b/tests/multi-runner-orchestration/mappings/webhook.ec2.get-organization-installation.json @@ -0,0 +1,21 @@ +{ + "name": "GitHub App - get organization installation", + "request": { + "method": "GET", + "urlPathPattern": "(?:/api/v3)?/orgs/[^/]+/installation" + }, + "response": { + "status": 200, + "jsonBody": { + "id": 123, + "account": { + "login": "test-owner", + "type": "Organization" + }, + "target_type": "Organization" + }, + "headers": { + "Content-Type": "application/json" + } + } +} diff --git a/tests/multi-runner-orchestration/smoke/support.mjs b/tests/multi-runner-orchestration/smoke/support.mjs index 4cc2f0d729..fa0ec69b98 100644 --- a/tests/multi-runner-orchestration/smoke/support.mjs +++ b/tests/multi-runner-orchestration/smoke/support.mjs @@ -271,7 +271,7 @@ export async function assertCloudWatchLogContains(logGroup, description, text, s function eventMatches(message, eventName, expectedFields) { try { const event = JSON.parse(message); - return event.event === eventName + return (event.event === eventName || event.message === eventName) && Object.entries(expectedFields).every(([key, value]) => event[key] === value); } catch { return message.includes(eventName) @@ -289,10 +289,16 @@ export async function assertCloudWatchEvent( logStreamName, ) { const description = `CloudWatch ${eventName} (${JSON.stringify(expectedFields)}) in ${logGroup}`; - const message = await waitFor(description, async () => { - const messages = await logMessages(logGroup, startTime, logStreamName); - return messages.find((entry) => eventMatches(entry, eventName, expectedFields)); - }); - - return message; + let recentMessages = []; + try { + return await waitFor(description, async () => { + recentMessages = await logMessages(logGroup, startTime, logStreamName); + return recentMessages.find((entry) => eventMatches(entry, eventName, expectedFields)); + }); + } catch (error) { + const logContext = recentMessages.length + ? recentMessages.slice(-10).join('\n') + : `No CloudWatch messages found in ${logGroup} after ${new Date(startTime).toISOString()}`; + throw new Error(`${error.message}\nRecent CloudWatch messages:\n${logContext}`, { cause: error }); + } } From 649255a6077d60d38df64afd01c0c7da6371cfe3 Mon Sep 17 00:00:00 2001 From: edersonbrilhante Date: Mon, 2 Nov 2026 17:45:47 +0100 Subject: [PATCH 6/7] ci: fix docker compose --- .github/workflows/smoke-tests.yml | 11 +- .../.terraform.lock.hcl | 38 +++---- examples/multi-runner-orchestration/main.tf | 1 + tests/multi-runner-orchestration/README.md | 9 +- .../docker-compose.yml | 16 ++- ...ebhook.ec2.delete-organization-runner.json | 10 ++ ...k.ec2.list-organization-runner-groups.json | 4 +- .../smoke/prepare-scale-set-credentials.mjs | 22 +--- .../smoke/reporter.mjs | 15 ++- .../smoke/support.mjs | 107 ++++++++++++++++-- .../smoke/webhook.ec2.test.mjs | 60 +++++++--- 11 files changed, 208 insertions(+), 85 deletions(-) create mode 100644 tests/multi-runner-orchestration/mappings/webhook.ec2.delete-organization-runner.json diff --git a/.github/workflows/smoke-tests.yml b/.github/workflows/smoke-tests.yml index 5e632fec88..a288497c79 100644 --- a/.github/workflows/smoke-tests.yml +++ b/.github/workflows/smoke-tests.yml @@ -34,15 +34,12 @@ jobs: with: persist-credentials: false - - name: Build + - name: Docker Compose Build and Run run: | compose_file=tests/multi-runner-orchestration/docker-compose.yml - docker compose -f "$compose_file" build - - - name: Run MiniStack smoke tests - run: | - compose_file=tests/multi-runner-orchestration/docker-compose.yml - docker compose -f "$compose_file" run --no-TTY --rm smoke-tests + docker compose --parallel 1 -f "$compose_file" pull + docker compose --parallel 1 -f "$compose_file" build + docker compose --parallel 1 -f "$compose_file" run --no-TTY --rm smoke-tests - name: Print MiniStack logs on failure if: failure() diff --git a/examples/multi-runner-orchestration/.terraform.lock.hcl b/examples/multi-runner-orchestration/.terraform.lock.hcl index effbe5314b..9112f6be79 100644 --- a/examples/multi-runner-orchestration/.terraform.lock.hcl +++ b/examples/multi-runner-orchestration/.terraform.lock.hcl @@ -2,28 +2,28 @@ # Manual edits may be lost in future updates. provider "registry.terraform.io/hashicorp/aws" { - version = "6.69.1" + version = "6.71.0" constraints = ">= 5.0.0, >= 6.21.0, >= 6.33.0" hashes = [ - "h1:VCLG26frd0ZRApDQ0l4pqjMCBUi1VPm0/M6c3pnUDgg=", - "h1:fdynuf4Y8WZ95RtQXa4tJpzcRwTnOio8XJiA/F8x7ms=", - "h1:j8OcftKysEw0hTrFA5FczfGrXOFGMyorSTEiQihUw4o=", - "zh:0ad1319a9fea21dacf084152e236cfd6c150ccc91854ad547c5ab9bc1cddad9a", - "zh:113d7ceaca21f0e4b84f45e69369263311512f858a402955f9e9a8a5bb7f3151", - "zh:1828581462a8a985361c2f4e9af90b2bd5b7d4603b00146bb23a88c5b7d50888", - "zh:28d454d8ac98a88903540bf17bf63d6935dd2e08e572e41152d06db926a5333a", - "zh:2a3b594a98fedc396a4eb5f0fd324e641f83db0906d0129c2ddeffd8002fbe86", - "zh:32c3103af70480b5c3be86ab00af2f46d3a2a6f269ddadf8c1e53a7a3ea0031b", - "zh:3df1dd01ec3a65291f51f26b4d28dcd1bf7b4c73ed0bd3ab1e94e1153ce9b13d", - "zh:42699fbd59bf14adb1cf2836c80c209b3ce815d252f4e983afc8defa754b1dfe", - "zh:5522afd762a1ee95e6ef0d7ad1f165185303a3039e45e1632a3cd13314b7a1a4", - "zh:69f3537ccb012cf24cc11af3cfc3b4be5602b4a3bfbaae2dcab78e41b40f280b", + "h1:HNddcXLjW2P3WWN1JFIqeg8o3ouCwit4fD14Emx6+rc=", + "h1:aLOQts7nCLMR0kb/jCdP56wlQQoGqhKLctKgtQjDopc=", + "h1:sZCupscga0Grk22+04FNY1RYDeOcVbCTd6ff8+hsF94=", + "zh:09aa14408c5ab26915fb2a469b5eb67c06393a2054d78592ba4c32f7a882e918", + "zh:1d1138128dc4866f6c16b1ef3b871b3fd9a3dc648e9056e06e210d1817aa052b", + "zh:2c17a79821a0e06fbf1834a12e92227db29c2001fef7ef0b347af2e6dad83f14", + "zh:55616730a485c9c7283546a84f1ac5f8341152c5572338a54fc7af5f69ebfb92", + "zh:7e6461ec2976b5961a1d4f3c08c68fae0645d7f5e0a404f7647baa3b88d337d8", + "zh:7ec935166b2251d075fea980012f5267078785399585e8b443dca484fc57df0f", + "zh:885b3290cd48738cbf9ee8a6d47a4452889843f8f53b8cf67fc8c9b16e553721", + "zh:8930fb33c6c965f7cc82230cb7fbb0cfff9bb30becdfce5e0c2242512d849894", "zh:9b12af85486a96aedd8d7984b0ff811a4b42e3d88dad1a3fb4c0b580d04fa425", - "zh:a0c5bf13ba5e17a993aab381603a32d6c9dc2ae8d9f982b6a8c6407b5d7c4a50", - "zh:b9f8a73e11e0b82ba51474b6a6eb4ac3080d7f268087162cb10884655878670a", - "zh:be1f9728404d67dee586d16b69a445931e5e4dafbf7b996df3a7eed519a557b0", - "zh:dc15fb3443312a42deb8b13f92c8d10a701e46eca0155f0709ad763057a85a1d", - "zh:e9f6567a13f21123838aaf78d94f76da8c8f7783eff7f7aa781fd1c21d18d86c", + "zh:a702f9a4dc7340182a8daf5b6ee562f14657fe14752f7d54dc799cf2fd91a868", + "zh:b624cb320d787fbfa6dc94be5a1fd52683d1ebd11b42646262b221a694e6ffb8", + "zh:bcbaded68b553afeea93604ec22c254b98f3951862bdeb68ae89c179c7dda117", + "zh:cb57bbcaf0fa3a70ea29b60d183d3a0dd020f041b18da5d1725d2acd17fa7d59", + "zh:d4ca164b1b6039557d8f5f5f911c4aaad1a187e9b3758f6885c95f8954f7de58", + "zh:eb0c705337cef7b96086e8b84702ba965eeeace7a4aa3235b2c8aad5f50f2fca", + "zh:f2787f40ce873d8c1308c92adec9b6adbd223506fe9d79bba447729e422f9146", ] } diff --git a/examples/multi-runner-orchestration/main.tf b/examples/multi-runner-orchestration/main.tf index ee771b4fe1..4dbe26d9ac 100644 --- a/examples/multi-runner-orchestration/main.tf +++ b/examples/multi-runner-orchestration/main.tf @@ -144,6 +144,7 @@ module "runners" { architecture = "x64" name_prefix = "ec2-" extra_labels = ["ec2"] + group_name = var.scale_set.runner_group_name } orchestration_provider = { webhook = { diff --git a/tests/multi-runner-orchestration/README.md b/tests/multi-runner-orchestration/README.md index c6914102e9..9bfe69b528 100644 --- a/tests/multi-runner-orchestration/README.md +++ b/tests/multi-runner-orchestration/README.md @@ -21,17 +21,18 @@ docker compose -f tests/multi-runner-orchestration/docker-compose.yml down ## MiniStack fixtures -`docker compose -f tests/multi-runner-orchestration/docker-compose.yml up --build` runs `.ci/build.sh` to build the Lambda distributions, starts MiniStack, seeds AMIs and the `scale-set-controller` ECR repository/policy, builds the controller for `linux/amd64` and `linux/arm64`, and pushes it to MiniStack. The Compose services use `linux/amd64`; CI enables QEMU to build the ARM64 image on its AMD64 runner. +`docker compose --parallel 1 -f tests/multi-runner-orchestration/docker-compose.yml up --build` runs `.ci/build.sh` to build the Lambda distributions, starts MiniStack, seeds the AMI, the `scale-set-controller` ECR repository/policy, builds the controller for `linux/amd64` and `linux/arm64`, and pushes it to MiniStack. The Compose services use `linux/amd64`; CI enables QEMU to build the ARM64 image on its AMD64 runner. The controller image build is configured directly in the `docker-buildx` Compose service; it is not a fixture in `fixtures/ministack-fixtures.json`. Compose creates the `multi-runner-orchestration` network and attaches MiniStack's nested ECS containers to it. The AWS CLI uses `ministack:4566` to reach MiniStack. The job resolves MiniStack's network IP for BuildKit's registry push, since BuildKit does not resolve Compose service DNS in this setup; it configures that registry as plain HTTP. ## Programmatic MiniStack smoke test -Compose owns the environment lifecycle: `run smoke-tests` starts the required WireMock and MiniStack dependencies, seeds AWS fixtures, builds the Lambda archives and scale-set controller image, applies `examples/multi-runner-orchestration`, then runs the smoke-test container. The command streams the Node.js reporter output and returns the smoke container's exit code. The job first registers a scale-set task-definition revision with dummy MiniStack credentials and updates the ECS service: +Compose owns the environment lifecycle. Run `terraform-apply` as a blocking step before starting the smoke test container; the smoke command uses `--no-deps` so it cannot begin early or trigger a second apply. The apply service starts its prerequisites, seeds AWS fixtures, builds the Lambda archives and scale-set controller image, then applies `examples/multi-runner-orchestration`. The smoke step runs only after that command exits successfully and returns the smoke container's exit code. The job first registers a scale-set task-definition revision with dummy MiniStack credentials and updates the ECS service: ```sh -docker compose -f tests/multi-runner-orchestration/docker-compose.yml build -docker compose -f tests/multi-runner-orchestration/docker-compose.yml run --no-TTY --rm smoke-tests +docker compose --parallel 1 -f tests/multi-runner-orchestration/docker-compose.yml build +docker compose -f tests/multi-runner-orchestration/docker-compose.yml run --no-TTY --rm terraform-apply +docker compose -f tests/multi-runner-orchestration/docker-compose.yml run --no-TTY --rm --no-deps smoke-tests ``` Stop the long-running MiniStack and WireMock services after the command finishes: diff --git a/tests/multi-runner-orchestration/docker-compose.yml b/tests/multi-runner-orchestration/docker-compose.yml index cde5c8d25e..c5c3c8480d 100644 --- a/tests/multi-runner-orchestration/docker-compose.yml +++ b/tests/multi-runner-orchestration/docker-compose.yml @@ -122,6 +122,8 @@ services: depends_on: github-api: condition: service_started + ministack: + condition: service_started aws-fixtures: condition: service_completed_successfully lambda-builder: @@ -139,16 +141,15 @@ services: *) echo "Unsupported host architecture: $$host_arch" >&2; exit 1 ;; esac sed "s/architecture = \".*\"/architecture = \"$$scale_set_arch\"/" \ - /workspace/tests/multi-runner-orchestration/fixtures/multi-runner-orchestration.tfvars \ + ./tests/multi-runner-orchestration/fixtures/multi-runner-orchestration.tfvars \ > /tmp/multi-runner-orchestration.tfvars - terraform -chdir=/workspace/examples/multi-runner-orchestration init -input=false - terraform -chdir=/workspace/examples/multi-runner-orchestration apply -auto-approve -input=false \ + terraform -chdir=./examples/multi-runner-orchestration init + terraform -chdir=./examples/multi-runner-orchestration apply -auto-approve \ -var-file=/tmp/multi-runner-orchestration.tfvars extra_hosts: - "host.docker.internal:host-gateway" volumes: - ../..:/workspace - - terraform-data:/workspace/examples/multi-runner-orchestration/.terraform environment: AWS_ACCESS_KEY_ID: "000000000000" AWS_SECRET_ACCESS_KEY: test-only @@ -158,7 +159,6 @@ services: AWS_ENDPOINT_URL: http://ministack:4566 TF_IN_AUTOMATION: "true" TF_INPUT: "false" - smoke-tests: image: hashicorp/terraform:1.17 platform: linux/amd64 @@ -169,6 +169,8 @@ services: entrypoint: ["/bin/sh", "-ec"] command: - | + terraform -chdir=./examples/multi-runner-orchestration init + terraform -chdir=./examples/multi-runner-orchestration output apk add --no-cache aws-cli nodejs node tests/multi-runner-orchestration/smoke/prepare-scale-set-credentials.mjs node --test --test-concurrency=1 \ @@ -176,7 +178,6 @@ services: tests/multi-runner-orchestration/smoke/*.test.mjs volumes: - ../..:/workspace - - terraform-data:/workspace/examples/multi-runner-orchestration/.terraform environment: AWS_ACCESS_KEY_ID: "000000000000" AWS_SECRET_ACCESS_KEY: test-only @@ -187,9 +188,6 @@ services: TF_IN_AUTOMATION: "true" TF_INPUT: "false" -volumes: - terraform-data: - networks: default: name: multi-runner-orchestration_default diff --git a/tests/multi-runner-orchestration/mappings/webhook.ec2.delete-organization-runner.json b/tests/multi-runner-orchestration/mappings/webhook.ec2.delete-organization-runner.json new file mode 100644 index 0000000000..7d16c49cf0 --- /dev/null +++ b/tests/multi-runner-orchestration/mappings/webhook.ec2.delete-organization-runner.json @@ -0,0 +1,10 @@ +{ + "name": "Runner - delete org runner", + "request": { + "method": "DELETE", + "urlPathPattern": "(?:/api/v3)?/orgs/[^/]+/actions/runners/\\d+" + }, + "response": { + "status": 204 + } +} diff --git a/tests/multi-runner-orchestration/mappings/webhook.ec2.list-organization-runner-groups.json b/tests/multi-runner-orchestration/mappings/webhook.ec2.list-organization-runner-groups.json index 24f03af319..c0b2b5af98 100644 --- a/tests/multi-runner-orchestration/mappings/webhook.ec2.list-organization-runner-groups.json +++ b/tests/multi-runner-orchestration/mappings/webhook.ec2.list-organization-runner-groups.json @@ -11,9 +11,9 @@ "runner_groups": [ { "id": 1, - "name": "Default", + "name": "experimental-euw1-sl-cicd-forge-emu", "size": 100, - "isDefaultGroup": true + "isDefaultGroup": false } ] }, diff --git a/tests/multi-runner-orchestration/smoke/prepare-scale-set-credentials.mjs b/tests/multi-runner-orchestration/smoke/prepare-scale-set-credentials.mjs index bee29333b4..b4bc79f821 100644 --- a/tests/multi-runner-orchestration/smoke/prepare-scale-set-credentials.mjs +++ b/tests/multi-runner-orchestration/smoke/prepare-scale-set-credentials.mjs @@ -3,26 +3,12 @@ import { mkdtemp, rm, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import path from 'node:path'; import { aws } from './support.mjs'; +import { findScaleSetControllerService } from './scale-set-support.mjs'; const cluster = process.env.MINISTACK_SCALE_SET_CLUSTER ?? 'multi-runner-webhook-scale-set'; -const serviceArns = (await aws('ecs', 'list-services', '--cluster', cluster)).serviceArns ?? []; -assert.ok(serviceArns.length > 0, `No ECS service found in cluster ${cluster}`); - -const serviceResponse = await aws( - 'ecs', 'describe-services', - '--cluster', cluster, - '--services', serviceArns[0], -); -const service = serviceResponse.services?.[0]; -assert.ok(service?.taskDefinition, `No task definition found for service ${serviceArns[0]}`); - -const taskDefinitionResponse = await aws( - 'ecs', 'describe-task-definition', - '--task-definition', service.taskDefinition, -); -const taskDefinition = taskDefinitionResponse.taskDefinition; -assert.ok(taskDefinition, `Could not describe task definition ${service.taskDefinition}`); +const controllerService = await findScaleSetControllerService(cluster); +const taskDefinition = structuredClone(controllerService.taskDefinition); for (const field of [ 'taskDefinitionArn', @@ -65,7 +51,7 @@ try { await aws( 'ecs', 'update-service', '--cluster', cluster, - '--service', service.serviceName, + '--service', controllerService.service.serviceName, '--task-definition', taskDefinitionArn, '--force-new-deployment', ); diff --git a/tests/multi-runner-orchestration/smoke/reporter.mjs b/tests/multi-runner-orchestration/smoke/reporter.mjs index a83a0d18ea..225ad1a45f 100644 --- a/tests/multi-runner-orchestration/smoke/reporter.mjs +++ b/tests/multi-runner-orchestration/smoke/reporter.mjs @@ -1,9 +1,20 @@ +function formatFailure(error) { + if (!error) return 'No error details in test event'; + + const messages = []; + const visited = new Set(); + for (let current = error; current && !visited.has(current); current = current.cause) { + visited.add(current); + messages.push(current.stack ?? current.message ?? String(current)); + } + return messages.join('\nCaused by: '); +} + export default async function* smokeReporter(events) { for await (const event of events) { const { name, details } = event.data ?? {}; const error = details?.error ?? event.data?.error; - const failure = error?.cause ?? error; - const failureText = failure?.stack ?? failure?.message ?? (failure ? String(failure) : 'No error details in test event'); + const failureText = formatFailure(error); if (details?.type !== 'test') continue; if (event.type === 'test:pass') { diff --git a/tests/multi-runner-orchestration/smoke/support.mjs b/tests/multi-runner-orchestration/smoke/support.mjs index fa0ec69b98..99d544779a 100644 --- a/tests/multi-runner-orchestration/smoke/support.mjs +++ b/tests/multi-runner-orchestration/smoke/support.mjs @@ -97,17 +97,61 @@ export async function assertEc2RunnerCount(expectedCount) { const environment = process.env.MINISTACK_RUNNER_ENVIRONMENT ?? 'multi-runner-webhook-ec2'; const description = `${expectedCount} active EC2 runner(s) in ${environment}`; - await waitFor(description, async () => { - const response = await aws( + try { + return await waitFor(description, async () => { + const response = await aws( + 'ec2', 'describe-instances', + '--filters', + `Name=tag:ghr:environment,Values=${environment}`, + 'Name=instance-state-name,Values=pending,running', + ); + const instances = (response.Reservations ?? []).flatMap((reservation) => reservation.Instances ?? []); + return instances.length === expectedCount ? instances : false; + }); + } catch (error) { + const allInstances = await aws( 'ec2', 'describe-instances', - '--filters', - `Name=tag:ghr:environment,Values=${environment}`, - 'Name=instance-state-name,Values=pending,running', + '--filters', `Name=tag:ghr:environment,Values=${environment}`, + ).catch((diagnosticError) => ({ diagnosticError: diagnosticError.message })); + const instances = (allInstances.Reservations ?? []) + .flatMap((reservation) => reservation.Instances ?? []) + .map(({ InstanceId, State, StateReason, LaunchTime, Tags }) => ({ + instanceId: InstanceId, + state: State?.Name, + stateReason: StateReason?.Message, + launchTime: LaunchTime, + name: Tags?.find(({ Key }) => Key === 'Name')?.Value, + })); + const logGroups = [ + '/aws/lambda/multi-runner-webhook-webhook', + `/aws/lambda/${environment}-scale-up`, + `/aws/lambda/${environment}-pool`, + ]; + const logs = await Promise.all(logGroups.map(async (logGroup) => ({ + logGroup, + messages: await logMessages(logGroup, Date.now() - 15 * 60_000), + }))); + const logContext = logs + .map(({ logGroup, messages }) => `${logGroup}:\n${messages.slice(-10).join('\n') || 'No recent messages'}`) + .join('\n'); + + throw new Error( + `${error.message}\nEC2 instances with environment ${environment}: ${JSON.stringify(instances.length ? instances : allInstances)}\nRecent Lambda logs:\n${logContext}`, + { cause: error }, ); - const instances = (response.Reservations ?? []).flatMap((reservation) => reservation.Instances ?? []); - return instances.length === expectedCount; - }); + } +} +export async function assertEc2InstancesTerminated(instances) { + for (const { InstanceId } of instances) { + await waitFor(`EC2 instance ${InstanceId} to reach terminated state`, async () => { + const response = await aws('ec2', 'describe-instances', '--instance-ids', InstanceId); + const instance = (response.Reservations ?? []) + .flatMap((reservation) => reservation.Instances ?? []) + .find(({ InstanceId: id }) => id === InstanceId); + return instance?.State?.Name === 'terminated'; + }); + } } export async function waitFor(description, predicate, attempts = 90) { @@ -174,13 +218,14 @@ export async function sendWorkflowJob(labels) { const signature = createHmac('sha256', secret).update(eventBody).digest('hex'); const webhookUrl = new URL(endpoint); const startTime = Date.now() - 1000; + const deliveryId = `node-smoke-${randomUUID()}`; const response = await postWebhook( webhookUrl.host, `${webhookUrl.pathname}${webhookUrl.search}`, { 'content-type': 'application/json', 'x-github-event': 'workflow_job', - 'x-github-delivery': `node-smoke-${randomUUID()}`, + 'x-github-delivery': deliveryId, 'x-github-hook-installation-target-id': '123', 'x-hub-signature-256': `sha256=${signature}`, }, @@ -191,7 +236,49 @@ export async function sendWorkflowJob(labels) { throw new Error(`Webhook failed with HTTP ${response.status}: ${response.body}`); } - return { jobId, startTime }; + return { jobId, deliveryId, startTime }; +} + +export async function assertWebhookJobReceivedAndDispatched(jobId, deliveryId, startTime) { + const webhookLogGroup = '/aws/lambda/multi-runner-webhook-webhook'; + const dispatcherLogGroup = '/aws/lambda/multi-runner-webhook-dispatch-to-runner'; + await assertCloudWatchLogContains( + webhookLogGroup, + `webhook delivery ${deliveryId} accepted for job ${jobId}`, + deliveryId, + startTime, + ); + await assertCloudWatchLogContains( + webhookLogGroup, + `webhook delivery ${deliveryId} parsed workflow job ${jobId}`, + `\"workflowJobId\":${jobId}`, + startTime, + ); + await assertCloudWatchLogContains( + dispatcherLogGroup, + `webhook delivery ${deliveryId} dispatched job ${jobId} to a runner queue`, + `Successfully dispatched job for test-owner/test-repo to the queue(s)`, + startTime, + ); + await assertCloudWatchLogContains( + dispatcherLogGroup, + `webhook delivery ${deliveryId} matched job ${jobId}`, + `Job ID: ${jobId}`, + startTime, + ); +} + +export async function assertEc2InstanceType(instances, expectedTypes, description) { + if (instances.length !== 1) { + throw new Error(`${description}: expected one EC2 instance, found ${instances.length}`); + } + + const [{ InstanceId, InstanceType }] = instances; + if (!expectedTypes.includes(InstanceType)) { + throw new Error( + `${description}: EC2 instance ${InstanceId} has type ${InstanceType}; expected ${expectedTypes.join(' or ')}`, + ); + } } export function postWebhook(host, pathname, headers, body) { diff --git a/tests/multi-runner-orchestration/smoke/webhook.ec2.test.mjs b/tests/multi-runner-orchestration/smoke/webhook.ec2.test.mjs index 3f3c18663c..504637d12c 100644 --- a/tests/multi-runner-orchestration/smoke/webhook.ec2.test.mjs +++ b/tests/multi-runner-orchestration/smoke/webhook.ec2.test.mjs @@ -1,7 +1,11 @@ import { test } from 'node:test'; import { assertEc2RunnerCount, + assertEc2InstanceType, + assertEc2InstancesTerminated, assertCloudWatchLogContains, + assertCloudWatchEvent, + assertWebhookJobReceivedAndDispatched, invokeLambda, sendWorkflowJob, } from './support.mjs'; @@ -9,19 +13,23 @@ import { const testTimeout = Number(process.env.MINISTACK_SMOKE_TIMEOUT_MS ?? 7200000); const scaleDownFunction = 'multi-runner-webhook-ec2-scale-down'; const scaleDownLogGroup = '/aws/lambda/multi-runner-webhook-ec2-scale-down'; +const configuredInstanceTypes = ['m7a.large', 'm5.large']; -async function scaleDown(description, startTime) { +async function scaleDown(description, startTime, instances) { // MiniStack does not boot the EC2 runner agent to register it with GitHub. // The first scheduled cleanup marks it orphaned; the next confirms GitHub // has no such runner and terminates the EC2 instance. await invokeLambda(scaleDownFunction); await invokeLambda(scaleDownFunction); - await assertCloudWatchLogContains( - scaleDownLogGroup, - `${description} runner instance was terminated`, - 'has been terminated.', - startTime, - ); + for (const { InstanceId } of instances) { + await assertCloudWatchLogContains( + scaleDownLogGroup, + `${description} runner instance ${InstanceId} was terminated`, + `Runner ${InstanceId} has been terminated.`, + startTime, + ); + } + await assertEc2InstancesTerminated(instances); await assertEc2RunnerCount(0); } @@ -29,10 +37,24 @@ test('EC2 Webhook: standard labels create and terminate a runner', { timeout: testTimeout, }, async () => { await assertEc2RunnerCount(0); - const { jobId } = await sendWorkflowJob(['self-hosted', 'linux', 'x64', 'ec2']); - await assertEc2RunnerCount(1); + const { jobId, deliveryId, startTime } = await sendWorkflowJob(['self-hosted', 'linux', 'x64', 'ec2']); + await assertWebhookJobReceivedAndDispatched(jobId, deliveryId, startTime); + await assertCloudWatchLogContains( + '/aws/lambda/multi-runner-webhook-ec2-scale-up', + `scale-up attempted runner launch for standard-label job ${jobId}`, + 'Runner creation summary.', + startTime, + ); + await assertCloudWatchEvent( + '/aws/lambda/multi-runner-webhook-ec2-scale-up', + 'Runner creation summary.', + { successfulRunnerCount: 1 }, + startTime, + ); + const instances = await assertEc2RunnerCount(1); + await assertEc2InstanceType(instances, configuredInstanceTypes, `standard-label job ${jobId}`); const scaleDownStartTime = Date.now() - 1000; - await scaleDown(`standard-label job ${jobId}`, scaleDownStartTime); + await scaleDown(`standard-label job ${jobId}`, scaleDownStartTime, instances); }); test('EC2 Webhook: dynamic labels apply m5.large and terminate the runner', { @@ -40,17 +62,26 @@ test('EC2 Webhook: dynamic labels apply m5.large and terminate the runner', { }, async () => { await assertEc2RunnerCount(0); const dynamicLabel = 'ghr-ec2-instance-type:m5.large'; - const { jobId, startTime } = await sendWorkflowJob([ + const { jobId, deliveryId, startTime } = await sendWorkflowJob([ 'self-hosted', 'linux', 'x64', 'ec2', dynamicLabel, ]); + await assertWebhookJobReceivedAndDispatched(jobId, deliveryId, startTime); await assertCloudWatchLogContains( '/aws/lambda/multi-runner-webhook-ec2-scale-up', `scale-up applied dynamic label ${dynamicLabel} for job ${jobId}`, '"ec2OverrideConfig":{"InstanceType":"m5.large"}', startTime, ); + await assertCloudWatchEvent( + '/aws/lambda/multi-runner-webhook-ec2-scale-up', + 'Runner creation summary.', + { successfulRunnerCount: 1 }, + startTime, + ); + const instances = await assertEc2RunnerCount(1); + await assertEc2InstanceType(instances, ['m5.large'], `dynamic-label job ${jobId}`); const scaleDownStartTime = Date.now() - 1000; - await scaleDown(`dynamic-label job ${jobId}`, scaleDownStartTime); + await scaleDown(`dynamic-label job ${jobId}`, scaleDownStartTime, instances); }); test('EC2 Pool: create and terminate one runner', { @@ -65,8 +96,9 @@ test('EC2 Pool: create and terminate one runner', { 'The pool will be topped up with 1 runners.', startTime, ); - await assertEc2RunnerCount(1); + const instances = await assertEc2RunnerCount(1); + await assertEc2InstanceType(instances, configuredInstanceTypes, 'pool'); const scaleDownStartTime = Date.now() - 1000; - await scaleDown('pool', scaleDownStartTime); + await scaleDown('pool', scaleDownStartTime, instances); }); From 638031baeac2f53926083422fc5908f3a1289f9c Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 2 Nov 2026 16:46:52 +0000 Subject: [PATCH 7/7] docs: auto update terraform docs --- examples/multi-runner-orchestration/README.md | 18 ++++-------------- 1 file changed, 4 insertions(+), 14 deletions(-) diff --git a/examples/multi-runner-orchestration/README.md b/examples/multi-runner-orchestration/README.md index ac7743ec48..ce53db3f48 100644 --- a/examples/multi-runner-orchestration/README.md +++ b/examples/multi-runner-orchestration/README.md @@ -12,46 +12,36 @@ ## Providers -| Name | Version | -|------|---------| -| [aws](#provider\_aws) | 6.66.0 | +No providers. ## Modules | Name | Source | Version | |------|--------|---------| | [base](#module\_base) | ../base | n/a | -| [microvm\_foundation](#module\_microvm\_foundation) | ../../modules/microvm-foundation | n/a | | [runners](#module\_runners) | ../../modules/multi-runner | n/a | ## Resources -| Name | Type | -|------|------| -| [aws_cloudwatch_log_group.aws_cloudwatch_log_group_microvm](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/cloudwatch_log_group) | resource | -| [aws_ecr_repository.base_ubuntu24](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/ecr_repository) | resource | -| [aws_ecr_repository_policy.repository_policy](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/ecr_repository_policy) | resource | -| [aws_caller_identity.current](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/caller_identity) | data source | -| [aws_iam_policy_document.ecr_repository_policy](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | +No resources. ## Inputs | Name | Description | Type | Default | Required | |------|-------------|------|---------|:--------:| | [aws\_region](#input\_aws\_region) | AWS Region where the runner control plane and compute provider resources are deployed. | `string` | `"eu-west-1"` | no | -| [compute\_provider](#input\_compute\_provider) | Provider-specific settings for the EC2 and MicroVM runner lanes. |
object({
aws = object({
ec2 = object({
instance_types = list(string)
ami = object({
filter = optional(map(list(string)), { state = ["available"] })
owners = optional(list(string), ["amazon"])
id_ssm_parameter = optional(object({
arn = string
}), null)
kms_key = optional(object({
arn = string
}), null)
})
})
microvm = object({
image_arn = string
image_version = optional(string, null)
ingress_network_connectors = optional(list(string), [])
egress_network_connectors = list(string)
})
})
})
| n/a | yes | +| [compute\_provider](#input\_compute\_provider) | Provider-specific settings for compute providers. |
object({
aws = object({
ec2 = object({
instance_types = list(string)
ami = object({
filter = optional(map(list(string)), { state = ["available"] })
owners = optional(list(string), ["amazon"])
id_ssm_parameter = optional(object({
arn = string
}), null)
kms_key = optional(object({
arn = string
}), null)
})
})
})
})
| n/a | yes | | [environment](#input\_environment) | Name prefix for the example resources. | `string` | n/a | yes | | [github](#input\_github) | Optional GitHub endpoint and scale-set ownership settings. |
object({
url = optional(string, null)
ssl_verify = optional(bool, true)
runner_owner = optional(string, null)
registration_level = optional(string, "organization")
})
| `{}` | no | | [github\_app](#input\_github\_app) | GitHub App ID, base64-encoded private key, and installation ID. |
object({
id = string
key_base64 = string
installation_id = optional(string, null)
webhook_secret = string
})
| n/a | yes | | [runners\_lambda\_zip](#input\_runners\_lambda\_zip) | Local ZIP file for the runner-control Lambda. | `string` | n/a | yes | -| [scale\_set](#input\_scale\_set) | GitHub Actions scale-set configuration. |
object({
name = string
runner_group_name = optional(string, "Default")
min_runners = optional(number, 0)
container = optional(object({
image = optional(string, null)
}), {})
})
| n/a | yes | +| [scale\_set](#input\_scale\_set) | GitHub Actions scale-set configuration. |
object({
name = string
runner_group_name = optional(string, "Default")
min_runners = optional(number, 0)
container = optional(object({
image = optional(string, null)
}), {})
ecs = optional(object({
task = optional(object({
architecture = optional(string, "x86_64")
}), {})
}), {})
})
| n/a | yes | | [webhook\_lambda\_zip](#input\_webhook\_lambda\_zip) | Local ZIP file for the webhook Lambda. | `string` | n/a | yes | ## Outputs | Name | Description | |------|-------------| -| [microvm](#output\_microvm) | n/a | | [webhook\_endpoint](#output\_webhook\_endpoint) | n/a | | [webhook\_secret](#output\_webhook\_secret) | n/a |