@@ -62811,7 +62811,12 @@ function buildUserDataScript(githubRegistrationToken, label) {
6281162811 '#!/bin/bash',
6281262812 `cd "${config.input.runnerHomeDir}"`,
6281362813 'export RUNNER_ALLOW_RUNASROOT=1',
62814- `./config.sh --url https://github.com/${config.githubContext.owner}/${config.githubContext.repo} --token ${githubRegistrationToken} --labels ${label}`,
62814+ 'sudo echo "export RUNNER_ALLOW_RUNASROOT=1" >> /etc/profile.d/env.sh',
62815+ 'export DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1',
62816+ 'sudo echo "export DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1" >> /etc/profile.d/env.sh',
62817+ 'export DOTNET_SYSTEM_GLOBALIZATION_PREDEFINED_CULTURES_ONLY=false',
62818+ 'sudo echo "export DOTNET_SYSTEM_GLOBALIZATION_PREDEFINED_CULTURES_ONLY=false" >> /etc/profile.d/env.sh',
62819+ `./config.sh --unattended --url https://github.com/${config.githubContext.owner}/${config.githubContext.repo} --token ${githubRegistrationToken} --labels ${label}`,
6281562820 './run.sh',
6281662821 ];
6281762822 } else {
@@ -62822,78 +62827,86 @@ function buildUserDataScript(githubRegistrationToken, label) {
6282262827 'curl -O -L https://github.com/actions/runner/releases/download/v2.299.1/actions-runner-linux-${RUNNER_ARCH}-2.299.1.tar.gz',
6282362828 'tar xzf ./actions-runner-linux-${RUNNER_ARCH}-2.299.1.tar.gz',
6282462829 'export RUNNER_ALLOW_RUNASROOT=1',
62825- `./config.sh --url https://github.com/${config.githubContext.owner}/${config.githubContext.repo} --token ${githubRegistrationToken} --labels ${label}`,
62830+ 'sudo echo "export RUNNER_ALLOW_RUNASROOT=1" >> /etc/profile.d/env.sh',
62831+ 'export DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1',
62832+ 'sudo echo "export DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1" >> /etc/profile.d/env.sh',
62833+ 'export DOTNET_SYSTEM_GLOBALIZATION_PREDEFINED_CULTURES_ONLY=false',
62834+ 'sudo echo "export DOTNET_SYSTEM_GLOBALIZATION_PREDEFINED_CULTURES_ONLY=false" >> /etc/profile.d/env.sh',
62835+ `./config.sh --unattended --url https://github.com/${config.githubContext.owner}/${config.githubContext.repo} --token ${githubRegistrationToken} --labels ${label}`,
6282662836 './run.sh',
6282762837 ];
6282862838 }
6282962839}
6283062840
62831- async function startEc2Instance (label, githubRegistrationToken) {
62841+ async function startEc2Instances (label, count , githubRegistrationToken) {
6283262842 const ec2 = new AWS.EC2();
6283362843
62844+ // User data scripts are run as the root user.
62845+ // Docker and git are necessary for GitHub runner and should be pre-installed on the AMI.
6283462846 const userData = buildUserDataScript(githubRegistrationToken, label);
6283562847
6283662848 const params = {
6283762849 ImageId: config.input.ec2ImageId,
6283862850 InstanceType: config.input.ec2InstanceType,
62839- MinCount: 1 ,
62840- MaxCount: 1 ,
62851+ MinCount: count ,
62852+ MaxCount: count ,
6284162853 UserData: Buffer.from(userData.join('\n')).toString('base64'),
6284262854 SubnetId: config.input.subnetId,
6284362855 SecurityGroupIds: [config.input.securityGroupId],
6284462856 IamInstanceProfile: { Name: config.input.iamRoleName },
6284562857 TagSpecifications: config.tagSpecifications,
62858+ KeyName: config.input.keyPairName
6284662859 };
6284762860
6284862861 try {
6284962862 const result = await ec2.runInstances(params).promise();
62850- const ec2InstanceId = result.Instances[0]. InstanceId;
62851- core.info(`AWS EC2 instance ${ec2InstanceId} is started`);
62852- return ec2InstanceId ;
62863+ const ec2InstanceIds = result.Instances.map(i => i. InstanceId) ;
62864+ core.info(`AWS EC2 instances ${JSON.stringify(ec2InstanceIds)} are started`);
62865+ return ec2InstanceIds ;
6285362866 } catch (error) {
6285462867 core.error('AWS EC2 instance starting error');
6285562868 throw error;
6285662869 }
6285762870}
6285862871
62859- async function terminateEc2Instance () {
62872+ async function terminateEc2Instances () {
6286062873 const ec2 = new AWS.EC2();
6286162874
6286262875 const params = {
62863- InstanceIds: [ config.input.ec2InstanceId] ,
62876+ InstanceIds: config.input.ec2InstanceIds ,
6286462877 };
6286562878
6286662879 try {
6286762880 await ec2.terminateInstances(params).promise();
62868- core.info(`AWS EC2 instance ${config.input.ec2InstanceId} is terminated`);
62881+ core.info(`AWS EC2 instances ${JSON.stringify( config.input.ec2InstanceIds)} are terminated`);
6286962882 return;
6287062883 } catch (error) {
62871- core.error(`AWS EC2 instance ${config.input.ec2InstanceId } termination error`);
62884+ core.error(`AWS EC2 instances ${JSON.stringify( config.input.ec2InstanceIds) } termination error`);
6287262885 throw error;
6287362886 }
6287462887}
6287562888
62876- async function waitForInstanceRunning(ec2InstanceId ) {
62889+ async function waitForInstancesRunning(ec2InstanceIds ) {
6287762890 const ec2 = new AWS.EC2();
6287862891
6287962892 const params = {
62880- InstanceIds: [ec2InstanceId] ,
62893+ InstanceIds: ec2InstanceIds ,
6288162894 };
6288262895
6288362896 try {
6288462897 await ec2.waitFor('instanceRunning', params).promise();
62885- core.info(`AWS EC2 instance ${ec2InstanceId} is up and running`);
62898+ core.info(`AWS EC2 instances ${JSON.stringify(ec2InstanceIds)} are up and running`);
6288662899 return;
6288762900 } catch (error) {
62888- core.error(`AWS EC2 instance ${ec2InstanceId } initialization error`);
62901+ core.error(`AWS EC2 instances ${JSON.stringify(ec2InstanceIds) } initialization error`);
6288962902 throw error;
6289062903 }
6289162904}
6289262905
6289362906module.exports = {
62894- startEc2Instance ,
62895- terminateEc2Instance ,
62896- waitForInstanceRunning ,
62907+ startEc2Instances ,
62908+ terminateEc2Instances ,
62909+ waitForInstancesRunning ,
6289762910};
6289862911
6289962912
@@ -62912,12 +62925,14 @@ class Config {
6291262925 githubToken: core.getInput('github-token'),
6291362926 ec2ImageId: core.getInput('ec2-image-id'),
6291462927 ec2InstanceType: core.getInput('ec2-instance-type'),
62928+ ec2InstanceCount: core.getInput('ec2-instance-count'),
6291562929 subnetId: core.getInput('subnet-id'),
6291662930 securityGroupId: core.getInput('security-group-id'),
6291762931 label: core.getInput('label'),
62918- ec2InstanceId : core.getInput('ec2-instance-id'),
62932+ ec2InstanceIds : core.getInput('ec2-instance-id'),
6291962933 iamRoleName: core.getInput('iam-role-name'),
62920- runnerHomeDir: core.getInput('runner-home-dir'),
62934+ keyPairName: core.getInput('key-pair-name'),
62935+ runnerHomeDir: core.getInput('runner-home-dir')
6292162936 };
6292262937
6292362938 const tags = JSON.parse(core.getInput('aws-resource-tags'));
@@ -62947,13 +62962,32 @@ class Config {
6294762962 }
6294862963
6294962964 if (this.input.mode === 'start') {
62950- if (!this.input.ec2ImageId || !this.input.ec2InstanceType || !this.input.subnetId || !this.input.securityGroupId) {
62965+ if (!this.input.ec2ImageId || !this.input.ec2InstanceType || !this.input.subnetId || !this.input.securityGroupId || !this.input.keyPairName ) {
6295162966 throw new Error(`Not all the required inputs are provided for the 'start' mode`);
6295262967 }
62968+
62969+ if (this.input.ec2InstanceCount === undefined) {
62970+ this.input.ec2InstanceCount = 1;
62971+ }
62972+ const parsedEc2InstanceCount = parseInt(this.input.ec2InstanceCount);
62973+ if (isNaN(parsedEc2InstanceCount)) {
62974+ throw new Error(`The 'ec2-instance-count' input has illegal value '${this.input.ec2InstanceCount}'`);
62975+ } else if (parsedEc2InstanceCount < 1) {
62976+ throw new Error(`The 'ec2-instance-count' input must be greater than zero`);
62977+ }
62978+ this.input.ec2InstanceCount = parsedEc2InstanceCount;
6295362979 } else if (this.input.mode === 'stop') {
62954- if (!this.input.label || !this.input.ec2InstanceId ) {
62980+ if (!this.input.label || !this.input.ec2InstanceIds ) {
6295562981 throw new Error(`Not all the required inputs are provided for the 'stop' mode`);
6295662982 }
62983+
62984+ try {
62985+ const parsedEc2InstanceIds = JSON.parse(this.input.ec2InstanceIds);
62986+ this.input.ec2InstanceIds = parsedEc2InstanceIds;
62987+ } catch (error) {
62988+ core.info(`Got error ${error} when parsing '${this.input.ec2InstanceIds}' as JSON, assuming that it is a raw string containing a single EC2 instance ID`);
62989+ this.input.ec2InstanceIds = [this.input.ec2InstanceIds];
62990+ }
6295762991 } else {
6295862992 throw new Error('Wrong mode. Allowed values: start, stop.');
6295962993 }
@@ -62984,15 +63018,15 @@ const config = __webpack_require__(34570);
6298463018
6298563019// use the unique label to find the runner
6298663020// as we don't have the runner's id, it's not possible to get it in any other way
62987- async function getRunner (label) {
63021+ async function getRunners (label) {
6298863022 const octokit = github.getOctokit(config.input.githubToken);
6298963023
6299063024 try {
6299163025 const runners = await octokit.paginate('GET /repos/{owner}/{repo}/actions/runners', config.githubContext);
62992- const foundRunners = _.filter(runners, { labels: [{ name: label }] });
62993- return foundRunners.length > 0 ? foundRunners[0] : null;
63026+ return _.filter(runners, { labels: [{ name: label }] });
6299463027 } catch (error) {
62995- return null;
63028+ core.error(`Error fetching current runners: ${error}`)
63029+ return [];
6299663030 }
6299763031}
6299863032
@@ -63010,48 +63044,48 @@ async function getRegistrationToken() {
6301063044 }
6301163045}
6301263046
63013- async function removeRunner () {
63014- const runner = await getRunner (config.input.label);
63047+ async function removeRunners () {
63048+ const runners = await getRunners (config.input.label);
6301563049 const octokit = github.getOctokit(config.input.githubToken);
6301663050
6301763051 // skip the runner removal process if the runner is not found
63018- if (!runner ) {
63019- core.info(`GitHub self-hosted runner with label ${config.input.label} is not found, so the removal is skipped`);
63052+ if (!runners || runners.length === 0 ) {
63053+ core.info(`GitHub self-hosted runners with label ${config.input.label} are not found, so the removal is skipped`);
6302063054 return;
6302163055 }
6302263056
6302363057 try {
63024- await octokit.request('DELETE /repos/{owner}/{repo}/actions/runners/{runner_id}', _.merge(config.githubContext, { runner_id: runner .id }));
63025- core.info(`GitHub self-hosted runner ${runner. name} is removed`);
63058+ await Promise.all(runners.map(r => octokit.request('DELETE /repos/{owner}/{repo}/actions/runners/{runner_id}', _.merge(config.githubContext, { runner_id: r .id })) ));
63059+ core.info(`GitHub self-hosted runners ${runners.map(r => r. name)} are removed`);
6302663060 return;
6302763061 } catch (error) {
63028- core.error('GitHub self-hosted runner removal error');
63062+ core.error('GitHub self-hosted runners removal error');
6302963063 throw error;
6303063064 }
6303163065}
6303263066
63033- async function waitForRunnerRegistered (label) {
63067+ async function waitForRunnersRegistered (label, expectedRunnerCount ) {
6303463068 const timeoutMinutes = 5;
6303563069 const retryIntervalSeconds = 10;
6303663070 const quietPeriodSeconds = 30;
6303763071 let waitSeconds = 0;
6303863072
63039- core.info(`Waiting ${quietPeriodSeconds}s for the AWS EC2 instance to be registered in GitHub as a new self-hosted runner `);
63073+ core.info(`Waiting ${quietPeriodSeconds}s for the AWS EC2 instances to be registered in GitHub as the new self-hosted runners `);
6304063074 await new Promise(r => setTimeout(r, quietPeriodSeconds * 1000));
63041- core.info(`Checking every ${retryIntervalSeconds}s if the GitHub self-hosted runner is registered`);
63075+ core.info(`Checking every ${retryIntervalSeconds}s if the GitHub self-hosted runners are registered`);
6304263076
6304363077 return new Promise((resolve, reject) => {
6304463078 const interval = setInterval(async () => {
63045- const runner = await getRunner (label);
63079+ const runners = await getRunners (label);
6304663080
6304763081 if (waitSeconds > timeoutMinutes * 60) {
63048- core.error('GitHub self-hosted runner registration error');
63082+ core.error('GitHub self-hosted runners registration error');
6304963083 clearInterval(interval);
63050- reject(`A timeout of ${timeoutMinutes} minutes is exceeded. Your AWS EC2 instance was not able to register itself in GitHub as a new self-hosted runner .`);
63084+ reject(`A timeout of ${timeoutMinutes} minutes is exceeded. Your AWS EC2 instances were not able to register themselves in GitHub as the new self-hosted runners .`);
6305163085 }
6305263086
63053- if (runner && runner. status === 'online') {
63054- core.info(`GitHub self-hosted runner ${runner. name} is registered and ready to use`);
63087+ if (runners && runners.length == expectedRunnerCount && runners.every(r => r. status === 'online') ) {
63088+ core.info(`GitHub self-hosted runners ${JSON.stringify(runners.map(r => r. name))} are registered and ready to use`);
6305563089 clearInterval(interval);
6305663090 resolve();
6305763091 } else {
@@ -63064,8 +63098,8 @@ async function waitForRunnerRegistered(label) {
6306463098
6306563099module.exports = {
6306663100 getRegistrationToken,
63067- removeRunner ,
63068- waitForRunnerRegistered ,
63101+ removeRunners ,
63102+ waitForRunnersRegistered ,
6306963103};
6307063104
6307163105
@@ -63079,23 +63113,23 @@ const gh = __webpack_require__(56989);
6307963113const config = __webpack_require__(34570);
6308063114const core = __webpack_require__(42186);
6308163115
63082- function setOutput(label, ec2InstanceId ) {
63116+ function setOutput(label, ec2InstanceIds ) {
6308363117 core.setOutput('label', label);
63084- core.setOutput('ec2-instance-id', ec2InstanceId );
63118+ core.setOutput('ec2-instance-id', JSON.stringify(ec2InstanceIds) );
6308563119}
6308663120
6308763121async function start() {
6308863122 const label = config.generateUniqueLabel();
6308963123 const githubRegistrationToken = await gh.getRegistrationToken();
63090- const ec2InstanceId = await aws.startEc2Instance (label, githubRegistrationToken);
63091- setOutput(label, ec2InstanceId );
63092- await aws.waitForInstanceRunning(ec2InstanceId );
63093- await gh.waitForRunnerRegistered (label);
63124+ const ec2InstanceIds = await aws.startEc2Instances (label, config.input.ec2InstanceCount , githubRegistrationToken);
63125+ setOutput(label, ec2InstanceIds );
63126+ await aws.waitForInstancesRunning(ec2InstanceIds );
63127+ await gh.waitForRunnersRegistered (label, config.input.ec2InstanceCount );
6309463128}
6309563129
6309663130async function stop() {
63097- await aws.terminateEc2Instance ();
63098- await gh.removeRunner ();
63131+ await aws.terminateEc2Instances ();
63132+ await gh.removeRunners ();
6309963133}
6310063134
6310163135(async function () {
@@ -69250,4 +69284,4 @@ module.exports = require("zlib");;
6925069284/******/ // Load entry module and return exports
6925169285/******/ return __webpack_require__(4351);
6925269286/******/ })()
69253- ;
69287+ ;
0 commit comments