diff --git a/env.json b/env.json new file mode 100644 index 0000000..11f748c --- /dev/null +++ b/env.json @@ -0,0 +1,11 @@ +{ + "GitHubActionHookFunction": { + "GITHUB_PAT_SECRET_NAME": "github-runner-autoscaler-pat", + "EXTRA_RUNNER_LABELS": ",vor_stream_sdlc", + "IMAGE_ID": "ami-0c0c88099397fccb4", + "SUBNET_ID": "subnet-0123456789def", + "SECURITY_GROUP_IDS": "sg-0123456789def", + "KEY_NAME": "terraform-2025051802", + "INSTANCE_PROFILE_ARN": "arn:aws:iam::123456789012:instance-profile/github-runner-autoscaler-RunnerInstanceProfile-XXXXX" + } +} \ No newline at end of file diff --git a/event-custom-instance.json b/event-custom-instance.json new file mode 100644 index 0000000..d5e853b --- /dev/null +++ b/event-custom-instance.json @@ -0,0 +1,55 @@ +{ + "resource": "/", + "path": "/", + "httpMethod": "POST", + "headers": { + "Accept": "application/json", + "Content-Type": "application/json", + "X-GitHub-Event": "workflow_job", + "X-GitHub-Delivery": "12345678-1234-1234-1234-123456789012", + "X-Hub-Signature-256": "sha256=fake_signature_for_testing" + }, + "multiValueHeaders": { + "Accept": ["application/json"], + "Content-Type": ["application/json"], + "X-GitHub-Event": ["workflow_job"], + "X-GitHub-Delivery": ["12345678-1234-1234-1234-123456789012"], + "X-Hub-Signature-256": ["sha256=fake_signature_for_testing"] + }, + "queryStringParameters": null, + "multiValueQueryStringParameters": null, + "pathParameters": null, + "stageVariables": null, + "requestContext": { + "resourceId": "123456", + "resourcePath": "/", + "httpMethod": "POST", + "extendedRequestId": "request-id", + "requestTime": "09/Apr/2024:08:00:00 +0000", + "path": "/", + "accountId": "123456789012", + "protocol": "HTTP/1.1", + "stage": "Prod", + "domainPrefix": "api", + "requestTimeEpoch": 1712649600000, + "requestId": "12345678-1234-1234-1234-123456789012", + "identity": { + "cognitoIdentityPoolId": null, + "accountId": null, + "cognitoIdentityId": null, + "caller": null, + "sourceIp": "192.0.2.1", + "principalOrgId": null, + "accessKey": null, + "cognitoAuthenticationType": null, + "cognitoAuthenticationProvider": null, + "userArn": null, + "userAgent": "GitHub-Hookshot/abc123", + "user": null + }, + "domainName": "api.example.com", + "apiId": "1234567890" + }, + "body": "{\"action\":\"queued\",\"workflow_job\":{\"id\":123456789,\"run_id\":987654321,\"workflow_name\":\"CI\",\"head_branch\":\"main\",\"run_url\":\"https://github.com/frgrisk/test-repo/actions/runs/987654321\",\"run_attempt\":1,\"node_id\":\"CR_kwDOABCDEF\",\"head_sha\":\"abcdef0123456789abcdef0123456789abcdef01\",\"url\":\"https://api.github.com/repos/frgrisk/test-repo/actions/jobs/123456789\",\"html_url\":\"https://github.com/frgrisk/test-repo/actions/runs/987654321/job/123456789\",\"status\":\"queued\",\"conclusion\":null,\"created_at\":\"2024-04-09T08:00:00Z\",\"started_at\":null,\"completed_at\":null,\"name\":\"test-job\",\"steps\":[],\"check_run_url\":\"https://api.github.com/repos/frgrisk/test-repo/check-runs/123456789\",\"labels\":[\"self-hosted\",\"ephemeral\",\"X64\",\"Linux\",\"instance-type:m5.large\"],\"runner_id\":null,\"runner_name\":null,\"runner_group_id\":null,\"runner_group_name\":null},\"repository\":{\"id\":123456789,\"node_id\":\"R_kgDOABCDEF\",\"name\":\"test-repo\",\"full_name\":\"frgrisk/test-repo\",\"private\":true,\"owner\":{\"login\":\"frgrisk\",\"id\":12345678,\"node_id\":\"O_kgDOABCDEF\",\"avatar_url\":\"https://avatars.githubusercontent.com/u/12345678?v=4\",\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/frgrisk\",\"html_url\":\"https://github.com/frgrisk\",\"followers_url\":\"https://api.github.com/users/frgrisk/followers\",\"following_url\":\"https://api.github.com/users/frgrisk/following{/other_user}\",\"gists_url\":\"https://api.github.com/users/frgrisk/gists{/gist_id}\",\"starred_url\":\"https://api.github.com/users/frgrisk/starred{/owner}{/repo}\",\"subscriptions_url\":\"https://api.github.com/users/frgrisk/subscriptions\",\"organizations_url\":\"https://api.github.com/users/frgrisk/orgs\",\"repos_url\":\"https://api.github.com/users/frgrisk/repos\",\"events_url\":\"https://api.github.com/users/frgrisk/events{/privacy}\",\"received_events_url\":\"https://api.github.com/users/frgrisk/received_events\",\"type\":\"Organization\",\"site_admin\":false},\"html_url\":\"https://github.com/frgrisk/test-repo\",\"description\":\"Test repository\",\"fork\":false,\"url\":\"https://api.github.com/repos/frgrisk/test-repo\"},\"organization\":{\"login\":\"frgrisk\",\"id\":12345678,\"node_id\":\"O_kgDOABCDEF\",\"url\":\"https://api.github.com/orgs/frgrisk\",\"repos_url\":\"https://api.github.com/orgs/frgrisk/repos\",\"events_url\":\"https://api.github.com/orgs/frgrisk/events\",\"hooks_url\":\"https://api.github.com/orgs/frgrisk/hooks\",\"issues_url\":\"https://api.github.com/orgs/frgrisk/issues\",\"members_url\":\"https://api.github.com/orgs/frgrisk/members{/member}\",\"public_members_url\":\"https://api.github.com/orgs/frgrisk/public_members{/member}\",\"avatar_url\":\"https://avatars.githubusercontent.com/u/12345678?v=4\",\"description\":\"FRG Risk\"},\"sender\":{\"login\":\"github-actions[bot]\",\"id\":41898282,\"node_id\":\"MDM6Qm90NDE4OTgyODI=\",\"avatar_url\":\"https://avatars.githubusercontent.com/in/15368?v=4\",\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/github-actions%5Bbot%5D\",\"html_url\":\"https://github.com/apps/github-actions\",\"followers_url\":\"https://api.github.com/users/github-actions%5Bbot%5D/followers\",\"following_url\":\"https://api.github.com/users/github-actions%5Bbot%5D/following{/other_user}\",\"gists_url\":\"https://api.github.com/users/github-actions%5Bbot%5D/gists{/gist_id}\",\"starred_url\":\"https://api.github.com/users/github-actions%5Bbot%5D/starred{/owner}{/repo}\",\"subscriptions_url\":\"https://api.github.com/users/github-actions%5Bbot%5D/subscriptions\",\"organizations_url\":\"https://api.github.com/users/github-actions%5Bbot%5D/orgs\",\"repos_url\":\"https://api.github.com/users/github-actions%5Bbot%5D/repos\",\"events_url\":\"https://api.github.com/users/github-actions%5Bbot%5D/events{/privacy}\",\"received_events_url\":\"https://api.github.com/users/github-actions%5Bbot%5D/received_events\",\"type\":\"Bot\",\"site_admin\":false}}", + "isBase64Encoded": false +} \ No newline at end of file diff --git a/event.json b/event.json new file mode 100644 index 0000000..2e82d7a --- /dev/null +++ b/event.json @@ -0,0 +1,55 @@ +{ + "resource": "/", + "path": "/", + "httpMethod": "POST", + "headers": { + "Accept": "application/json", + "Content-Type": "application/json", + "X-GitHub-Event": "workflow_job", + "X-GitHub-Delivery": "12345678-1234-1234-1234-123456789012", + "X-Hub-Signature-256": "sha256=fake_signature_for_testing" + }, + "multiValueHeaders": { + "Accept": ["application/json"], + "Content-Type": ["application/json"], + "X-GitHub-Event": ["workflow_job"], + "X-GitHub-Delivery": ["12345678-1234-1234-1234-123456789012"], + "X-Hub-Signature-256": ["sha256=fake_signature_for_testing"] + }, + "queryStringParameters": null, + "multiValueQueryStringParameters": null, + "pathParameters": null, + "stageVariables": null, + "requestContext": { + "resourceId": "123456", + "resourcePath": "/", + "httpMethod": "POST", + "extendedRequestId": "request-id", + "requestTime": "09/Apr/2024:08:00:00 +0000", + "path": "/", + "accountId": "123456789012", + "protocol": "HTTP/1.1", + "stage": "Prod", + "domainPrefix": "api", + "requestTimeEpoch": 1712649600000, + "requestId": "12345678-1234-1234-1234-123456789012", + "identity": { + "cognitoIdentityPoolId": null, + "accountId": null, + "cognitoIdentityId": null, + "caller": null, + "sourceIp": "192.0.2.1", + "principalOrgId": null, + "accessKey": null, + "cognitoAuthenticationType": null, + "cognitoAuthenticationProvider": null, + "userArn": null, + "userAgent": "GitHub-Hookshot/abc123", + "user": null + }, + "domainName": "api.example.com", + "apiId": "1234567890" + }, + "body": "{\"action\":\"queued\",\"workflow_job\":{\"id\":123456789,\"run_id\":987654321,\"workflow_name\":\"CI\",\"head_branch\":\"main\",\"run_url\":\"https://github.com/frgrisk/test-repo/actions/runs/987654321\",\"run_attempt\":1,\"node_id\":\"CR_kwDOABCDEF\",\"head_sha\":\"abcdef0123456789abcdef0123456789abcdef01\",\"url\":\"https://api.github.com/repos/frgrisk/test-repo/actions/jobs/123456789\",\"html_url\":\"https://github.com/frgrisk/test-repo/actions/runs/987654321/job/123456789\",\"status\":\"queued\",\"conclusion\":null,\"created_at\":\"2024-04-09T08:00:00Z\",\"started_at\":null,\"completed_at\":null,\"name\":\"test-job\",\"steps\":[],\"check_run_url\":\"https://api.github.com/repos/frgrisk/test-repo/check-runs/123456789\",\"labels\":[\"self-hosted\",\"ephemeral\",\"X64\",\"Linux\"],\"runner_id\":null,\"runner_name\":null,\"runner_group_id\":null,\"runner_group_name\":null},\"repository\":{\"id\":123456789,\"node_id\":\"R_kgDOABCDEF\",\"name\":\"test-repo\",\"full_name\":\"frgrisk/test-repo\",\"private\":true,\"owner\":{\"login\":\"frgrisk\",\"id\":12345678,\"node_id\":\"O_kgDOABCDEF\",\"avatar_url\":\"https://avatars.githubusercontent.com/u/12345678?v=4\",\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/frgrisk\",\"html_url\":\"https://github.com/frgrisk\",\"followers_url\":\"https://api.github.com/users/frgrisk/followers\",\"following_url\":\"https://api.github.com/users/frgrisk/following{/other_user}\",\"gists_url\":\"https://api.github.com/users/frgrisk/gists{/gist_id}\",\"starred_url\":\"https://api.github.com/users/frgrisk/starred{/owner}{/repo}\",\"subscriptions_url\":\"https://api.github.com/users/frgrisk/subscriptions\",\"organizations_url\":\"https://api.github.com/users/frgrisk/orgs\",\"repos_url\":\"https://api.github.com/users/frgrisk/repos\",\"events_url\":\"https://api.github.com/users/frgrisk/events{/privacy}\",\"received_events_url\":\"https://api.github.com/users/frgrisk/received_events\",\"type\":\"Organization\",\"site_admin\":false},\"html_url\":\"https://github.com/frgrisk/test-repo\",\"description\":\"Test repository\",\"fork\":false,\"url\":\"https://api.github.com/repos/frgrisk/test-repo\"},\"organization\":{\"login\":\"frgrisk\",\"id\":12345678,\"node_id\":\"O_kgDOABCDEF\",\"url\":\"https://api.github.com/orgs/frgrisk\",\"repos_url\":\"https://api.github.com/orgs/frgrisk/repos\",\"events_url\":\"https://api.github.com/orgs/frgrisk/events\",\"hooks_url\":\"https://api.github.com/orgs/frgrisk/hooks\",\"issues_url\":\"https://api.github.com/orgs/frgrisk/issues\",\"members_url\":\"https://api.github.com/orgs/frgrisk/members{/member}\",\"public_members_url\":\"https://api.github.com/orgs/frgrisk/public_members{/member}\",\"avatar_url\":\"https://avatars.githubusercontent.com/u/12345678?v=4\",\"description\":\"FRG Risk\"},\"sender\":{\"login\":\"github-actions[bot]\",\"id\":41898282,\"node_id\":\"MDM6Qm90NDE4OTgyODI=\",\"avatar_url\":\"https://avatars.githubusercontent.com/in/15368?v=4\",\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/github-actions%5Bbot%5D\",\"html_url\":\"https://github.com/apps/github-actions\",\"followers_url\":\"https://api.github.com/users/github-actions%5Bbot%5D/followers\",\"following_url\":\"https://api.github.com/users/github-actions%5Bbot%5D/following{/other_user}\",\"gists_url\":\"https://api.github.com/users/github-actions%5Bbot%5D/gists{/gist_id}\",\"starred_url\":\"https://api.github.com/users/github-actions%5Bbot%5D/starred{/owner}{/repo}\",\"subscriptions_url\":\"https://api.github.com/users/github-actions%5Bbot%5D/subscriptions\",\"organizations_url\":\"https://api.github.com/users/github-actions%5Bbot%5D/orgs\",\"repos_url\":\"https://api.github.com/users/github-actions%5Bbot%5D/repos\",\"events_url\":\"https://api.github.com/users/github-actions%5Bbot%5D/events{/privacy}\",\"received_events_url\":\"https://api.github.com/users/github-actions%5Bbot%5D/received_events\",\"type\":\"Bot\",\"site_admin\":false}}", + "isBase64Encoded": false +} \ No newline at end of file diff --git a/main.go b/main.go index f7af115..d7b91ef 100644 --- a/main.go +++ b/main.go @@ -79,7 +79,7 @@ func handler(request events.APIGatewayProxyRequest) (events.APIGatewayProxyRespo secretOut, err := sm.GetSecretValue(context.TODO(), &secretsmanager.GetSecretValueInput{SecretId: aws.String(secretName)}) if err != nil { - slog.Error("failed to get secret", "error", err.Error()) + slog.Error("failed to get secret", "secret", secretName, "error", err.Error()) return events.APIGatewayProxyResponse{StatusCode: http.StatusInternalServerError}, err } @@ -114,6 +114,13 @@ func handler(request events.APIGatewayProxyRequest) (events.APIGatewayProxyRespo return events.APIGatewayProxyResponse{StatusCode: http.StatusInternalServerError}, errors.New("key name missing") } + instanceProfileArn := os.Getenv("INSTANCE_PROFILE_ARN") + if instanceProfileArn == "" { + slog.Error("INSTANCE_PROFILE_ARN env var not set") + + return events.APIGatewayProxyResponse{StatusCode: http.StatusInternalServerError}, errors.New("instance profile arn missing") + } + imageID := os.Getenv("IMAGE_ID") if imageID == "" { slog.Error("IMAGE_ID env var not set") @@ -175,6 +182,9 @@ func handler(request events.APIGatewayProxyRequest) (events.APIGatewayProxyRespo ImageId: aws.String(imageID), InstanceInitiatedShutdownBehavior: types.ShutdownBehaviorTerminate, InstanceType: instanceType, + IamInstanceProfile: &types.IamInstanceProfileSpecification{ + Arn: aws.String(instanceProfileArn), + }, NetworkInterfaces: []types.InstanceNetworkInterfaceSpecification{ { AssociatePublicIpAddress: aws.Bool(true), diff --git a/template.yaml b/template.yaml index b1b0d1a..ee8f81b 100644 --- a/template.yaml +++ b/template.yaml @@ -29,6 +29,37 @@ Globals: MemorySize: 128 Resources: + # IAM Role for EC2 instances + RunnerInstanceRole: + Type: AWS::IAM::Role + Properties: + AssumeRolePolicyDocument: + Version: '2012-10-17' + Statement: + - Effect: Allow + Principal: + Service: ec2.amazonaws.com + Action: 'sts:AssumeRole' + ManagedPolicyArns: + - arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore + Policies: + - PolicyName: CloudWatchLogsPolicy + PolicyDocument: + Version: '2012-10-17' + Statement: + - Effect: Allow + Action: + - logs:CreateLogGroup + - logs:CreateLogStream + - logs:PutLogEvents + - logs:DescribeLogStreams + Resource: !Sub 'arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/ec2/github-runner:*' + + RunnerInstanceProfile: + Type: AWS::IAM::InstanceProfile + Properties: + Roles: + - !Ref RunnerInstanceRole GitHubActionHookFunction: Type: AWS::Serverless::Function # More info about Function Resource: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#awsserverlessfunction Properties: @@ -54,6 +85,7 @@ Resources: SUBNET_ID: !Ref SubnetId SECURITY_GROUP_IDS: !Ref SecurityGroupIds KEY_NAME: !Ref KeyName + INSTANCE_PROFILE_ARN: !GetAtt RunnerInstanceProfile.Arn Policies: - Statement: - Sid: RunInstances diff --git a/user-data.sh b/user-data.sh index 301f394..7225372 100644 --- a/user-data.sh +++ b/user-data.sh @@ -1,25 +1,179 @@ #!/bin/bash +set -euo pipefail set -x + +# Get instance ID early for log stream naming +TOKEN=$(curl -s -X PUT "http://169.254.169.254/latest/api/token" -H "X-aws-ec2-metadata-token-ttl-seconds: 21600") +INSTANCE_ID=$(curl -s -H "X-aws-ec2-metadata-token: $TOKEN" http://169.254.169.254/latest/meta-data/instance-id) + +# CloudWatch logging setup +LOG_GROUP="/aws/ec2/github-runner" +LOG_STREAM="runner-${INSTANCE_ID}-$(date +%Y%m%d-%H%M%S)" +REGION="us-east-2" + +# Configure AWS CLI default region +aws configure set default.region ${REGION} + +# Function to log to CloudWatch +log_to_cloudwatch() { + local level=$1 + local message=$2 + local timestamp=$(date -u +%Y-%m-%dT%H:%M:%S.%3NZ) + + # Also log to console + echo "[${timestamp}] [${level}] ${message}" + + # Escape quotes and backslashes in message for JSON + message=$(echo "$message" | sed 's/\\/\\\\/g' | sed 's/"/\\"/g') + + # Create properly formatted JSON for log event + local log_event=$(printf '{"timestamp":%s,"message":"[%s] %s"}' "$(date +%s000)" "${level}" "${message}") + + # Send to CloudWatch + if ! aws logs put-log-events \ + --log-group-name "${LOG_GROUP}" \ + --log-stream-name "${LOG_STREAM}" \ + --log-events "${log_event}" \ + --region "${REGION}" 2>&1; then + echo "Failed to send log to CloudWatch" + fi +} + +# Create log group and stream +aws logs create-log-group --log-group-name "${LOG_GROUP}" --region "${REGION}" 2>/dev/null || true +aws logs create-log-stream --log-group-name "${LOG_GROUP}" --log-stream-name "${LOG_STREAM}" --region "${REGION}" 2>/dev/null || true + +log_to_cloudwatch "INFO" "Starting GitHub runner setup" + START_TIME=$(date +%s) + +# Set shutdown timer - this is the overall timeout shutdown +60 +log_to_cloudwatch "INFO" "Set 60-minute shutdown timer" + +# Update apt sources if needed sed -i 's/ap-southeast-3/us-east-2/g' /etc/apt/sources.list + +# Add ubuntu user to docker group usermod -aG docker ubuntu + +# Setup runner directory cd /opt -mkdir actions-runner +mkdir -p actions-runner chown -R ubuntu:ubuntu actions-runner cd actions-runner -sudo -u ubuntu tar xzf ../runner-cache/actions-runner-linux-* -C . -TOKEN=$(curl -X PUT "http://169.254.169.254/latest/api/token" -H "X-aws-ec2-metadata-token-ttl-seconds: 21600") -INSTANCE_TYPE=$(curl -H "X-aws-ec2-metadata-token: $TOKEN" http://169.254.169.254/latest/meta-data/instance-type) -GITHUB_TOKEN=$(curl -L \ - -X POST \ - -H "Accept: application/vnd.github+json" \ - -H "Authorization: Bearer {{.GitHubPAT}}" \ - -H "X-GitHub-Api-Version: 2022-11-28" \ - https://api.github.com/orgs/frgrisk/actions/runners/registration-token | jq -r .token) -sudo -u ubuntu ./config.sh --url https://github.com/frgrisk --token $GITHUB_TOKEN --disableupdate --ephemeral --labels $INSTANCE_TYPE,ephemeral,X64{{.ExtraLabels}} --unattended + +# Extract runner +log_to_cloudwatch "INFO" "Extracting GitHub runner" +if ! sudo -u ubuntu tar xzf ../runner-cache/actions-runner-linux-* -C .; then + log_to_cloudwatch "ERROR" "Failed to extract runner archive" + shutdown now + exit 1 +fi + +# Get instance type (we already have instance ID from earlier) +INSTANCE_TYPE=$(curl -s -H "X-aws-ec2-metadata-token: $TOKEN" http://169.254.169.254/latest/meta-data/instance-type) + +log_to_cloudwatch "INFO" "Instance: ${INSTANCE_ID}, Type: ${INSTANCE_TYPE}" + +# Function to get GitHub registration token with retry +get_github_token() { + local max_attempts=5 + local attempt=1 + local delay=5 + + while [ $attempt -le $max_attempts ]; do + log_to_cloudwatch "INFO" "Attempting to get GitHub registration token (attempt ${attempt}/${max_attempts})" + + GITHUB_TOKEN=$(curl -s -L \ + -X POST \ + -H "Accept: application/vnd.github+json" \ + -H "Authorization: Bearer {{.GitHubPAT}}" \ + -H "X-GitHub-Api-Version: 2022-11-28" \ + https://api.github.com/orgs/frgrisk/actions/runners/registration-token | jq -r .token) + + if [ -n "$GITHUB_TOKEN" ] && [ "$GITHUB_TOKEN" != "null" ]; then + log_to_cloudwatch "INFO" "Successfully obtained GitHub registration token" + return 0 + fi + + log_to_cloudwatch "WARN" "Failed to get GitHub token, retrying in ${delay} seconds..." + sleep $delay + delay=$((delay * 2)) + attempt=$((attempt + 1)) + done + + log_to_cloudwatch "ERROR" "Failed to get GitHub registration token after ${max_attempts} attempts" + return 1 +} + +# Get GitHub registration token +if ! get_github_token; then + log_to_cloudwatch "ERROR" "Unable to proceed without registration token" + shutdown now + exit 1 +fi + +# Configure runner with retry +log_to_cloudwatch "INFO" "Configuring GitHub runner" +max_config_attempts=3 +config_attempt=1 + +while [ $config_attempt -le $max_config_attempts ]; do + if sudo -u ubuntu ./config.sh \ + --url https://github.com/frgrisk \ + --token "$GITHUB_TOKEN" \ + --disableupdate \ + --ephemeral \ + --labels "${INSTANCE_TYPE},ephemeral,X64{{.ExtraLabels}}" \ + --unattended \ + --name "ephemeral-${INSTANCE_ID}" \ + --work _work; then + + log_to_cloudwatch "INFO" "Runner configured successfully" + break + else + log_to_cloudwatch "WARN" "Runner configuration failed (attempt ${config_attempt}/${max_config_attempts})" + config_attempt=$((config_attempt + 1)) + if [ $config_attempt -le $max_config_attempts ]; then + sleep 10 + fi + fi +done + +if [ $config_attempt -gt $max_config_attempts ]; then + log_to_cloudwatch "ERROR" "Failed to configure runner after ${max_config_attempts} attempts" + shutdown now + exit 1 +fi + END_TIME=$(date +%s) EXECUTION_TIME=$((END_TIME - START_TIME)) -echo "Script execution time: $EXECUTION_TIME seconds" | tee -a /var/log/setup-time.log -sudo -u ubuntu ./run.sh -shutdown now +log_to_cloudwatch "INFO" "Setup completed in ${EXECUTION_TIME} seconds" + +# Start the runner and wait for it to complete +log_to_cloudwatch "INFO" "Starting GitHub runner" + +# Create a temporary file to capture runner output +RUNNER_LOG=$(mktemp /tmp/runner-output.XXXXXX) + +if sudo -u ubuntu ./run.sh 2>&1 | tee "${RUNNER_LOG}"; then + log_to_cloudwatch "INFO" "Runner completed successfully" +else + EXIT_CODE=$? + log_to_cloudwatch "ERROR" "Runner exited with error code: ${EXIT_CODE}" + + # Send the last 50 lines of runner output to CloudWatch + RUNNER_ERROR=$(tail -n 50 "${RUNNER_LOG}" | head -c 4096) + log_to_cloudwatch "ERROR" "Runner output: ${RUNNER_ERROR}" +fi + +# Clean up +rm -f "${RUNNER_LOG}" + +# Cancel the 60-minute shutdown timer since we're shutting down normally +shutdown -c 2>/dev/null || true + +# Shutdown the instance +log_to_cloudwatch "INFO" "Shutting down instance after runner completion" +shutdown now \ No newline at end of file