├── .gitignore ├── .vscode └── tasks.json ├── LICENSE ├── README.md ├── deploy ├── main.tf ├── runner_module │ ├── main.tf │ └── variables.tf └── webhook_module │ ├── main.tf │ ├── outputs.tf │ └── variables.tf ├── deployment.drawio.svg ├── scripts ├── deploy-runner.sh ├── deploy-webhook.sh └── deploy.sh └── src ├── lambda-github-runner ├── Dockerfile ├── Dockerfile.base ├── go.mod ├── go.sum └── main.go └── lambda-github-webhook ├── go.mod ├── go.sum └── main.go /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # Dependency directories (remove the comment below to include it) 15 | # vendor/ 16 | 17 | # Local .terraform directories 18 | **/.terraform/* 19 | 20 | # .tfstate files 21 | *.tfstate 22 | *.tfstate.* 23 | 24 | # Crash log files 25 | crash.log 26 | 27 | # Exclude all .tfvars files, which are likely to contain sentitive data, such as 28 | # password, private keys, and other secrets. These should not be part of version 29 | # control as they are data points which are potentially sensitive and subject 30 | # to change depending on the environment. 31 | # 32 | *.tfvars 33 | 34 | # Ignore override files as they are usually used to override resources locally and so 35 | # are not checked in 36 | override.tf 37 | override.tf.json 38 | *_override.tf 39 | *_override.tf.json 40 | 41 | # Include override files you do wish to add to version control using negated pattern 42 | # 43 | # !example_override.tf 44 | 45 | # Include tfplan files to ignore the plan output of command: terraform plan -out=tfplan 46 | # example: *tfplan* 47 | 48 | # Ignore CLI configuration files 49 | .terraformrc 50 | terraform.rc 51 | src/lambda-github-webhook/lambda-github-webhook 52 | deploy/.terraform.lock.hcl 53 | deploy/runner_module/.terraform.lock.hcl 54 | deploy/webhook_module/.terraform.lock.hcl 55 | files/ 56 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=733558 3 | // for the documentation about the tasks.json format 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "label": "Build Base Runner Image", 8 | "type": "shell", 9 | "command": "docker", 10 | "args": [ 11 | "build", 12 | "--pull", 13 | "--rm", 14 | "-f", 15 | "src/lambda-github-runner/Dockerfile.base", 16 | "-t", 17 | "lambda-github-runner-base:latest", 18 | "src/lambda-github-runner" 19 | ], 20 | "problemMatcher": [] 21 | }, 22 | { 23 | "label": "Get AWS Lambda Container", 24 | "type": "shell", 25 | "command": "docker", 26 | "args": [ 27 | "pull", 28 | "public.ecr.aws/lambda/provided:al2" 29 | ], 30 | "problemMatcher": [] 31 | }, 32 | { 33 | "label": "Build Runner Image", 34 | "type": "shell", 35 | "command": "docker", 36 | "args": [ 37 | "build", 38 | "--rm", 39 | "-f", 40 | "src/lambda-github-runner/Dockerfile", 41 | "-t", 42 | "lambda-github-runner:latest", 43 | "src/lambda-github-runner" 44 | ], 45 | "dependsOn": [ 46 | "Get AWS Lambda Container", 47 | "Build Base Runner Image" 48 | ], 49 | "problemMatcher": [] 50 | }, 51 | { 52 | "label": "Get Webhook Mods", 53 | "type": "shell", 54 | "command": "go", 55 | "args": [ 56 | "mod", 57 | "download" 58 | ], 59 | "options": { 60 | "cwd": "src/lambda-github-webhook" 61 | }, 62 | "problemMatcher": [] 63 | }, 64 | { 65 | "label": "Build Webhook", 66 | "type": "shell", 67 | "command": "go", 68 | "args": [ 69 | "build" 70 | ], 71 | "options": { 72 | "env": { 73 | "GOOS": "linux" 74 | }, 75 | "cwd": "src/lambda-github-webhook" 76 | }, 77 | "dependsOn": [ 78 | "Get Webhook Mods" 79 | ], 80 | "problemMatcher": [] 81 | }, 82 | { 83 | "label": "Validate Terraform", 84 | "type": "shell", 85 | "command": "terraform", 86 | "args": [ 87 | "validate" 88 | ], 89 | "options": { 90 | "cwd": "deploy" 91 | }, 92 | "problemMatcher": [] 93 | }, 94 | { 95 | "label": "Plan Terraform", 96 | "type": "shell", 97 | "command": "terraform", 98 | "args": [ 99 | "plan", 100 | "-var-file=.tfvars" 101 | ], 102 | "options": { 103 | "cwd": "deploy" 104 | }, 105 | "problemMatcher": [] 106 | }, 107 | { 108 | "label": "Destroy Terraform", 109 | "type": "shell", 110 | "command": "terraform", 111 | "args": [ 112 | "destroy", 113 | "-var-file:.tfvars" 114 | ], 115 | "options": { 116 | "cwd": "deploy" 117 | }, 118 | "problemMatcher": [] 119 | }, 120 | { 121 | "label": "Apply Terraform", 122 | "type": "shell", 123 | "command": "terraform", 124 | "args": [ 125 | "apply", 126 | "-var-file:.tfvars" 127 | ], 128 | "options": { 129 | "cwd": "deploy" 130 | }, 131 | "problemMatcher": [] 132 | } 133 | ] 134 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Nathan Westfall 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Lambda GitHub Runner 2 | A completely serverless way to have simple self-hosted runners for GitHub Actions. Lightweight, fast and easy to deploy. 3 | 4 | ## Why it was built 5 | Once [Lambda Containers](https://aws.amazon.com/blogs/aws/new-for-aws-lambda-container-image-support/) came out, I wanted to see how far they could go. I know there is already a serverless way of doing Github Runners with AWS Fargate, but if you can do it all in Lambda why not? 6 | 7 | I also wanted to develop a way to do the last "job" in my actions in my own environment. This way, I could assign the AWS permissions to the lambda function and not have to rotate keys in GitHub, just use IAM roles. 8 | 9 | ## How it Works 10 | Using GitHub webhooks, we listen for the `check_run` event and then start up a runner when it's needed. This is now possible through [Lambda Containers](https://aws.amazon.com/blogs/aws/new-for-aws-lambda-container-image-support/). It uses a few different AWS services to make this possible. 11 | 12 | - Lambda (obviously) 13 | - `lambda-github-webhook` - This function handles the incoming webhooks to start/stop/setup runners 14 | - `lambda-github-runner` - This function is the actual self-hosted runner! 15 | - API Gateway 16 | - Gives you an endpoint for the GitHub webhooks 17 | - SQS 18 | - Used to "stop" the self-hosted runners in lambda when the action is complete 19 | - ECR 20 | - You are required to host the docker image in your own account for lambda (copies from public ECR repository) 21 | - Cloudwatch 22 | - All applications need logging, even if it's simple 23 | 24 | Using the lifecycle of an action and the `check_run` webhook, we are able to dynamically add/remove self-hosted runners from GitHub as they are needed. 25 | 26 | See working example - https://github.com/nwestfall/lambda-runner-test/actions 27 | 28 | ## Deployment Overview 29 | ![Deployment Diagram](deployment.drawio.svg) 30 | 31 | ### Generate a GitHub Token 32 | A GitHub token is required to setup the project. This token just needs `repo` access. It is used to generate GitHub Runner tokens used to add/remove the runners. 33 | 34 | ### How to Deploy 35 | This project includes a Terraform so you can essentially set this up in your environment with little to no effort. It uses the AWS credentials on your machine and pulls the lambda functions (`.zip` and docker image) from a public source, so you don't have to build locally (but you can if you want). You will need the following on your machine to get started 36 | 37 | - Terraform 38 | - AWS CLI v2 (yes, v2 is important) 39 | - Docker (to pull and push the images) 40 | 41 | Once you have everything above installed, just clone this repo, navigate to the `deploy` folder and run 42 | 43 | `terraform init` 44 | 45 | then 46 | 47 | `terraform plan` 48 | 49 | There are a number of variables you can provide before building to customize it in your account (listed below). Once you feel confortable about what it is setting up, just run the final command 50 | 51 | `terraform apply` 52 | 53 | Once it is successfully deployed, it will output your webhook URL (generated by API Gateway). This is what you will bring to your GitHub repository to setup webhooks. 54 | 55 | ### Setting up GitHub Webhooks 56 | Once you have the project deployed in your AWS account, you just need to setup the webhook endpoint (provided at the end of the terraform process) in the GitHub repository you wish to have use the runners. 57 | 58 | Go to the GitHub repository and select "Settings" -> "WebHooks". You will see an option to "Add webhook". Enter and change the following information. 59 | 60 | - Payload URL: The URL you got from the terraform script 61 | - Content Type: Change to "application/json" 62 | - Secret: Only enter if you set one up when deploying to AWS 63 | - "Which events would you like to trigger this webhook?": Select "Let me select individual events" and make sure just "check_run" is selected (by default, just "push" is selected). Other events are automatically ignored, but there is no sense in sending extra data. 64 | 65 | Once all this is entered, go ahead and create the webhook! The first delivery you should see is a "ping" event. This is to test the webhook and make sure everything is ok. If it returns a `200`, then you are good to go! 66 | 67 | Something to note is that to make sure an Action doesn't fail, a self-hosted runner **MUST** be setup (even if it's not running). If none are setup, the action will just fail rather they "wait" for an available runner. We use the "ping" event to setup a default runner called "DEFAULT-LAMBDA-DO-NOT-REMOVE". This should never be "active", but is used as a place holder to ensure that your actions don't immediately fail. All other runners will have a unique name and be short lived. 68 | 69 | ** Note that it is possible to accidently remove the default runner if you run an action immediately after setting it up. Please give it some time for the lambda function to become "cold" so that a new one is used (removing the other action) ** 70 | 71 | ## Known Limitations 72 | - GitHub Action steps that need Docker will **NOT** work. [Lambda Containers](https://aws.amazon.com/blogs/aws/new-for-aws-lambda-container-image-support/) currently don't support "docker-in-docker" 73 | - You only have 15 minutes to complete the job. Lambda functions timeout after that. (You can configure in the deployment for them to timeout sooner) 74 | 75 | ## More to Come 76 | I do have some future plans for this project, including supporting a hidden "configuration" file in the repoistory (like how GitHub does workflows in `.github`) to configure more aspects of the runner and maybe even use "AWS Fargate" over Lambda in certain cases due to a longer execution time or some other variable. Open to ideas! 77 | -------------------------------------------------------------------------------- /deploy/main.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_providers { 3 | aws = { 4 | source = "hashicorp/aws" 5 | } 6 | } 7 | } 8 | 9 | data "aws_caller_identity" "current" {} 10 | 11 | variable "aws_region" { 12 | description = "AWS Region to deploy in" 13 | default = "us-east-1" 14 | } 15 | 16 | variable "github_token" { 17 | description = "Github PAT token with REPO access" 18 | sensitive = true 19 | } 20 | 21 | variable "github_secret" { 22 | description = "Github Webhook Secret to validate payload signature" 23 | sensitive = true 24 | } 25 | 26 | variable "sqs_name" { 27 | description = "Name of the SQS queue for the lambda runner." 28 | default = "lambda-github-runner-queue" 29 | } 30 | 31 | variable "api_gateway_name" { 32 | description = "Name of the API Gateway for the lambda webhook." 33 | default = "lambda-github-webook" 34 | } 35 | 36 | variable "webhook_lambda_name" { 37 | description = "Name of the lambda function for the github webhook." 38 | default = "lambda-github-webhook" 39 | } 40 | 41 | variable "runner_lambda_name" { 42 | description = "Name of the lambda function for the github action runner." 43 | default = "lambda-github-runner" 44 | } 45 | 46 | variable "runner_repo_uri" { 47 | description = "Repo URI to use for Lambda Runner (only if you want a custom version)" 48 | default = "public.ecr.aws/n9q0k4a8" 49 | } 50 | 51 | variable "runner_image_uri" { 52 | description = "Image URI to use for Lambda Runner (only change if you want a custom version)" 53 | default = "public.ecr.aws/n9q0k4a8/lambda-github-runner:latest" 54 | } 55 | 56 | variable "runner_timeout" { 57 | description = "Timeout of Github Runner in seconds" 58 | default = 900 59 | type = number 60 | validation { 61 | condition = var.runner_timeout > 0 && var.runner_timeout < 901 62 | error_message = "Runner timeout must be in-between 1 and 900." 63 | } 64 | } 65 | 66 | variable "runner_memory" { 67 | description = "Memory configuration for the Github runner" 68 | default = 2048 69 | type = number 70 | validation { 71 | condition = var.runner_memory >= 128 && var.runner_memory <= 10240 && var.runner_memory % 128 == 0 72 | error_message = "Runner memory must be in-between 128 and 10240, and in increments of 128." 73 | } 74 | } 75 | 76 | variable "cloudwatch_retention_days" { 77 | description = "Number of days to keep cloudwatch logs." 78 | default = 14 79 | type = number 80 | validation { 81 | condition = var.cloudwatch_retention_days > 0 82 | error_message = "Cloudwatch retention days must be greater then 0." 83 | } 84 | } 85 | 86 | provider "aws" { 87 | region = var.aws_region 88 | } 89 | 90 | resource "aws_iam_policy" "lambda_runner_logging" { 91 | name = "${var.runner_lambda_name}_logging" 92 | path = "/" 93 | description = "IAM policy for logging from ${var.runner_lambda_name}" 94 | 95 | policy = < 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 |
48 |
49 |
50 | ping/check_run events 51 |
52 |
53 |
54 |
55 | 56 | ping/check_run events 57 | 58 |
59 |
60 | 61 | 62 | 63 | 64 |
65 |
66 |
67 | if `ping` or `check_run` has 68 |
69 | the action `created`, start the runner 70 |
71 |
72 |
73 |
74 | 75 | if `ping` or `check_run` has... 76 | 77 |
78 |
79 | 80 | 81 | 82 | 83 |
84 |
85 |
86 | Access Logs 87 |
88 |
89 |
90 |
91 | 92 | Access Logs 93 | 94 |
95 |
96 | 97 | 98 | 99 | 100 |
101 |
102 |
103 | if `check_run` has the 104 |
105 | action `completed`, send 106 |
107 | completion message to SQS 108 |
109 |
110 |
111 |
112 | 113 | if `check_run` has the... 114 | 115 |
116 |
117 | 118 | 119 | 120 | 121 |
122 |
123 |
124 | Listen for events related to 125 |
126 | the check_run_id, 127 |
128 | stop if received 129 |
130 |
131 |
132 |
133 | 134 | Listen for events related... 135 | 136 |
137 |
138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 |
146 |
147 |
148 | Log Messages 149 |
150 | Advanced logs from 151 |
152 | GitHub Runner available 153 |
154 |
155 |
156 |
157 | 158 | Log Messages... 159 | 160 |
161 |
162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 |
170 |
171 |
172 | Log Messages 173 |
174 |
175 |
176 |
177 | 178 | Log Messages 179 | 180 |
181 |
182 | 183 | 184 | 185 | 186 |
187 |
188 |
189 | Invoke Webhook Function 190 |
191 |
192 |
193 |
194 | 195 | Invoke Webhook Function 196 | 197 |
198 |
199 | 200 | 201 | 202 | 203 |
204 |
205 |
206 | Add or Remove Runner from GitHub Actions 207 |
208 |
209 |
210 |
211 | 212 | Add or Remove Runner from GitHub Actions 213 | 214 |
215 |
216 |
217 | 218 | 219 | 220 | 221 | Viewer does not support full SVG 1.1 222 | 223 | 224 | 225 | -------------------------------------------------------------------------------- /scripts/deploy-runner.sh: -------------------------------------------------------------------------------- 1 | echo "Building Runner Base" 2 | docker build --pull --rm -f "../src/lambda-github-runner/Dockerfile.base" -t lambda-github-runner-base:latest "../src/lambda-github-runner" 3 | echo "Runner Base Built" 4 | echo "Building Runner" 5 | docker build --rm -f "../src/lambda-github-runner/Dockerfile" -t public.ecr.aws/n9q0k4a8/lambda-github-runner:latest "../src/lambda-github-runner" 6 | echo "Runner Built" 7 | aws ecr-public get-login-password --region us-east-1 | docker login --username AWS --password-stdin public.ecr.aws/n9q0k4a8 8 | echo "Pushing Runner Container" 9 | docker push public.ecr.aws/n9q0k4a8/lambda-github-runner:latest 10 | echo "Runner Container Pushed" -------------------------------------------------------------------------------- /scripts/deploy-webhook.sh: -------------------------------------------------------------------------------- 1 | cd ../src/lambda-github-webhook 2 | go mod download 3 | GOOS=linux go build -o ../../main 4 | cd ../../ 5 | zip lambda-github-webhook-function.zip main 6 | 7 | echo "Updating Webhook" 8 | aws s3 cp lambda-github-webhook-function.zip s3://lambda-github-webhook 9 | echo "Webhook Updated" 10 | 11 | rm -rf main 12 | rm -rf lambda-github-webhook-function.zip -------------------------------------------------------------------------------- /scripts/deploy.sh: -------------------------------------------------------------------------------- 1 | bash deploy-webhook.sh 2 | bash deploy-runner.sh -------------------------------------------------------------------------------- /src/lambda-github-runner/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM public.ecr.aws/lambda/provided:al2 as build 2 | # install compiler 3 | RUN yum install -y golang 4 | RUN go env -w GOPROXY=direct 5 | # cache dependencies 6 | ADD go.mod go.sum ./ 7 | RUN go mod download 8 | # build 9 | ADD main.go . 10 | RUN go build -o /main 11 | 12 | FROM lambda-github-runner-base:latest 13 | 14 | # Copy Lambda Entrypoint 15 | COPY --from=build /main /main 16 | 17 | ENTRYPOINT ["/main"] -------------------------------------------------------------------------------- /src/lambda-github-runner/Dockerfile.base: -------------------------------------------------------------------------------- 1 | FROM debian:buster-slim 2 | 3 | ARG DEBIAN_FRONTEND=noninteractive 4 | ARG GIT_VERSION="2.26.2" 5 | ARG GH_RUNNER_VERSION 6 | ARG DOCKER_COMPOSE_VERSION="1.27.4" 7 | 8 | ENV RUNNER_NAME="" 9 | ENV RUNNER_WORK_DIRECTORY="_work" 10 | ENV RUNNER_TOKEN="" 11 | ENV RUNNER_REPOSITORY_URL="" 12 | ENV RUNNER_LABELS="" 13 | ENV RUNNER_ALLOW_RUNASROOT=true 14 | ENV GITHUB_ACCESS_TOKEN="" 15 | ENV AGENT_TOOLSDIRECTORY=/opt/hostedtoolcache 16 | 17 | RUN DEBIAN_FRONTEND=noninteractive apt-get update && \ 18 | apt-get install -y \ 19 | curl \ 20 | unzip \ 21 | apt-transport-https \ 22 | ca-certificates \ 23 | software-properties-common \ 24 | sudo \ 25 | supervisor \ 26 | jq \ 27 | iputils-ping \ 28 | build-essential \ 29 | zlib1g-dev \ 30 | gettext \ 31 | liblttng-ust0 \ 32 | libcurl4-openssl-dev \ 33 | openssh-client && \ 34 | rm -rf /var/lib/apt/lists/* && \ 35 | apt-get clean 36 | 37 | RUN cd /tmp && \ 38 | curl -sL -o git.tgz \ 39 | https://www.kernel.org/pub/software/scm/git/git-${GIT_VERSION}.tar.gz && \ 40 | tar zxf git.tgz && \ 41 | cd git-${GIT_VERSION} && \ 42 | ./configure --prefix=/usr && \ 43 | make && \ 44 | make install && \ 45 | rm -rf /tmp/* 46 | 47 | RUN mkdir -p /runner ${AGENT_TOOLSDIRECTORY} 48 | 49 | WORKDIR /runner 50 | 51 | RUN GH_RUNNER_VERSION=${GH_RUNNER_VERSION:-$(curl --silent "https://api.github.com/repos/actions/runner/releases/latest" | grep tag_name | sed -E 's/.*"v([^"]+)".*/\1/')} \ 52 | && curl -L -O https://github.com/actions/runner/releases/download/v${GH_RUNNER_VERSION}/actions-runner-linux-x64-${GH_RUNNER_VERSION}.tar.gz \ 53 | && tar -zxf actions-runner-linux-x64-${GH_RUNNER_VERSION}.tar.gz \ 54 | && rm -f actions-runner-linux-x64-${GH_RUNNER_VERSION}.tar.gz \ 55 | && ./bin/installdependencies.sh \ 56 | && chown -R root: /runner \ 57 | && rm -rf /var/lib/apt/lists/* \ 58 | && apt-get clean 59 | 60 | ENV RUNNER_TOOL_CACHE="/tmp/toolcache" -------------------------------------------------------------------------------- /src/lambda-github-runner/go.mod: -------------------------------------------------------------------------------- 1 | module lambda-github-runner 2 | 3 | go 1.15 4 | 5 | require ( 6 | github.com/aws/aws-lambda-go v1.20.0 7 | github.com/aws/aws-sdk-go v1.36.15 8 | ) 9 | -------------------------------------------------------------------------------- /src/lambda-github-runner/go.sum: -------------------------------------------------------------------------------- 1 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 2 | github.com/aws/aws-lambda-go v1.20.0 h1:ZSweJx/Hy9BoIDXKBEh16vbHH0t0dehnF8MKpMiOWc0= 3 | github.com/aws/aws-lambda-go v1.20.0/go.mod h1:jJmlefzPfGnckuHdXX7/80O3BvUUi12XOkbv4w9SGLU= 4 | github.com/aws/aws-sdk-go v1.36.15 h1:nGqgPlXegCKPZOKXvWnYCLvLPJPRoSOHHn9d0N0DG7Y= 5 | github.com/aws/aws-sdk-go v1.36.15/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro= 6 | github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= 7 | github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= 8 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 9 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 10 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 11 | github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= 12 | github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= 13 | github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= 14 | github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= 15 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 16 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 17 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 18 | github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 19 | github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= 20 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 21 | github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= 22 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 23 | github.com/urfave/cli/v2 v2.2.0/go.mod h1:SE9GqnLQmjVa0iPEY0f1w3ygNIYcIJ0OKPMoW2caLfQ= 24 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 25 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 26 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 27 | golang.org/x/net v0.0.0-20201110031124-69a78807bb2b h1:uwuIcX0g4Yl1NC5XAz37xsr2lTtcqevgzYNVt49waME= 28 | golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 29 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 30 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 31 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 32 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 33 | golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k= 34 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 35 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 36 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 37 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 38 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 39 | gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= 40 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 41 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 42 | gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776 h1:tQIYjPdBoyREyB9XMu+nnTclpTYkz2zFM+lzLJFO4gQ= 43 | gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 44 | -------------------------------------------------------------------------------- /src/lambda-github-runner/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "io/ioutil" 8 | "net/http" 9 | "os" 10 | "os/exec" 11 | "path/filepath" 12 | "strings" 13 | "time" 14 | 15 | "github.com/aws/aws-lambda-go/lambda" 16 | "github.com/aws/aws-lambda-go/lambdacontext" 17 | 18 | "github.com/aws/aws-sdk-go/aws" 19 | "github.com/aws/aws-sdk-go/aws/session" 20 | "github.com/aws/aws-sdk-go/service/sqs" 21 | ) 22 | 23 | // RunnerEvent is the event passed in 24 | type RunnerEvent struct { 25 | QueueURL string `json:"queue_url"` 26 | RepoURL string `json:"repo_url"` 27 | RepoFullName string `json:"repo_fullname"` 28 | Token string `json:"token"` 29 | VirtualID string `json:"virtual_id"` 30 | Event string `json:"event"` 31 | } 32 | 33 | // RunnerToken is the token used for the github runner 34 | type RunnerToken struct { 35 | Token string `json:"token"` 36 | } 37 | 38 | // HandleRequest handles the lambda request 39 | func HandleRequest(ctx context.Context, event RunnerEvent) (string, error) { 40 | defer func() { 41 | if e := recover(); e != nil { 42 | fmt.Println("Recovered from panic", e) 43 | } 44 | }() 45 | 46 | tempToken := event.Token 47 | event.Token = "******" 48 | fmt.Println(event) 49 | event.Token = tempToken 50 | 51 | lc, _ := lambdacontext.FromContext(ctx) 52 | fmt.Println("Getting runner token") 53 | client := http.Client{ 54 | Timeout: time.Duration(30 * time.Second), 55 | } 56 | url := "https://api.github.com/repos/" + event.RepoFullName + "/actions/runners/registration-token" 57 | fmt.Println(url) 58 | request, _ := http.NewRequest("POST", url, nil) 59 | request.Header.Set("Accept", "application/vnd.github.v3+json") 60 | request.Header.Set("Authorization", "token "+event.Token) 61 | request.Header.Set("User-Agent", "lambda-github-runner") 62 | resp, err := client.Do(request) 63 | if err != nil || resp.StatusCode != 201 { 64 | fmt.Println("Unable to get runner registrtation token", err) 65 | fmt.Println(resp) 66 | return "Unable to get runner registration token", err 67 | } 68 | defer resp.Body.Close() 69 | regToken := RunnerToken{} 70 | regData, _ := ioutil.ReadAll(resp.Body) 71 | json.Unmarshal(regData, ®Token) 72 | 73 | fmt.Println("Move runner directory to lambda /tmp") 74 | err = os.Mkdir("/tmp/runner", 0755) 75 | if os.IsExist(err) == false { 76 | err = copy("/runner", "/tmp/runner") 77 | if err != nil { 78 | fmt.Println("Unable to copy runner", err) 79 | return "Unable to copy runner", err 80 | } 81 | } 82 | err = os.Mkdir("/tmp/toolcache", 0755) 83 | 84 | fmt.Println("Removing runner in case one already exists") 85 | err = stopAndDecomissionRunner(regToken) 86 | if err != nil { 87 | fmt.Printf("Unable to remove runner, going to continue anyway\n", err) 88 | } 89 | 90 | fmt.Printf("Configuring runner (Request: %s|RepoUrl: %s|RepoFullName: %s|QueueUrl: %s)...\n", lc.AwsRequestID, event.RepoURL, event.RepoFullName, event.QueueURL) 91 | runnerName := "lambda-" + lc.AwsRequestID 92 | if event.Event == "create" { 93 | runnerName = "DEFAULT-LAMBDA-DO-NOT-REMOVE" 94 | fmt.Println("Creating default runner") 95 | } 96 | 97 | configcmd := exec.Command("/tmp/runner/config.sh", "--url", event.RepoURL, "--token", regToken.Token, "--name", runnerName, "--runnergroup", "lambda", "--labels", "lambda", "--work", "_work", "--replace") 98 | out, err := configcmd.Output() 99 | if err != nil { 100 | fmt.Println(string(out), err) 101 | readRunnerLogs() 102 | return fmt.Sprint(out), err 103 | } 104 | if os.Getenv("ALWAYS_PRINT_LOGS") == "true" { 105 | readRunnerLogs() 106 | } 107 | 108 | // if event is 'created', bail after configured 109 | if event.Event == "create" { 110 | fmt.Println("This is a create event, stopping runner") 111 | return fmt.Sprint("Runner created"), nil 112 | } 113 | 114 | fmt.Println("Starting runner...") 115 | err = startRunner() 116 | if err != nil { 117 | return fmt.Sprint("Unable to start runner"), err 118 | } 119 | 120 | fmt.Println("Runner started...") 121 | // Setup Virtual Queue and wait for Message 122 | sess := session.Must(session.NewSessionWithOptions(session.Options{ 123 | SharedConfigState: session.SharedConfigEnable, 124 | })) 125 | 126 | svc := sqs.New(sess) 127 | fmt.Println("Starting to listener for complete message") 128 | for { 129 | msgResult, err := svc.ReceiveMessage(&sqs.ReceiveMessageInput{ 130 | AttributeNames: []*string{ 131 | aws.String(sqs.MessageSystemAttributeNameSentTimestamp), 132 | }, 133 | MessageAttributeNames: []*string{ 134 | aws.String(sqs.QueueAttributeNameAll), 135 | }, 136 | QueueUrl: aws.String(event.QueueURL), 137 | MaxNumberOfMessages: aws.Int64(1), 138 | VisibilityTimeout: aws.Int64(60), 139 | }) 140 | 141 | if err != nil { 142 | fmt.Println("Error while receiving messages", err) 143 | } 144 | 145 | if len(msgResult.Messages) > 0 { 146 | if strings.Compare(*msgResult.Messages[0].Body, event.VirtualID) == 0 { 147 | fmt.Println("Message received, closing runner") 148 | _, err = svc.DeleteMessage(&sqs.DeleteMessageInput{ 149 | QueueUrl: aws.String(event.QueueURL), 150 | ReceiptHandle: msgResult.Messages[0].ReceiptHandle, 151 | }) 152 | if err != nil { 153 | fmt.Println("Error while deleting message", err) 154 | } 155 | break 156 | } 157 | } 158 | 159 | deadline, _ := ctx.Deadline() 160 | deadline = deadline.Add(-30 * time.Second) 161 | 162 | if time.Until(deadline).Seconds() < 0 { 163 | fmt.Println("Function is about to timeout, decomissioning") 164 | break 165 | } 166 | } 167 | 168 | err = stopAndDecomissionRunner(regToken) 169 | 170 | fmt.Println("Complete!") 171 | return fmt.Sprint("Complete!"), err 172 | } 173 | 174 | // startRunner starts the github runner 175 | func startRunner() error { 176 | runcmd := exec.Command("/tmp/runner/run.sh") 177 | // Something with output 178 | return runcmd.Start() 179 | } 180 | 181 | // stopAndDecomissionRunner stops and removes the runner 182 | func stopAndDecomissionRunner(event RunnerToken) error { 183 | fmt.Println("Removing runner...") 184 | configcmd := exec.Command("/tmp/runner/config.sh", "remove", "--token", event.Token) 185 | _, err := configcmd.Output() 186 | if err != nil { 187 | fmt.Println("Unable to remove runner", err) 188 | return err 189 | } 190 | fmt.Println("Runner removed") 191 | return nil 192 | } 193 | 194 | // readRunnerLogs reads the runners logs to console 195 | func readRunnerLogs() { 196 | fmt.Println("Reading logs...") 197 | // Try to get logs 198 | files, _ := ioutil.ReadDir("/tmp/runner/_diag") 199 | for _, f := range files { 200 | fmt.Println(f.Name()) 201 | content, _ := ioutil.ReadFile("/tmp/runner/_diag/" + f.Name()) 202 | fmt.Println(string(content)) 203 | } 204 | fmt.Println("Done reading logs...") 205 | } 206 | 207 | // copy copies the source to destination directories recursively 208 | func copy(source, destination string) error { 209 | var err error = filepath.Walk(source, func(path string, info os.FileInfo, err error) error { 210 | var relPath string = strings.Replace(path, source, "", 1) 211 | if relPath == "" { 212 | return nil 213 | } 214 | if info.IsDir() { 215 | return os.Mkdir(filepath.Join(destination, relPath), 0755) 216 | } 217 | 218 | var data, err1 = ioutil.ReadFile(filepath.Join(source, relPath)) 219 | if err1 != nil { 220 | return err1 221 | } 222 | return ioutil.WriteFile(filepath.Join(destination, relPath), data, 0777) 223 | }) 224 | return err 225 | } 226 | 227 | func main() { 228 | lambda.Start(HandleRequest) 229 | } 230 | -------------------------------------------------------------------------------- /src/lambda-github-webhook/go.mod: -------------------------------------------------------------------------------- 1 | module lambda-github-webhook 2 | 3 | go 1.15 4 | 5 | require ( 6 | "github.com/aws/aws-lambda-go" v1.20.0 7 | "github.com/aws/aws-sdk-go" v1.36.15 8 | ) -------------------------------------------------------------------------------- /src/lambda-github-webhook/go.sum: -------------------------------------------------------------------------------- 1 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 2 | github.com/aws/aws-lambda-go v1.20.0 h1:ZSweJx/Hy9BoIDXKBEh16vbHH0t0dehnF8MKpMiOWc0= 3 | github.com/aws/aws-lambda-go v1.20.0/go.mod h1:jJmlefzPfGnckuHdXX7/80O3BvUUi12XOkbv4w9SGLU= 4 | github.com/aws/aws-sdk-go v1.36.15 h1:nGqgPlXegCKPZOKXvWnYCLvLPJPRoSOHHn9d0N0DG7Y= 5 | github.com/aws/aws-sdk-go v1.36.15/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro= 6 | github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= 7 | github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= 8 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 9 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 10 | github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= 11 | github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= 12 | github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= 13 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 14 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 15 | github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 16 | github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= 17 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 18 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 19 | github.com/urfave/cli/v2 v2.2.0/go.mod h1:SE9GqnLQmjVa0iPEY0f1w3ygNIYcIJ0OKPMoW2caLfQ= 20 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 21 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 22 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 23 | golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 24 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 25 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 26 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 27 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 28 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 29 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 30 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 31 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 32 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 33 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 34 | gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 35 | -------------------------------------------------------------------------------- /src/lambda-github-webhook/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "crypto/hmac" 6 | "crypto/sha256" 7 | "encoding/hex" 8 | "encoding/json" 9 | "fmt" 10 | "os" 11 | "strconv" 12 | "strings" 13 | "time" 14 | 15 | "github.com/aws/aws-lambda-go/events" 16 | "github.com/aws/aws-lambda-go/lambda" 17 | "github.com/aws/aws-sdk-go/aws" 18 | "github.com/aws/aws-sdk-go/aws/session" 19 | lmbda "github.com/aws/aws-sdk-go/service/lambda" 20 | "github.com/aws/aws-sdk-go/service/sqs" 21 | ) 22 | 23 | // AutoGenerated is the github webhook event payload 24 | type AutoGenerated struct { 25 | Action string `json:"action"` 26 | CheckRun struct { 27 | ID int `json:"id"` 28 | NodeID string `json:"node_id"` 29 | HeadSha string `json:"head_sha"` 30 | ExternalID string `json:"external_id"` 31 | URL string `json:"url"` 32 | HTMLURL string `json:"html_url"` 33 | DetailsURL string `json:"details_url"` 34 | Status string `json:"status"` 35 | Conclusion interface{} `json:"conclusion"` 36 | StartedAt time.Time `json:"started_at"` 37 | CompletedAt interface{} `json:"completed_at"` 38 | Output struct { 39 | Title interface{} `json:"title"` 40 | Summary interface{} `json:"summary"` 41 | Text interface{} `json:"text"` 42 | AnnotationsCount int `json:"annotations_count"` 43 | AnnotationsURL string `json:"annotations_url"` 44 | } `json:"output"` 45 | Name string `json:"name"` 46 | CheckSuite struct { 47 | ID int `json:"id"` 48 | NodeID string `json:"node_id"` 49 | HeadBranch string `json:"head_branch"` 50 | HeadSha string `json:"head_sha"` 51 | Status string `json:"status"` 52 | Conclusion interface{} `json:"conclusion"` 53 | URL string `json:"url"` 54 | Before string `json:"before"` 55 | After string `json:"after"` 56 | PullRequests []interface{} `json:"pull_requests"` 57 | App struct { 58 | ID int `json:"id"` 59 | Slug string `json:"slug"` 60 | NodeID string `json:"node_id"` 61 | Owner struct { 62 | Login string `json:"login"` 63 | ID int `json:"id"` 64 | NodeID string `json:"node_id"` 65 | AvatarURL string `json:"avatar_url"` 66 | GravatarID string `json:"gravatar_id"` 67 | URL string `json:"url"` 68 | HTMLURL string `json:"html_url"` 69 | FollowersURL string `json:"followers_url"` 70 | FollowingURL string `json:"following_url"` 71 | GistsURL string `json:"gists_url"` 72 | StarredURL string `json:"starred_url"` 73 | SubscriptionsURL string `json:"subscriptions_url"` 74 | OrganizationsURL string `json:"organizations_url"` 75 | ReposURL string `json:"repos_url"` 76 | EventsURL string `json:"events_url"` 77 | ReceivedEventsURL string `json:"received_events_url"` 78 | Type string `json:"type"` 79 | SiteAdmin bool `json:"site_admin"` 80 | } `json:"owner"` 81 | Name string `json:"name"` 82 | Description string `json:"description"` 83 | ExternalURL string `json:"external_url"` 84 | HTMLURL string `json:"html_url"` 85 | CreatedAt time.Time `json:"created_at"` 86 | UpdatedAt time.Time `json:"updated_at"` 87 | Permissions struct { 88 | Actions string `json:"actions"` 89 | Checks string `json:"checks"` 90 | Contents string `json:"contents"` 91 | Deployments string `json:"deployments"` 92 | Issues string `json:"issues"` 93 | Metadata string `json:"metadata"` 94 | Packages string `json:"packages"` 95 | Pages string `json:"pages"` 96 | PullRequests string `json:"pull_requests"` 97 | RepositoryHooks string `json:"repository_hooks"` 98 | RepositoryProjects string `json:"repository_projects"` 99 | SecurityEvents string `json:"security_events"` 100 | Statuses string `json:"statuses"` 101 | VulnerabilityAlerts string `json:"vulnerability_alerts"` 102 | } `json:"permissions"` 103 | Events []string `json:"events"` 104 | } `json:"app"` 105 | CreatedAt time.Time `json:"created_at"` 106 | UpdatedAt time.Time `json:"updated_at"` 107 | } `json:"check_suite"` 108 | App struct { 109 | ID int `json:"id"` 110 | Slug string `json:"slug"` 111 | NodeID string `json:"node_id"` 112 | Owner struct { 113 | Login string `json:"login"` 114 | ID int `json:"id"` 115 | NodeID string `json:"node_id"` 116 | AvatarURL string `json:"avatar_url"` 117 | GravatarID string `json:"gravatar_id"` 118 | URL string `json:"url"` 119 | HTMLURL string `json:"html_url"` 120 | FollowersURL string `json:"followers_url"` 121 | FollowingURL string `json:"following_url"` 122 | GistsURL string `json:"gists_url"` 123 | StarredURL string `json:"starred_url"` 124 | SubscriptionsURL string `json:"subscriptions_url"` 125 | OrganizationsURL string `json:"organizations_url"` 126 | ReposURL string `json:"repos_url"` 127 | EventsURL string `json:"events_url"` 128 | ReceivedEventsURL string `json:"received_events_url"` 129 | Type string `json:"type"` 130 | SiteAdmin bool `json:"site_admin"` 131 | } `json:"owner"` 132 | Name string `json:"name"` 133 | Description string `json:"description"` 134 | ExternalURL string `json:"external_url"` 135 | HTMLURL string `json:"html_url"` 136 | CreatedAt time.Time `json:"created_at"` 137 | UpdatedAt time.Time `json:"updated_at"` 138 | Permissions struct { 139 | Actions string `json:"actions"` 140 | Checks string `json:"checks"` 141 | Contents string `json:"contents"` 142 | Deployments string `json:"deployments"` 143 | Issues string `json:"issues"` 144 | Metadata string `json:"metadata"` 145 | Packages string `json:"packages"` 146 | Pages string `json:"pages"` 147 | PullRequests string `json:"pull_requests"` 148 | RepositoryHooks string `json:"repository_hooks"` 149 | RepositoryProjects string `json:"repository_projects"` 150 | SecurityEvents string `json:"security_events"` 151 | Statuses string `json:"statuses"` 152 | VulnerabilityAlerts string `json:"vulnerability_alerts"` 153 | } `json:"permissions"` 154 | Events []string `json:"events"` 155 | } `json:"app"` 156 | PullRequests []interface{} `json:"pull_requests"` 157 | } `json:"check_run"` 158 | Repository struct { 159 | ID int `json:"id"` 160 | NodeID string `json:"node_id"` 161 | Name string `json:"name"` 162 | FullName string `json:"full_name"` 163 | Private bool `json:"private"` 164 | Owner struct { 165 | Login string `json:"login"` 166 | ID int `json:"id"` 167 | NodeID string `json:"node_id"` 168 | AvatarURL string `json:"avatar_url"` 169 | GravatarID string `json:"gravatar_id"` 170 | URL string `json:"url"` 171 | HTMLURL string `json:"html_url"` 172 | FollowersURL string `json:"followers_url"` 173 | FollowingURL string `json:"following_url"` 174 | GistsURL string `json:"gists_url"` 175 | StarredURL string `json:"starred_url"` 176 | SubscriptionsURL string `json:"subscriptions_url"` 177 | OrganizationsURL string `json:"organizations_url"` 178 | ReposURL string `json:"repos_url"` 179 | EventsURL string `json:"events_url"` 180 | ReceivedEventsURL string `json:"received_events_url"` 181 | Type string `json:"type"` 182 | SiteAdmin bool `json:"site_admin"` 183 | } `json:"owner"` 184 | HTMLURL string `json:"html_url"` 185 | Description string `json:"description"` 186 | Fork bool `json:"fork"` 187 | URL string `json:"url"` 188 | ForksURL string `json:"forks_url"` 189 | KeysURL string `json:"keys_url"` 190 | CollaboratorsURL string `json:"collaborators_url"` 191 | TeamsURL string `json:"teams_url"` 192 | HooksURL string `json:"hooks_url"` 193 | IssueEventsURL string `json:"issue_events_url"` 194 | EventsURL string `json:"events_url"` 195 | AssigneesURL string `json:"assignees_url"` 196 | BranchesURL string `json:"branches_url"` 197 | TagsURL string `json:"tags_url"` 198 | BlobsURL string `json:"blobs_url"` 199 | GitTagsURL string `json:"git_tags_url"` 200 | GitRefsURL string `json:"git_refs_url"` 201 | TreesURL string `json:"trees_url"` 202 | StatusesURL string `json:"statuses_url"` 203 | LanguagesURL string `json:"languages_url"` 204 | StargazersURL string `json:"stargazers_url"` 205 | ContributorsURL string `json:"contributors_url"` 206 | SubscribersURL string `json:"subscribers_url"` 207 | SubscriptionURL string `json:"subscription_url"` 208 | CommitsURL string `json:"commits_url"` 209 | GitCommitsURL string `json:"git_commits_url"` 210 | CommentsURL string `json:"comments_url"` 211 | IssueCommentURL string `json:"issue_comment_url"` 212 | ContentsURL string `json:"contents_url"` 213 | CompareURL string `json:"compare_url"` 214 | MergesURL string `json:"merges_url"` 215 | ArchiveURL string `json:"archive_url"` 216 | DownloadsURL string `json:"downloads_url"` 217 | IssuesURL string `json:"issues_url"` 218 | PullsURL string `json:"pulls_url"` 219 | MilestonesURL string `json:"milestones_url"` 220 | NotificationsURL string `json:"notifications_url"` 221 | LabelsURL string `json:"labels_url"` 222 | ReleasesURL string `json:"releases_url"` 223 | DeploymentsURL string `json:"deployments_url"` 224 | CreatedAt time.Time `json:"created_at"` 225 | UpdatedAt time.Time `json:"updated_at"` 226 | PushedAt time.Time `json:"pushed_at"` 227 | GitURL string `json:"git_url"` 228 | SSHURL string `json:"ssh_url"` 229 | CloneURL string `json:"clone_url"` 230 | SvnURL string `json:"svn_url"` 231 | Homepage interface{} `json:"homepage"` 232 | Size int `json:"size"` 233 | StargazersCount int `json:"stargazers_count"` 234 | WatchersCount int `json:"watchers_count"` 235 | Language string `json:"language"` 236 | HasIssues bool `json:"has_issues"` 237 | HasProjects bool `json:"has_projects"` 238 | HasDownloads bool `json:"has_downloads"` 239 | HasWiki bool `json:"has_wiki"` 240 | HasPages bool `json:"has_pages"` 241 | ForksCount int `json:"forks_count"` 242 | MirrorURL interface{} `json:"mirror_url"` 243 | Archived bool `json:"archived"` 244 | Disabled bool `json:"disabled"` 245 | OpenIssuesCount int `json:"open_issues_count"` 246 | License interface{} `json:"license"` 247 | Forks int `json:"forks"` 248 | OpenIssues int `json:"open_issues"` 249 | Watchers int `json:"watchers"` 250 | DefaultBranch string `json:"default_branch"` 251 | } `json:"repository"` 252 | Sender struct { 253 | Login string `json:"login"` 254 | ID int `json:"id"` 255 | NodeID string `json:"node_id"` 256 | AvatarURL string `json:"avatar_url"` 257 | GravatarID string `json:"gravatar_id"` 258 | URL string `json:"url"` 259 | HTMLURL string `json:"html_url"` 260 | FollowersURL string `json:"followers_url"` 261 | FollowingURL string `json:"following_url"` 262 | GistsURL string `json:"gists_url"` 263 | StarredURL string `json:"starred_url"` 264 | SubscriptionsURL string `json:"subscriptions_url"` 265 | OrganizationsURL string `json:"organizations_url"` 266 | ReposURL string `json:"repos_url"` 267 | EventsURL string `json:"events_url"` 268 | ReceivedEventsURL string `json:"received_events_url"` 269 | Type string `json:"type"` 270 | SiteAdmin bool `json:"site_admin"` 271 | } `json:"sender"` 272 | } 273 | 274 | // RunnerEvent is the event passed to the lambda-github-runner 275 | type RunnerEvent struct { 276 | QueueURL string `json:"queue_url"` 277 | RepoURL string `json:"repo_url"` 278 | RepoFullName string `json:"repo_fullname"` 279 | Token string `json:"token"` 280 | VirtualID string `json:"virtual_id"` 281 | Event string `json:"event"` 282 | } 283 | 284 | func TriggerRunner(payloadEvent RunnerEvent) error { 285 | sess := session.Must(session.NewSessionWithOptions(session.Options{ 286 | SharedConfigState: session.SharedConfigEnable, 287 | })) 288 | 289 | payload, _ := json.Marshal(&payloadEvent) 290 | 291 | client := lmbda.New(sess, &aws.Config{Region: aws.String("us-east-1")}) 292 | _, err := client.Invoke(&lmbda.InvokeInput{FunctionName: aws.String("lambda-github-runner"), InvocationType: aws.String("Event"), Payload: payload}) 293 | return err 294 | } 295 | 296 | // HandleRequest handles Lambda Request 297 | func HandleRequest(ctx context.Context, req events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) { 298 | fmt.Println(req.Body) 299 | deliveryID := req.Headers["X-GitHub-Delivery"] 300 | fmt.Printf("Delivery ID - %s\n", deliveryID) 301 | fmt.Printf("Event - %s\n", req.Headers["X-GitHub-Event"]) 302 | 303 | // Only care about "check_run"/"ping" events 304 | if req.Headers["X-GitHub-Event"] != "check_run" && req.Headers["X-GitHub-Event"] != "ping" { 305 | return events.APIGatewayProxyResponse{StatusCode: 404}, nil 306 | } 307 | 308 | // if a github webhook secret it provided, require the check 309 | if os.Getenv("GITHUB_WEBHOOK_SECRET") != "" { 310 | secretKey := []byte(os.Getenv("GITHUB_WEBHOOK_SECRET")) 311 | sha256Header := req.Headers["X-Hub-Signature-256"] 312 | sha256HeaderParts := strings.SplitN(sha256Header, "=", 2) 313 | buf, err := hex.DecodeString(sha256HeaderParts[1]) 314 | if err != nil { 315 | fmt.Errorf("Error decoding signature %q: %w", sha256Header, err) 316 | return events.APIGatewayProxyResponse{StatusCode: 400, Body: "Error decoding signature"}, nil 317 | } 318 | hashFunc := sha256.New 319 | mac := hmac.New(hashFunc, secretKey) 320 | mac.Write([]byte(req.Body)) 321 | ms := mac.Sum(nil) 322 | if !hmac.Equal(buf, ms) { 323 | fmt.Errorf("Payload signature check failed") 324 | return events.APIGatewayProxyResponse{StatusCode: 417, Body: "Payload signature check failed"}, nil 325 | } 326 | } 327 | 328 | data := AutoGenerated{} 329 | json.Unmarshal([]byte(req.Body), &data) 330 | 331 | virtualQueue := data.Repository.Name + strconv.Itoa(data.CheckRun.ID) 332 | queueURL := os.Getenv("SQS_QUEUE_URL") 333 | if queueURL == "" { 334 | fmt.Println("No SQS Queue URL is configured") 335 | return events.APIGatewayProxyResponse{StatusCode: 400, Body: "No SQS Queue URL is configured"}, nil 336 | } 337 | 338 | // If ping, send different event 339 | if req.Headers["X-GitHub-Event"] == "ping" { 340 | // Create new lambda 341 | payloadEvent := RunnerEvent{ 342 | QueueURL: queueURL + "#" + virtualQueue, 343 | RepoURL: data.Repository.HTMLURL, 344 | RepoFullName: data.Repository.FullName, 345 | Token: os.Getenv("GITHUB_TOKEN"), 346 | VirtualID: virtualQueue, 347 | Event: "create", 348 | } 349 | 350 | err := TriggerRunner(payloadEvent) 351 | if err != nil { 352 | fmt.Println("Error calling lambda-github-runner", err) 353 | return events.APIGatewayProxyResponse{StatusCode: 400, Body: "Unable to start runner"}, nil 354 | } 355 | fmt.Println("lambda-github-runner triggered") 356 | return events.APIGatewayProxyResponse{StatusCode: 200, Body: "Setting up default runner"}, nil 357 | } 358 | 359 | if data.Action == "created" || data.Action == "rerequested" { 360 | // Create new lambda 361 | payloadEvent := RunnerEvent{ 362 | QueueURL: queueURL + "#" + virtualQueue, 363 | RepoURL: data.Repository.HTMLURL, 364 | RepoFullName: data.Repository.FullName, 365 | Token: os.Getenv("GITHUB_TOKEN"), 366 | VirtualID: virtualQueue, 367 | Event: "default", 368 | } 369 | 370 | err := TriggerRunner(payloadEvent) 371 | if err != nil { 372 | fmt.Println("Error calling lambda-github-runner", err) 373 | return events.APIGatewayProxyResponse{StatusCode: 400, Body: "Unable to start runner"}, nil 374 | } 375 | fmt.Println("lambda-github-runner triggered") 376 | } else if data.Action == "completed" { 377 | // Stop lambda 378 | sess := session.Must(session.NewSessionWithOptions(session.Options{ 379 | SharedConfigState: session.SharedConfigEnable, 380 | })) 381 | 382 | svc := sqs.New(sess) 383 | sr, err := svc.SendMessage(&sqs.SendMessageInput{ 384 | DelaySeconds: aws.Int64(0), 385 | MessageBody: aws.String(virtualQueue), 386 | QueueUrl: aws.String(queueURL + "#" + virtualQueue), 387 | }) 388 | 389 | if err != nil { 390 | fmt.Println("Error sending message to virtual queue", err) 391 | return events.APIGatewayProxyResponse{StatusCode: 400, Body: "Error sending message to virtual queue"}, nil 392 | } 393 | 394 | fmt.Println("Message send to SQS") 395 | fmt.Println(sr) 396 | } else { 397 | fmt.Printf("Unknown action - %s\n", data.Action) 398 | return events.APIGatewayProxyResponse{StatusCode: 412, Body: "Unknown action"}, nil 399 | } 400 | return events.APIGatewayProxyResponse{StatusCode: 204}, nil 401 | } 402 | 403 | func main() { 404 | lambda.Start(HandleRequest) 405 | } 406 | --------------------------------------------------------------------------------