Skip to content

Commit ba56723

Browse files
authored
feat!: add packages input that installs packages on the AWS instance as part of cloud-init (machulav#241)
* add ability to specify packages, switch to yaml cloud-init input * debug-print the user data * add the packages parameter to action.yaml * packaged the action * write the script to a file then execute the file, to allow multi line input * write pre-runner-script.sh using write_files * remove debug print used while developing * always write pre-runner script (even if empty) since runner-setup.sh always sources it * enable debugging * remove debug-printing the user-data content * clean up printing of internal logs, used in development * bring back the user-data.log prinitng, it was in upstream - no need to remove * remove the dependency on jq * make the github api return value parsing more robust to changes in whitespace * run npm run package
1 parent a6dbcef commit ba56723

File tree

4 files changed

+153
-47
lines changed

4 files changed

+153
-47
lines changed

action.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,12 @@ inputs:
129129
description: >-
130130
JSON string specifying the metadata options for the EC2 instance.
131131
Example: '{"HttpTokens": "required", "HttpEndpoint": "enabled", "HttpPutResponseHopLimit": 2, "InstanceMetadataTags": "enabled"}'
132+
packages:
133+
description: >-
134+
JSON array of packages to install via cloud-init.
135+
Example: '["git", "docker.io", "nodejs"]'
136+
required: false
137+
default: '[]'
132138
outputs:
133139
label:
134140
description: >-

dist/index.js

Lines changed: 89 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -127451,7 +127451,7 @@ module.exports = {
127451127451

127452127452

127453127453
const { parseSetCookie } = __nccwpck_require__(4408)
127454-
const { stringify } = __nccwpck_require__(3121)
127454+
const { stringify, getHeadersList } = __nccwpck_require__(3121)
127455127455
const { webidl } = __nccwpck_require__(1744)
127456127456
const { Headers } = __nccwpck_require__(554)
127457127457

@@ -127527,13 +127527,14 @@ function getSetCookies (headers) {
127527127527

127528127528
webidl.brandCheck(headers, Headers, { strict: false })
127529127529

127530-
const cookies = headers.getSetCookie()
127530+
const cookies = getHeadersList(headers).cookies
127531127531

127532127532
if (!cookies) {
127533127533
return []
127534127534
}
127535127535

127536-
return cookies.map((pair) => parseSetCookie(pair))
127536+
// In older versions of undici, cookies is a list of name:value.
127537+
return cookies.map((pair) => parseSetCookie(Array.isArray(pair) ? pair[1] : pair))
127537127538
}
127538127539

127539127540
/**
@@ -127961,15 +127962,14 @@ module.exports = {
127961127962
/***/ }),
127962127963

127963127964
/***/ 3121:
127964-
/***/ ((module) => {
127965+
/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => {
127965127966

127966127967
"use strict";
127967127968

127968127969

127969-
/**
127970-
* @param {string} value
127971-
* @returns {boolean}
127972-
*/
127970+
const assert = __nccwpck_require__(9491)
127971+
const { kHeadersList } = __nccwpck_require__(2785)
127972+
127973127973
function isCTLExcludingHtab (value) {
127974127974
if (value.length === 0) {
127975127975
return false
@@ -128230,13 +128230,31 @@ function stringify (cookie) {
128230128230
return out.join('; ')
128231128231
}
128232128232

128233+
let kHeadersListNode
128234+
128235+
function getHeadersList (headers) {
128236+
if (headers[kHeadersList]) {
128237+
return headers[kHeadersList]
128238+
}
128239+
128240+
if (!kHeadersListNode) {
128241+
kHeadersListNode = Object.getOwnPropertySymbols(headers).find(
128242+
(symbol) => symbol.description === 'headers list'
128243+
)
128244+
128245+
assert(kHeadersListNode, 'Headers cannot be parsed')
128246+
}
128247+
128248+
const headersList = headers[kHeadersListNode]
128249+
assert(headersList)
128250+
128251+
return headersList
128252+
}
128253+
128233128254
module.exports = {
128234128255
isCTLExcludingHtab,
128235-
validateCookieName,
128236-
validateCookiePath,
128237-
validateCookieValue,
128238-
toIMFDate,
128239-
stringify
128256+
stringify,
128257+
getHeadersList
128240128258
}
128241128259

128242128260

@@ -132240,7 +132258,6 @@ const {
132240132258
isValidHeaderName,
132241132259
isValidHeaderValue
132242132260
} = __nccwpck_require__(2538)
132243-
const util = __nccwpck_require__(3837)
132244132261
const { webidl } = __nccwpck_require__(1744)
132245132262
const assert = __nccwpck_require__(9491)
132246132263

@@ -132794,9 +132811,6 @@ Object.defineProperties(Headers.prototype, {
132794132811
[Symbol.toStringTag]: {
132795132812
value: 'Headers',
132796132813
configurable: true
132797-
},
132798-
[util.inspect.custom]: {
132799-
enumerable: false
132800132814
}
132801132815
})
132802132816

@@ -141973,20 +141987,6 @@ class Pool extends PoolBase {
141973141987
? { ...options.interceptors }
141974141988
: undefined
141975141989
this[kFactory] = factory
141976-
141977-
this.on('connectionError', (origin, targets, error) => {
141978-
// If a connection error occurs, we remove the client from the pool,
141979-
// and emit a connectionError event. They will not be re-used.
141980-
// Fixes https://github.com/nodejs/undici/issues/3895
141981-
for (const target of targets) {
141982-
// Do not use kRemoveClient here, as it will close the client,
141983-
// but the client cannot be closed in this state.
141984-
const idx = this[kClients].indexOf(target)
141985-
if (idx !== -1) {
141986-
this[kClients].splice(idx, 1)
141987-
}
141988-
}
141989-
})
141990141990
}
141991141991

141992141992
[kGetDispatcher] () {
@@ -145011,8 +145011,8 @@ const { EC2Client, RunInstancesCommand, TerminateInstancesCommand, waitUntilInst
145011145011
const core = __nccwpck_require__(2186);
145012145012
const config = __nccwpck_require__(4570);
145013145013

145014-
// User data scripts are run as the root user
145015-
function buildUserDataScript(githubRegistrationToken, label) {
145014+
// Build the commands to run on the instance
145015+
function buildRunCommands(githubRegistrationToken, label) {
145016145016
let userData;
145017145017
if (config.input.runnerHomeDir) {
145018145018
// If runner home directory is specified, we expect the actions-runner software (and dependencies)
@@ -145021,8 +145021,7 @@ function buildUserDataScript(githubRegistrationToken, label) {
145021145021
'#!/bin/bash',
145022145022
'exec > >(tee /var/log/user-data.log|logger -t user-data -s 2>/dev/console) 2>&1',
145023145023
`cd "${config.input.runnerHomeDir}"`,
145024-
`echo "${config.input.preRunnerScript}" > pre-runner-script.sh`,
145025-
'source pre-runner-script.sh',
145024+
'source /tmp/pre-runner-script.sh',
145026145025
'export RUNNER_ALLOW_RUNASROOT=1',
145027145026
`./config.sh --url https://github.com/${config.githubContext.owner}/${config.githubContext.repo} --token ${githubRegistrationToken} --labels ${label}`,
145028145027
];
@@ -145031,10 +145030,9 @@ function buildUserDataScript(githubRegistrationToken, label) {
145031145030
'#!/bin/bash',
145032145031
'exec > >(tee /var/log/user-data.log|logger -t user-data -s 2>/dev/console) 2>&1',
145033145032
'mkdir actions-runner && cd actions-runner',
145034-
`echo "${config.input.preRunnerScript}" > pre-runner-script.sh`,
145035-
'source pre-runner-script.sh',
145033+
'source /tmp/pre-runner-script.sh',
145036145034
'case $(uname -m) in aarch64) ARCH="arm64" ;; amd64|x86_64) ARCH="x64" ;; esac && export RUNNER_ARCH=${ARCH}',
145037-
'RUNNER_VERSION=$(curl -s "https://api.github.com/repos/actions/runner/releases/latest" | jq -r ".name" | tr -d "v")',
145035+
`RUNNER_VERSION=$(curl -s "https://api.github.com/repos/actions/runner/releases/latest" | grep -o '"tag_name"[[:space:]]*:[[:space:]]*"[^"]*"' | sed 's/.*"tag_name"[[:space:]]*:[[:space:]]*"\\([^"]*\\)".*/\\1/' | tr -d "v")`,
145038145036
'curl -O -L https://github.com/actions/runner/releases/download/v${RUNNER_VERSION}/actions-runner-linux-${RUNNER_ARCH}-${RUNNER_VERSION}.tar.gz',
145039145037
'tar xzf ./actions-runner-linux-${RUNNER_ARCH}-${RUNNER_VERSION}.tar.gz',
145040145038
'export RUNNER_ALLOW_RUNASROOT=1',
@@ -145053,6 +145051,57 @@ function buildUserDataScript(githubRegistrationToken, label) {
145053145051
return userData;
145054145052
}
145055145053

145054+
// Build cloud-init YAML user data
145055+
function buildUserDataScript(githubRegistrationToken, label) {
145056+
const runCommands = buildRunCommands(githubRegistrationToken, label);
145057+
145058+
// Create a script file with all commands to avoid YAML escaping issues
145059+
const scriptContent = runCommands.join('\n');
145060+
145061+
// Start with cloud-init header
145062+
let yamlContent = '#cloud-config\n';
145063+
145064+
// Add packages if specified
145065+
if (config.input.packages && config.input.packages.length > 0) {
145066+
yamlContent += 'packages:\n';
145067+
config.input.packages.forEach(pkg => {
145068+
yamlContent += ` - ${pkg}\n`;
145069+
});
145070+
}
145071+
145072+
// Write files
145073+
yamlContent += 'write_files:\n';
145074+
145075+
// Always write pre-runner script (even if empty) since runner-setup.sh always sources it
145076+
yamlContent += ' - path: /tmp/pre-runner-script.sh\n';
145077+
yamlContent += ' permissions: "0755"\n';
145078+
yamlContent += ' content: |\n';
145079+
145080+
if (config.input.preRunnerScript) {
145081+
config.input.preRunnerScript.split('\n').forEach(line => {
145082+
yamlContent += ` ${line}\n`;
145083+
});
145084+
} else {
145085+
yamlContent += ' #!/bin/bash\n';
145086+
}
145087+
145088+
// Write main setup script
145089+
yamlContent += ' - path: /tmp/runner-setup.sh\n';
145090+
yamlContent += ' permissions: "0755"\n';
145091+
yamlContent += ' content: |\n';
145092+
145093+
// Add each line of the script with proper indentation
145094+
scriptContent.split('\n').forEach(line => {
145095+
yamlContent += ` ${line}\n`;
145096+
});
145097+
145098+
// Execute the script
145099+
yamlContent += 'runcmd:\n';
145100+
yamlContent += ' - /tmp/runner-setup.sh\n';
145101+
145102+
return yamlContent;
145103+
}
145104+
145056145105
function buildMarketOptions() {
145057145106
if (config.input.marketType !== 'spot') {
145058145107
return undefined;
@@ -145080,7 +145129,7 @@ async function createEc2InstanceWithParams(imageId, subnetId, securityGroupId, l
145080145129
MinCount: 1,
145081145130
SecurityGroupIds: [securityGroupId],
145082145131
SubnetId: subnetId,
145083-
UserData: Buffer.from(userData.join('\n')).toString('base64'),
145132+
UserData: Buffer.from(userData).toString('base64'),
145084145133
IamInstanceProfile: config.input.iamRoleName ? { Name: config.input.iamRoleName } : undefined,
145085145134
TagSpecifications: config.tagSpecifications,
145086145135
InstanceMarketOptions: buildMarketOptions(),
@@ -145237,6 +145286,7 @@ class Config {
145237145286
blockDeviceMappings: JSON.parse(core.getInput('block-device-mappings') || '[]'),
145238145287
availabilityZonesConfig: core.getInput('availability-zones-config'),
145239145288
metadataOptions: JSON.parse(core.getInput('metadata-options') || '{}'),
145289+
packages: JSON.parse(core.getInput('packages') || '[]'),
145240145290
};
145241145291

145242145292
// Get the AWS_REGION environment variable

src/aws.js

Lines changed: 57 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@ const { EC2Client, RunInstancesCommand, TerminateInstancesCommand, waitUntilInst
33
const core = require('@actions/core');
44
const config = require('./config');
55

6-
// User data scripts are run as the root user
7-
function buildUserDataScript(githubRegistrationToken, label) {
6+
// Build the commands to run on the instance
7+
function buildRunCommands(githubRegistrationToken, label) {
88
let userData;
99
if (config.input.runnerHomeDir) {
1010
// If runner home directory is specified, we expect the actions-runner software (and dependencies)
@@ -13,8 +13,7 @@ function buildUserDataScript(githubRegistrationToken, label) {
1313
'#!/bin/bash',
1414
'exec > >(tee /var/log/user-data.log|logger -t user-data -s 2>/dev/console) 2>&1',
1515
`cd "${config.input.runnerHomeDir}"`,
16-
`echo "${config.input.preRunnerScript}" > pre-runner-script.sh`,
17-
'source pre-runner-script.sh',
16+
'source /tmp/pre-runner-script.sh',
1817
'export RUNNER_ALLOW_RUNASROOT=1',
1918
`./config.sh --url https://github.com/${config.githubContext.owner}/${config.githubContext.repo} --token ${githubRegistrationToken} --labels ${label}`,
2019
];
@@ -23,10 +22,9 @@ function buildUserDataScript(githubRegistrationToken, label) {
2322
'#!/bin/bash',
2423
'exec > >(tee /var/log/user-data.log|logger -t user-data -s 2>/dev/console) 2>&1',
2524
'mkdir actions-runner && cd actions-runner',
26-
`echo "${config.input.preRunnerScript}" > pre-runner-script.sh`,
27-
'source pre-runner-script.sh',
25+
'source /tmp/pre-runner-script.sh',
2826
'case $(uname -m) in aarch64) ARCH="arm64" ;; amd64|x86_64) ARCH="x64" ;; esac && export RUNNER_ARCH=${ARCH}',
29-
'RUNNER_VERSION=$(curl -s "https://api.github.com/repos/actions/runner/releases/latest" | jq -r ".name" | tr -d "v")',
27+
`RUNNER_VERSION=$(curl -s "https://api.github.com/repos/actions/runner/releases/latest" | grep -o '"tag_name"[[:space:]]*:[[:space:]]*"[^"]*"' | sed 's/.*"tag_name"[[:space:]]*:[[:space:]]*"\\([^"]*\\)".*/\\1/' | tr -d "v")`,
3028
'curl -O -L https://github.com/actions/runner/releases/download/v${RUNNER_VERSION}/actions-runner-linux-${RUNNER_ARCH}-${RUNNER_VERSION}.tar.gz',
3129
'tar xzf ./actions-runner-linux-${RUNNER_ARCH}-${RUNNER_VERSION}.tar.gz',
3230
'export RUNNER_ALLOW_RUNASROOT=1',
@@ -45,6 +43,57 @@ function buildUserDataScript(githubRegistrationToken, label) {
4543
return userData;
4644
}
4745

46+
// Build cloud-init YAML user data
47+
function buildUserDataScript(githubRegistrationToken, label) {
48+
const runCommands = buildRunCommands(githubRegistrationToken, label);
49+
50+
// Create a script file with all commands to avoid YAML escaping issues
51+
const scriptContent = runCommands.join('\n');
52+
53+
// Start with cloud-init header
54+
let yamlContent = '#cloud-config\n';
55+
56+
// Add packages if specified
57+
if (config.input.packages && config.input.packages.length > 0) {
58+
yamlContent += 'packages:\n';
59+
config.input.packages.forEach(pkg => {
60+
yamlContent += ` - ${pkg}\n`;
61+
});
62+
}
63+
64+
// Write files
65+
yamlContent += 'write_files:\n';
66+
67+
// Always write pre-runner script (even if empty) since runner-setup.sh always sources it
68+
yamlContent += ' - path: /tmp/pre-runner-script.sh\n';
69+
yamlContent += ' permissions: "0755"\n';
70+
yamlContent += ' content: |\n';
71+
72+
if (config.input.preRunnerScript) {
73+
config.input.preRunnerScript.split('\n').forEach(line => {
74+
yamlContent += ` ${line}\n`;
75+
});
76+
} else {
77+
yamlContent += ' #!/bin/bash\n';
78+
}
79+
80+
// Write main setup script
81+
yamlContent += ' - path: /tmp/runner-setup.sh\n';
82+
yamlContent += ' permissions: "0755"\n';
83+
yamlContent += ' content: |\n';
84+
85+
// Add each line of the script with proper indentation
86+
scriptContent.split('\n').forEach(line => {
87+
yamlContent += ` ${line}\n`;
88+
});
89+
90+
// Execute the script
91+
yamlContent += 'runcmd:\n';
92+
yamlContent += ' - /tmp/runner-setup.sh\n';
93+
94+
return yamlContent;
95+
}
96+
4897
function buildMarketOptions() {
4998
if (config.input.marketType !== 'spot') {
5099
return undefined;
@@ -72,7 +121,7 @@ async function createEc2InstanceWithParams(imageId, subnetId, securityGroupId, l
72121
MinCount: 1,
73122
SecurityGroupIds: [securityGroupId],
74123
SubnetId: subnetId,
75-
UserData: Buffer.from(userData.join('\n')).toString('base64'),
124+
UserData: Buffer.from(userData).toString('base64'),
76125
IamInstanceProfile: config.input.iamRoleName ? { Name: config.input.iamRoleName } : undefined,
77126
TagSpecifications: config.tagSpecifications,
78127
InstanceMarketOptions: buildMarketOptions(),

src/config.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ class Config {
2727
blockDeviceMappings: JSON.parse(core.getInput('block-device-mappings') || '[]'),
2828
availabilityZonesConfig: core.getInput('availability-zones-config'),
2929
metadataOptions: JSON.parse(core.getInput('metadata-options') || '{}'),
30+
packages: JSON.parse(core.getInput('packages') || '[]'),
3031
};
3132

3233
// Get the AWS_REGION environment variable

0 commit comments

Comments
 (0)