@@ -60899,7 +60899,7 @@ function buildUserDataScript(githubRegistrationToken, label) {
6089960899 }
6090060900}
6090160901
60902- async function startEc2Instance (label, githubRegistrationToken) {
60902+ async function startEc2Instances (label, count , githubRegistrationToken) {
6090360903 const ec2 = new AWS.EC2();
6090460904
6090560905 // User data scripts are run as the root user.
@@ -60909,8 +60909,8 @@ async function startEc2Instance(label, githubRegistrationToken) {
6090960909 const params = {
6091060910 ImageId: config.input.ec2ImageId,
6091160911 InstanceType: config.input.ec2InstanceType,
60912- MinCount: 1 ,
60913- MaxCount: 1 ,
60912+ MinCount: count ,
60913+ MaxCount: count ,
6091460914 UserData: Buffer.from(userData.join('\n')).toString('base64'),
6091560915 SubnetId: config.input.subnetId,
6091660916 SecurityGroupIds: [config.input.securityGroupId],
@@ -60921,53 +60921,53 @@ async function startEc2Instance(label, githubRegistrationToken) {
6092160921
6092260922 try {
6092360923 const result = await ec2.runInstances(params).promise();
60924- const ec2InstanceId = result.Instances[0]. InstanceId;
60925- core.info(`AWS EC2 instance ${ec2InstanceId} is started`);
60926- return ec2InstanceId ;
60924+ const ec2InstanceIds = result.Instances.map(i => i. InstanceId) ;
60925+ core.info(`AWS EC2 instances ${JSON.stringify(ec2InstanceIds)} are started`);
60926+ return ec2InstanceIds ;
6092760927 } catch (error) {
6092860928 core.error('AWS EC2 instance starting error');
6092960929 throw error;
6093060930 }
6093160931}
6093260932
60933- async function terminateEc2Instance () {
60933+ async function terminateEc2Instances () {
6093460934 const ec2 = new AWS.EC2();
6093560935
6093660936 const params = {
60937- InstanceIds: [ config.input.ec2InstanceId] ,
60937+ InstanceIds: config.input.ec2InstanceIds ,
6093860938 };
6093960939
6094060940 try {
6094160941 await ec2.terminateInstances(params).promise();
60942- core.info(`AWS EC2 instance ${config.input.ec2InstanceId} is terminated`);
60942+ core.info(`AWS EC2 instances ${JSON.stringify( config.input.ec2InstanceIds)} are terminated`);
6094360943 return;
6094460944 } catch (error) {
60945- core.error(`AWS EC2 instance ${config.input.ec2InstanceId } termination error`);
60945+ core.error(`AWS EC2 instances ${JSON.stringify( config.input.ec2InstanceIds) } termination error`);
6094660946 throw error;
6094760947 }
6094860948}
6094960949
60950- async function waitForInstanceRunning(ec2InstanceId ) {
60950+ async function waitForInstancesRunning(ec2InstanceIds ) {
6095160951 const ec2 = new AWS.EC2();
6095260952
6095360953 const params = {
60954- InstanceIds: [ec2InstanceId] ,
60954+ InstanceIds: ec2InstanceIds ,
6095560955 };
6095660956
6095760957 try {
6095860958 await ec2.waitFor('instanceRunning', params).promise();
60959- core.info(`AWS EC2 instance ${ec2InstanceId} is up and running`);
60959+ core.info(`AWS EC2 instances ${JSON.stringify(ec2InstanceIds)} are up and running`);
6096060960 return;
6096160961 } catch (error) {
60962- core.error(`AWS EC2 instance ${ec2InstanceId } initialization error`);
60962+ core.error(`AWS EC2 instances ${JSON.stringify(ec2InstanceIds) } initialization error`);
6096360963 throw error;
6096460964 }
6096560965}
6096660966
6096760967module.exports = {
60968- startEc2Instance ,
60969- terminateEc2Instance ,
60970- waitForInstanceRunning ,
60968+ startEc2Instances ,
60969+ terminateEc2Instances ,
60970+ waitForInstancesRunning ,
6097160971};
6097260972
6097360973
@@ -60986,10 +60986,11 @@ class Config {
6098660986 githubToken: core.getInput('github-token'),
6098760987 ec2ImageId: core.getInput('ec2-image-id'),
6098860988 ec2InstanceType: core.getInput('ec2-instance-type'),
60989+ ec2InstanceCount: core.getInput('ec2-instance-count'),
6098960990 subnetId: core.getInput('subnet-id'),
6099060991 securityGroupId: core.getInput('security-group-id'),
6099160992 label: core.getInput('label'),
60992- ec2InstanceId : core.getInput('ec2-instance-id'),
60993+ ec2InstanceIds : core.getInput('ec2-instance-id'),
6099360994 iamRoleName: core.getInput('iam-role-name'),
6099460995 keyPairName: core.getInput('key-pair-name'),
6099560996 runnerHomeDir: core.getInput('runner-home-dir')
@@ -61025,10 +61026,29 @@ class Config {
6102561026 if (!this.input.ec2ImageId || !this.input.ec2InstanceType || !this.input.subnetId || !this.input.securityGroupId || !this.input.keyPairName) {
6102661027 throw new Error(`Not all the required inputs are provided for the 'start' mode`);
6102761028 }
61029+
61030+ if (this.input.ec2InstanceCount === undefined) {
61031+ this.input.ec2InstanceCount = 1;
61032+ }
61033+ const parsedEc2InstanceCount = parseInt(this.input.ec2InstanceCount);
61034+ if (isNaN(parsedEc2InstanceCount)) {
61035+ throw new Error(`The 'ec2-instance-count' input has illegal value '${this.input.ec2InstanceCount}'`);
61036+ } else if (parsedEc2InstanceCount < 1) {
61037+ throw new Error(`The 'ec2-instance-count' input must be greater than zero`);
61038+ }
61039+ this.input.ec2InstanceCount = parsedEc2InstanceCount;
6102861040 } else if (this.input.mode === 'stop') {
61029- if (!this.input.label || !this.input.ec2InstanceId ) {
61041+ if (!this.input.label || !this.input.ec2InstanceIds ) {
6103061042 throw new Error(`Not all the required inputs are provided for the 'stop' mode`);
6103161043 }
61044+
61045+ try {
61046+ const parsedEc2InstanceIds = JSON.parse(this.input.ec2InstanceIds);
61047+ this.input.ec2InstanceIds = parsedEc2InstanceIds;
61048+ } catch (error) {
61049+ 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`);
61050+ this.input.ec2InstanceIds = [this.input.ec2InstanceIds];
61051+ }
6103261052 } else {
6103361053 throw new Error('Wrong mode. Allowed values: start, stop.');
6103461054 }
@@ -61059,15 +61079,15 @@ const config = __webpack_require__(34570);
6105961079
6106061080// use the unique label to find the runner
6106161081// as we don't have the runner's id, it's not possible to get it in any other way
61062- async function getRunner (label) {
61082+ async function getRunners (label) {
6106361083 const octokit = github.getOctokit(config.input.githubToken);
6106461084
6106561085 try {
6106661086 const runners = await octokit.paginate('GET /repos/{owner}/{repo}/actions/runners', config.githubContext);
61067- const foundRunners = _.filter(runners, { labels: [{ name: label }] });
61068- return foundRunners.length > 0 ? foundRunners[0] : null;
61087+ return _.filter(runners, { labels: [{ name: label }] });
6106961088 } catch (error) {
61070- return null;
61089+ core.error(`Error fetching current runners: ${error}`)
61090+ return [];
6107161091 }
6107261092}
6107361093
@@ -61085,48 +61105,48 @@ async function getRegistrationToken() {
6108561105 }
6108661106}
6108761107
61088- async function removeRunner () {
61089- const runner = await getRunner (config.input.label);
61108+ async function removeRunners () {
61109+ const runners = await getRunners (config.input.label);
6109061110 const octokit = github.getOctokit(config.input.githubToken);
6109161111
6109261112 // skip the runner removal process if the runner is not found
61093- if (!runner ) {
61094- core.info(`GitHub self-hosted runner with label ${config.input.label} is not found, so the removal is skipped`);
61113+ if (!runners || runners.length === 0 ) {
61114+ core.info(`GitHub self-hosted runners with label ${config.input.label} are not found, so the removal is skipped`);
6109561115 return;
6109661116 }
6109761117
6109861118 try {
61099- await octokit.request('DELETE /repos/{owner}/{repo}/actions/runners/{runner_id}', _.merge(config.githubContext, { runner_id: runner .id }));
61100- core.info(`GitHub self-hosted runner ${runner. name} is removed`);
61119+ await Promise.all(runners.map(r => octokit.request('DELETE /repos/{owner}/{repo}/actions/runners/{runner_id}', _.merge(config.githubContext, { runner_id: r .id })) ));
61120+ core.info(`GitHub self-hosted runners ${runners.map(r => r. name)} are removed`);
6110161121 return;
6110261122 } catch (error) {
61103- core.error('GitHub self-hosted runner removal error');
61123+ core.error('GitHub self-hosted runners removal error');
6110461124 throw error;
6110561125 }
6110661126}
6110761127
61108- async function waitForRunnerRegistered (label) {
61128+ async function waitForRunnersRegistered (label, expectedRunnerCount ) {
6110961129 const timeoutMinutes = 5;
6111061130 const retryIntervalSeconds = 10;
6111161131 const quietPeriodSeconds = 30;
6111261132 let waitSeconds = 0;
6111361133
61114- core.info(`Waiting ${quietPeriodSeconds}s for the AWS EC2 instance to be registered in GitHub as a new self-hosted runner `);
61134+ core.info(`Waiting ${quietPeriodSeconds}s for the AWS EC2 instances to be registered in GitHub as the new self-hosted runners `);
6111561135 await new Promise(r => setTimeout(r, quietPeriodSeconds * 1000));
61116- core.info(`Checking every ${retryIntervalSeconds}s if the GitHub self-hosted runner is registered`);
61136+ core.info(`Checking every ${retryIntervalSeconds}s if the GitHub self-hosted runners are registered`);
6111761137
6111861138 return new Promise((resolve, reject) => {
6111961139 const interval = setInterval(async () => {
61120- const runner = await getRunner (label);
61140+ const runners = await getRunners (label);
6112161141
6112261142 if (waitSeconds > timeoutMinutes * 60) {
61123- core.error('GitHub self-hosted runner registration error');
61143+ core.error('GitHub self-hosted runners registration error');
6112461144 clearInterval(interval);
61125- 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 .`);
61145+ 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 .`);
6112661146 }
6112761147
61128- if (runner && runner. status === 'online') {
61129- core.info(`GitHub self-hosted runner ${runner. name} is registered and ready to use`);
61148+ if (runners && runners.length == expectedRunnerCount && runners.every(r => r. status === 'online') ) {
61149+ core.info(`GitHub self-hosted runners ${JSON.stringify(runners.map(r => r. name))} are registered and ready to use`);
6113061150 clearInterval(interval);
6113161151 resolve();
6113261152 } else {
@@ -61139,8 +61159,8 @@ async function waitForRunnerRegistered(label) {
6113961159
6114061160module.exports = {
6114161161 getRegistrationToken,
61142- removeRunner ,
61143- waitForRunnerRegistered ,
61162+ removeRunners ,
61163+ waitForRunnersRegistered ,
6114461164};
6114561165
6114661166
@@ -61154,23 +61174,23 @@ const gh = __webpack_require__(56989);
6115461174const config = __webpack_require__(34570);
6115561175const core = __webpack_require__(42186);
6115661176
61157- function setOutput(label, ec2InstanceId ) {
61177+ function setOutput(label, ec2InstanceIds ) {
6115861178 core.setOutput('label', label);
61159- core.setOutput('ec2-instance-id', ec2InstanceId );
61179+ core.setOutput('ec2-instance-id', JSON.stringify(ec2InstanceIds) );
6116061180}
6116161181
6116261182async function start() {
6116361183 const label = config.generateUniqueLabel();
6116461184 const githubRegistrationToken = await gh.getRegistrationToken();
61165- const ec2InstanceId = await aws.startEc2Instance (label, githubRegistrationToken);
61166- setOutput(label, ec2InstanceId );
61167- await aws.waitForInstanceRunning(ec2InstanceId );
61168- await gh.waitForRunnerRegistered (label);
61185+ const ec2InstanceIds = await aws.startEc2Instances (label, config.input.ec2InstanceCount , githubRegistrationToken);
61186+ setOutput(label, ec2InstanceIds );
61187+ await aws.waitForInstancesRunning(ec2InstanceIds );
61188+ await gh.waitForRunnersRegistered (label, config.input.ec2InstanceCount );
6116961189}
6117061190
6117161191async function stop() {
61172- await aws.terminateEc2Instance ();
61173- await gh.removeRunner ();
61192+ await aws.terminateEc2Instances ();
61193+ await gh.removeRunners ();
6117461194}
6117561195
6117661196(async function () {
0 commit comments