diff --git a/.eslintrc.json b/.eslintrc.json index f6d8ce26..a2ba6c68 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -157,6 +157,7 @@ "src/setup.lambda.ts", "src/status.lambda.ts", "src/token-retriever.lambda.ts", + "src/troubleshoot.lambda.ts", "src/warm-runner-manager.lambda.ts", "src/webhook-handler.lambda.ts", "src/webhook-redelivery.lambda.ts", @@ -167,6 +168,7 @@ "src/image-builders/aws-image-builder/filter-failed-builds.lambda.ts", "src/lambda-helpers.ts", "src/lambda-github.ts", + "src/troubleshoot-helpers.ts", "setup/src/main.ts" ], "optionalDependencies": false, diff --git a/.gitattributes b/.gitattributes index 2b27cc45..b790d1d1 100644 --- a/.gitattributes +++ b/.gitattributes @@ -39,6 +39,7 @@ /src/setup-function.ts linguist-generated /src/status-function.ts linguist-generated /src/token-retriever-function.ts linguist-generated +/src/troubleshoot-function.ts linguist-generated /src/warm-runner-manager-function.ts linguist-generated /src/webhook-handler-function.ts linguist-generated /src/webhook-redelivery-function.ts linguist-generated diff --git a/.gitignore b/.gitignore index b390061d..6e162c5c 100644 --- a/.gitignore +++ b/.gitignore @@ -60,6 +60,7 @@ tsconfig.json !/src/setup-function.ts !/src/status-function.ts !/src/token-retriever-function.ts +!/src/troubleshoot-function.ts !/src/warm-runner-manager-function.ts !/src/webhook-handler-function.ts !/src/webhook-redelivery-function.ts diff --git a/.projen/deps.json b/.projen/deps.json index 60bfb45e..5b94b6ef 100644 --- a/.projen/deps.json +++ b/.projen/deps.json @@ -4,6 +4,10 @@ "name": "@aws-sdk/client-cloudformation", "type": "build" }, + { + "name": "@aws-sdk/client-cloudwatch-logs", + "type": "build" + }, { "name": "@aws-sdk/client-codebuild", "type": "build" diff --git a/.projen/files.json b/.projen/files.json index e39ae026..ebe5b1e5 100644 --- a/.projen/files.json +++ b/.projen/files.json @@ -26,6 +26,7 @@ "src/setup-function.ts", "src/status-function.ts", "src/token-retriever-function.ts", + "src/troubleshoot-function.ts", "src/warm-runner-manager-function.ts", "src/webhook-handler-function.ts", "src/webhook-redelivery-function.ts", diff --git a/.projen/tasks.json b/.projen/tasks.json index c48633f7..914109b2 100644 --- a/.projen/tasks.json +++ b/.projen/tasks.json @@ -61,6 +61,9 @@ { "spawn": "bundle:token-retriever.lambda" }, + { + "spawn": "bundle:troubleshoot.lambda" + }, { "spawn": "bundle:warm-runner-manager.lambda" }, @@ -279,6 +282,24 @@ } ] }, + "bundle:troubleshoot.lambda": { + "name": "bundle:troubleshoot.lambda", + "description": "Create a JavaScript bundle from src/troubleshoot.lambda.ts", + "steps": [ + { + "exec": "esbuild --bundle src/troubleshoot.lambda.ts --target=\"node22\" --platform=\"node\" --outfile=\"assets/troubleshoot.lambda/index.js\" --tsconfig=\"tsconfig.dev.json\" --external:@aws-sdk/*" + } + ] + }, + "bundle:troubleshoot.lambda:watch": { + "name": "bundle:troubleshoot.lambda:watch", + "description": "Continuously update the JavaScript bundle from src/troubleshoot.lambda.ts", + "steps": [ + { + "exec": "esbuild --bundle src/troubleshoot.lambda.ts --target=\"node22\" --platform=\"node\" --outfile=\"assets/troubleshoot.lambda/index.js\" --tsconfig=\"tsconfig.dev.json\" --external:@aws-sdk/* --watch" + } + ] + }, "bundle:warm-runner-manager.lambda": { "name": "bundle:warm-runner-manager.lambda", "description": "Create a JavaScript bundle from src/warm-runner-manager.lambda.ts", @@ -693,13 +714,13 @@ }, "steps": [ { - "exec": "pnpm dlx npm-check-updates@18 --upgrade --target=minor --cooldown=5 --peer --no-deprecated --dep=dev,peer,prod,optional --filter=@aws-sdk/client-cloudformation,@aws-sdk/client-codebuild,@aws-sdk/client-ec2,@aws-sdk/client-ecr,@aws-sdk/client-imagebuilder,@aws-sdk/client-lambda,@aws-sdk/client-secrets-manager,@aws-sdk/client-sfn,@aws-sdk/client-sns,@aws-sdk/client-sqs,@aws-sdk/client-ssm,@octokit/auth-app,@octokit/core,@octokit/request-error,@octokit/rest,@types/aws-lambda,@types/jest,@types/node,esbuild,eslint-import-resolver-typescript,eslint-plugin-import,jest,jsii-diff,jsii-pacmak,projen,shx,ts-jest,ts-node,typescript,cron-parser" + "exec": "pnpm dlx npm-check-updates@18 --upgrade --target=minor --cooldown=5 --peer --no-deprecated --dep=dev,peer,prod,optional --filter=@aws-sdk/client-cloudformation,@aws-sdk/client-cloudwatch-logs,@aws-sdk/client-codebuild,@aws-sdk/client-ec2,@aws-sdk/client-ecr,@aws-sdk/client-imagebuilder,@aws-sdk/client-lambda,@aws-sdk/client-secrets-manager,@aws-sdk/client-sfn,@aws-sdk/client-sns,@aws-sdk/client-sqs,@aws-sdk/client-ssm,@octokit/auth-app,@octokit/core,@octokit/request-error,@octokit/rest,@types/aws-lambda,@types/jest,@types/node,esbuild,eslint-import-resolver-typescript,eslint-plugin-import,jest,jsii-diff,jsii-pacmak,projen,shx,ts-jest,ts-node,typescript,cron-parser" }, { "exec": "pnpm i --no-frozen-lockfile" }, { - "exec": "pnpm update @aws-sdk/client-cloudformation @aws-sdk/client-codebuild @aws-sdk/client-ec2 @aws-sdk/client-ecr @aws-sdk/client-imagebuilder @aws-sdk/client-lambda @aws-sdk/client-secrets-manager @aws-sdk/client-sfn @aws-sdk/client-sns @aws-sdk/client-sqs @aws-sdk/client-ssm @octokit/auth-app @octokit/core @octokit/request-error @octokit/rest @stylistic/eslint-plugin @sveltejs/vite-plugin-svelte @tsconfig/svelte @types/aws-lambda @types/jest @types/node @typescript-eslint/eslint-plugin @typescript-eslint/parser aws-cdk bootstrap commit-and-tag-version esbuild eslint-import-resolver-typescript eslint-plugin-import eslint-plugin-svelte eslint jest jest-junit jsii-diff jsii-docgen jsii-pacmak jsii-rosetta jsii projen sass shx svelte-check svelte-preprocess svelte ts-jest ts-node typescript vite-plugin-singlefile vite cron-parser aws-cdk-lib constructs --config.minimum-release-age=7200" + "exec": "pnpm update @aws-sdk/client-cloudformation @aws-sdk/client-cloudwatch-logs @aws-sdk/client-codebuild @aws-sdk/client-ec2 @aws-sdk/client-ecr @aws-sdk/client-imagebuilder @aws-sdk/client-lambda @aws-sdk/client-secrets-manager @aws-sdk/client-sfn @aws-sdk/client-sns @aws-sdk/client-sqs @aws-sdk/client-ssm @octokit/auth-app @octokit/core @octokit/request-error @octokit/rest @stylistic/eslint-plugin @sveltejs/vite-plugin-svelte @tsconfig/svelte @types/aws-lambda @types/jest @types/node @typescript-eslint/eslint-plugin @typescript-eslint/parser aws-cdk bootstrap commit-and-tag-version esbuild eslint-import-resolver-typescript eslint-plugin-import eslint-plugin-svelte eslint jest jest-junit jsii-diff jsii-docgen jsii-pacmak jsii-rosetta jsii projen sass shx svelte-check svelte-preprocess svelte ts-jest ts-node typescript vite-plugin-singlefile vite cron-parser aws-cdk-lib constructs --config.minimum-release-age=7200" }, { "exec": "npx projen" diff --git a/.projenrc.js b/.projenrc.js index 3f24b539..6f60e3b8 100644 --- a/.projenrc.js +++ b/.projenrc.js @@ -31,6 +31,7 @@ const project = new awscdk.AwsCdkConstructLibrary({ '@aws-sdk/client-sqs', '@aws-sdk/client-ssm', '@aws-sdk/client-sfn', + '@aws-sdk/client-cloudwatch-logs', '@types/aws-lambda', // for setup ui '@sveltejs/vite-plugin-svelte@^6', @@ -173,6 +174,7 @@ cdkConfig.json.addDeletionOverride('output'); // allow lambda utility files to import dev dependencies project.eslint.allowDevDeps('src/lambda-helpers.ts'); project.eslint.allowDevDeps('src/lambda-github.ts'); +project.eslint.allowDevDeps('src/troubleshoot-helpers.ts'); project.eslint.allowDevDeps('setup/src/main.ts'); // vscode auto formatting diff --git a/API.md b/API.md index 8f9297be..0452397c 100644 --- a/API.md +++ b/API.md @@ -8299,6 +8299,7 @@ const gitHubRunnersProps: GitHubRunnersProps = { ... } | securityGroups | aws-cdk-lib.aws_ec2.ISecurityGroup[] | Security groups attached to all management functions. | | setupAccess | LambdaAccess | Access configuration for the setup function. | | statusAccess | LambdaAccess | Access configuration for the status function. | +| troubleshootAccess | LambdaAccess | Access configuration for the troubleshoot function. | | vpc | aws-cdk-lib.aws_ec2.IVpc | VPC used for all management functions. Use this with GitHub Enterprise Server hosted that's inaccessible from outside the VPC. | | vpcSubnets | aws-cdk-lib.aws_ec2.SubnetSelection | VPC subnets used for all management functions. | | webhookAccess | LambdaAccess | Access configuration for the webhook function. | @@ -8525,6 +8526,21 @@ This function returns a lot of sensitive information about the runner, so you sh --- +##### `troubleshootAccess`Optional + +```typescript +public readonly troubleshootAccess: LambdaAccess; +``` + +- *Type:* LambdaAccess +- *Default:* LambdaAccess.noAccess() + +Access configuration for the troubleshoot function. + +This function analyzes runner health and returns detailed diagnostic information including execution history, so you should only allow access to it from trusted IPs, if at all. + +--- + ##### `vpc`Optional ```typescript diff --git a/README.md b/README.md index b00a72d8..67361904 100644 --- a/README.md +++ b/README.md @@ -559,29 +559,56 @@ If you have more to share, please open a PR adding examples to the `examples` fo ## Troubleshooting -Runners are started in response to a webhook coming in from GitHub. If there are any issues starting the runner like missing capacity or transient API issues, the provider will keep retrying for 24 hours. Configuration issue related errors like pointing to a missing AMI will not be retried. GitHub itself will cancel the job if it can't find a runner for 24 hours. If your jobs don't start, follow the steps below to examine all parts of this workflow. - -1. Always start with the status function, make sure no errors are reported, and confirm all status codes are OK -2. Make sure `runs-on` in the workflow matches the expected labels set in the runner provider -3. Diagnose relevant executions of the orchestrator step function by visiting the URL in `troubleshooting.stepFunctionUrl` from `status.json` - 1. If the execution failed, check your runner provider configuration for errors - 2. If the execution is still running for a long time, check the execution events to see why runner starting is being retried - 3. If there are no relevant executions, move to the next step -4. Confirm the webhook Lambda was called by visiting the URL in `troubleshooting.webhookHandlerUrl` from `status.json` - 1. If it's not called or logs errors, confirm the webhook settings on the GitHub side - 2. If you see too many errors, make sure you're only sending `workflow_job` events -5. When using GitHub app, make sure there are active installations in `github.auth.app.installations` +Runners are started in response to a webhook coming in from GitHub. If there are any issues starting the runner like missing capacity or transient API issues, the provider will keep retrying for 24 hours. Configuration issue related errors like pointing to a missing AMI will not be retried. GitHub itself will cancel the job if it can't find a runner for 24 hours. -All logs are saved in CloudWatch. -* Log group names can be found in `status.json` for each provider, image builder, and other parts of the system -* Some useful Logs Insights queries can be enabled with `GitHubRunners.createLogsInsightsQueries()` +### Step 1: Check status -To get `status.json`, check out the CloudFormation stack output for a command that generates it. The command looks like: +Start with the **status function** to verify everything is connected. Execute the status command from your CloudFormation stack outputs and open the resulting `status.json`: ``` aws --region us-east-1 lambda invoke --function-name status-XYZ123 status.json ``` +Confirm `github.auth.status` and `github.webhook.status` are OK. Make sure `runs-on` in the workflow matches the expected labels set in the runner provider. + +### Step 2: Run the troubleshooter + +For deeper diagnosis, use the **troubleshoot function**. It analyzes Step Functions execution history, webhook delivery health, label mismatches, and more -- then returns categorized issues with direct links to the relevant CloudWatch log groups and Step Functions executions. + +``` +aws --region us-east-1 lambda invoke --function-name troubleshoot-XYZ123 troubleshoot.json +``` + +The troubleshooter detects: + +* **Configuration issues**: GitHub auth failures, webhook URL mismatches, unreadable secrets +* **Stolen runners**: runners provisioned for repos where the GitHub App is not installed +* **Capacity failures**: spot interruptions, insufficient capacity, CodeBuild/Fargate capacity errors +* **Failed starts**: runner token errors, early provider task failures +* **Idle reaps**: excessive idle runners from `max-parallel` workflows +* **Stuck executions**: runners that have been running for an unusually long time +* **Execution name collisions**: duplicate execution names from long repo names +* **Webhook delivery failures**: failed webhook deliveries from GitHub +* **Label mismatches**: webhooks ignored because `runs-on` labels don't match any provider +* **Stale images**: container images or AMIs that haven't been rebuilt recently +* **EC2 API throttling**: throttled API calls during burst starts + +Each issue includes a severity level, actionable suggestion, and direct links to the AWS Console for further investigation. + +To expose the troubleshoot function via URL, set the `troubleshootAccess` prop: + +```typescript +new GitHubRunners(this, 'runners', { + troubleshootAccess: LambdaAccess.lambdaUrl(), +}); +``` + +### Manual investigation + +All logs are saved in CloudWatch. +* Log group names can be found in `status.json` for each provider, image builder, and other parts of the system +* Some useful Logs Insights queries can be enabled with `GitHubRunners.createLogsInsightsQueries()` + ## Monitoring There are two important ways to monitor your runners: diff --git a/package.json b/package.json index 242e3215..8c92fda3 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,8 @@ "bundle:status.lambda:watch": "npx projen bundle:status.lambda:watch", "bundle:token-retriever.lambda": "npx projen bundle:token-retriever.lambda", "bundle:token-retriever.lambda:watch": "npx projen bundle:token-retriever.lambda:watch", + "bundle:troubleshoot.lambda": "npx projen bundle:troubleshoot.lambda", + "bundle:troubleshoot.lambda:watch": "npx projen bundle:troubleshoot.lambda:watch", "bundle:warm-runner-manager.lambda": "npx projen bundle:warm-runner-manager.lambda", "bundle:warm-runner-manager.lambda:watch": "npx projen bundle:warm-runner-manager.lambda:watch", "bundle:webhook-handler.lambda": "npx projen bundle:webhook-handler.lambda", @@ -73,6 +75,7 @@ }, "devDependencies": { "@aws-sdk/client-cloudformation": "^3.1019.0", + "@aws-sdk/client-cloudwatch-logs": "^3.1034.0", "@aws-sdk/client-codebuild": "^3.1019.0", "@aws-sdk/client-ec2": "^3.1019.0", "@aws-sdk/client-ecr": "^3.1019.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ec8bbf74..8e149f01 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -15,6 +15,9 @@ importers: '@aws-sdk/client-cloudformation': specifier: ^3.1019.0 version: 3.1019.0 + '@aws-sdk/client-cloudwatch-logs': + specifier: ^3.1034.0 + version: 3.1034.0 '@aws-sdk/client-codebuild': specifier: ^3.1019.0 version: 3.1019.0 @@ -202,6 +205,10 @@ packages: resolution: {integrity: sha512-RNBtkQQ5IUqTdxaAe7ADwlJ/1qqW5kONLD1Mxr7PUWteEQwYR9ZJYscDul2qNkCWhu/vMKhk+qwJKPkdu2TNzA==} engines: {node: '>=20.0.0'} + '@aws-sdk/client-cloudwatch-logs@3.1034.0': + resolution: {integrity: sha512-awp1kT4HbT1DJ+izsqptqwHVRQvGGz944piXhs7kzyHJfUjNXo6njvktSKPkuRmO/bWb4SjecyW0Wm0grRrLaw==} + engines: {node: '>=20.0.0'} + '@aws-sdk/client-codebuild@3.1019.0': resolution: {integrity: sha512-7CH8/oZG7eg5NLGPo1h3HSDrS/6qH/UFJK4L9HMSzGVB/nLuLqdtoS+GVJYQ80V7cIrSxleUQbHgYtVRyoRR+Q==} engines: {node: '>=20.0.0'} @@ -246,46 +253,94 @@ packages: resolution: {integrity: sha512-A/E6n2W42ruU+sfWk+mMUOyVXbsSgGrY3MJ9/0Az5qUdG67y8I6HYzzoAa+e/lzxxl1uCYmEL6BTMi9ZiZnplQ==} engines: {node: '>=20.0.0'} + '@aws-sdk/core@3.974.3': + resolution: {integrity: sha512-W3aJJm2clu8OmsrwMOMnfof13O6LGnbknnZIQeSRbxjqKah2nVvkjbUBBZVhWrt08KC69H7WsINTdrxC/2SXQw==} + engines: {node: '>=20.0.0'} + '@aws-sdk/credential-provider-env@3.972.24': resolution: {integrity: sha512-FWg8uFmT6vQM7VuzELzwVo5bzExGaKHdubn0StjgrcU5FvuLExUe+k06kn/40uKv59rYzhez8eFNM4yYE/Yb/w==} engines: {node: '>=20.0.0'} + '@aws-sdk/credential-provider-env@3.972.29': + resolution: {integrity: sha512-rf+AlUxgTeSzQ/4zoS0D+Bt7XvgpY48PnWG8Yg/N9fdMgyK2Jaqa+6tLZp4MYMIMHkGrfAxnbSeb2YLMGFMg6g==} + engines: {node: '>=20.0.0'} + '@aws-sdk/credential-provider-http@3.972.26': resolution: {integrity: sha512-CY4ppZ+qHYqcXqBVi//sdHST1QK3KzOEiLtpLsc9W2k2vfZPKExGaQIsOwcyvjpjUEolotitmd3mUNY56IwDEA==} engines: {node: '>=20.0.0'} + '@aws-sdk/credential-provider-http@3.972.31': + resolution: {integrity: sha512-TR2/lQ3qKFj2EOrsiASzemsNEz2uzZ/SUBf48+U4Cr9a/FZlHfH/hwAeBJNBp1gMyJNxROJZhT3dn1cO+jnYfQ==} + engines: {node: '>=20.0.0'} + '@aws-sdk/credential-provider-ini@3.972.28': resolution: {integrity: sha512-wXYvq3+uQcZV7k+bE4yDXCTBdzWTU9x/nMiKBfzInmv6yYK1veMK0AKvRfRBd72nGWYKcL6AxwiPg9z/pYlgpw==} engines: {node: '>=20.0.0'} + '@aws-sdk/credential-provider-ini@3.972.33': + resolution: {integrity: sha512-UwdbJbOrgnOxZbshaNZ4DzX35h5wQd33MNYTGzWhN3ORG9lG9KQbDX6l6tDJSAdaGTktJoZPSritmUoW1rYkRA==} + engines: {node: '>=20.0.0'} + '@aws-sdk/credential-provider-login@3.972.28': resolution: {integrity: sha512-ZSTfO6jqUTCysbdBPtEX5OUR//3rbD0lN7jO3sQeS2Gjr/Y+DT6SbIJ0oT2cemNw3UzKu97sNONd1CwNMthuZQ==} engines: {node: '>=20.0.0'} + '@aws-sdk/credential-provider-login@3.972.33': + resolution: {integrity: sha512-WyZuPVoDM1HGNl41eVg8HSSXIB+FGkuuK63GhDbh4TMdfWU03AciWvF/QqOVWvJtWVYaLddANJ+aUklVr2ieuw==} + engines: {node: '>=20.0.0'} + '@aws-sdk/credential-provider-node@3.972.29': resolution: {integrity: sha512-clSzDcvndpFJAggLDnDb36sPdlZYyEs5Zm6zgZjjUhwsJgSWiWKwFIXUVBcbruidNyBdbpOv2tNDL9sX8y3/0g==} engines: {node: '>=20.0.0'} + '@aws-sdk/credential-provider-node@3.972.34': + resolution: {integrity: sha512-sPcisURibKU4x0PCWJkWF1KJYm49Cph9dCn/PAnG5FU0wq5Id3g2v7RuEWAtNlKv1Af4gUJYBVGOeNpSEEx41A==} + engines: {node: '>=20.0.0'} + '@aws-sdk/credential-provider-process@3.972.24': resolution: {integrity: sha512-Q2k/XLrFXhEztPHqj4SLCNID3hEPdlhh1CDLBpNnM+1L8fq7P+yON9/9M1IGN/dA5W45v44ylERfXtDAlmMNmw==} engines: {node: '>=20.0.0'} + '@aws-sdk/credential-provider-process@3.972.29': + resolution: {integrity: sha512-DURisqWS3bUgiwMXTmzymVNGlcRW0FnbPZ3SZknhmxnCXm3n9idkTJ6T+Uir359KRKtJNFLRViskk8HsSVLi1w==} + engines: {node: '>=20.0.0'} + '@aws-sdk/credential-provider-sso@3.972.28': resolution: {integrity: sha512-IoUlmKMLEITFn1SiCTjPfR6KrE799FBo5baWyk/5Ppar2yXZoUdaRqZzJzK6TcJxx450M8m8DbpddRVYlp5R/A==} engines: {node: '>=20.0.0'} + '@aws-sdk/credential-provider-sso@3.972.33': + resolution: {integrity: sha512-9y9obU4IQWru9f+NiiscUeyCe5ZmQav4FKEb1qfUNrik/C3BzBGUnHQWyPEyXjOX9cb+vx1TYx0qZBtinKdzTA==} + engines: {node: '>=20.0.0'} + '@aws-sdk/credential-provider-web-identity@3.972.28': resolution: {integrity: sha512-d+6h0SD8GGERzKe27v5rOzNGKOl0D+l0bWJdqrxH8WSQzHzjsQFIAPgIeOTUwBHVsKKwtSxc91K/SWax6XgswQ==} engines: {node: '>=20.0.0'} + '@aws-sdk/credential-provider-web-identity@3.972.33': + resolution: {integrity: sha512-RazhlN0YAkna2T2p2v4YuuRlVBVRNo8V0SL+9JePTWDndEUAeOBAjYeQfAMbtDyCh120+zA0Op6V0jS4dw2+iw==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/middleware-host-header@3.972.10': + resolution: {integrity: sha512-IJSsIMeVQ8MMCPbuh1AbltkFhLBLXn7aejzfX5YKT/VLDHn++Dcz8886tXckE+wQssyPUhaXrJhdakO2VilRhg==} + engines: {node: '>=20.0.0'} + '@aws-sdk/middleware-host-header@3.972.8': resolution: {integrity: sha512-wAr2REfKsqoKQ+OkNqvOShnBoh+nkPurDKW7uAeVSu6kUECnWlSJiPvnoqxGlfousEY/v9LfS9sNc46hjSYDIQ==} engines: {node: '>=20.0.0'} + '@aws-sdk/middleware-logger@3.972.10': + resolution: {integrity: sha512-OOuGvvz1Dm20SjZo5oEBePFqxt5nf8AwkNDSyUHvD9/bfNASmstcYxFAHUowy4n6Io7mWUZ04JURZwSBvyQanQ==} + engines: {node: '>=20.0.0'} + '@aws-sdk/middleware-logger@3.972.8': resolution: {integrity: sha512-CWl5UCM57WUFaFi5kB7IBY1UmOeLvNZAZ2/OZ5l20ldiJ3TiIz1pC65gYj8X0BCPWkeR1E32mpsCk1L1I4n+lA==} engines: {node: '>=20.0.0'} + '@aws-sdk/middleware-recursion-detection@3.972.11': + resolution: {integrity: sha512-+zz6f79Kj9V5qFK2P+D8Ehjnw4AhphAlCAsPjUqEcInA9umtSSKMrHbSagEeOIsDNuvVrH98bjRHcyQukTrhaQ==} + engines: {node: '>=20.0.0'} + '@aws-sdk/middleware-recursion-detection@3.972.9': resolution: {integrity: sha512-/Wt5+CT8dpTFQxEJ9iGy/UGrXr7p2wlIOEHvIr/YcHYByzoLjrqkYqXdJjd9UIgWjv7eqV2HnFJen93UTuwfTQ==} engines: {node: '>=20.0.0'} @@ -294,6 +349,10 @@ packages: resolution: {integrity: sha512-ZbZlOxCRHm9mQSWcQuaZZ3KoV5+XiyHmq6m4sKDfVsD8SQ1XblacJPyNJzNkDB9Do4pO+J5FPj/nvSj+zVn7Mw==} engines: {node: '>=20.0.0'} + '@aws-sdk/middleware-sdk-s3@3.972.32': + resolution: {integrity: sha512-dc2O2x0V5pGJhmdQYQveUIFtMZsur7GrGuSgoKM4oQJuEcfvwnJ3sj+ip6WnxR5l6TrX5zkl4KgcgswOy3wAzQ==} + engines: {node: '>=20.0.0'} + '@aws-sdk/middleware-sdk-sqs@3.972.18': resolution: {integrity: sha512-BdsGFuBJUX5PnuZkEV6JRB5g/6ts7iGmN3pXwyoiGCCM2HHXrlFqjkBs+iPX7yO884WqYeQJpme7nwn4DzU5xw==} engines: {node: '>=20.0.0'} @@ -302,26 +361,58 @@ packages: resolution: {integrity: sha512-cfWZFlVh7Va9lRay4PN2A9ARFzaBYcA097InT5M2CdRS05ECF5yaz86jET8Wsl2WcyKYEvVr/QNmKtYtafUHtQ==} engines: {node: '>=20.0.0'} + '@aws-sdk/middleware-user-agent@3.972.33': + resolution: {integrity: sha512-mqtT3Fo7xanWMk2SbAcKLGGI/q1GHWNrExBj7cnWP2W2mkTMheXB4ntJvwPZ1UxPrQobrsv2dWFXmaOJeSOiDg==} + engines: {node: '>=20.0.0'} + '@aws-sdk/nested-clients@3.996.18': resolution: {integrity: sha512-c7ZSIXrESxHKx2Mcopgd8AlzZgoXMr20fkx5ViPWPOLBvmyhw9VwJx/Govg8Ef/IhEon5R9l53Z8fdYSEmp6VA==} engines: {node: '>=20.0.0'} + '@aws-sdk/nested-clients@3.997.1': + resolution: {integrity: sha512-Afc9hc2WZs3X4Jb8dnxyuYiZsLoWRO51roTCRf497gPnAKN2WRdXANu1vaVCTzwnDMOYFXb/cYv4ZSjxqAqcKA==} + engines: {node: '>=20.0.0'} + '@aws-sdk/region-config-resolver@3.972.10': resolution: {integrity: sha512-1dq9ToC6e070QvnVhhbAs3bb5r6cQ10gTVc6cyRV5uvQe7P138TV2uG2i6+Yok4bAkVAcx5AqkTEBUvWEtBlsQ==} engines: {node: '>=20.0.0'} + '@aws-sdk/region-config-resolver@3.972.13': + resolution: {integrity: sha512-CvJ2ZIjK/jVD/lbOpowBVElJyC1YxLTIJ13yM0AEo0t2v7swOzGjSA6lJGH+DwZXQhcjUjoYwc8bVYCX5MDr1A==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/signature-v4-multi-region@3.996.20': + resolution: {integrity: sha512-MEj6DhEcaO8RgVtFCJ+xpCQnZC3Iesr09avdY75qkMQfckQULu447IegK7Rs1MCGerVBfKnJQ4q+pQq9hI5lng==} + engines: {node: '>=20.0.0'} + '@aws-sdk/token-providers@3.1021.0': resolution: {integrity: sha512-TKY6h9spUk3OLs5v1oAgW9mAeBE3LAGNBwJokLy96wwmd4W2v/tYlXseProyed9ValDj2u1jK/4Rg1T+1NXyJA==} engines: {node: '>=20.0.0'} + '@aws-sdk/token-providers@3.1034.0': + resolution: {integrity: sha512-8E+KGcD4ET0H9FXJ2/ZWbfFnQNYEkTZZYJxAs1lkdJlve1AYuqaydInIFfvNgoz5GbYtzbK8/ugsSMu5wPm6kA==} + engines: {node: '>=20.0.0'} + '@aws-sdk/types@3.973.6': resolution: {integrity: sha512-Atfcy4E++beKtwJHiDln2Nby8W/mam64opFPTiHEqgsthqeydFS1pY+OUlN1ouNOmf8ArPU/6cDS65anOP3KQw==} engines: {node: '>=20.0.0'} + '@aws-sdk/types@3.973.8': + resolution: {integrity: sha512-gjlAdtHMbtR9X5iIhVUvbVcy55KnznpC6bkDUWW9z915bi0ckdUr5cjf16Kp6xq0bP5HBD2xzgbL9F9Quv5vUw==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/util-arn-parser@3.972.3': + resolution: {integrity: sha512-HzSD8PMFrvgi2Kserxuff5VitNq2sgf3w9qxmskKDiDTThWfVteJxuCS9JXiPIPtmCrp+7N9asfIaVhBFORllA==} + engines: {node: '>=20.0.0'} + '@aws-sdk/util-endpoints@3.996.5': resolution: {integrity: sha512-Uh93L5sXFNbyR5sEPMzUU8tJ++Ku97EY4udmC01nB8Zu+xfBPwpIwJ6F7snqQeq8h2pf+8SGN5/NoytfKgYPIw==} engines: {node: '>=20.0.0'} + '@aws-sdk/util-endpoints@3.996.8': + resolution: {integrity: sha512-oOZHcRDihk5iEe5V25NVWg45b3qEA8OpHWVdU/XQh8Zj4heVPAJqWvMphQnU7LkufmUo10EpvFPZuQMiFLJK3g==} + engines: {node: '>=20.0.0'} + '@aws-sdk/util-format-url@3.972.8': resolution: {integrity: sha512-J6DS9oocrgxM8xlUTTmQOuwRF6rnAGEujAN9SAzllcrQmwn5iJ58ogxy3SEhD0Q7JZvlA5jvIXBkpQRqEqlE9A==} engines: {node: '>=20.0.0'} @@ -330,6 +421,9 @@ packages: resolution: {integrity: sha512-WhlJNNINQB+9qtLtZJcpQdgZw3SCDCpXdUJP7cToGwHbCWCnRckGlc6Bx/OhWwIYFNAn+FIydY8SZ0QmVu3xTQ==} engines: {node: '>=20.0.0'} + '@aws-sdk/util-user-agent-browser@3.972.10': + resolution: {integrity: sha512-FAzqXvfEssGdSIz8ejatan0bOdx1qefBWKF/gWmVBXIP1HkS7v/wjjaqrAGGKvyihrXTXW00/2/1nTJtxpXz7g==} + '@aws-sdk/util-user-agent-browser@3.972.8': resolution: {integrity: sha512-B3KGXJviV2u6Cdw2SDY2aDhoJkVfY/Q/Trwk2CMSkikE1Oi6gRzxhvhIfiRpHfmIsAhV4EA54TVEX8K6CbHbkA==} @@ -342,10 +436,23 @@ packages: aws-crt: optional: true + '@aws-sdk/util-user-agent-node@3.973.19': + resolution: {integrity: sha512-ZAfHjpzdbrzkAftC139JoYGfXzDh5HY+AxRzw8pGJ8cULsf+l721sKAMK8mV5NvRETaW/BwghSwQhGgoNgrxMw==} + engines: {node: '>=20.0.0'} + peerDependencies: + aws-crt: '>=1.0.0' + peerDependenciesMeta: + aws-crt: + optional: true + '@aws-sdk/xml-builder@3.972.16': resolution: {integrity: sha512-iu2pyvaqmeatIJLURLqx9D+4jKAdTH20ntzB6BFwjyN7V960r4jK32mx0Zf7YbtOYAbmbtQfDNuL60ONinyw7A==} engines: {node: '>=20.0.0'} + '@aws-sdk/xml-builder@3.972.18': + resolution: {integrity: sha512-BMDNVG1ETXRhl1tnisQiYBef3RShJ1kfZA7x7afivTFMLirfHNTb6U71K569HNXhSXbQZsweHvSDZ6euBw8hPA==} + engines: {node: '>=20.0.0'} + '@aws/lambda-invoke-store@0.2.4': resolution: {integrity: sha512-iY8yvjE0y651BixKNPgmv1WrQc+GZ142sb0z4gYnChDDY2YqI4P/jsSopBWrKfAt7LOJAkOXt7rC/hms+WclQQ==} engines: {node: '>=18.0.0'} @@ -1167,46 +1274,90 @@ packages: resolution: {integrity: sha512-iIzMC5NmOUP6WL6o8iPBjFhUhBZ9pPjpUpQYWMUFQqKyXXzOftbfK8zcQCz/jFV1Psmf05BK5ypx4K2r4Tnwdg==} engines: {node: '>=18.0.0'} + '@smithy/config-resolver@4.4.17': + resolution: {integrity: sha512-TzDZcAnhTyAHbXVxWZo7/tEcrIeFq20IBk8So3OLOetWpR8EwY/yEqBMBFaJMeyEiREDq4NfEl+qO3OAUD+vbQ==} + engines: {node: '>=18.0.0'} + '@smithy/core@3.23.13': resolution: {integrity: sha512-J+2TT9D6oGsUVXVEMvz8h2EmdVnkBiy2auCie4aSJMvKlzUtO5hqjEzXhoCUkIMo7gAYjbQcN0g/MMSXEhDs1Q==} engines: {node: '>=18.0.0'} + '@smithy/core@3.23.16': + resolution: {integrity: sha512-JStomOrINQA1VqNEopLsgcdgwd42au7mykKqVr30XFw89wLt9sDxJDi4djVPRwQmmzyTGy/uOvTc2ultMpFi1w==} + engines: {node: '>=18.0.0'} + '@smithy/credential-provider-imds@4.2.12': resolution: {integrity: sha512-cr2lR792vNZcYMriSIj+Um3x9KWrjcu98kn234xA6reOAFMmbRpQMOv8KPgEmLLtx3eldU6c5wALKFqNOhugmg==} engines: {node: '>=18.0.0'} + '@smithy/credential-provider-imds@4.2.14': + resolution: {integrity: sha512-Au28zBN48ZAoXdooGUHemuVBrkE+Ie6RPmGNIAJsFqj33Vhb6xAgRifUydZ2aY+M+KaMAETAlKk5NC5h1G7wpg==} + engines: {node: '>=18.0.0'} + '@smithy/eventstream-codec@4.2.12': resolution: {integrity: sha512-FE3bZdEl62ojmy8x4FHqxq2+BuOHlcxiH5vaZ6aqHJr3AIZzwF5jfx8dEiU/X0a8RboyNDjmXjlbr8AdEyLgiA==} engines: {node: '>=18.0.0'} + '@smithy/eventstream-codec@4.2.14': + resolution: {integrity: sha512-erZq0nOIpzfeZdCyzZjdJb4nVSKLUmSkaQUVkRGQTXs30gyUGeKnrYEg+Xe1W5gE3aReS7IgsvANwVPxSzY6Pw==} + engines: {node: '>=18.0.0'} + '@smithy/eventstream-serde-browser@4.2.12': resolution: {integrity: sha512-XUSuMxlTxV5pp4VpqZf6Sa3vT/Q75FVkLSpSSE3KkWBvAQWeuWt1msTv8fJfgA4/jcJhrbrbMzN1AC/hvPmm5A==} engines: {node: '>=18.0.0'} + '@smithy/eventstream-serde-browser@4.2.14': + resolution: {integrity: sha512-8IelTCtTctWRbb+0Dcy+C0aICh1qa0qWXqgjcXDmMuCvPJRnv26hiDZoAau2ILOniki65mCPKqOQs/BaWvO4CQ==} + engines: {node: '>=18.0.0'} + '@smithy/eventstream-serde-config-resolver@4.3.12': resolution: {integrity: sha512-7epsAZ3QvfHkngz6RXQYseyZYHlmWXSTPOfPmXkiS+zA6TBNo1awUaMFL9vxyXlGdoELmCZyZe1nQE+imbmV+Q==} engines: {node: '>=18.0.0'} + '@smithy/eventstream-serde-config-resolver@4.3.14': + resolution: {integrity: sha512-sqHiHpYRYo3FJlaIxD1J8PhbcmJAm7IuM16mVnwSkCToD7g00IBZzKuiLNMGmftULmEUX6/UAz8/NN5uMP8bVA==} + engines: {node: '>=18.0.0'} + '@smithy/eventstream-serde-node@4.2.12': resolution: {integrity: sha512-D1pFuExo31854eAvg89KMn9Oab/wEeJR6Buy32B49A9Ogdtx5fwZPqBHUlDzaCDpycTFk2+fSQgX689Qsk7UGA==} engines: {node: '>=18.0.0'} + '@smithy/eventstream-serde-node@4.2.14': + resolution: {integrity: sha512-Ht/8BuGlKfFTy0H3+8eEu0vdpwGztCnaLLXtpXNdQqiR7Hj4vFScU3T436vRAjATglOIPjJXronY+1WxxNLSiw==} + engines: {node: '>=18.0.0'} + '@smithy/eventstream-serde-universal@4.2.12': resolution: {integrity: sha512-+yNuTiyBACxOJUTvbsNsSOfH9G9oKbaJE1lNL3YHpGcuucl6rPZMi3nrpehpVOVR2E07YqFFmtwpImtpzlouHQ==} engines: {node: '>=18.0.0'} + '@smithy/eventstream-serde-universal@4.2.14': + resolution: {integrity: sha512-lWyt4T2XQZUZgK3tQ3Wn0w3XBvZsK/vjTuJl6bXbnGZBHH0ZUSONTYiK9TgjTTzU54xQr3DRFwpjmhp0oLm3gg==} + engines: {node: '>=18.0.0'} + '@smithy/fetch-http-handler@5.3.15': resolution: {integrity: sha512-T4jFU5N/yiIfrtrsb9uOQn7RdELdM/7HbyLNr6uO/mpkj1ctiVs7CihVr51w4LyQlXWDpXFn4BElf1WmQvZu/A==} engines: {node: '>=18.0.0'} + '@smithy/fetch-http-handler@5.3.17': + resolution: {integrity: sha512-bXOvQzaSm6MnmLaWA1elgfQcAtN4UP3vXqV97bHuoOrHQOJiLT3ds6o9eo5bqd0TJfRFpzdGnDQdW3FACiAVdw==} + engines: {node: '>=18.0.0'} + '@smithy/hash-node@4.2.12': resolution: {integrity: sha512-QhBYbGrbxTkZ43QoTPrK72DoYviDeg6YKDrHTMJbbC+A0sml3kSjzFtXP7BtbyJnXojLfTQldGdUR0RGD8dA3w==} engines: {node: '>=18.0.0'} + '@smithy/hash-node@4.2.14': + resolution: {integrity: sha512-8ZBDY2DD4wr+GGjTpPtiglEsqr0lUP+KHqgZcWczFf6qeZ/YRjMIOoQWVQlmwu7EtxKTd8YXD8lblmYcpBIA1g==} + engines: {node: '>=18.0.0'} + '@smithy/invalid-dependency@4.2.12': resolution: {integrity: sha512-/4F1zb7Z8LOu1PalTdESFHR0RbPwHd3FcaG1sI3UEIriQTWakysgJr65lc1jj6QY5ye7aFsisajotH6UhWfm/g==} engines: {node: '>=18.0.0'} + '@smithy/invalid-dependency@4.2.14': + resolution: {integrity: sha512-c21qJiTSb25xvvOp+H2TNZzPCngrvl5vIPqPB8zQ/DmJF4QWXO19x1dWfMJZ6wZuuWUPPm0gV8C0cU3+ifcWuw==} + engines: {node: '>=18.0.0'} + '@smithy/is-array-buffer@2.2.0': resolution: {integrity: sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==} engines: {node: '>=14.0.0'} @@ -1223,58 +1374,118 @@ packages: resolution: {integrity: sha512-YE58Yz+cvFInWI/wOTrB+DbvUVz/pLn5mC5MvOV4fdRUc6qGwygyngcucRQjAhiCEbmfLOXX0gntSIcgMvAjmA==} engines: {node: '>=18.0.0'} + '@smithy/middleware-content-length@4.2.14': + resolution: {integrity: sha512-xhHq7fX4/3lv5NHxLUk3OeEvl0xZ+Ek3qIbWaCL4f9JwgDZEclPBElljaZCAItdGPQl/kSM4LPMOpy1MYgprpw==} + engines: {node: '>=18.0.0'} + '@smithy/middleware-endpoint@4.4.28': resolution: {integrity: sha512-p1gfYpi91CHcs5cBq982UlGlDrxoYUX6XdHSo91cQ2KFuz6QloHosO7Jc60pJiVmkWrKOV8kFYlGFFbQ2WUKKQ==} engines: {node: '>=18.0.0'} + '@smithy/middleware-endpoint@4.4.31': + resolution: {integrity: sha512-KJPdCIN2kOE2aGmqZd7eUTr4WQwOGgtLWgUkswGJggs7rBcQYQjcZMEDa3C0DwbOiXS9L8/wDoQHkfxBYLfiLw==} + engines: {node: '>=18.0.0'} + '@smithy/middleware-retry@4.4.46': resolution: {integrity: sha512-SpvWNNOPOrKQGUqZbEPO+es+FRXMWvIyzUKUOYdDgdlA6BdZj/R58p4umoQ76c2oJC44PiM7mKizyyex1IJzow==} engines: {node: '>=18.0.0'} + '@smithy/middleware-retry@4.5.4': + resolution: {integrity: sha512-/z7nIFK+ZRW3Ie/l3NEVGdy34LvmEOzBrtBAvgWZ/4PrKX0xP3kWm8pkfcwUk523SqxZhdbQP9JSXgjF77Uhpw==} + engines: {node: '>=18.0.0'} + '@smithy/middleware-serde@4.2.16': resolution: {integrity: sha512-beqfV+RZ9RSv+sQqor3xroUUYgRFCGRw6niGstPG8zO9LgTl0B0MCucxjmrH/2WwksQN7UUgI7KNANoZv+KALA==} engines: {node: '>=18.0.0'} + '@smithy/middleware-serde@4.2.19': + resolution: {integrity: sha512-Q6y+W9h3iYVMCKWDoVge+OC1LKFqbEKaq8SIWG2X2bWJRpd/6dDLyICcNLT6PbjH3Rr6bmg/SeDB25XFOFfeEw==} + engines: {node: '>=18.0.0'} + '@smithy/middleware-stack@4.2.12': resolution: {integrity: sha512-kruC5gRHwsCOuyCd4ouQxYjgRAym2uDlCvQ5acuMtRrcdfg7mFBg6blaxcJ09STpt3ziEkis6bhg1uwrWU7txw==} engines: {node: '>=18.0.0'} + '@smithy/middleware-stack@4.2.14': + resolution: {integrity: sha512-2dvkUKLuFdKsCRmOE4Mn63co0Djtsm+JMh0bYZQupN1pJwMeE8FmQmRLLzzEMN0dnNi7CDCYYH8F0EVwWiPBeA==} + engines: {node: '>=18.0.0'} + '@smithy/node-config-provider@4.3.12': resolution: {integrity: sha512-tr2oKX2xMcO+rBOjobSwVAkV05SIfUKz8iI53rzxEmgW3GOOPOv0UioSDk+J8OpRQnpnhsO3Af6IEBabQBVmiw==} engines: {node: '>=18.0.0'} + '@smithy/node-config-provider@4.3.14': + resolution: {integrity: sha512-S+gFjyo/weSVL0P1b9Ts8C/CwIfNCgUPikk3sl6QVsfE/uUuO+QsF+NsE/JkpvWqqyz1wg7HFdiaZuj5CoBMRg==} + engines: {node: '>=18.0.0'} + '@smithy/node-http-handler@4.5.1': resolution: {integrity: sha512-ejjxdAXjkPIs9lyYyVutOGNOraqUE9v/NjGMKwwFrfOM354wfSD8lmlj8hVwUzQmlLLF4+udhfCX9Exnbmvfzw==} engines: {node: '>=18.0.0'} + '@smithy/node-http-handler@4.6.0': + resolution: {integrity: sha512-P734cAoTFtuGfWa/R3jgBnGlURt2w9bYEBwQNMKf58sRM9RShirB2mKwLsVP+jlG/wxpCu8abv8NxdUts8tdLA==} + engines: {node: '>=18.0.0'} + '@smithy/property-provider@4.2.12': resolution: {integrity: sha512-jqve46eYU1v7pZ5BM+fmkbq3DerkSluPr5EhvOcHxygxzD05ByDRppRwRPPpFrsFo5yDtCYLKu+kreHKVrvc7A==} engines: {node: '>=18.0.0'} + '@smithy/property-provider@4.2.14': + resolution: {integrity: sha512-WuM31CgfsnQ/10i7NYr0PyxqknD72Y5uMfUMVSniPjbEPceiTErb4eIqJQ+pdxNEAUEWrewrGjIRjVbVHsxZiQ==} + engines: {node: '>=18.0.0'} + '@smithy/protocol-http@5.3.12': resolution: {integrity: sha512-fit0GZK9I1xoRlR4jXmbLhoN0OdEpa96ul8M65XdmXnxXkuMxM0Y8HDT0Fh0Xb4I85MBvBClOzgSrV1X2s1Hxw==} engines: {node: '>=18.0.0'} + '@smithy/protocol-http@5.3.14': + resolution: {integrity: sha512-dN5F8kHx8RNU0r+pCwNmFZyz6ChjMkzShy/zup6MtkRmmix4vZzJdW+di7x//b1LiynIev88FM18ie+wwPcQtQ==} + engines: {node: '>=18.0.0'} + '@smithy/querystring-builder@4.2.12': resolution: {integrity: sha512-6wTZjGABQufekycfDGMEB84BgtdOE/rCVTov+EDXQ8NHKTUNIp/j27IliwP7tjIU9LR+sSzyGBOXjeEtVgzCHg==} engines: {node: '>=18.0.0'} + '@smithy/querystring-builder@4.2.14': + resolution: {integrity: sha512-XYA5Z0IqTeF+5XDdh4BBmSA0HvbgVZIyv4cmOoUheDNR57K1HgBp9ukUMx3Cr3XpDHHpLBnexPE3LAtDsZkj2A==} + engines: {node: '>=18.0.0'} + '@smithy/querystring-parser@4.2.12': resolution: {integrity: sha512-P2OdvrgiAKpkPNKlKUtWbNZKB1XjPxM086NeVhK+W+wI46pIKdWBe5QyXvhUm3MEcyS/rkLvY8rZzyUdmyDZBw==} engines: {node: '>=18.0.0'} + '@smithy/querystring-parser@4.2.14': + resolution: {integrity: sha512-hr+YyqBD23GVvRxGGrcc/oOeNlK3PzT5Fu4dzrDXxzS1LpFiuL2PQQqKPs87M79aW7ziMs+nvB3qdw77SqE7Lw==} + engines: {node: '>=18.0.0'} + '@smithy/service-error-classification@4.2.12': resolution: {integrity: sha512-LlP29oSQN0Tw0b6D0Xo6BIikBswuIiGYbRACy5ujw/JgWSzTdYj46U83ssf6Ux0GyNJVivs2uReU8pt7Eu9okQ==} engines: {node: '>=18.0.0'} + '@smithy/service-error-classification@4.3.0': + resolution: {integrity: sha512-9jKsBYQRPR0xBLgc2415RsA5PIcP2sis4oBdN9s0D13cg1B1284mNTjx9Yc+BEERXzuPm5ObktI96OxsKh8E9A==} + engines: {node: '>=18.0.0'} + '@smithy/shared-ini-file-loader@4.4.7': resolution: {integrity: sha512-HrOKWsUb+otTeo1HxVWeEb99t5ER1XrBi/xka2Wv6NVmTbuCUC1dvlrksdvxFtODLBjsC+PHK+fuy2x/7Ynyiw==} engines: {node: '>=18.0.0'} + '@smithy/shared-ini-file-loader@4.4.9': + resolution: {integrity: sha512-495/V2I15SHgedSJoDPD23JuSfKAp726ZI1V0wtjB07Wh7q/0tri/0e0DLefZCHgxZonrGKt/OCTpAtP1wE1kQ==} + engines: {node: '>=18.0.0'} + '@smithy/signature-v4@5.3.12': resolution: {integrity: sha512-B/FBwO3MVOL00DaRSXfXfa/TRXRheagt/q5A2NM13u7q+sHS59EOVGQNfG7DkmVtdQm5m3vOosoKAXSqn/OEgw==} engines: {node: '>=18.0.0'} + '@smithy/signature-v4@5.3.14': + resolution: {integrity: sha512-1D9Y/nmlVjCeSivCbhZ7hgEpmHyY1h0GvpSZt3l0xcD9JjmjVC1CHOozS6+Gh+/ldMH8JuJ6cujObQqfayAVFA==} + engines: {node: '>=18.0.0'} + + '@smithy/smithy-client@4.12.12': + resolution: {integrity: sha512-daO7SJn4eM6ArbmrEs+/BTbH7af8AEbSL3OMQdcRvvn8tuUcR5rU2n6DgxIV53aXMS42uwK8NgKKCh5XgqYOPQ==} + engines: {node: '>=18.0.0'} + '@smithy/smithy-client@4.12.8': resolution: {integrity: sha512-aJaAX7vHe5i66smoSSID7t4rKY08PbD8EBU7DOloixvhOozfYWdcSYE4l6/tjkZ0vBZhGjheWzB2mh31sLgCMA==} engines: {node: '>=18.0.0'} @@ -1283,10 +1494,18 @@ packages: resolution: {integrity: sha512-787F3yzE2UiJIQ+wYW1CVg2odHjmaWLGksnKQHUrK/lYZSEcy1msuLVvxaR/sI2/aDe9U+TBuLsXnr3vod1g0g==} engines: {node: '>=18.0.0'} + '@smithy/types@4.14.1': + resolution: {integrity: sha512-59b5HtSVrVR/eYNei3BUj3DCPKD/G7EtDDe7OEJE7i7FtQFugYo6MxbotS8mVJkLNVf8gYaAlEBwwtJ9HzhWSg==} + engines: {node: '>=18.0.0'} + '@smithy/url-parser@4.2.12': resolution: {integrity: sha512-wOPKPEpso+doCZGIlr+e1lVI6+9VAKfL4kZWFgzVgGWY2hZxshNKod4l2LXS3PRC9otH/JRSjtEHqQ/7eLciRA==} engines: {node: '>=18.0.0'} + '@smithy/url-parser@4.2.14': + resolution: {integrity: sha512-p06BiBigJ8bTA3MgnOfCtDUWnAMY0YfedO/GRpmc7p+wg3KW8vbXy1xwSu5ASy0wV7rRYtlfZOIKH4XqfhjSQQ==} + engines: {node: '>=18.0.0'} + '@smithy/util-base64@4.3.2': resolution: {integrity: sha512-XRH6b0H/5A3SgblmMa5ErXQ2XKhfbQB+Fm/oyLZ2O2kCUrwgg55bU0RekmzAhuwOjA9qdN5VU2BprOvGGUkOOQ==} engines: {node: '>=18.0.0'} @@ -1315,14 +1534,26 @@ packages: resolution: {integrity: sha512-eZg6XzaCbVr2S5cAErU5eGBDaOVTuTo1I65i4tQcHENRcZ8rMWhQy1DaIYUSLyZjsfXvmCqZrstSMYyGFocvHA==} engines: {node: '>=18.0.0'} + '@smithy/util-defaults-mode-browser@4.3.48': + resolution: {integrity: sha512-hxVRVPYaRDWa6YQdse1aWX1qrksmLsvNyGBKdc32q4jFzSjxYVNWfstknAfR228TnzS4tzgswXRuYIbhXBuXFQ==} + engines: {node: '>=18.0.0'} + '@smithy/util-defaults-mode-node@4.2.48': resolution: {integrity: sha512-FqOKTlqSaoV3nzO55pMs5NBnZX8EhoI0DGmn9kbYeXWppgHD6dchyuj2HLqp4INJDJbSrj6OFYJkAh/WhSzZPg==} engines: {node: '>=18.0.0'} + '@smithy/util-defaults-mode-node@4.2.53': + resolution: {integrity: sha512-ybgCk+9JdBq8pYC8Y6U5fjyS8e4sboyAShetxPNL0rRBtaVl56GSFAxsolVBIea1tXR4LPIzL8i6xqmcf0+DCQ==} + engines: {node: '>=18.0.0'} + '@smithy/util-endpoints@3.3.3': resolution: {integrity: sha512-VACQVe50j0HZPjpwWcjyT51KUQ4AnsvEaQ2lKHOSL4mNLD0G9BjEniQ+yCt1qqfKfiAHRAts26ud7hBjamrwig==} engines: {node: '>=18.0.0'} + '@smithy/util-endpoints@3.4.2': + resolution: {integrity: sha512-a55Tr+3OKld4TTtnT+RhKOQHyPxm3j/xL4OR83WBUhLJaKDS9dnJ7arRMOp3t31dcLhApwG9bgvrRXBHlLdIkg==} + engines: {node: '>=18.0.0'} + '@smithy/util-hex-encoding@4.2.2': resolution: {integrity: sha512-Qcz3W5vuHK4sLQdyT93k/rfrUwdJ8/HZ+nMUOyGdpeGA1Wxt65zYwi3oEl9kOM+RswvYq90fzkNDahPS8K0OIg==} engines: {node: '>=18.0.0'} @@ -1331,14 +1562,26 @@ packages: resolution: {integrity: sha512-Er805uFUOvgc0l8nv0e0su0VFISoxhJ/AwOn3gL2NWNY2LUEldP5WtVcRYSQBcjg0y9NfG8JYrCJaYDpupBHJQ==} engines: {node: '>=18.0.0'} + '@smithy/util-middleware@4.2.14': + resolution: {integrity: sha512-1Su2vj9RYNDEv/V+2E+jXkkwGsgR7dc4sfHn9Z7ruzQHJIEni9zzw5CauvRXlFJfmgcqYP8fWa0dkh2Q2YaQyw==} + engines: {node: '>=18.0.0'} + '@smithy/util-retry@4.2.13': resolution: {integrity: sha512-qQQsIvL0MGIbUjeSrg0/VlQ3jGNKyM3/2iU3FPNgy01z+Sp4OvcaxbgIoFOTvB61ZoohtutuOvOcgmhbD0katQ==} engines: {node: '>=18.0.0'} + '@smithy/util-retry@4.3.3': + resolution: {integrity: sha512-idjUvd4M9Jj6rXkhqw4H4reHoweuK4ZxYWyOrEp4N2rOF5VtaOlQGLDQJva/8WanNXk9ScQtsAb7o5UHGvFm4A==} + engines: {node: '>=18.0.0'} + '@smithy/util-stream@4.5.21': resolution: {integrity: sha512-KzSg+7KKywLnkoKejRtIBXDmwBfjGvg1U1i/etkC7XSWUyFCoLno1IohV2c74IzQqdhX5y3uE44r/8/wuK+A7Q==} engines: {node: '>=18.0.0'} + '@smithy/util-stream@4.5.24': + resolution: {integrity: sha512-na5vv2mBSDzXewLEEoWGI7LQQkfpmFEomBsmOpzLFjqGctm0iMwXY5lAwesY9pIaErkccW0qzEOUcYP+WKneXg==} + engines: {node: '>=18.0.0'} + '@smithy/util-uri-escape@4.2.2': resolution: {integrity: sha512-2kAStBlvq+lTXHyAZYfJRb/DfS3rsinLiwb+69SstC9Vb0s9vNWkRwpnj918Pfi85mzi42sOqdV72OLxWAISnw==} engines: {node: '>=18.0.0'} @@ -4163,7 +4406,7 @@ snapshots: '@aws-crypto/sha256-js@5.2.0': dependencies: '@aws-crypto/util': 5.2.0 - '@aws-sdk/types': 3.973.6 + '@aws-sdk/types': 3.973.8 tslib: 2.8.1 '@aws-crypto/supports-web-crypto@5.2.0': @@ -4221,6 +4464,53 @@ snapshots: transitivePeerDependencies: - aws-crt + '@aws-sdk/client-cloudwatch-logs@3.1034.0': + dependencies: + '@aws-crypto/sha256-browser': 5.2.0 + '@aws-crypto/sha256-js': 5.2.0 + '@aws-sdk/core': 3.974.3 + '@aws-sdk/credential-provider-node': 3.972.34 + '@aws-sdk/middleware-host-header': 3.972.10 + '@aws-sdk/middleware-logger': 3.972.10 + '@aws-sdk/middleware-recursion-detection': 3.972.11 + '@aws-sdk/middleware-user-agent': 3.972.33 + '@aws-sdk/region-config-resolver': 3.972.13 + '@aws-sdk/types': 3.973.8 + '@aws-sdk/util-endpoints': 3.996.8 + '@aws-sdk/util-user-agent-browser': 3.972.10 + '@aws-sdk/util-user-agent-node': 3.973.19 + '@smithy/config-resolver': 4.4.17 + '@smithy/core': 3.23.16 + '@smithy/eventstream-serde-browser': 4.2.14 + '@smithy/eventstream-serde-config-resolver': 4.3.14 + '@smithy/eventstream-serde-node': 4.2.14 + '@smithy/fetch-http-handler': 5.3.17 + '@smithy/hash-node': 4.2.14 + '@smithy/invalid-dependency': 4.2.14 + '@smithy/middleware-content-length': 4.2.14 + '@smithy/middleware-endpoint': 4.4.31 + '@smithy/middleware-retry': 4.5.4 + '@smithy/middleware-serde': 4.2.19 + '@smithy/middleware-stack': 4.2.14 + '@smithy/node-config-provider': 4.3.14 + '@smithy/node-http-handler': 4.6.0 + '@smithy/protocol-http': 5.3.14 + '@smithy/smithy-client': 4.12.12 + '@smithy/types': 4.14.1 + '@smithy/url-parser': 4.2.14 + '@smithy/util-base64': 4.3.2 + '@smithy/util-body-length-browser': 4.2.2 + '@smithy/util-body-length-node': 4.2.3 + '@smithy/util-defaults-mode-browser': 4.3.48 + '@smithy/util-defaults-mode-node': 4.2.53 + '@smithy/util-endpoints': 3.4.2 + '@smithy/util-middleware': 4.2.14 + '@smithy/util-retry': 4.3.3 + '@smithy/util-utf8': 4.2.2 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + '@aws-sdk/client-codebuild@3.1019.0': dependencies: '@aws-crypto/sha256-browser': 5.2.0 @@ -4688,6 +4978,23 @@ snapshots: '@smithy/util-utf8': 4.2.2 tslib: 2.8.1 + '@aws-sdk/core@3.974.3': + dependencies: + '@aws-sdk/types': 3.973.8 + '@aws-sdk/xml-builder': 3.972.18 + '@smithy/core': 3.23.16 + '@smithy/node-config-provider': 4.3.14 + '@smithy/property-provider': 4.2.14 + '@smithy/protocol-http': 5.3.14 + '@smithy/signature-v4': 5.3.14 + '@smithy/smithy-client': 4.12.12 + '@smithy/types': 4.14.1 + '@smithy/util-base64': 4.3.2 + '@smithy/util-middleware': 4.2.14 + '@smithy/util-retry': 4.3.3 + '@smithy/util-utf8': 4.2.2 + tslib: 2.8.1 + '@aws-sdk/credential-provider-env@3.972.24': dependencies: '@aws-sdk/core': 3.973.26 @@ -4696,6 +5003,14 @@ snapshots: '@smithy/types': 4.13.1 tslib: 2.8.1 + '@aws-sdk/credential-provider-env@3.972.29': + dependencies: + '@aws-sdk/core': 3.974.3 + '@aws-sdk/types': 3.973.8 + '@smithy/property-provider': 4.2.14 + '@smithy/types': 4.14.1 + tslib: 2.8.1 + '@aws-sdk/credential-provider-http@3.972.26': dependencies: '@aws-sdk/core': 3.973.26 @@ -4709,6 +5024,19 @@ snapshots: '@smithy/util-stream': 4.5.21 tslib: 2.8.1 + '@aws-sdk/credential-provider-http@3.972.31': + dependencies: + '@aws-sdk/core': 3.974.3 + '@aws-sdk/types': 3.973.8 + '@smithy/fetch-http-handler': 5.3.17 + '@smithy/node-http-handler': 4.6.0 + '@smithy/property-provider': 4.2.14 + '@smithy/protocol-http': 5.3.14 + '@smithy/smithy-client': 4.12.12 + '@smithy/types': 4.14.1 + '@smithy/util-stream': 4.5.24 + tslib: 2.8.1 + '@aws-sdk/credential-provider-ini@3.972.28': dependencies: '@aws-sdk/core': 3.973.26 @@ -4728,6 +5056,25 @@ snapshots: transitivePeerDependencies: - aws-crt + '@aws-sdk/credential-provider-ini@3.972.33': + dependencies: + '@aws-sdk/core': 3.974.3 + '@aws-sdk/credential-provider-env': 3.972.29 + '@aws-sdk/credential-provider-http': 3.972.31 + '@aws-sdk/credential-provider-login': 3.972.33 + '@aws-sdk/credential-provider-process': 3.972.29 + '@aws-sdk/credential-provider-sso': 3.972.33 + '@aws-sdk/credential-provider-web-identity': 3.972.33 + '@aws-sdk/nested-clients': 3.997.1 + '@aws-sdk/types': 3.973.8 + '@smithy/credential-provider-imds': 4.2.14 + '@smithy/property-provider': 4.2.14 + '@smithy/shared-ini-file-loader': 4.4.9 + '@smithy/types': 4.14.1 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + '@aws-sdk/credential-provider-login@3.972.28': dependencies: '@aws-sdk/core': 3.973.26 @@ -4741,6 +5088,19 @@ snapshots: transitivePeerDependencies: - aws-crt + '@aws-sdk/credential-provider-login@3.972.33': + dependencies: + '@aws-sdk/core': 3.974.3 + '@aws-sdk/nested-clients': 3.997.1 + '@aws-sdk/types': 3.973.8 + '@smithy/property-provider': 4.2.14 + '@smithy/protocol-http': 5.3.14 + '@smithy/shared-ini-file-loader': 4.4.9 + '@smithy/types': 4.14.1 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + '@aws-sdk/credential-provider-node@3.972.29': dependencies: '@aws-sdk/credential-provider-env': 3.972.24 @@ -4758,6 +5118,23 @@ snapshots: transitivePeerDependencies: - aws-crt + '@aws-sdk/credential-provider-node@3.972.34': + dependencies: + '@aws-sdk/credential-provider-env': 3.972.29 + '@aws-sdk/credential-provider-http': 3.972.31 + '@aws-sdk/credential-provider-ini': 3.972.33 + '@aws-sdk/credential-provider-process': 3.972.29 + '@aws-sdk/credential-provider-sso': 3.972.33 + '@aws-sdk/credential-provider-web-identity': 3.972.33 + '@aws-sdk/types': 3.973.8 + '@smithy/credential-provider-imds': 4.2.14 + '@smithy/property-provider': 4.2.14 + '@smithy/shared-ini-file-loader': 4.4.9 + '@smithy/types': 4.14.1 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + '@aws-sdk/credential-provider-process@3.972.24': dependencies: '@aws-sdk/core': 3.973.26 @@ -4767,6 +5144,15 @@ snapshots: '@smithy/types': 4.13.1 tslib: 2.8.1 + '@aws-sdk/credential-provider-process@3.972.29': + dependencies: + '@aws-sdk/core': 3.974.3 + '@aws-sdk/types': 3.973.8 + '@smithy/property-provider': 4.2.14 + '@smithy/shared-ini-file-loader': 4.4.9 + '@smithy/types': 4.14.1 + tslib: 2.8.1 + '@aws-sdk/credential-provider-sso@3.972.28': dependencies: '@aws-sdk/core': 3.973.26 @@ -4780,6 +5166,19 @@ snapshots: transitivePeerDependencies: - aws-crt + '@aws-sdk/credential-provider-sso@3.972.33': + dependencies: + '@aws-sdk/core': 3.974.3 + '@aws-sdk/nested-clients': 3.997.1 + '@aws-sdk/token-providers': 3.1034.0 + '@aws-sdk/types': 3.973.8 + '@smithy/property-provider': 4.2.14 + '@smithy/shared-ini-file-loader': 4.4.9 + '@smithy/types': 4.14.1 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + '@aws-sdk/credential-provider-web-identity@3.972.28': dependencies: '@aws-sdk/core': 3.973.26 @@ -4792,6 +5191,25 @@ snapshots: transitivePeerDependencies: - aws-crt + '@aws-sdk/credential-provider-web-identity@3.972.33': + dependencies: + '@aws-sdk/core': 3.974.3 + '@aws-sdk/nested-clients': 3.997.1 + '@aws-sdk/types': 3.973.8 + '@smithy/property-provider': 4.2.14 + '@smithy/shared-ini-file-loader': 4.4.9 + '@smithy/types': 4.14.1 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/middleware-host-header@3.972.10': + dependencies: + '@aws-sdk/types': 3.973.8 + '@smithy/protocol-http': 5.3.14 + '@smithy/types': 4.14.1 + tslib: 2.8.1 + '@aws-sdk/middleware-host-header@3.972.8': dependencies: '@aws-sdk/types': 3.973.6 @@ -4799,12 +5217,26 @@ snapshots: '@smithy/types': 4.13.1 tslib: 2.8.1 + '@aws-sdk/middleware-logger@3.972.10': + dependencies: + '@aws-sdk/types': 3.973.8 + '@smithy/types': 4.14.1 + tslib: 2.8.1 + '@aws-sdk/middleware-logger@3.972.8': dependencies: '@aws-sdk/types': 3.973.6 '@smithy/types': 4.13.1 tslib: 2.8.1 + '@aws-sdk/middleware-recursion-detection@3.972.11': + dependencies: + '@aws-sdk/types': 3.973.8 + '@aws/lambda-invoke-store': 0.2.4 + '@smithy/protocol-http': 5.3.14 + '@smithy/types': 4.14.1 + tslib: 2.8.1 + '@aws-sdk/middleware-recursion-detection@3.972.9': dependencies: '@aws-sdk/types': 3.973.6 @@ -4824,6 +5256,23 @@ snapshots: '@smithy/types': 4.13.1 tslib: 2.8.1 + '@aws-sdk/middleware-sdk-s3@3.972.32': + dependencies: + '@aws-sdk/core': 3.974.3 + '@aws-sdk/types': 3.973.8 + '@aws-sdk/util-arn-parser': 3.972.3 + '@smithy/core': 3.23.16 + '@smithy/node-config-provider': 4.3.14 + '@smithy/protocol-http': 5.3.14 + '@smithy/signature-v4': 5.3.14 + '@smithy/smithy-client': 4.12.12 + '@smithy/types': 4.14.1 + '@smithy/util-config-provider': 4.2.2 + '@smithy/util-middleware': 4.2.14 + '@smithy/util-stream': 4.5.24 + '@smithy/util-utf8': 4.2.2 + tslib: 2.8.1 + '@aws-sdk/middleware-sdk-sqs@3.972.18': dependencies: '@aws-sdk/types': 3.973.6 @@ -4844,6 +5293,17 @@ snapshots: '@smithy/util-retry': 4.2.13 tslib: 2.8.1 + '@aws-sdk/middleware-user-agent@3.972.33': + dependencies: + '@aws-sdk/core': 3.974.3 + '@aws-sdk/types': 3.973.8 + '@aws-sdk/util-endpoints': 3.996.8 + '@smithy/core': 3.23.16 + '@smithy/protocol-http': 5.3.14 + '@smithy/types': 4.14.1 + '@smithy/util-retry': 4.3.3 + tslib: 2.8.1 + '@aws-sdk/nested-clients@3.996.18': dependencies: '@aws-crypto/sha256-browser': 5.2.0 @@ -4887,6 +5347,50 @@ snapshots: transitivePeerDependencies: - aws-crt + '@aws-sdk/nested-clients@3.997.1': + dependencies: + '@aws-crypto/sha256-browser': 5.2.0 + '@aws-crypto/sha256-js': 5.2.0 + '@aws-sdk/core': 3.974.3 + '@aws-sdk/middleware-host-header': 3.972.10 + '@aws-sdk/middleware-logger': 3.972.10 + '@aws-sdk/middleware-recursion-detection': 3.972.11 + '@aws-sdk/middleware-user-agent': 3.972.33 + '@aws-sdk/region-config-resolver': 3.972.13 + '@aws-sdk/signature-v4-multi-region': 3.996.20 + '@aws-sdk/types': 3.973.8 + '@aws-sdk/util-endpoints': 3.996.8 + '@aws-sdk/util-user-agent-browser': 3.972.10 + '@aws-sdk/util-user-agent-node': 3.973.19 + '@smithy/config-resolver': 4.4.17 + '@smithy/core': 3.23.16 + '@smithy/fetch-http-handler': 5.3.17 + '@smithy/hash-node': 4.2.14 + '@smithy/invalid-dependency': 4.2.14 + '@smithy/middleware-content-length': 4.2.14 + '@smithy/middleware-endpoint': 4.4.31 + '@smithy/middleware-retry': 4.5.4 + '@smithy/middleware-serde': 4.2.19 + '@smithy/middleware-stack': 4.2.14 + '@smithy/node-config-provider': 4.3.14 + '@smithy/node-http-handler': 4.6.0 + '@smithy/protocol-http': 5.3.14 + '@smithy/smithy-client': 4.12.12 + '@smithy/types': 4.14.1 + '@smithy/url-parser': 4.2.14 + '@smithy/util-base64': 4.3.2 + '@smithy/util-body-length-browser': 4.2.2 + '@smithy/util-body-length-node': 4.2.3 + '@smithy/util-defaults-mode-browser': 4.3.48 + '@smithy/util-defaults-mode-node': 4.2.53 + '@smithy/util-endpoints': 3.4.2 + '@smithy/util-middleware': 4.2.14 + '@smithy/util-retry': 4.3.3 + '@smithy/util-utf8': 4.2.2 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + '@aws-sdk/region-config-resolver@3.972.10': dependencies: '@aws-sdk/types': 3.973.6 @@ -4895,6 +5399,23 @@ snapshots: '@smithy/types': 4.13.1 tslib: 2.8.1 + '@aws-sdk/region-config-resolver@3.972.13': + dependencies: + '@aws-sdk/types': 3.973.8 + '@smithy/config-resolver': 4.4.17 + '@smithy/node-config-provider': 4.3.14 + '@smithy/types': 4.14.1 + tslib: 2.8.1 + + '@aws-sdk/signature-v4-multi-region@3.996.20': + dependencies: + '@aws-sdk/middleware-sdk-s3': 3.972.32 + '@aws-sdk/types': 3.973.8 + '@smithy/protocol-http': 5.3.14 + '@smithy/signature-v4': 5.3.14 + '@smithy/types': 4.14.1 + tslib: 2.8.1 + '@aws-sdk/token-providers@3.1021.0': dependencies: '@aws-sdk/core': 3.973.26 @@ -4907,11 +5428,32 @@ snapshots: transitivePeerDependencies: - aws-crt + '@aws-sdk/token-providers@3.1034.0': + dependencies: + '@aws-sdk/core': 3.974.3 + '@aws-sdk/nested-clients': 3.997.1 + '@aws-sdk/types': 3.973.8 + '@smithy/property-provider': 4.2.14 + '@smithy/shared-ini-file-loader': 4.4.9 + '@smithy/types': 4.14.1 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + '@aws-sdk/types@3.973.6': dependencies: '@smithy/types': 4.13.1 tslib: 2.8.1 + '@aws-sdk/types@3.973.8': + dependencies: + '@smithy/types': 4.14.1 + tslib: 2.8.1 + + '@aws-sdk/util-arn-parser@3.972.3': + dependencies: + tslib: 2.8.1 + '@aws-sdk/util-endpoints@3.996.5': dependencies: '@aws-sdk/types': 3.973.6 @@ -4920,6 +5462,14 @@ snapshots: '@smithy/util-endpoints': 3.3.3 tslib: 2.8.1 + '@aws-sdk/util-endpoints@3.996.8': + dependencies: + '@aws-sdk/types': 3.973.8 + '@smithy/types': 4.14.1 + '@smithy/url-parser': 4.2.14 + '@smithy/util-endpoints': 3.4.2 + tslib: 2.8.1 + '@aws-sdk/util-format-url@3.972.8': dependencies: '@aws-sdk/types': 3.973.6 @@ -4931,6 +5481,13 @@ snapshots: dependencies: tslib: 2.8.1 + '@aws-sdk/util-user-agent-browser@3.972.10': + dependencies: + '@aws-sdk/types': 3.973.8 + '@smithy/types': 4.14.1 + bowser: 2.14.1 + tslib: 2.8.1 + '@aws-sdk/util-user-agent-browser@3.972.8': dependencies: '@aws-sdk/types': 3.973.6 @@ -4947,12 +5504,27 @@ snapshots: '@smithy/util-config-provider': 4.2.2 tslib: 2.8.1 + '@aws-sdk/util-user-agent-node@3.973.19': + dependencies: + '@aws-sdk/middleware-user-agent': 3.972.33 + '@aws-sdk/types': 3.973.8 + '@smithy/node-config-provider': 4.3.14 + '@smithy/types': 4.14.1 + '@smithy/util-config-provider': 4.2.2 + tslib: 2.8.1 + '@aws-sdk/xml-builder@3.972.16': dependencies: '@smithy/types': 4.13.1 fast-xml-parser: 5.5.8 tslib: 2.8.1 + '@aws-sdk/xml-builder@3.972.18': + dependencies: + '@smithy/types': 4.14.1 + fast-xml-parser: 5.5.8 + tslib: 2.8.1 + '@aws/lambda-invoke-store@0.2.4': {} '@babel/code-frame@7.29.0': @@ -5765,6 +6337,15 @@ snapshots: '@smithy/util-middleware': 4.2.12 tslib: 2.8.1 + '@smithy/config-resolver@4.4.17': + dependencies: + '@smithy/node-config-provider': 4.3.14 + '@smithy/types': 4.14.1 + '@smithy/util-config-provider': 4.2.2 + '@smithy/util-endpoints': 3.4.2 + '@smithy/util-middleware': 4.2.14 + tslib: 2.8.1 + '@smithy/core@3.23.13': dependencies: '@smithy/protocol-http': 5.3.12 @@ -5778,6 +6359,19 @@ snapshots: '@smithy/uuid': 1.1.2 tslib: 2.8.1 + '@smithy/core@3.23.16': + dependencies: + '@smithy/protocol-http': 5.3.14 + '@smithy/types': 4.14.1 + '@smithy/url-parser': 4.2.14 + '@smithy/util-base64': 4.3.2 + '@smithy/util-body-length-browser': 4.2.2 + '@smithy/util-middleware': 4.2.14 + '@smithy/util-stream': 4.5.24 + '@smithy/util-utf8': 4.2.2 + '@smithy/uuid': 1.1.2 + tslib: 2.8.1 + '@smithy/credential-provider-imds@4.2.12': dependencies: '@smithy/node-config-provider': 4.3.12 @@ -5786,6 +6380,14 @@ snapshots: '@smithy/url-parser': 4.2.12 tslib: 2.8.1 + '@smithy/credential-provider-imds@4.2.14': + dependencies: + '@smithy/node-config-provider': 4.3.14 + '@smithy/property-provider': 4.2.14 + '@smithy/types': 4.14.1 + '@smithy/url-parser': 4.2.14 + tslib: 2.8.1 + '@smithy/eventstream-codec@4.2.12': dependencies: '@aws-crypto/crc32': 5.2.0 @@ -5793,29 +6395,59 @@ snapshots: '@smithy/util-hex-encoding': 4.2.2 tslib: 2.8.1 + '@smithy/eventstream-codec@4.2.14': + dependencies: + '@aws-crypto/crc32': 5.2.0 + '@smithy/types': 4.14.1 + '@smithy/util-hex-encoding': 4.2.2 + tslib: 2.8.1 + '@smithy/eventstream-serde-browser@4.2.12': dependencies: '@smithy/eventstream-serde-universal': 4.2.12 '@smithy/types': 4.13.1 tslib: 2.8.1 + '@smithy/eventstream-serde-browser@4.2.14': + dependencies: + '@smithy/eventstream-serde-universal': 4.2.14 + '@smithy/types': 4.14.1 + tslib: 2.8.1 + '@smithy/eventstream-serde-config-resolver@4.3.12': dependencies: '@smithy/types': 4.13.1 tslib: 2.8.1 + '@smithy/eventstream-serde-config-resolver@4.3.14': + dependencies: + '@smithy/types': 4.14.1 + tslib: 2.8.1 + '@smithy/eventstream-serde-node@4.2.12': dependencies: '@smithy/eventstream-serde-universal': 4.2.12 '@smithy/types': 4.13.1 tslib: 2.8.1 + '@smithy/eventstream-serde-node@4.2.14': + dependencies: + '@smithy/eventstream-serde-universal': 4.2.14 + '@smithy/types': 4.14.1 + tslib: 2.8.1 + '@smithy/eventstream-serde-universal@4.2.12': dependencies: '@smithy/eventstream-codec': 4.2.12 '@smithy/types': 4.13.1 tslib: 2.8.1 + '@smithy/eventstream-serde-universal@4.2.14': + dependencies: + '@smithy/eventstream-codec': 4.2.14 + '@smithy/types': 4.14.1 + tslib: 2.8.1 + '@smithy/fetch-http-handler@5.3.15': dependencies: '@smithy/protocol-http': 5.3.12 @@ -5824,6 +6456,14 @@ snapshots: '@smithy/util-base64': 4.3.2 tslib: 2.8.1 + '@smithy/fetch-http-handler@5.3.17': + dependencies: + '@smithy/protocol-http': 5.3.14 + '@smithy/querystring-builder': 4.2.14 + '@smithy/types': 4.14.1 + '@smithy/util-base64': 4.3.2 + tslib: 2.8.1 + '@smithy/hash-node@4.2.12': dependencies: '@smithy/types': 4.13.1 @@ -5831,11 +6471,23 @@ snapshots: '@smithy/util-utf8': 4.2.2 tslib: 2.8.1 + '@smithy/hash-node@4.2.14': + dependencies: + '@smithy/types': 4.14.1 + '@smithy/util-buffer-from': 4.2.2 + '@smithy/util-utf8': 4.2.2 + tslib: 2.8.1 + '@smithy/invalid-dependency@4.2.12': dependencies: '@smithy/types': 4.13.1 tslib: 2.8.1 + '@smithy/invalid-dependency@4.2.14': + dependencies: + '@smithy/types': 4.14.1 + tslib: 2.8.1 + '@smithy/is-array-buffer@2.2.0': dependencies: tslib: 2.8.1 @@ -5856,6 +6508,12 @@ snapshots: '@smithy/types': 4.13.1 tslib: 2.8.1 + '@smithy/middleware-content-length@4.2.14': + dependencies: + '@smithy/protocol-http': 5.3.14 + '@smithy/types': 4.14.1 + tslib: 2.8.1 + '@smithy/middleware-endpoint@4.4.28': dependencies: '@smithy/core': 3.23.13 @@ -5867,6 +6525,17 @@ snapshots: '@smithy/util-middleware': 4.2.12 tslib: 2.8.1 + '@smithy/middleware-endpoint@4.4.31': + dependencies: + '@smithy/core': 3.23.16 + '@smithy/middleware-serde': 4.2.19 + '@smithy/node-config-provider': 4.3.14 + '@smithy/shared-ini-file-loader': 4.4.9 + '@smithy/types': 4.14.1 + '@smithy/url-parser': 4.2.14 + '@smithy/util-middleware': 4.2.14 + tslib: 2.8.1 + '@smithy/middleware-retry@4.4.46': dependencies: '@smithy/node-config-provider': 4.3.12 @@ -5879,6 +6548,19 @@ snapshots: '@smithy/uuid': 1.1.2 tslib: 2.8.1 + '@smithy/middleware-retry@4.5.4': + dependencies: + '@smithy/core': 3.23.16 + '@smithy/node-config-provider': 4.3.14 + '@smithy/protocol-http': 5.3.14 + '@smithy/service-error-classification': 4.3.0 + '@smithy/smithy-client': 4.12.12 + '@smithy/types': 4.14.1 + '@smithy/util-middleware': 4.2.14 + '@smithy/util-retry': 4.3.3 + '@smithy/uuid': 1.1.2 + tslib: 2.8.1 + '@smithy/middleware-serde@4.2.16': dependencies: '@smithy/core': 3.23.13 @@ -5886,11 +6568,23 @@ snapshots: '@smithy/types': 4.13.1 tslib: 2.8.1 + '@smithy/middleware-serde@4.2.19': + dependencies: + '@smithy/core': 3.23.16 + '@smithy/protocol-http': 5.3.14 + '@smithy/types': 4.14.1 + tslib: 2.8.1 + '@smithy/middleware-stack@4.2.12': dependencies: '@smithy/types': 4.13.1 tslib: 2.8.1 + '@smithy/middleware-stack@4.2.14': + dependencies: + '@smithy/types': 4.14.1 + tslib: 2.8.1 + '@smithy/node-config-provider@4.3.12': dependencies: '@smithy/property-provider': 4.2.12 @@ -5898,6 +6592,13 @@ snapshots: '@smithy/types': 4.13.1 tslib: 2.8.1 + '@smithy/node-config-provider@4.3.14': + dependencies: + '@smithy/property-provider': 4.2.14 + '@smithy/shared-ini-file-loader': 4.4.9 + '@smithy/types': 4.14.1 + tslib: 2.8.1 + '@smithy/node-http-handler@4.5.1': dependencies: '@smithy/protocol-http': 5.3.12 @@ -5905,36 +6606,73 @@ snapshots: '@smithy/types': 4.13.1 tslib: 2.8.1 + '@smithy/node-http-handler@4.6.0': + dependencies: + '@smithy/protocol-http': 5.3.14 + '@smithy/querystring-builder': 4.2.14 + '@smithy/types': 4.14.1 + tslib: 2.8.1 + '@smithy/property-provider@4.2.12': dependencies: '@smithy/types': 4.13.1 tslib: 2.8.1 + '@smithy/property-provider@4.2.14': + dependencies: + '@smithy/types': 4.14.1 + tslib: 2.8.1 + '@smithy/protocol-http@5.3.12': dependencies: '@smithy/types': 4.13.1 tslib: 2.8.1 + '@smithy/protocol-http@5.3.14': + dependencies: + '@smithy/types': 4.14.1 + tslib: 2.8.1 + '@smithy/querystring-builder@4.2.12': dependencies: '@smithy/types': 4.13.1 '@smithy/util-uri-escape': 4.2.2 tslib: 2.8.1 + '@smithy/querystring-builder@4.2.14': + dependencies: + '@smithy/types': 4.14.1 + '@smithy/util-uri-escape': 4.2.2 + tslib: 2.8.1 + '@smithy/querystring-parser@4.2.12': dependencies: '@smithy/types': 4.13.1 tslib: 2.8.1 + '@smithy/querystring-parser@4.2.14': + dependencies: + '@smithy/types': 4.14.1 + tslib: 2.8.1 + '@smithy/service-error-classification@4.2.12': dependencies: '@smithy/types': 4.13.1 + '@smithy/service-error-classification@4.3.0': + dependencies: + '@smithy/types': 4.14.1 + '@smithy/shared-ini-file-loader@4.4.7': dependencies: '@smithy/types': 4.13.1 tslib: 2.8.1 + '@smithy/shared-ini-file-loader@4.4.9': + dependencies: + '@smithy/types': 4.14.1 + tslib: 2.8.1 + '@smithy/signature-v4@5.3.12': dependencies: '@smithy/is-array-buffer': 4.2.2 @@ -5946,6 +6684,27 @@ snapshots: '@smithy/util-utf8': 4.2.2 tslib: 2.8.1 + '@smithy/signature-v4@5.3.14': + dependencies: + '@smithy/is-array-buffer': 4.2.2 + '@smithy/protocol-http': 5.3.14 + '@smithy/types': 4.14.1 + '@smithy/util-hex-encoding': 4.2.2 + '@smithy/util-middleware': 4.2.14 + '@smithy/util-uri-escape': 4.2.2 + '@smithy/util-utf8': 4.2.2 + tslib: 2.8.1 + + '@smithy/smithy-client@4.12.12': + dependencies: + '@smithy/core': 3.23.16 + '@smithy/middleware-endpoint': 4.4.31 + '@smithy/middleware-stack': 4.2.14 + '@smithy/protocol-http': 5.3.14 + '@smithy/types': 4.14.1 + '@smithy/util-stream': 4.5.24 + tslib: 2.8.1 + '@smithy/smithy-client@4.12.8': dependencies: '@smithy/core': 3.23.13 @@ -5960,12 +6719,22 @@ snapshots: dependencies: tslib: 2.8.1 + '@smithy/types@4.14.1': + dependencies: + tslib: 2.8.1 + '@smithy/url-parser@4.2.12': dependencies: '@smithy/querystring-parser': 4.2.12 '@smithy/types': 4.13.1 tslib: 2.8.1 + '@smithy/url-parser@4.2.14': + dependencies: + '@smithy/querystring-parser': 4.2.14 + '@smithy/types': 4.14.1 + tslib: 2.8.1 + '@smithy/util-base64@4.3.2': dependencies: '@smithy/util-buffer-from': 4.2.2 @@ -6001,6 +6770,13 @@ snapshots: '@smithy/types': 4.13.1 tslib: 2.8.1 + '@smithy/util-defaults-mode-browser@4.3.48': + dependencies: + '@smithy/property-provider': 4.2.14 + '@smithy/smithy-client': 4.12.12 + '@smithy/types': 4.14.1 + tslib: 2.8.1 + '@smithy/util-defaults-mode-node@4.2.48': dependencies: '@smithy/config-resolver': 4.4.13 @@ -6011,12 +6787,28 @@ snapshots: '@smithy/types': 4.13.1 tslib: 2.8.1 + '@smithy/util-defaults-mode-node@4.2.53': + dependencies: + '@smithy/config-resolver': 4.4.17 + '@smithy/credential-provider-imds': 4.2.14 + '@smithy/node-config-provider': 4.3.14 + '@smithy/property-provider': 4.2.14 + '@smithy/smithy-client': 4.12.12 + '@smithy/types': 4.14.1 + tslib: 2.8.1 + '@smithy/util-endpoints@3.3.3': dependencies: '@smithy/node-config-provider': 4.3.12 '@smithy/types': 4.13.1 tslib: 2.8.1 + '@smithy/util-endpoints@3.4.2': + dependencies: + '@smithy/node-config-provider': 4.3.14 + '@smithy/types': 4.14.1 + tslib: 2.8.1 + '@smithy/util-hex-encoding@4.2.2': dependencies: tslib: 2.8.1 @@ -6026,12 +6818,23 @@ snapshots: '@smithy/types': 4.13.1 tslib: 2.8.1 + '@smithy/util-middleware@4.2.14': + dependencies: + '@smithy/types': 4.14.1 + tslib: 2.8.1 + '@smithy/util-retry@4.2.13': dependencies: '@smithy/service-error-classification': 4.2.12 '@smithy/types': 4.13.1 tslib: 2.8.1 + '@smithy/util-retry@4.3.3': + dependencies: + '@smithy/service-error-classification': 4.3.0 + '@smithy/types': 4.14.1 + tslib: 2.8.1 + '@smithy/util-stream@4.5.21': dependencies: '@smithy/fetch-http-handler': 5.3.15 @@ -6043,6 +6846,17 @@ snapshots: '@smithy/util-utf8': 4.2.2 tslib: 2.8.1 + '@smithy/util-stream@4.5.24': + dependencies: + '@smithy/fetch-http-handler': 5.3.17 + '@smithy/node-http-handler': 4.6.0 + '@smithy/types': 4.14.1 + '@smithy/util-base64': 4.3.2 + '@smithy/util-buffer-from': 4.2.2 + '@smithy/util-hex-encoding': 4.2.2 + '@smithy/util-utf8': 4.2.2 + tslib: 2.8.1 + '@smithy/util-uri-escape@4.2.2': dependencies: tslib: 2.8.1 diff --git a/src/runner.ts b/src/runner.ts index 474c5d12..e4395a64 100644 --- a/src/runner.ts +++ b/src/runner.ts @@ -34,6 +34,7 @@ import { Secrets } from './secrets'; import { SetupFunction } from './setup-function'; import { StatusFunction } from './status-function'; import { TokenRetrieverFunction } from './token-retriever-function'; +import { TroubleshootFunction } from './troubleshoot-function'; import { discoverCertificateFiles, singletonLogGroup, SingletonLogType } from './utils'; import { WarmRunnerManagerFunction } from './warm-runner-manager-function'; import { GithubWebhookHandler } from './webhook'; @@ -177,6 +178,13 @@ export interface GitHubRunnersProps { */ readonly statusAccess?: LambdaAccess; + /** + * Access configuration for the troubleshoot function. This function analyzes runner health and returns detailed diagnostic information including execution history, so you should only allow access to it from trusted IPs, if at all. + * + * @default LambdaAccess.noAccess() + */ + readonly troubleshootAccess?: LambdaAccess; + /** * Options to retry operation in case of failure like missing capacity, or API quota issues. * @@ -369,6 +377,7 @@ export class GitHubRunners extends Construct implements ec2.IConnectable { this.setupUrl = this.setupFunction(); this.statusFunction(); + this.troubleshootFunction(); } private stateMachine(props?: GitHubRunnersProps) { @@ -655,6 +664,83 @@ export class GitHubRunners extends Construct implements ec2.IConnectable { } } + private troubleshootFunction() { + const troubleshootFn = new TroubleshootFunction( + this, + 'troubleshoot', + { + description: 'Diagnose issues with self-hosted GitHub Actions runners', + environment: { + WEBHOOK_SECRET_ARN: this.secrets.webhook.secretArn, + GITHUB_SECRET_ARN: this.secrets.github.secretArn, + GITHUB_PRIVATE_KEY_SECRET_ARN: this.secrets.githubPrivateKey.secretArn, + WEBHOOK_URL: this.webhook.url, + WEBHOOK_HANDLER_ARN: this.webhook.handler.latestVersion.functionArn, + STEP_FUNCTION_ARN: this.orchestrator.stateMachineArn, + STEP_FUNCTION_LOG_GROUP: this.stateMachineLogGroup?.logGroupName ?? '', + ...this.extraLambdaEnv, + }, + timeout: cdk.Duration.minutes(5), + logGroup: singletonLogGroup(this, SingletonLogType.SETUP), + loggingFormat: lambda.LoggingFormat.JSON, + ...this.extraLambdaProps, + }, + ); + + const providers = this.providers.flatMap(provider => { + const status = provider.status(troubleshootFn); + return Array.isArray(status) ? status : [status]; + }); + + const stack = cdk.Stack.of(this); + const f = (troubleshootFn.node.defaultChild as lambda.CfnFunction); + f.addPropertyOverride('Environment.Variables.LOGICAL_ID', f.logicalId); + f.addPropertyOverride('Environment.Variables.STACK_NAME', stack.stackName); + f.addMetadata('providers', providers); + troubleshootFn.addToRolePolicy(new iam.PolicyStatement({ + actions: ['cloudformation:DescribeStackResource'], + resources: [stack.stackId], + })); + + this.secrets.webhook.grantRead(troubleshootFn); + this.secrets.github.grantRead(troubleshootFn); + this.secrets.githubPrivateKey.grantRead(troubleshootFn); + + // SFN permissions beyond grantRead: GetExecutionHistory + this.orchestrator.grantRead(troubleshootFn); + troubleshootFn.addToRolePolicy(new iam.PolicyStatement({ + actions: ['states:GetExecutionHistory'], + resources: [`${this.orchestrator.stateMachineArn}:*`, this.orchestrator.stateMachineArn], + })); + + // CloudWatch Logs permissions for webhook handler log analysis + troubleshootFn.addToRolePolicy(new iam.PolicyStatement({ + actions: ['logs:FilterLogEvents'], + resources: [this.webhook.handler.logGroup.logGroupArn], + })); + + new cdk.CfnOutput( + this, + 'troubleshoot command', + { + value: `aws --region ${stack.region} lambda invoke --function-name ${troubleshootFn.functionName} troubleshoot.json`, + }, + ); + + const access = this.props?.troubleshootAccess ?? LambdaAccess.noAccess(); + const url = access.bind(this, 'troubleshoot access', troubleshootFn); + + if (url !== '') { + new cdk.CfnOutput( + this, + 'troubleshoot url', + { + value: url, + }, + ); + } + } + private setupFunction(): string { const setupFunction = new SetupFunction( this, diff --git a/src/status.lambda.ts b/src/status.lambda.ts index 7be2adfc..e592b3cf 100644 --- a/src/status.lambda.ts +++ b/src/status.lambda.ts @@ -1,87 +1,11 @@ -import { CloudFormationClient, DescribeStackResourceCommand } from '@aws-sdk/client-cloudformation'; -import { DescribeLaunchTemplateVersionsCommand, EC2Client } from '@aws-sdk/client-ec2'; -import { ECRClient, DescribeImagesCommand } from '@aws-sdk/client-ecr'; import { DescribeExecutionCommand, ListExecutionsCommand, SFNClient } from '@aws-sdk/client-sfn'; import * as AWSLambda from 'aws-lambda'; import { baseUrlFromDomain, GitHubSecrets, loadOctokitAuthApp, loadOctokitCore } from './lambda-github'; import { getSecretJsonValue, getSecretValue } from './lambda-helpers'; +import { generateProvidersStatus, lambdaArnToLogGroup, lambdaArnToUrl, secretArnToUrl, stepFunctionArnToUrl } from './troubleshoot-helpers'; -const cfn = new CloudFormationClient(); -const ec2 = new EC2Client(); -const ecr = new ECRClient(); const sf = new SFNClient(); -function secretArnToUrl(arn: string) { - const parts = arn.split(':'); // arn:aws:secretsmanager:us-east-1:12345678:secret:secret-name-REVISION - const region = parts[3]; - const fullName = parts[6]; - const name = fullName.slice(0, fullName.lastIndexOf('-')); - - return `https://${region}.console.aws.amazon.com/secretsmanager/home?region=${region}#!/secret?name=${name}`; -} - -function lambdaArnToUrl(arn: string) { - const parts = arn.split(':'); // arn:aws:lambda:us-east-1:12345678:function:name-XYZ - const region = parts[3]; - const name = parts[6]; - - return `https://${region}.console.aws.amazon.com/lambda/home?region=${region}#/functions/${name}?tab=monitoring`; -} - -function lambdaArnToLogGroup(arn: string) { - const parts = arn.split(':'); // arn:aws:lambda:us-east-1:12345678:function:name-XYZ - const name = parts[6]; - - return `/aws/lambda/${name}`; -} - -function stepFunctionArnToUrl(arn: string) { - const parts = arn.split(':'); // arn:aws:states:us-east-1:12345678:stateMachine:name-XYZ - const region = parts[3]; - - return `https://${region}.console.aws.amazon.com/states/home?region=${region}#/statemachines/view/${arn}`; -} - -async function generateProvidersStatus(stack: string, logicalId: string) { - const resource = await cfn.send(new DescribeStackResourceCommand({ StackName: stack, LogicalResourceId: logicalId })); - const providers = JSON.parse(resource.StackResourceDetail?.Metadata ?? '{}').providers as any[] | undefined; - - if (!providers) { - return {}; - } - - return Promise.all(providers.map(async (p) => { - // add ECR data, if image is from ECR - if (p.image?.imageRepository?.match(/[0-9]+\.dkr\.ecr\.[a-z0-9\-]+\.amazonaws\.com\/.+/)) { - const tags = await ecr.send(new DescribeImagesCommand({ - repositoryName: p.image.imageRepository.split('/')[1], - filter: { - tagStatus: 'TAGGED', - }, - maxResults: 1, - })); - if (tags.imageDetails && tags.imageDetails?.length >= 1) { - p.image.latestImage = { - tags: tags.imageDetails[0].imageTags, - digest: tags.imageDetails[0].imageDigest, - date: tags.imageDetails[0].imagePushedAt, - }; - } - } - // add AMI data, if image is AMI - if (p.ami?.launchTemplate) { - const versions = await ec2.send(new DescribeLaunchTemplateVersionsCommand({ - LaunchTemplateId: p.ami.launchTemplate, - Versions: ['$Default'], - })); - if (versions.LaunchTemplateVersions && versions.LaunchTemplateVersions.length >= 1) { - p.ami.latestAmi = versions.LaunchTemplateVersions[0].LaunchTemplateData?.ImageId; - } - } - return p; - })); -} - interface AppInstallation { readonly id: number; readonly url: string; diff --git a/src/troubleshoot-function.ts b/src/troubleshoot-function.ts new file mode 100644 index 00000000..d2d93f8e --- /dev/null +++ b/src/troubleshoot-function.ts @@ -0,0 +1,32 @@ +// ~~ Generated by projen. To modify, edit .projenrc.js and run "npx projen". +import * as path from 'path'; +import * as lambda from 'aws-cdk-lib/aws-lambda'; +import { determineLatestNodeRuntime } from 'aws-cdk-lib/aws-lambda'; +import { Construct } from 'constructs'; + +/** + * Props for TroubleshootFunction + */ +export interface TroubleshootFunctionProps extends lambda.FunctionOptions { + /** + * The Lambda runtime to use. + * @default - Latest Node.js runtime available in the deployment region + */ + readonly runtime?: lambda.Runtime; +} + +/** + * An AWS Lambda function which executes src/troubleshoot. + */ +export class TroubleshootFunction extends lambda.Function { + constructor(scope: Construct, id: string, props?: TroubleshootFunctionProps) { + super(scope, id, { + description: 'src/troubleshoot.lambda.ts', + ...props, + runtime: props?.runtime ?? determineLatestNodeRuntime(scope), + handler: 'index.handler', + code: lambda.Code.fromAsset(path.join(__dirname, '../assets/troubleshoot.lambda')), + }); + this.addEnvironment('AWS_NODEJS_CONNECTION_REUSE_ENABLED', '1', { removeInEdge: true }); + } +} \ No newline at end of file diff --git a/src/troubleshoot-helpers.ts b/src/troubleshoot-helpers.ts new file mode 100644 index 00000000..4938df9c --- /dev/null +++ b/src/troubleshoot-helpers.ts @@ -0,0 +1,98 @@ +import { CloudFormationClient, DescribeStackResourceCommand } from '@aws-sdk/client-cloudformation'; +import { DescribeLaunchTemplateVersionsCommand, EC2Client } from '@aws-sdk/client-ec2'; +import { ECRClient, DescribeImagesCommand } from '@aws-sdk/client-ecr'; + +const cfn = new CloudFormationClient(); +const ec2 = new EC2Client(); +const ecr = new ECRClient(); + +export function secretArnToUrl(arn: string) { + const parts = arn.split(':'); + const region = parts[3]; + const fullName = parts[6]; + const name = fullName.slice(0, fullName.lastIndexOf('-')); + + return `https://${region}.console.aws.amazon.com/secretsmanager/home?region=${region}#!/secret?name=${name}`; +} + +export function lambdaArnToUrl(arn: string) { + const parts = arn.split(':'); + const region = parts[3]; + const name = parts[6]; + + return `https://${region}.console.aws.amazon.com/lambda/home?region=${region}#/functions/${name}?tab=monitoring`; +} + +export function lambdaArnToLogGroup(arn: string) { + const parts = arn.split(':'); + const name = parts[6]; + + return `/aws/lambda/${name}`; +} + +export function stepFunctionArnToUrl(arn: string) { + const parts = arn.split(':'); + const region = parts[3]; + + return `https://${region}.console.aws.amazon.com/states/home?region=${region}#/statemachines/view/${arn}`; +} + +export function executionArnToUrl(arn: string) { + const parts = arn.split(':'); + const region = parts[3]; + + return `https://${region}.console.aws.amazon.com/states/home?region=${region}#/v2/executions/details/${arn}`; +} + +export function logGroupUrl(region: string, logGroupName: string) { + const encoded = encodeURIComponent(encodeURIComponent(logGroupName)).replace(/%/g, '$'); + return `https://${region}.console.aws.amazon.com/cloudwatch/home?region=${region}#logsV2:log-groups/log-group/${encoded}`; +} + +export function logStreamUrl(region: string, logGroupName: string, logStreamName: string) { + const encodedGroup = encodeURIComponent(encodeURIComponent(logGroupName)).replace(/%/g, '$'); + const encodedStream = encodeURIComponent(encodeURIComponent(logStreamName)).replace(/%/g, '$'); + return `https://${region}.console.aws.amazon.com/cloudwatch/home?region=${region}#logsV2:log-groups/log-group/${encodedGroup}/log-events/${encodedStream}`; +} + +export function regionFromArn(arn: string) { + return arn.split(':')[3]; +} + +export async function generateProvidersStatus(stack: string, logicalId: string) { + const resource = await cfn.send(new DescribeStackResourceCommand({ StackName: stack, LogicalResourceId: logicalId })); + const providers = JSON.parse(resource.StackResourceDetail?.Metadata ?? '{}').providers as any[] | undefined; + + if (!providers) { + return {}; + } + + return Promise.all(providers.map(async (p) => { + if (p.image?.imageRepository?.match(/[0-9]+\.dkr\.ecr\.[a-z0-9\-]+\.amazonaws\.com\/.+/)) { + const tags = await ecr.send(new DescribeImagesCommand({ + repositoryName: p.image.imageRepository.split('/')[1], + filter: { + tagStatus: 'TAGGED', + }, + maxResults: 1, + })); + if (tags.imageDetails && tags.imageDetails?.length >= 1) { + p.image.latestImage = { + tags: tags.imageDetails[0].imageTags, + digest: tags.imageDetails[0].imageDigest, + date: tags.imageDetails[0].imagePushedAt, + }; + } + } + if (p.ami?.launchTemplate) { + const versions = await ec2.send(new DescribeLaunchTemplateVersionsCommand({ + LaunchTemplateId: p.ami.launchTemplate, + Versions: ['$Default'], + })); + if (versions.LaunchTemplateVersions && versions.LaunchTemplateVersions.length >= 1) { + p.ami.latestAmi = versions.LaunchTemplateVersions[0].LaunchTemplateData?.ImageId; + } + } + return p; + })); +} diff --git a/src/troubleshoot.lambda.ts b/src/troubleshoot.lambda.ts new file mode 100644 index 00000000..679ed58a --- /dev/null +++ b/src/troubleshoot.lambda.ts @@ -0,0 +1,795 @@ +import { CloudWatchLogsClient, FilterLogEventsCommand } from '@aws-sdk/client-cloudwatch-logs'; +import { + DescribeExecutionCommand, + ExecutionStatus, + GetExecutionHistoryCommand, + HistoryEventType, + ListExecutionsCommand, + SFNClient, +} from '@aws-sdk/client-sfn'; +import * as AWSLambda from 'aws-lambda'; +import { getAppOctokit, getOctokit } from './lambda-github'; +import { getSecretJsonValue, getSecretValue } from './lambda-helpers'; +import { + executionArnToUrl, + generateProvidersStatus, + lambdaArnToLogGroup, + logGroupUrl, + logStreamUrl, + regionFromArn, + secretArnToUrl, + stepFunctionArnToUrl, +} from './troubleshoot-helpers'; + +const sf = new SFNClient(); +const cwl = new CloudWatchLogsClient(); + +const STALE_IMAGE_DAYS = 14; +const STUCK_EXECUTION_HOURS = 2; +const LOG_LOOKBACK_MS = 24 * 60 * 60 * 1000; +const MAX_EXECUTION_PAGES = 10; + +// ---- types ---- + +interface Issue { + severity: 'critical' | 'warning' | 'info'; + category: string; + title: string; + description: string; + suggestion: string; + executionArn?: string; + links: Record; +} + +interface TroubleshootReport { + config: { + auth: 'ok' | string; + webhook: 'ok' | string; + secrets: 'ok' | string; + }; + summary: { + executions: { total: number; succeeded: number; failed: number; aborted: number; timedOut: number; running: number }; + health: 'healthy' | 'warning' | 'critical'; + analysisWindow: string; + }; + issues: Issue[]; + deepLinks: { + stepFunction: string; + webhookHandlerLogs: string; + orchestratorLogs: string; + providerLogs: Record; + }; +} + +interface ExecutionInfo { + executionArn: string; + name: string; + status: string; + startDate?: Date; + stopDate?: Date; + input?: any; + error?: string; + cause?: string; +} + +// ---- parallel batch helper ---- + +async function parallelBatch(items: T[], batchSize: number, fn: (item: T) => Promise): Promise { + const results: R[] = []; + for (let i = 0; i < items.length; i += batchSize) { + const batch = items.slice(i, i + batchSize); + results.push(...await Promise.all(batch.map(fn))); + } + return results; +} + +// ---- data fetchers ---- + +async function fetchAllExecutions(stateMachineArn: string): Promise { + const executions: ExecutionInfo[] = []; + let nextToken: string | undefined; + + for (let page = 0; page < MAX_EXECUTION_PAGES; page++) { + const result = await sf.send(new ListExecutionsCommand({ + stateMachineArn, + maxResults: 100, + nextToken, + })); + + for (const ex of result.executions ?? []) { + executions.push({ + executionArn: ex.executionArn!, + name: ex.name!, + status: ex.status ?? 'UNKNOWN', + startDate: ex.startDate, + stopDate: ex.stopDate, + }); + } + + nextToken = result.nextToken; + if (!nextToken) break; + } + + return executions; +} + +async function enrichExecution(ex: ExecutionInfo): Promise { + try { + const detail = await sf.send(new DescribeExecutionCommand({ executionArn: ex.executionArn })); + ex.input = JSON.parse(detail.input || '{}'); + ex.error = detail.error; + ex.cause = detail.cause; + } catch (e) { + ex.error = `DescribeExecution failed: ${e}`; + } + return ex; +} + +interface HistoryAnalysis { + failedStateName?: string; + failedStateError?: string; + failedStateCause?: string; + hasCapacityError: boolean; + hasThrottleRetry: boolean; + providerTaskOutput?: any; +} + +const CAPACITY_PATTERNS = [ + 'InsufficientInstanceCapacity', + 'SpotMaxPriceTooLow', + 'CapacityError', + 'Insufficient capacity', + 'ResourceInitializationError', + 'CannotPullContainerError', +]; + +const THROTTLE_PATTERNS = [ + 'Request limit exceeded', + 'Rate exceeded', + 'Throttling', + 'TooManyRequestsException', +]; + +async function analyzeHistory(executionArn: string): Promise { + const result: HistoryAnalysis = { + hasCapacityError: false, + hasThrottleRetry: false, + }; + + try { + let nextToken: string | undefined; + const events = []; + + // paginate through all history events + do { + const resp = await sf.send(new GetExecutionHistoryCommand({ + executionArn, + reverseOrder: true, + maxResults: 100, + nextToken, + })); + events.push(...(resp.events ?? [])); + nextToken = resp.nextToken; + } while (nextToken); + + for (const event of events) { + const type = event.type; + + if (type === HistoryEventType.TaskFailed || type === HistoryEventType.ExecutionFailed) { + const details = event.taskFailedEventDetails ?? event.executionFailedEventDetails; + if (details) { + if (!result.failedStateName) { + result.failedStateError = details.error; + result.failedStateCause = details.cause; + } + + const text = `${details.error ?? ''} ${details.cause ?? ''}`; + if (CAPACITY_PATTERNS.some(p => text.includes(p))) { + result.hasCapacityError = true; + } + if (THROTTLE_PATTERNS.some(p => text.includes(p))) { + result.hasThrottleRetry = true; + } + } + } + + if (type === HistoryEventType.TaskStateEntered) { + const details = event.stateEnteredEventDetails; + if (details && !result.failedStateName) { + result.failedStateName = details.name; + } + } + + if (type === HistoryEventType.TaskSucceeded) { + const details = event.taskSucceededEventDetails; + if (details?.output && !result.providerTaskOutput) { + try { + result.providerTaskOutput = JSON.parse(details.output); + } catch { /* ignore parse errors */ } + } + } + + if (type === HistoryEventType.TaskSubmitFailed || type === HistoryEventType.TaskTimedOut) { + const details = event.taskSubmitFailedEventDetails ?? event.taskTimedOutEventDetails; + if (details) { + const text = `${(details as any).error ?? ''} ${(details as any).cause ?? ''}`; + if (THROTTLE_PATTERNS.some(p => text.includes(p))) { + result.hasThrottleRetry = true; + } + } + } + } + } catch (e) { + result.failedStateError = `GetExecutionHistory failed: ${e}`; + } + + return result; +} + +async function fetchLogMatches(logGroupName: string, filterPattern: string, lookbackMs: number): Promise { + try { + let count = 0; + let nextToken: string | undefined; + const startTime = Date.now() - lookbackMs; + + do { + const resp = await cwl.send(new FilterLogEventsCommand({ + logGroupName, + filterPattern, + startTime, + nextToken, + limit: 100, + })); + count += resp.events?.length ?? 0; + nextToken = resp.nextToken; + } while (nextToken); + + return count; + } catch { + return -1; // signal that log query failed + } +} + +async function fetchInstallationRepos(appOctokit: any): Promise> { + const repos = new Set(); + + try { + const installations = (await appOctokit.request('GET /app/installations')).data; + for (const installation of installations) { + try { + const token = (await appOctokit.auth({ + type: 'installation', + installationId: installation.id, + }) as any).token; + + const { Octokit } = await import('@octokit/rest'); + const installOctokit = new Octokit({ auth: token }); + + let page = 1; + while (true) { + const resp = await installOctokit.request('GET /installation/repositories', { + per_page: 100, + page, + }); + for (const repo of resp.data.repositories) { + repos.add((repo.full_name as string).toLowerCase()); + } + if (resp.data.repositories.length < 100) break; + page++; + } + } catch { /* skip installations we can't auth to */ } + } + } catch { /* skip if can't list installations */ } + + return repos; +} + +async function fetchWebhookDeliveryHealth(appOctokit: any): Promise<{ total: number; failed: number }> { + try { + const resp = await appOctokit.request('GET /app/hook/deliveries', { per_page: 100 }); + const deliveries = resp.data as any[]; + const total = deliveries.length; + const failed = deliveries.filter((d: any) => d.status === 'failed' || (d.status_code && d.status_code >= 400)).length; + return { total, failed }; + } catch { + return { total: -1, failed: -1 }; + } +} + +// ---- detection functions ---- + +function checkConfigHealth(configResult: { auth: string; webhook: string; secrets: string }): Issue[] { + const issues: Issue[] = []; + + if (configResult.secrets !== 'ok') { + issues.push({ + severity: 'critical', + category: 'config', + title: 'Secret read failure', + description: configResult.secrets, + suggestion: 'Verify the secret exists and the troubleshoot Lambda has secretsmanager:GetSecretValue permission.', + links: {}, + }); + } + + if (configResult.auth !== 'ok') { + issues.push({ + severity: 'critical', + category: 'config', + title: 'GitHub authentication failure', + description: configResult.auth, + suggestion: 'Check the GitHub secret value. For GitHub App, verify the app ID and private key. For PAT, verify the token is valid.', + links: { + githubSecret: secretArnToUrl(process.env.GITHUB_SECRET_ARN!), + }, + }); + } + + if (configResult.webhook !== 'ok') { + issues.push({ + severity: 'critical', + category: 'config', + title: 'Webhook URL mismatch', + description: configResult.webhook, + suggestion: 'Update the webhook URL in your GitHub App settings to match the expected URL.', + links: {}, + }); + } + + return issues; +} + +function detectStolenRunners(executions: ExecutionInfo[], installationRepos: Set): Issue[] { + if (installationRepos.size === 0) return []; + + const issues: Issue[] = []; + for (const ex of executions) { + if (!ex.input?.owner || !ex.input?.repo) continue; + const fullName = `${ex.input.owner}/${ex.input.repo}`.toLowerCase(); + if (!installationRepos.has(fullName)) { + issues.push({ + severity: 'critical', + category: 'stolen-runner', + title: `Runner provisioned for uninstalled repo: ${fullName}`, + description: `Execution ${ex.name} provisioned a runner for ${fullName}, but the GitHub App is not installed on that repository.`, + suggestion: `Install the GitHub App on ${fullName}, or investigate why this repo triggered a runner.`, + executionArn: ex.executionArn, + links: { + execution: executionArnToUrl(ex.executionArn), + }, + }); + } + } + return issues; +} + +function detectCapacityFailures(executions: ExecutionInfo[], histories: Map): Issue[] { + const issues: Issue[] = []; + for (const ex of executions) { + if (ex.status !== ExecutionStatus.FAILED) continue; + const history = histories.get(ex.executionArn); + if (!history?.hasCapacityError) continue; + + issues.push({ + severity: 'critical', + category: 'spot-interruption', + title: `Capacity failure: ${ex.name}`, + description: `Execution failed due to capacity issues at state "${history.failedStateName}": ${history.failedStateCause?.slice(0, 200) ?? 'unknown'}`, + suggestion: 'Consider adding fallback providers, using on-demand instances, or increasing spot price.', + executionArn: ex.executionArn, + links: { + execution: executionArnToUrl(ex.executionArn), + }, + }); + } + return issues; +} + +function detectFailedStarts(executions: ExecutionInfo[], histories: Map): Issue[] { + const issues: Issue[] = []; + const tokenStateNames = ['Get Runner Token', 'GetRunnerToken']; + + for (const ex of executions) { + if (ex.status !== ExecutionStatus.FAILED) continue; + const history = histories.get(ex.executionArn); + if (!history) continue; + if (history.hasCapacityError) continue; // already reported as capacity failure + + const failedAt = history.failedStateName ?? 'unknown'; + const isTokenFailure = tokenStateNames.some(n => failedAt.includes(n)); + const isEarlyFailure = failedAt.includes('Error Handler') || failedAt.includes('Run Providers'); + + if (isTokenFailure || isEarlyFailure) { + issues.push({ + severity: 'critical', + category: 'failed-start', + title: `Runner failed to start: ${ex.name}`, + description: `Execution failed at "${failedAt}": ${history.failedStateError ?? ''} ${history.failedStateCause?.slice(0, 200) ?? ''}`.trim(), + suggestion: isTokenFailure + ? 'Check GitHub App permissions and runner registration token generation.' + : 'Check provider logs for infrastructure startup failures.', + executionArn: ex.executionArn, + links: { + execution: executionArnToUrl(ex.executionArn), + }, + }); + } + } + return issues; +} + +function detectIdleReaps(executions: ExecutionInfo[]): Issue[] { + const issues: Issue[] = []; + const idleReaps = executions.filter(ex => ex.status === ExecutionStatus.ABORTED && ex.error === 'IdleRunner'); + const total = executions.length; + + if (idleReaps.length === 0) return []; + + // group by repo + const byRepo = new Map(); + for (const ex of idleReaps) { + const key = ex.input ? `${ex.input.owner}/${ex.input.repo}` : 'unknown'; + byRepo.set(key, (byRepo.get(key) ?? 0) + 1); + } + + const topRepos = [...byRepo.entries()].sort((a, b) => b[1] - a[1]).slice(0, 5); + const repoSummary = topRepos.map(([repo, count]) => `${repo} (${count})`).join(', '); + + issues.push({ + severity: idleReaps.length > total * 0.3 ? 'warning' : 'info', + category: 'idle-reap', + title: `${idleReaps.length} of ${total} executions were idle-reaped`, + description: `Runners were started but never picked up a job. Top repos: ${repoSummary}`, + suggestion: idleReaps.length > total * 0.3 + ? 'If many idle reaps come from the same repo, the workflow likely uses max-parallel. Consider increasing idleTimeout or using a provider selector.' + : 'A small number of idle reaps is normal.', + links: {}, + }); + + return issues; +} + +function detectStuckExecutions(executions: ExecutionInfo[]): Issue[] { + const issues: Issue[] = []; + const threshold = STUCK_EXECUTION_HOURS * 60 * 60 * 1000; + const now = Date.now(); + + for (const ex of executions) { + if (ex.status !== ExecutionStatus.RUNNING) continue; + if (!ex.startDate) continue; + + const runningMs = now - ex.startDate.getTime(); + if (runningMs > threshold) { + const hours = Math.floor(runningMs / (60 * 60 * 1000)); + issues.push({ + severity: 'warning', + category: 'stuck-execution', + title: `Execution running for ${hours}h: ${ex.name}`, + description: `Execution has been running for ${hours} hours, which exceeds the ${STUCK_EXECUTION_HOURS}h threshold.`, + suggestion: 'Check if the runner is responsive via SSM. If the runner is OOMing, consider a larger instance type. You may need to manually stop this execution.', + executionArn: ex.executionArn, + links: { + execution: executionArnToUrl(ex.executionArn), + }, + }); + } + } + return issues; +} + +function detectExecutionNameCollisions(collisionCount: number, region: string, webhookLogGroup: string): Issue[] { + if (collisionCount <= 0) return []; + return [{ + severity: 'warning', + category: 'name-collision', + title: `${collisionCount} execution name collision(s) in last 24h`, + description: 'StartExecution returned ExecutionAlreadyExists. This causes 502 responses to GitHub and runners that never start.', + suggestion: 'This usually means repo names are too long for the execution name limit. Upgrade to the latest version which includes truncation fixes.', + links: { + webhookHandlerLogs: logGroupUrl(region, webhookLogGroup), + }, + }]; +} + +function detectWebhookFailures(health: { total: number; failed: number }, region: string, webhookLogGroup: string): Issue[] { + if (health.total <= 0) return []; + if (health.failed === 0) return []; + + const rate = Math.round((health.failed / health.total) * 100); + return [{ + severity: rate > 10 ? 'warning' : 'info', + category: 'webhook-delivery', + title: `${health.failed}/${health.total} webhook deliveries failed (${rate}%)`, + description: `${health.failed} of the last ${health.total} webhook deliveries from GitHub failed.`, + suggestion: 'Check the webhook handler Lambda logs for errors. Verify the webhook URL and secret are configured correctly.', + links: { + webhookHandlerLogs: logGroupUrl(region, webhookLogGroup), + }, + }]; +} + +function detectLabelMismatches(mismatchCount: number, region: string, webhookLogGroup: string): Issue[] { + if (mismatchCount <= 0) return []; + return [{ + severity: 'info', + category: 'label-mismatch', + title: `${mismatchCount} webhook(s) ignored due to label mismatch in last 24h`, + description: 'Webhooks were received but no provider matched the requested labels.', + suggestion: 'Check that your workflow runs-on labels match your provider labels. Common mistake: using runs-on: [self-hosted, linux, codebuild] when the provider only has [self-hosted, codebuild].', + links: { + webhookHandlerLogs: logGroupUrl(region, webhookLogGroup), + }, + }]; +} + +function detectStaleImages(providers: any[]): Issue[] { + const issues: Issue[] = []; + const threshold = STALE_IMAGE_DAYS * 24 * 60 * 60 * 1000; + const now = Date.now(); + + if (!Array.isArray(providers)) return []; + + for (const p of providers) { + const name = p.constructPath ?? p.type ?? 'unknown'; + + if (p.image?.latestImage?.date) { + const age = now - new Date(p.image.latestImage.date).getTime(); + if (age > threshold) { + const days = Math.floor(age / (24 * 60 * 60 * 1000)); + issues.push({ + severity: 'warning', + category: 'stale-image', + title: `Container image for ${name} is ${days} days old`, + description: `The latest container image was pushed ${days} days ago.`, + suggestion: 'Rebuild runner images to get the latest security patches and runner version.', + links: {}, + }); + } + } + + if (p.ami?.latestAmi && p.ami?.launchTemplate) { + // AMI age not directly available from DescribeLaunchTemplateVersions, but we flag if no recent image + // This is a heuristic; the image builder should run periodically + } + } + return issues; +} + +function detectThrottling(executions: ExecutionInfo[], histories: Map): Issue[] { + let throttledCount = 0; + for (const ex of executions) { + const history = histories.get(ex.executionArn); + if (history?.hasThrottleRetry) throttledCount++; + } + + if (throttledCount === 0) return []; + return [{ + severity: 'info', + category: 'ec2-throttling', + title: `EC2 API throttling detected in ${throttledCount} execution(s)`, + description: 'RunInstances or other EC2 API calls were throttled during burst starts. The system retried automatically.', + suggestion: 'Consider staggering job starts or using CodeBuild/Fargate providers for burst workloads.', + links: {}, + }]; +} + +function buildExecutionSummary(executions: ExecutionInfo[]): TroubleshootReport['summary'] { + const counts = { total: executions.length, succeeded: 0, failed: 0, aborted: 0, timedOut: 0, running: 0 }; + for (const ex of executions) { + switch (ex.status) { + case ExecutionStatus.SUCCEEDED: counts.succeeded++; break; + case ExecutionStatus.FAILED: counts.failed++; break; + case ExecutionStatus.ABORTED: counts.aborted++; break; + case ExecutionStatus.TIMED_OUT: counts.timedOut++; break; + case ExecutionStatus.RUNNING: counts.running++; break; + } + } + + let health: 'healthy' | 'warning' | 'critical' = 'healthy'; + if (counts.failed > 0 || counts.timedOut > 0) health = 'critical'; + else if (counts.aborted > counts.total * 0.3) health = 'warning'; + + return { + executions: counts, + health, + analysisWindow: `last ${counts.total} executions`, + }; +} + +// ---- config checking ---- + +async function checkConfig(): Promise<{ auth: string; webhook: string; secrets: string }> { + const result = { auth: 'ok', webhook: 'ok', secrets: 'ok' }; + + // check secrets are readable + try { + await getSecretJsonValue(process.env.GITHUB_SECRET_ARN); + } catch (e) { + result.secrets = `Cannot read GitHub secret: ${e}`; + return result; + } + + try { + await getSecretValue(process.env.GITHUB_PRIVATE_KEY_SECRET_ARN); + } catch (e) { + result.secrets = `Cannot read private key secret: ${e}`; + return result; + } + + try { + await getSecretValue(process.env.WEBHOOK_SECRET_ARN); + } catch (e) { + result.secrets = `Cannot read webhook secret: ${e}`; + return result; + } + + // check GitHub auth + try { + const appOctokit = await getAppOctokit(); + if (appOctokit) { + await appOctokit.request('GET /app'); + result.auth = 'ok'; + + // check webhook URL + try { + const hookConfig = await appOctokit.request('GET /app/hook/config', {}); + if (hookConfig.data.url !== process.env.WEBHOOK_URL) { + result.webhook = `GitHub App webhook URL (${hookConfig.data.url}) does not match expected (${process.env.WEBHOOK_URL})`; + } + } catch (e) { + result.webhook = `Cannot check webhook config: ${e}`; + } + } else { + // PAT auth -- try getOctokit which validates the token + const { octokit } = await getOctokit(); + await octokit.request('GET /user'); + result.auth = 'ok'; + result.webhook = 'Cannot verify automatically with PAT auth'; + } + } catch (e) { + result.auth = `GitHub authentication failed: ${e}`; + } + + return result; +} + +// ---- deep links ---- + +function buildDeepLinks(region: string, webhookHandlerArn: string, sfnArn: string, sfnLogGroup: string, providers: any[]): TroubleshootReport['deepLinks'] { + const webhookLogGroup = lambdaArnToLogGroup(webhookHandlerArn); + const providerLogs: Record = {}; + + if (Array.isArray(providers)) { + for (const p of providers) { + const name = p.constructPath ?? p.type ?? 'unknown'; + if (p.logGroup) { + providerLogs[name] = logGroupUrl(region, p.logGroup); + } + } + } + + return { + stepFunction: stepFunctionArnToUrl(sfnArn), + webhookHandlerLogs: logGroupUrl(region, webhookLogGroup), + orchestratorLogs: sfnLogGroup ? logGroupUrl(region, sfnLogGroup) : '', + providerLogs, + }; +} + +function addProviderLogLinks(issues: Issue[], executions: ExecutionInfo[], providers: any[], region: string) { + for (const issue of issues) { + if (!issue.executionArn) continue; + const ex = executions.find(e => e.executionArn === issue.executionArn); + if (!ex) continue; + + // find the provider from execution input + const providerPath = ex.input?.provider; + if (!providerPath || !Array.isArray(providers)) continue; + + const provider = providers.find((p: any) => p.constructPath === providerPath); + if (!provider?.logGroup) continue; + + issue.links.providerLogGroup = logGroupUrl(region, provider.logGroup); + + // for EC2, the log stream name is the execution name (runner name) + if (provider.type === 'ec2' || provider.constructPath?.toLowerCase().includes('ec2')) { + issue.links.providerLogStream = logStreamUrl(region, provider.logGroup, ex.name); + } + } +} + +// ---- main handler ---- + +function safeReturnValue(event: Partial, report: any) { + if (event.path) { + return { + statusCode: 200, + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(report), + }; + } + return report; +} + +export async function handler(event: Partial) { + if (!process.env.GITHUB_SECRET_ARN || !process.env.GITHUB_PRIVATE_KEY_SECRET_ARN || !process.env.LOGICAL_ID || + !process.env.WEBHOOK_HANDLER_ARN || !process.env.STEP_FUNCTION_ARN || !process.env.WEBHOOK_SECRET_ARN || + !process.env.STACK_NAME) { + throw new Error('Missing environment variables'); + } + + const region = regionFromArn(process.env.STEP_FUNCTION_ARN); + const webhookLogGroup = lambdaArnToLogGroup(process.env.WEBHOOK_HANDLER_ARN); + + // -- Phase 0: config check (short-circuit on failure) -- + const configResult = await checkConfig(); + const configIssues = checkConfigHealth(configResult); + const configBroken = configIssues.some(i => i.severity === 'critical'); + + if (configBroken) { + const report: TroubleshootReport = { + config: configResult as any, + summary: { executions: { total: 0, succeeded: 0, failed: 0, aborted: 0, timedOut: 0, running: 0 }, health: 'critical', analysisWindow: 'none (config broken)' }, + issues: configIssues, + deepLinks: buildDeepLinks(region, process.env.WEBHOOK_HANDLER_ARN, process.env.STEP_FUNCTION_ARN, process.env.STEP_FUNCTION_LOG_GROUP ?? '', []), + }; + return safeReturnValue(event, report); + } + + // -- Phase 1: parallel data fetch -- + const appOctokit = await getAppOctokit(); + + const [executions, installationRepos, providers, collisionCount, mismatchCount, webhookHealth] = await Promise.all([ + fetchAllExecutions(process.env.STEP_FUNCTION_ARN), + appOctokit ? fetchInstallationRepos(appOctokit) : Promise.resolve(new Set()), + generateProvidersStatus(process.env.STACK_NAME, process.env.LOGICAL_ID).then(r => Array.isArray(r) ? r : []), + fetchLogMatches(webhookLogGroup, 'ExecutionAlreadyExists', LOG_LOOKBACK_MS), + fetchLogMatches(webhookLogGroup, 'Ignoring labels', LOG_LOOKBACK_MS), + appOctokit ? fetchWebhookDeliveryHealth(appOctokit) : Promise.resolve({ total: -1, failed: -1 }), + ]); + + // -- Phase 2: enrich executions -- + const enriched = await parallelBatch(executions, 10, enrichExecution); + + // -- Phase 3: get execution history for non-succeeded -- + const nonSucceeded = enriched.filter(ex => ex.status !== ExecutionStatus.SUCCEEDED && ex.status !== ExecutionStatus.RUNNING); + const historyResults = await parallelBatch(nonSucceeded, 10, async (ex) => { + const analysis = await analyzeHistory(ex.executionArn); + return { arn: ex.executionArn, analysis }; + }); + const histories = new Map(); + for (const { arn, analysis } of historyResults) { + histories.set(arn, analysis); + } + + // -- Phase 4: run all detections -- + const issues: Issue[] = [ + ...configIssues, + ...detectStolenRunners(enriched, installationRepos), + ...detectCapacityFailures(enriched, histories), + ...detectFailedStarts(enriched, histories), + ...detectIdleReaps(enriched), + ...detectStuckExecutions(enriched), + ...detectExecutionNameCollisions(collisionCount, region, webhookLogGroup), + ...detectWebhookFailures(webhookHealth, region, webhookLogGroup), + ...detectLabelMismatches(mismatchCount, region, webhookLogGroup), + ...detectStaleImages(providers), + ...detectThrottling(enriched, histories), + ]; + + // enrich issues with provider log links + addProviderLogLinks(issues, enriched, providers, region); + + // sort: critical first, then warning, then info + const severityOrder = { critical: 0, warning: 1, info: 2 }; + issues.sort((a, b) => severityOrder[a.severity] - severityOrder[b.severity]); + + const report: TroubleshootReport = { + config: configResult as any, + summary: buildExecutionSummary(enriched), + issues, + deepLinks: buildDeepLinks(region, process.env.WEBHOOK_HANDLER_ARN, process.env.STEP_FUNCTION_ARN, process.env.STEP_FUNCTION_LOG_GROUP ?? '', providers), + }; + + return safeReturnValue(event, report); +} diff --git a/test/default.integ.snapshot/github-runners-test.assets.json b/test/default.integ.snapshot/github-runners-test.assets.json index 5c5d5619..ef66a0d2 100644 --- a/test/default.integ.snapshot/github-runners-test.assets.json +++ b/test/default.integ.snapshot/github-runners-test.assets.json @@ -197,16 +197,30 @@ } } }, - "29d9261cc2661978b9a70c19a722a4f2b8fb9f7b3254cc0fba0ec8955c7afdd2": { + "470f7398690390f5a87b48833f57690f3e2c482b3818606ef30a3343f533894a": { "displayName": "runners/status/Code", "source": { - "path": "asset.29d9261cc2661978b9a70c19a722a4f2b8fb9f7b3254cc0fba0ec8955c7afdd2.lambda", + "path": "asset.470f7398690390f5a87b48833f57690f3e2c482b3818606ef30a3343f533894a.lambda", "packaging": "zip" }, "destinations": { - "current_account-current_region-761a4add": { + "current_account-current_region-519e49d3": { "bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}", - "objectKey": "29d9261cc2661978b9a70c19a722a4f2b8fb9f7b3254cc0fba0ec8955c7afdd2.zip", + "objectKey": "470f7398690390f5a87b48833f57690f3e2c482b3818606ef30a3343f533894a.zip", + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}" + } + } + }, + "c3b65ca124889c9832034dd0145ee97ac3d81059d28f432dc8fee5b132f340a1": { + "displayName": "runners/troubleshoot/Code", + "source": { + "path": "asset.c3b65ca124889c9832034dd0145ee97ac3d81059d28f432dc8fee5b132f340a1.lambda", + "packaging": "zip" + }, + "destinations": { + "current_account-current_region-f92a858b": { + "bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}", + "objectKey": "c3b65ca124889c9832034dd0145ee97ac3d81059d28f432dc8fee5b132f340a1.zip", "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}" } } @@ -225,16 +239,16 @@ } } }, - "a158a49a410d7399ce09fc80263af96f69dc3f9ce0089cc9d805c6f5be8c7641": { + "df4c3a549584d23ef64323457fd3452cd88d0df51616fe31a9894153a0ed3708": { "displayName": "github-runners-test Template", "source": { "path": "github-runners-test.template.json", "packaging": "file" }, "destinations": { - "current_account-current_region-d1d722ea": { + "current_account-current_region-01f0c914": { "bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}", - "objectKey": "a158a49a410d7399ce09fc80263af96f69dc3f9ce0089cc9d805c6f5be8c7641.json", + "objectKey": "df4c3a549584d23ef64323457fd3452cd88d0df51616fe31a9894153a0ed3708.json", "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}" } } diff --git a/test/default.integ.snapshot/github-runners-test.template.json b/test/default.integ.snapshot/github-runners-test.template.json index 13d37dac..5dbff5b4 100644 --- a/test/default.integ.snapshot/github-runners-test.template.json +++ b/test/default.integ.snapshot/github-runners-test.template.json @@ -18915,7 +18915,7 @@ "S3Bucket": { "Fn::Sub": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}" }, - "S3Key": "29d9261cc2661978b9a70c19a722a4f2b8fb9f7b3254cc0fba0ec8955c7afdd2.zip" + "S3Key": "470f7398690390f5a87b48833f57690f3e2c482b3818606ef30a3343f533894a.zip" }, "Description": "Provide user with status about self-hosted GitHub Actions runners", "Environment": { @@ -20720,6 +20720,2069 @@ ] } }, + "runnerstroubleshootServiceRoleEBD5244C": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ] + ] + } + ] + } + }, + "runnerstroubleshootServiceRoleDefaultPolicy11DB7B45": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "ecr:DescribeImages", + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "CodeBuildImageBuilderRepository9DE3B6F0", + "Arn" + ] + } + }, + { + "Action": "ecr:DescribeImages", + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "CodeBuildUbuntu2404ImageBuilderRepositoryFA75695E", + "Arn" + ] + } + }, + { + "Action": "ecr:DescribeImages", + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "CodeBuildImageBuilderarmRepositoryE967421B", + "Arn" + ] + } + }, + { + "Action": "ecr:DescribeImages", + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "WindowsImageBuilderRepositoryA4CBB6D8", + "Arn" + ] + } + }, + { + "Action": "ecr:DescribeImages", + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "LambdaImageBuilderx64Repository57F632F1", + "Arn" + ] + } + }, + { + "Action": "ecr:DescribeImages", + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "LambdaImageBuilderzRepository7C7AD146", + "Arn" + ] + } + }, + { + "Action": "ecr:DescribeImages", + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "FargatebuilderRepository8F7BA13C", + "Arn" + ] + } + }, + { + "Action": "ecr:DescribeImages", + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "FargatebuilderarmRepository77DCC132", + "Arn" + ] + } + }, + { + "Action": "ec2:DescribeLaunchTemplateVersions", + "Effect": "Allow", + "Resource": "*" + }, + { + "Action": "cloudformation:DescribeStackResource", + "Effect": "Allow", + "Resource": { + "Ref": "AWS::StackId" + } + }, + { + "Action": [ + "secretsmanager:GetSecretValue", + "secretsmanager:DescribeSecret" + ], + "Effect": "Allow", + "Resource": { + "Ref": "runnersSecretsWebhook7AF0D74E" + } + }, + { + "Action": [ + "secretsmanager:GetSecretValue", + "secretsmanager:DescribeSecret" + ], + "Effect": "Allow", + "Resource": { + "Ref": "runnersSecretsGitHubEFD96479" + } + }, + { + "Action": [ + "secretsmanager:GetSecretValue", + "secretsmanager:DescribeSecret" + ], + "Effect": "Allow", + "Resource": { + "Ref": "runnersSecretsGitHubPrivateKey79498F91" + } + }, + { + "Action": [ + "states:ListExecutions", + "states:ListStateMachines" + ], + "Effect": "Allow", + "Resource": { + "Ref": "runnersRunnerOrchestratorF9B66EBA" + } + }, + { + "Action": [ + "states:DescribeExecution", + "states:DescribeStateMachineForExecution", + "states:GetExecutionHistory" + ], + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":states:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":execution:", + { + "Fn::Select": [ + 6, + { + "Fn::Split": [ + ":", + { + "Ref": "runnersRunnerOrchestratorF9B66EBA" + } + ] + } + ] + }, + ":*" + ] + ] + } + }, + { + "Action": [ + "states:ListActivities", + "states:DescribeStateMachine", + "states:DescribeActivity" + ], + "Effect": "Allow", + "Resource": "*" + }, + { + "Action": "states:GetExecutionHistory", + "Effect": "Allow", + "Resource": [ + { + "Fn::Join": [ + "", + [ + { + "Ref": "runnersRunnerOrchestratorF9B66EBA" + }, + ":*" + ] + ] + }, + { + "Ref": "runnersRunnerOrchestratorF9B66EBA" + } + ] + }, + { + "Action": "logs:FilterLogEvents", + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "OrchestratorLogFB9610E7", + "Arn" + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "runnerstroubleshootServiceRoleDefaultPolicy11DB7B45", + "Roles": [ + { + "Ref": "runnerstroubleshootServiceRoleEBD5244C" + } + ] + } + }, + "runnerstroubleshoot44EEF479": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": { + "Fn::Sub": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}" + }, + "S3Key": "c3b65ca124889c9832034dd0145ee97ac3d81059d28f432dc8fee5b132f340a1.zip" + }, + "Description": "Diagnose issues with self-hosted GitHub Actions runners", + "Environment": { + "Variables": { + "WEBHOOK_SECRET_ARN": { + "Ref": "runnersSecretsWebhook7AF0D74E" + }, + "GITHUB_SECRET_ARN": { + "Ref": "runnersSecretsGitHubEFD96479" + }, + "GITHUB_PRIVATE_KEY_SECRET_ARN": { + "Ref": "runnersSecretsGitHubPrivateKey79498F91" + }, + "WEBHOOK_URL": { + "Fn::GetAtt": [ + "runnersWebhookHandlerwebhookhandlerFunctionUrlC8FB3D17", + "FunctionUrl" + ] + }, + "WEBHOOK_HANDLER_ARN": { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "runnersWebhookHandlerwebhookhandler22779A81", + "Arn" + ] + }, + ":$LATEST" + ] + ] + }, + "STEP_FUNCTION_ARN": { + "Ref": "runnersRunnerOrchestratorF9B66EBA" + }, + "STEP_FUNCTION_LOG_GROUP": "", + "AWS_NODEJS_CONNECTION_REUSE_ENABLED": "1", + "LOGICAL_ID": "runnerstroubleshoot44EEF479", + "STACK_NAME": "github-runners-test" + } + }, + "Handler": "index.handler", + "LoggingConfig": { + "LogFormat": "JSON", + "LogGroup": { + "Ref": "SetupLog8A8B0E5C" + } + }, + "Role": { + "Fn::GetAtt": [ + "runnerstroubleshootServiceRoleEBD5244C", + "Arn" + ] + }, + "Runtime": "nodejs22.x", + "Timeout": 300 + }, + "DependsOn": [ + "runnerstroubleshootServiceRoleDefaultPolicy11DB7B45", + "runnerstroubleshootServiceRoleEBD5244C" + ], + "Metadata": { + "providers": [ + { + "type": "CodeBuildRunnerProvider", + "labels": [ + "codebuild-x64" + ], + "constructPath": "github-runners-test/CodeBuildx64", + "roleArn": { + "Fn::GetAtt": [ + "CodeBuildx64CodeBuildRole32E5C764", + "Arn" + ] + }, + "logGroup": { + "Ref": "CodeBuildx64LogsA21ECD59" + }, + "image": { + "imageRepository": { + "Fn::Join": [ + "", + [ + { + "Fn::Select": [ + 4, + { + "Fn::Split": [ + ":", + { + "Fn::GetAtt": [ + "CodeBuildImageBuilderRepository9DE3B6F0", + "Arn" + ] + } + ] + } + ] + }, + ".dkr.ecr.", + { + "Fn::Select": [ + 3, + { + "Fn::Split": [ + ":", + { + "Fn::GetAtt": [ + "CodeBuildImageBuilderRepository9DE3B6F0", + "Arn" + ] + } + ] + } + ] + }, + ".", + { + "Ref": "AWS::URLSuffix" + }, + "/", + { + "Ref": "CodeBuildImageBuilderRepository9DE3B6F0" + } + ] + ] + }, + "imageTag": "latest", + "imageBuilderLogGroup": { + "Ref": "CodeBuildImageBuilderLogsE4CADFCC" + } + } + }, + { + "type": "CodeBuildRunnerProvider", + "labels": [ + "codebuild-ubuntu-2404-x64" + ], + "constructPath": "github-runners-test/CodeBuildUbuntu2404x64", + "roleArn": { + "Fn::GetAtt": [ + "CodeBuildUbuntu2404x64CodeBuildRoleD05C8D92", + "Arn" + ] + }, + "logGroup": { + "Ref": "CodeBuildUbuntu2404x64Logs27D26829" + }, + "image": { + "imageRepository": { + "Fn::Join": [ + "", + [ + { + "Fn::Select": [ + 4, + { + "Fn::Split": [ + ":", + { + "Fn::GetAtt": [ + "CodeBuildUbuntu2404ImageBuilderRepositoryFA75695E", + "Arn" + ] + } + ] + } + ] + }, + ".dkr.ecr.", + { + "Fn::Select": [ + 3, + { + "Fn::Split": [ + ":", + { + "Fn::GetAtt": [ + "CodeBuildUbuntu2404ImageBuilderRepositoryFA75695E", + "Arn" + ] + } + ] + } + ] + }, + ".", + { + "Ref": "AWS::URLSuffix" + }, + "/", + { + "Ref": "CodeBuildUbuntu2404ImageBuilderRepositoryFA75695E" + } + ] + ] + }, + "imageTag": "latest", + "imageBuilderLogGroup": { + "Ref": "CodeBuildUbuntu2404ImageBuilderLogsAD7DFF28" + } + } + }, + { + "type": "CodeBuildRunnerProvider", + "labels": [ + "codebuild", + "linux", + "arm64" + ], + "constructPath": "github-runners-test/CodeBuildARM", + "roleArn": { + "Fn::GetAtt": [ + "CodeBuildARMCodeBuildRole74DB4985", + "Arn" + ] + }, + "logGroup": { + "Ref": "CodeBuildARMLogs7C7FC5A7" + }, + "image": { + "imageRepository": { + "Fn::Join": [ + "", + [ + { + "Fn::Select": [ + 4, + { + "Fn::Split": [ + ":", + { + "Fn::GetAtt": [ + "CodeBuildImageBuilderarmRepositoryE967421B", + "Arn" + ] + } + ] + } + ] + }, + ".dkr.ecr.", + { + "Fn::Select": [ + 3, + { + "Fn::Split": [ + ":", + { + "Fn::GetAtt": [ + "CodeBuildImageBuilderarmRepositoryE967421B", + "Arn" + ] + } + ] + } + ] + }, + ".", + { + "Ref": "AWS::URLSuffix" + }, + "/", + { + "Ref": "CodeBuildImageBuilderarmRepositoryE967421B" + } + ] + ] + }, + "imageTag": "latest", + "imageBuilderLogGroup": { + "Ref": "CodeBuildImageBuilderarmLogs5A60CB81" + } + } + }, + { + "type": "CodeBuildRunnerProvider", + "labels": [ + "codebuild", + "windows", + "x64" + ], + "constructPath": "github-runners-test/CodeBuildWindows", + "roleArn": { + "Fn::GetAtt": [ + "CodeBuildWindowsCodeBuildRoleDC93697E", + "Arn" + ] + }, + "logGroup": { + "Ref": "CodeBuildWindowsLogsF4DD93A5" + }, + "image": { + "imageRepository": { + "Fn::Join": [ + "", + [ + { + "Fn::Select": [ + 4, + { + "Fn::Split": [ + ":", + { + "Fn::GetAtt": [ + "WindowsImageBuilderRepositoryA4CBB6D8", + "Arn" + ] + } + ] + } + ] + }, + ".dkr.ecr.", + { + "Fn::Select": [ + 3, + { + "Fn::Split": [ + ":", + { + "Fn::GetAtt": [ + "WindowsImageBuilderRepositoryA4CBB6D8", + "Arn" + ] + } + ] + } + ] + }, + ".", + { + "Ref": "AWS::URLSuffix" + }, + "/", + { + "Ref": "WindowsImageBuilderRepositoryA4CBB6D8" + } + ] + ] + }, + "imageTag": "latest", + "imageBuilderLogGroup": { + "Ref": "WindowsImageBuilderDockerLogE660E23E" + } + } + }, + { + "type": "EcsRunnerProvider", + "labels": [ + "ecs", + "linux", + "x64" + ], + "constructPath": "github-runners-test/ECS Spot", + "vpcArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":ec2:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":vpc/", + { + "Ref": "Vpc8378EB38" + } + ] + ] + }, + "securityGroups": [ + { + "Fn::GetAtt": [ + "ECSSpotsecuritygroupF7F9906A", + "GroupId" + ] + } + ], + "roleArn": { + "Fn::GetAtt": [ + "ECSSpottaskTaskRole9096D8F9", + "Arn" + ] + }, + "logGroup": { + "Ref": "ECSSpotlogs75CE3929" + }, + "image": { + "imageRepository": { + "Fn::Join": [ + "", + [ + { + "Fn::Select": [ + 4, + { + "Fn::Split": [ + ":", + { + "Fn::GetAtt": [ + "CodeBuildImageBuilderRepository9DE3B6F0", + "Arn" + ] + } + ] + } + ] + }, + ".dkr.ecr.", + { + "Fn::Select": [ + 3, + { + "Fn::Split": [ + ":", + { + "Fn::GetAtt": [ + "CodeBuildImageBuilderRepository9DE3B6F0", + "Arn" + ] + } + ] + } + ] + }, + ".", + { + "Ref": "AWS::URLSuffix" + }, + "/", + { + "Ref": "CodeBuildImageBuilderRepository9DE3B6F0" + } + ] + ] + }, + "imageTag": "latest", + "imageBuilderLogGroup": { + "Ref": "CodeBuildImageBuilderLogsE4CADFCC" + } + } + }, + { + "type": "EcsRunnerProvider", + "labels": [ + "ecs", + "linux", + "x64" + ], + "constructPath": "github-runners-test/ECS Non-Spot", + "vpcArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":ec2:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":vpc/", + { + "Ref": "Vpc8378EB38" + } + ] + ] + }, + "securityGroups": [ + { + "Fn::GetAtt": [ + "ECSNonSpotsecuritygroup20AE443A", + "GroupId" + ] + } + ], + "roleArn": { + "Fn::GetAtt": [ + "ECSNonSpottaskTaskRole7AF36D7C", + "Arn" + ] + }, + "logGroup": { + "Ref": "ECSNonSpotlogsA90C16E5" + }, + "image": { + "imageRepository": { + "Fn::Join": [ + "", + [ + { + "Fn::Select": [ + 4, + { + "Fn::Split": [ + ":", + { + "Fn::GetAtt": [ + "CodeBuildImageBuilderRepository9DE3B6F0", + "Arn" + ] + } + ] + } + ] + }, + ".dkr.ecr.", + { + "Fn::Select": [ + 3, + { + "Fn::Split": [ + ":", + { + "Fn::GetAtt": [ + "CodeBuildImageBuilderRepository9DE3B6F0", + "Arn" + ] + } + ] + } + ] + }, + ".", + { + "Ref": "AWS::URLSuffix" + }, + "/", + { + "Ref": "CodeBuildImageBuilderRepository9DE3B6F0" + } + ] + ] + }, + "imageTag": "latest", + "imageBuilderLogGroup": { + "Ref": "CodeBuildImageBuilderLogsE4CADFCC" + } + } + }, + { + "type": "EcsRunnerProvider", + "labels": [ + "ecs-ubuntu-2404", + "x64" + ], + "constructPath": "github-runners-test/ECS Ubuntu 2404", + "vpcArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":ec2:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":vpc/", + { + "Ref": "Vpc8378EB38" + } + ] + ] + }, + "securityGroups": [ + { + "Fn::GetAtt": [ + "ECSUbuntu2404securitygroupEE405C76", + "GroupId" + ] + } + ], + "roleArn": { + "Fn::GetAtt": [ + "ECSUbuntu2404taskTaskRoleDF683F3B", + "Arn" + ] + }, + "logGroup": { + "Ref": "ECSUbuntu2404logsFB15C25B" + }, + "image": { + "imageRepository": { + "Fn::Join": [ + "", + [ + { + "Fn::Select": [ + 4, + { + "Fn::Split": [ + ":", + { + "Fn::GetAtt": [ + "CodeBuildUbuntu2404ImageBuilderRepositoryFA75695E", + "Arn" + ] + } + ] + } + ] + }, + ".dkr.ecr.", + { + "Fn::Select": [ + 3, + { + "Fn::Split": [ + ":", + { + "Fn::GetAtt": [ + "CodeBuildUbuntu2404ImageBuilderRepositoryFA75695E", + "Arn" + ] + } + ] + } + ] + }, + ".", + { + "Ref": "AWS::URLSuffix" + }, + "/", + { + "Ref": "CodeBuildUbuntu2404ImageBuilderRepositoryFA75695E" + } + ] + ] + }, + "imageTag": "latest", + "imageBuilderLogGroup": { + "Ref": "CodeBuildUbuntu2404ImageBuilderLogsAD7DFF28" + } + } + }, + { + "type": "EcsRunnerProvider", + "labels": [ + "ecs", + "linux", + "arm64" + ], + "constructPath": "github-runners-test/ECS ARM64", + "vpcArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":ec2:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":vpc/", + { + "Ref": "Vpc8378EB38" + } + ] + ] + }, + "securityGroups": [ + { + "Fn::GetAtt": [ + "ECSARM64securitygroup281D94B2", + "GroupId" + ] + } + ], + "roleArn": { + "Fn::GetAtt": [ + "ECSARM64taskTaskRole23B1CB4E", + "Arn" + ] + }, + "logGroup": { + "Ref": "ECSARM64logs1A2F1E00" + }, + "image": { + "imageRepository": { + "Fn::Join": [ + "", + [ + { + "Fn::Select": [ + 4, + { + "Fn::Split": [ + ":", + { + "Fn::GetAtt": [ + "CodeBuildImageBuilderarmRepositoryE967421B", + "Arn" + ] + } + ] + } + ] + }, + ".dkr.ecr.", + { + "Fn::Select": [ + 3, + { + "Fn::Split": [ + ":", + { + "Fn::GetAtt": [ + "CodeBuildImageBuilderarmRepositoryE967421B", + "Arn" + ] + } + ] + } + ] + }, + ".", + { + "Ref": "AWS::URLSuffix" + }, + "/", + { + "Ref": "CodeBuildImageBuilderarmRepositoryE967421B" + } + ] + ] + }, + "imageTag": "latest", + "imageBuilderLogGroup": { + "Ref": "CodeBuildImageBuilderarmLogs5A60CB81" + } + } + }, + { + "type": "EcsRunnerProvider", + "labels": [ + "ecs", + "windows", + "x64" + ], + "constructPath": "github-runners-test/ECS Windows", + "vpcArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":ec2:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":vpc/", + { + "Ref": "Vpc8378EB38" + } + ] + ] + }, + "securityGroups": [ + { + "Fn::GetAtt": [ + "ECSWindowssecuritygroupB4EE54DA", + "GroupId" + ] + } + ], + "roleArn": { + "Fn::GetAtt": [ + "ECSWindowstaskTaskRole17C8DA48", + "Arn" + ] + }, + "logGroup": { + "Ref": "ECSWindowslogs83D9D352" + }, + "image": { + "imageRepository": { + "Fn::Join": [ + "", + [ + { + "Fn::Select": [ + 4, + { + "Fn::Split": [ + ":", + { + "Fn::GetAtt": [ + "WindowsImageBuilderRepositoryA4CBB6D8", + "Arn" + ] + } + ] + } + ] + }, + ".dkr.ecr.", + { + "Fn::Select": [ + 3, + { + "Fn::Split": [ + ":", + { + "Fn::GetAtt": [ + "WindowsImageBuilderRepositoryA4CBB6D8", + "Arn" + ] + } + ] + } + ] + }, + ".", + { + "Ref": "AWS::URLSuffix" + }, + "/", + { + "Ref": "WindowsImageBuilderRepositoryA4CBB6D8" + } + ] + ] + }, + "imageTag": "latest", + "imageBuilderLogGroup": { + "Ref": "WindowsImageBuilderDockerLogE660E23E" + } + } + }, + { + "type": "LambdaRunnerProvider", + "labels": [ + "lambda", + "x64" + ], + "constructPath": "github-runners-test/Lambda", + "roleArn": { + "Fn::GetAtt": [ + "LambdaFunctionServiceRoleB1826A50", + "Arn" + ] + }, + "logGroup": { + "Ref": "LambdaLog2764B04F" + }, + "image": { + "imageRepository": { + "Fn::Join": [ + "", + [ + { + "Fn::Select": [ + 4, + { + "Fn::Split": [ + ":", + { + "Fn::GetAtt": [ + "LambdaImageBuilderx64Repository57F632F1", + "Arn" + ] + } + ] + } + ] + }, + ".dkr.ecr.", + { + "Fn::Select": [ + 3, + { + "Fn::Split": [ + ":", + { + "Fn::GetAtt": [ + "LambdaImageBuilderx64Repository57F632F1", + "Arn" + ] + } + ] + } + ] + }, + ".", + { + "Ref": "AWS::URLSuffix" + }, + "/", + { + "Ref": "LambdaImageBuilderx64Repository57F632F1" + } + ] + ] + }, + "imageTag": "latest", + "imageBuilderLogGroup": { + "Ref": "LambdaImageBuilderx64Logs1C003BB4" + } + } + }, + { + "type": "LambdaRunnerProvider", + "labels": [ + "lambda", + "arm64" + ], + "constructPath": "github-runners-test/LambdaARM", + "roleArn": { + "Fn::GetAtt": [ + "LambdaARMFunctionServiceRole136069A0", + "Arn" + ] + }, + "logGroup": { + "Ref": "LambdaARMLogAA2DA09F" + }, + "image": { + "imageRepository": { + "Fn::Join": [ + "", + [ + { + "Fn::Select": [ + 4, + { + "Fn::Split": [ + ":", + { + "Fn::GetAtt": [ + "LambdaImageBuilderzRepository7C7AD146", + "Arn" + ] + } + ] + } + ] + }, + ".dkr.ecr.", + { + "Fn::Select": [ + 3, + { + "Fn::Split": [ + ":", + { + "Fn::GetAtt": [ + "LambdaImageBuilderzRepository7C7AD146", + "Arn" + ] + } + ] + } + ] + }, + ".", + { + "Ref": "AWS::URLSuffix" + }, + "/", + { + "Ref": "LambdaImageBuilderzRepository7C7AD146" + } + ] + ] + }, + "imageTag": "latest", + "imageBuilderLogGroup": { + "Ref": "LambdaImageBuilderzLogsC9FB42C8" + } + } + }, + { + "type": "FargateRunnerProvider", + "labels": [ + "fargate", + "linux", + "x64" + ], + "constructPath": "github-runners-test/Fargate", + "vpcArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":ec2:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":vpc/", + { + "Ref": "Vpc8378EB38" + } + ] + ] + }, + "securityGroups": [ + { + "Fn::GetAtt": [ + "FargatesecuritygroupAFCAFD34", + "GroupId" + ] + } + ], + "roleArn": { + "Fn::GetAtt": [ + "FargatetaskTaskRoleEFFCDAF8", + "Arn" + ] + }, + "logGroup": { + "Ref": "FargatelogsCB753638" + }, + "image": { + "imageRepository": { + "Fn::Join": [ + "", + [ + { + "Fn::Select": [ + 4, + { + "Fn::Split": [ + ":", + { + "Fn::GetAtt": [ + "FargatebuilderRepository8F7BA13C", + "Arn" + ] + } + ] + } + ] + }, + ".dkr.ecr.", + { + "Fn::Select": [ + 3, + { + "Fn::Split": [ + ":", + { + "Fn::GetAtt": [ + "FargatebuilderRepository8F7BA13C", + "Arn" + ] + } + ] + } + ] + }, + ".", + { + "Ref": "AWS::URLSuffix" + }, + "/", + { + "Ref": "FargatebuilderRepository8F7BA13C" + } + ] + ] + }, + "imageTag": "latest", + "imageBuilderLogGroup": { + "Ref": "FargatebuilderLogs2F794091" + } + } + }, + { + "type": "FargateRunnerProvider", + "labels": [ + "fargate-spot", + "linux", + "x64" + ], + "constructPath": "github-runners-test/Fargate-x64-spot subnet 1", + "vpcArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":ec2:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":vpc/", + { + "Ref": "Vpc8378EB38" + } + ] + ] + }, + "securityGroups": [ + { + "Fn::GetAtt": [ + "Fargatex64spotsubnet1securitygroupE34C79C3", + "GroupId" + ] + } + ], + "roleArn": { + "Fn::GetAtt": [ + "Fargatex64spotsubnet1taskTaskRole2D726991", + "Arn" + ] + }, + "logGroup": { + "Ref": "Fargatex64spotsubnet1logs7081E8E6" + }, + "image": { + "imageRepository": { + "Fn::Join": [ + "", + [ + { + "Fn::Select": [ + 4, + { + "Fn::Split": [ + ":", + { + "Fn::GetAtt": [ + "FargatebuilderRepository8F7BA13C", + "Arn" + ] + } + ] + } + ] + }, + ".dkr.ecr.", + { + "Fn::Select": [ + 3, + { + "Fn::Split": [ + ":", + { + "Fn::GetAtt": [ + "FargatebuilderRepository8F7BA13C", + "Arn" + ] + } + ] + } + ] + }, + ".", + { + "Ref": "AWS::URLSuffix" + }, + "/", + { + "Ref": "FargatebuilderRepository8F7BA13C" + } + ] + ] + }, + "imageTag": "latest", + "imageBuilderLogGroup": { + "Ref": "FargatebuilderLogs2F794091" + } + } + }, + { + "type": "FargateRunnerProvider", + "labels": [ + "fargate-spot", + "linux", + "x64" + ], + "constructPath": "github-runners-test/Fargate-x64-spot subnet 2", + "vpcArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":ec2:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":vpc/", + { + "Ref": "Vpc8378EB38" + } + ] + ] + }, + "securityGroups": [ + { + "Fn::GetAtt": [ + "Fargatex64spotsubnet2securitygroupF09DD2CB", + "GroupId" + ] + } + ], + "roleArn": { + "Fn::GetAtt": [ + "Fargatex64spotsubnet2taskTaskRole0072C9A8", + "Arn" + ] + }, + "logGroup": { + "Ref": "Fargatex64spotsubnet2logs305194C7" + }, + "image": { + "imageRepository": { + "Fn::Join": [ + "", + [ + { + "Fn::Select": [ + 4, + { + "Fn::Split": [ + ":", + { + "Fn::GetAtt": [ + "FargatebuilderRepository8F7BA13C", + "Arn" + ] + } + ] + } + ] + }, + ".dkr.ecr.", + { + "Fn::Select": [ + 3, + { + "Fn::Split": [ + ":", + { + "Fn::GetAtt": [ + "FargatebuilderRepository8F7BA13C", + "Arn" + ] + } + ] + } + ] + }, + ".", + { + "Ref": "AWS::URLSuffix" + }, + "/", + { + "Ref": "FargatebuilderRepository8F7BA13C" + } + ] + ] + }, + "imageTag": "latest", + "imageBuilderLogGroup": { + "Ref": "FargatebuilderLogs2F794091" + } + } + }, + { + "type": "FargateRunnerProvider", + "labels": [ + "fargate", + "linux", + "arm64" + ], + "constructPath": "github-runners-test/Fargate-arm64", + "vpcArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":ec2:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":vpc/", + { + "Ref": "Vpc8378EB38" + } + ] + ] + }, + "securityGroups": [ + { + "Fn::GetAtt": [ + "Fargatearm64securitygroup08ABAD9E", + "GroupId" + ] + } + ], + "roleArn": { + "Fn::GetAtt": [ + "Fargatearm64taskTaskRoleD3C2CD58", + "Arn" + ] + }, + "logGroup": { + "Ref": "Fargatearm64logs00171A52" + }, + "image": { + "imageRepository": { + "Fn::Join": [ + "", + [ + { + "Fn::Select": [ + 4, + { + "Fn::Split": [ + ":", + { + "Fn::GetAtt": [ + "FargatebuilderarmRepository77DCC132", + "Arn" + ] + } + ] + } + ] + }, + ".dkr.ecr.", + { + "Fn::Select": [ + 3, + { + "Fn::Split": [ + ":", + { + "Fn::GetAtt": [ + "FargatebuilderarmRepository77DCC132", + "Arn" + ] + } + ] + } + ] + }, + ".", + { + "Ref": "AWS::URLSuffix" + }, + "/", + { + "Ref": "FargatebuilderarmRepository77DCC132" + } + ] + ] + }, + "imageTag": "latest", + "imageBuilderLogGroup": { + "Ref": "FargatebuilderarmLogs63D60F4D" + } + } + }, + { + "type": "FargateRunnerProvider", + "labels": [ + "fargate-spot", + "linux", + "arm64" + ], + "constructPath": "github-runners-test/Fargate-arm64-spot", + "vpcArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":ec2:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":vpc/", + { + "Ref": "Vpc8378EB38" + } + ] + ] + }, + "securityGroups": [ + { + "Fn::GetAtt": [ + "Fargatearm64spotsecuritygroup20C885E4", + "GroupId" + ] + } + ], + "roleArn": { + "Fn::GetAtt": [ + "Fargatearm64spottaskTaskRole0F078C81", + "Arn" + ] + }, + "logGroup": { + "Ref": "Fargatearm64spotlogsEFCCEC59" + }, + "image": { + "imageRepository": { + "Fn::Join": [ + "", + [ + { + "Fn::Select": [ + 4, + { + "Fn::Split": [ + ":", + { + "Fn::GetAtt": [ + "FargatebuilderarmRepository77DCC132", + "Arn" + ] + } + ] + } + ] + }, + ".dkr.ecr.", + { + "Fn::Select": [ + 3, + { + "Fn::Split": [ + ":", + { + "Fn::GetAtt": [ + "FargatebuilderarmRepository77DCC132", + "Arn" + ] + } + ] + } + ] + }, + ".", + { + "Ref": "AWS::URLSuffix" + }, + "/", + { + "Ref": "FargatebuilderarmRepository77DCC132" + } + ] + ] + }, + "imageTag": "latest", + "imageBuilderLogGroup": { + "Ref": "FargatebuilderarmLogs63D60F4D" + } + } + }, + { + "type": "FargateRunnerProvider", + "labels": [ + "fargate", + "windows", + "x64" + ], + "constructPath": "github-runners-test/Fargate-Windows", + "vpcArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":ec2:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":vpc/", + { + "Ref": "Vpc8378EB38" + } + ] + ] + }, + "securityGroups": [ + { + "Fn::GetAtt": [ + "FargateWindowssecuritygroupE684A7B0", + "GroupId" + ] + } + ], + "roleArn": { + "Fn::GetAtt": [ + "FargateWindowstaskTaskRole364508C8", + "Arn" + ] + }, + "logGroup": { + "Ref": "FargateWindowslogs52881DBF" + }, + "image": { + "imageRepository": { + "Fn::Join": [ + "", + [ + { + "Fn::Select": [ + 4, + { + "Fn::Split": [ + ":", + { + "Fn::GetAtt": [ + "WindowsImageBuilderRepositoryA4CBB6D8", + "Arn" + ] + } + ] + } + ] + }, + ".dkr.ecr.", + { + "Fn::Select": [ + 3, + { + "Fn::Split": [ + ":", + { + "Fn::GetAtt": [ + "WindowsImageBuilderRepositoryA4CBB6D8", + "Arn" + ] + } + ] + } + ] + }, + ".", + { + "Ref": "AWS::URLSuffix" + }, + "/", + { + "Ref": "WindowsImageBuilderRepositoryA4CBB6D8" + } + ] + ] + }, + "imageTag": "latest", + "imageBuilderLogGroup": { + "Ref": "WindowsImageBuilderDockerLogE660E23E" + } + } + }, + { + "type": "Ec2RunnerProvider", + "labels": [ + "ec2", + "linux", + "x64" + ], + "constructPath": "github-runners-test/EC2 Linux", + "securityGroups": [ + { + "Fn::GetAtt": [ + "EC2LinuxSGF5B89300", + "GroupId" + ] + } + ], + "roleArn": { + "Fn::GetAtt": [ + "EC2LinuxRole8B6519A2", + "Arn" + ] + }, + "logGroup": { + "Ref": "EC2LinuxLogsC4CD8F14" + }, + "ami": { + "launchTemplate": { + "Ref": "AMILinuxBuilderLaunchtemplateA29452C4" + }, + "amiBuilderLogGroup": { + "Ref": "AMILinuxBuilderAmiLog0DB16E38" + } + } + }, + { + "type": "Ec2RunnerProvider", + "labels": [ + "ec2", + "linux", + "x64" + ], + "constructPath": "github-runners-test/EC2 Linux 2", + "securityGroups": [ + { + "Fn::GetAtt": [ + "EC2Linux2SG0F0DCCF1", + "GroupId" + ] + } + ], + "roleArn": { + "Fn::GetAtt": [ + "EC2Linux2Role25F5EE10", + "Arn" + ] + }, + "logGroup": { + "Ref": "EC2Linux2Logs666ED658" + }, + "ami": { + "launchTemplate": { + "Ref": "AMILinuxBuilderLaunchtemplateA29452C4" + }, + "amiBuilderLogGroup": { + "Ref": "AMILinuxBuilderAmiLog0DB16E38" + } + } + }, + { + "type": "Ec2RunnerProvider", + "labels": [ + "ec2-spot", + "linux", + "x64" + ], + "constructPath": "github-runners-test/EC2 Spot Linux", + "securityGroups": [ + { + "Fn::GetAtt": [ + "EC2SpotLinuxSG8D846B64", + "GroupId" + ] + } + ], + "roleArn": { + "Fn::GetAtt": [ + "EC2SpotLinuxRole86333E5D", + "Arn" + ] + }, + "logGroup": { + "Ref": "EC2SpotLinuxLogsF78D5F0E" + }, + "ami": { + "launchTemplate": { + "Ref": "AMILinuxBuilderLaunchtemplateA29452C4" + }, + "amiBuilderLogGroup": { + "Ref": "AMILinuxBuilderAmiLog0DB16E38" + } + } + }, + { + "type": "Ec2RunnerProvider", + "labels": [ + "ec2", + "linux", + "arm64" + ], + "constructPath": "github-runners-test/EC2 Linux arm64", + "securityGroups": [ + { + "Fn::GetAtt": [ + "EC2Linuxarm64SG550ECD6C", + "GroupId" + ] + } + ], + "roleArn": { + "Fn::GetAtt": [ + "EC2Linuxarm64Role242F68FF", + "Arn" + ] + }, + "logGroup": { + "Ref": "EC2Linuxarm64Logs577E371E" + }, + "ami": { + "launchTemplate": { + "Ref": "AMILinuxarm64BuilderLaunchtemplate8F5EFF44" + }, + "amiBuilderLogGroup": { + "Ref": "AMILinuxarm64BuilderAmiLog84A9D94A" + } + } + }, + { + "type": "Ec2RunnerProvider", + "labels": [ + "ec2", + "windows", + "x64" + ], + "constructPath": "github-runners-test/EC2 Windows", + "securityGroups": [ + { + "Fn::GetAtt": [ + "EC2WindowsSG13E24976", + "GroupId" + ] + } + ], + "roleArn": { + "Fn::GetAtt": [ + "EC2WindowsRoleC0D850D2", + "Arn" + ] + }, + "logGroup": { + "Ref": "EC2WindowsLogsDC1F2ABF" + }, + "ami": { + "launchTemplate": { + "Ref": "WindowsEC2BuilderLaunchtemplate0A66E9C2" + }, + "amiBuilderLogGroup": { + "Ref": "WindowsEC2BuilderAmiLog126E54B2" + } + } + } + ] + } + }, "runnersFailedRunnerImageBuilds233D2237": { "Type": "AWS::SNS::Topic" }, @@ -21081,6 +23144,24 @@ ] ] } + }, + "runnerstroubleshootcommandCEDDFA8F": { + "Value": { + "Fn::Join": [ + "", + [ + "aws --region ", + { + "Ref": "AWS::Region" + }, + " lambda invoke --function-name ", + { + "Ref": "runnerstroubleshoot44EEF479" + }, + " troubleshoot.json" + ] + ] + } } }, "Rules": { diff --git a/test/runner.test.ts b/test/runner.test.ts index 0f7abe35..ecbb802c 100644 --- a/test/runner.test.ts +++ b/test/runner.test.ts @@ -2,7 +2,7 @@ import * as cdk from 'aws-cdk-lib'; import { aws_ec2 as ec2, aws_ecr as ecr } from 'aws-cdk-lib'; import { Annotations, Match, Template } from 'aws-cdk-lib/assertions'; import { CloudAssembly } from 'aws-cdk-lib/cx-api'; -import { CodeBuildRunnerProvider, CompositeProvider, GitHubRunners, LambdaRunnerProvider, StaticRunnerImage } from '../src'; +import { CodeBuildRunnerProvider, CompositeProvider, GitHubRunners, LambdaAccess, LambdaRunnerProvider, StaticRunnerImage } from '../src'; let app: cdk.App; let stack: cdk.Stack; @@ -367,4 +367,66 @@ describe('GitHubRunners', () => { }), }); }); + + test('Troubleshoot function is always created with CfnOutput', () => { + new GitHubRunners(stack, 'runners', { + providers: [new LambdaRunnerProvider(stack, 'p1')], + }); + + const template = Template.fromStack(stack); + + template.hasResourceProperties('AWS::Lambda::Function', { + Description: 'Diagnose issues with self-hosted GitHub Actions runners', + Timeout: 300, + }); + + // verify CfnOutput for troubleshoot command exists + const outputs = template.findOutputs('*'); + const troubleshootOutputs = Object.entries(outputs).filter(([, v]) => + JSON.stringify(v).includes('troubleshoot.json'), + ); + expect(troubleshootOutputs.length).toBe(1); + }); + + test('Troubleshoot access with Lambda URL', () => { + new GitHubRunners(stack, 'runners', { + providers: [new LambdaRunnerProvider(stack, 'p1')], + troubleshootAccess: LambdaAccess.lambdaUrl(), + }); + + const template = Template.fromStack(stack); + + // setup + webhook + troubleshoot = 3 Lambda URLs + template.resourceCountIs('AWS::Lambda::Url', 3); + }); + + test('Troubleshoot function has SFN and logs permissions', () => { + new GitHubRunners(stack, 'runners', { + providers: [new LambdaRunnerProvider(stack, 'p1')], + }); + + const template = Template.fromStack(stack); + + template.hasResourceProperties('AWS::IAM::Policy', Match.objectLike({ + PolicyDocument: Match.objectLike({ + Statement: Match.arrayWith([ + Match.objectLike({ + Action: 'states:GetExecutionHistory', + Effect: 'Allow', + }), + ]), + }), + })); + + template.hasResourceProperties('AWS::IAM::Policy', Match.objectLike({ + PolicyDocument: Match.objectLike({ + Statement: Match.arrayWith([ + Match.objectLike({ + Action: 'logs:FilterLogEvents', + Effect: 'Allow', + }), + ]), + }), + })); + }); });