diff --git a/images/github-runner/Dockerfile b/images/github-runner/Dockerfile new file mode 100644 index 0000000..3d090be --- /dev/null +++ b/images/github-runner/Dockerfile @@ -0,0 +1,51 @@ +FROM nestybox/ubuntu-focal-docker + +# Extra deps for GHA Runner +ENV DEBIAN_FRONTEND=noninteractive +RUN apt-get update \ + && apt-get install -y \ + curl \ + jq \ + sudo \ + unzip \ + wget \ + zip \ + git \ + && rm -rf /var/lib/apt/list/* + +# Add and config runner user as sudo +RUN useradd -m runner \ + && usermod -aG sudo runner \ + && usermod -aG docker runner \ + && echo "%sudo ALL=(ALL:ALL) NOPASSWD:ALL" > /etc/sudoers + +# Build args +ARG TARGETPLATFORM=x64 +ARG RUNNER_VERSION=2.301.0 +WORKDIR /runner + +# Runner download supports amd64 as x64 +RUN export ARCH=$(echo ${TARGETPLATFORM} | cut -d / -f2) \ + && if [ "$ARCH" = "amd64" ]; then export ARCH=x64 ; fi \ + && curl -Ls -o runner.tar.gz https://github.com/uazo/runner/releases/download/v${RUNNER_VERSION}/actions-runner-linux-${ARCH}-${RUNNER_VERSION}.tar.gz \ + && tar xzf ./runner.tar.gz \ + && rm runner.tar.gz \ + && ./bin/installdependencies.sh \ + && rm -rf /var/lib/apt/lists/* + +# Dumb Init +#RUN export ARCH=$(echo ${TARGETPLATFORM} | cut -d / -f2) \ +# && curl -Ls -o /usr/local/bin/dumb-init https://github.com/Yelp/dumb-init/releases/download/v1.2.2/dumb-init_1.2.2_${ARCH} \ +# && chmod +x /usr/local/bin/dumb-init + +COPY startup.sh . + +# Add patched scripts from GHA runner (svc.sh and RunnerService.js) +COPY --chown=runner:runner patched/ ./patched/ + +RUN chmod +x ./patched/runsvc.sh ./startup.sh + +USER runner + +#ENTRYPOINT ["/usr/local/bin/dumb-init", "--"] +CMD ./startup.sh diff --git a/images/github-runner/patched/RunnerService.js b/images/github-runner/patched/RunnerService.js new file mode 100644 index 0000000..778717f --- /dev/null +++ b/images/github-runner/patched/RunnerService.js @@ -0,0 +1,91 @@ +#!/usr/bin/env node +// Copyright (c) GitHub. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +var childProcess = require("child_process"); +var path = require("path") + +var supported = ['linux', 'darwin'] + +if (supported.indexOf(process.platform) == -1) { + console.log('Unsupported platform: ' + process.platform); + console.log('Supported platforms are: ' + supported.toString()); + process.exit(1); +} + +var stopping = false; +var listener = null; + +var runService = function() { + var listenerExePath = path.join(__dirname, '../bin/Runner.Listener'); + var interactive = process.argv[2] === "interactive"; + + if(!stopping) { + try { + if (interactive) { + console.log('Starting Runner listener interactively'); + listener = childProcess.spawn(listenerExePath, ['run'].concat(process.argv.slice(3)), { env: process.env }); + } else { + console.log('Starting Runner listener with startup type: service'); + listener = childProcess.spawn(listenerExePath, ['run', '--startuptype', 'service'].concat(process.argv.slice(2)), { env: process.env }); + } + + console.log('Started listener process'); + + listener.stdout.on('data', (data) => { + process.stdout.write(data.toString('utf8')); + }); + + listener.stderr.on('data', (data) => { + process.stdout.write(data.toString('utf8')); + }); + + listener.on('close', (code) => { + console.log(`Runner listener exited with error code ${code}`); + + if (code === 0) { + console.log('Runner listener exit with 0 return code, stop the service, no retry needed.'); + stopping = true; + } else if (code === 1) { + console.log('Runner listener exit with terminated error, stop the service, no retry needed.'); + stopping = true; + } else if (code === 2) { + console.log('Runner listener exit with retryable error, re-launch runner in 5 seconds.'); + } else if (code === 3) { + console.log('Runner listener exit because of updating, re-launch runner in 5 seconds.'); + } else { + console.log('Runner listener exit with undefined return code, re-launch runner in 5 seconds.'); + } + + if(!stopping) { + setTimeout(runService, 5000); + } + }); + + } catch(ex) { + console.log(ex); + } + } +} + +runService(); +console.log('Started running service'); + +var gracefulShutdown = function(code) { + console.log('Shutting down runner listener'); + stopping = true; + if (listener) { + console.log('Sending SIGINT to runner listener to stop'); + listener.kill('SIGINT'); + + // TODO wait for 30 seconds and send a SIGKILL + } +} + +process.on('SIGINT', () => { + gracefulShutdown(0); +}); + +process.on('SIGTERM', () => { + gracefulShutdown(0); +}); diff --git a/images/github-runner/patched/runsvc.sh b/images/github-runner/patched/runsvc.sh new file mode 100644 index 0000000..f73ef32 --- /dev/null +++ b/images/github-runner/patched/runsvc.sh @@ -0,0 +1,20 @@ +#!/bin/bash + +# convert SIGTERM signal to SIGINT +# for more info on how to propagate SIGTERM to a child process see: http://veithen.github.io/2014/11/16/sigterm-propagation.html +trap 'kill -INT $PID' TERM INT + +if [ -f ".path" ]; then + # configure + export PATH=`cat .path` + echo ".path=${PATH}" +fi + +# insert anything to setup env when running as a service + +# run the host process which keep the listener alive +./externals/node12/bin/node ./bin/RunnerService.js $* & +PID=$! +wait $PID +trap - TERM INT +wait $PID diff --git a/images/github-runner/startup.sh b/images/github-runner/startup.sh new file mode 100644 index 0000000..6969781 --- /dev/null +++ b/images/github-runner/startup.sh @@ -0,0 +1,55 @@ + +#!/bin/bash + +echo "Starting supervisor (Docker)" +sudo service docker start + +if [ -n "${GITHUB_REPOSITORY}" ] +then + auth_url="https://api.github.com/repos/${GITHUB_OWNER}/${GITHUB_REPOSITORY}/actions/runners/registration-token" + registration_url="https://github.com/${GITHUB_OWNER}/${GITHUB_REPOSITORY}" +else + auth_url="https://api.github.com/orgs/${GITHUB_OWNER}/actions/runners/registration-token" + registration_url="https://github.com/${GITHUB_OWNER}" +fi + +generate_token() { + payload=$(curl -sX POST -H "Authorization: token ${GITHUB_PERSONAL_TOKEN}" "${auth_url}") + runner_token=$(echo "${payload}" | jq .token --raw-output) + + if [ "${runner_token}" == "null" ] + then + echo "${payload}" + exit 1 + fi + + echo "${runner_token}" +} + +remove_runner() { + ./config.sh remove --unattended --token "$(generate_token)" +} + +service docker status +runner_id=${RUNNER_NAME}_$(openssl rand -hex 6) +echo "Registering runner ${runner_id}" + +./config.sh \ + --name "${runner_id}" \ + --labels "${RUNNER_LABELS}" \ + --token "$(generate_token)" \ + --url "${registration_url}" \ + --allowedauthorslist "${ALLOWEDAUTHORSLIST}" \ + --unattended \ + --replace + +trap 'remove_runner; exit 130' SIGINT +trap 'remove_runner; exit 143' SIGTERM + +for f in runsvc.sh RunnerService.js; do + mv bin/${f}{,.bak} + mv {patched,bin}/${f} +done + +./bin/runsvc.sh --once "$*" +remove_runner