@@ -145105,10 +145105,62 @@ function buildRunCommands(githubRegistrationToken, label) {
145105145105 return userData.filter(Boolean);
145106145106}
145107145107
145108+ // Build the commands to run on the instance for JIT mode.
145109+ // JIT runners skip config.sh entirely and pass the encoded config directly to run.sh.
145110+ function buildJitRunCommands(encodedJitConfig) {
145111+ const debug = config.input.runnerDebug;
145112+ const dbg = (cmd) => debug ? cmd : null;
145113+
145114+ // Common preamble: fail-fast and log capture
145115+ const preamble = [
145116+ '#!/bin/bash',
145117+ 'LOGFILE=/tmp/runner-setup.log',
145118+ 'exec > >(tee -a "$LOGFILE") 2>&1',
145119+ 'set -e',
145120+ dbg('echo "[RUNNER] =========================================="'),
145121+ dbg('echo "[RUNNER] JIT Setup script started at $(date -u)"'),
145122+ dbg('echo "[RUNNER] =========================================="'),
145123+ ].filter(Boolean);
145124+
145125+ let userData;
145126+ if (config.input.runnerHomeDir) {
145127+ userData = [
145128+ ...preamble,
145129+ `cd "${config.input.runnerHomeDir}"`,
145130+ 'source /tmp/pre-runner-script.sh',
145131+ 'export RUNNER_ALLOW_RUNASROOT=1',
145132+ // Remove stale runner config from AMI so run.sh doesn't get confused
145133+ 'rm -f .runner .credentials .credentials_rsaparams',
145134+ ];
145135+ } else {
145136+ userData = [
145137+ ...preamble,
145138+ 'mkdir actions-runner && cd actions-runner',
145139+ 'source /tmp/pre-runner-script.sh',
145140+ 'case $(uname -m) in aarch64) ARCH="arm64" ;; amd64|x86_64) ARCH="x64" ;; esac && export RUNNER_ARCH=${ARCH}',
145141+ `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")`,
145142+ 'curl -O -L https://github.com/actions/runner/releases/download/v${RUNNER_VERSION}/actions-runner-linux-${RUNNER_ARCH}-${RUNNER_VERSION}.tar.gz',
145143+ 'tar xzf ./actions-runner-linux-${RUNNER_ARCH}-${RUNNER_VERSION}.tar.gz',
145144+ 'export RUNNER_ALLOW_RUNASROOT=1',
145145+ ];
145146+ }
145147+
145148+ if (config.input.runAsUser) {
145149+ userData.push(`chown -R ${config.input.runAsUser} . 2>&1 || true`);
145150+ userData.push(`runuser -u ${config.input.runAsUser} -- ./run.sh --jitconfig ${encodedJitConfig}`);
145151+ } else {
145152+ userData.push(`./run.sh --jitconfig ${encodedJitConfig}`);
145153+ }
145154+
145155+ return userData;
145156+ }
145157+
145108145158// Build user data as a cloud-boothook (runs during cloud-init init stage,
145109145159// bypassing cloud_final_modules which may be empty on some AMIs)
145110- function buildUserDataScript(githubRegistrationToken, label) {
145111- const runCommands = buildRunCommands(githubRegistrationToken, label);
145160+ function buildUserDataScript(githubRegistrationToken, label, encodedJitConfig) {
145161+ const runCommands = encodedJitConfig
145162+ ? buildJitRunCommands(encodedJitConfig)
145163+ : buildRunCommands(githubRegistrationToken, label);
145112145164
145113145165 const lines = [];
145114145166
@@ -145173,12 +145225,12 @@ function buildMarketOptions() {
145173145225 };
145174145226}
145175145227
145176- async function createEc2InstanceWithParams(imageId, subnetId, securityGroupId, label, githubRegistrationToken, region) {
145228+ async function createEc2InstanceWithParams(imageId, subnetId, securityGroupId, label, githubRegistrationToken, region, encodedJitConfig ) {
145177145229 // Region is always specified now, so we can directly use it
145178145230 const ec2ClientOptions = { region };
145179145231 const ec2 = new EC2Client(ec2ClientOptions);
145180145232
145181- const userData = buildUserDataScript(githubRegistrationToken, label);
145233+ const userData = buildUserDataScript(githubRegistrationToken, label, encodedJitConfig );
145182145234
145183145235 const params = {
145184145236 ImageId: imageId,
@@ -145215,7 +145267,7 @@ async function createEc2InstanceWithParams(imageId, subnetId, securityGroupId, l
145215145267 return ec2InstanceId;
145216145268}
145217145269
145218- async function startEc2Instance(label, githubRegistrationToken) {
145270+ async function startEc2Instance(label, githubRegistrationToken, encodedJitConfig ) {
145219145271 core.info(`Attempting to start EC2 instance using ${config.availabilityZones.length} availability zone configuration(s)`);
145220145272
145221145273 const errors = [];
@@ -145235,7 +145287,8 @@ async function startEc2Instance(label, githubRegistrationToken) {
145235145287 azConfig.securityGroupId,
145236145288 label,
145237145289 githubRegistrationToken,
145238- region
145290+ region,
145291+ encodedJitConfig
145239145292 );
145240145293
145241145294 core.info(`Successfully started AWS EC2 instance ${ec2InstanceId} using availability zone configuration ${i + 1} in region ${region}`);
@@ -145341,6 +145394,8 @@ module.exports = {
145341145394 terminateEc2Instance,
145342145395 waitForInstanceRunning,
145343145396 getInstanceConsoleOutput,
145397+ // Exposed for testing only
145398+ _buildUserDataScriptForTest: buildUserDataScript,
145344145399};
145345145400
145346145401
@@ -145379,12 +145434,14 @@ class Config {
145379145434 availabilityZonesConfig: core.getInput('availability-zones-config'),
145380145435 metadataOptions: JSON.parse(core.getInput('metadata-options') || '{}'),
145381145436 packages: JSON.parse(core.getInput('packages') || '[]'),
145437+ useJit: core.getInput('use-jit') === 'true',
145438+ runnerGroupId: parseInt(core.getInput('runner-group-id') || '1', 10),
145382145439 runnerDebug: core.getInput('runner-debug') === 'true',
145383145440 };
145384145441
145385145442 // Get the AWS_REGION environment variable
145386145443 this.defaultRegion = process.env.AWS_REGION;
145387-
145444+
145388145445 const tags = JSON.parse(core.getInput('aws-resource-tags'));
145389145446 this.tagSpecifications = null;
145390145447 if (tags.length > 0) {
@@ -145422,12 +145479,12 @@ class Config {
145422145479 if (this.input.availabilityZonesConfig) {
145423145480 try {
145424145481 this.availabilityZones = JSON.parse(this.input.availabilityZonesConfig);
145425-
145482+
145426145483 // Validate each availability zone configuration
145427145484 if (!Array.isArray(this.availabilityZones)) {
145428145485 throw new Error('availability-zones-config must be a JSON array');
145429145486 }
145430-
145487+
145431145488 this.availabilityZones.forEach((az, index) => {
145432145489 if (!az.imageId) {
145433145490 throw new Error(`Missing imageId in availability-zones-config at index ${index}`);
@@ -145460,7 +145517,7 @@ class Config {
145460145517 `Either provide 'availability-zones-config' or all of the following: 'ec2-image-id', 'subnet-id', 'security-group-id'`
145461145518 );
145462145519 }
145463-
145520+
145464145521 // Convert individual parameters to a single availability zone config
145465145522 this.availabilityZones.push({
145466145523 imageId: this.input.ec2ImageId,
@@ -145469,10 +145526,17 @@ class Config {
145469145526 // Add default region when using legacy configuration
145470145527 region: this.defaultRegion
145471145528 });
145472-
145529+
145473145530 core.info('Using individual parameters as a single availability zone configuration');
145474145531 }
145475145532
145533+ if (this.input.useJit && this.input.runAsService) {
145534+ throw new Error(
145535+ "The 'use-jit' and 'run-runner-as-service' inputs are incompatible. " +
145536+ 'JIT runners are single-use and cannot run as a service.'
145537+ );
145538+ }
145539+
145476145540 if (this.marketType?.length > 0 && this.input.marketType !== 'spot') {
145477145541 throw new Error('Invalid `market-type` input. Allowed values: spot.');
145478145542 }
@@ -145500,6 +145564,8 @@ try {
145500145564 core.setFailed(error.message);
145501145565}
145502145566
145567+ module.exports.Config = Config;
145568+
145503145569
145504145570/***/ }),
145505145571
@@ -145539,6 +145605,33 @@ async function getRegistrationToken() {
145539145605 }
145540145606}
145541145607
145608+ // generate a JIT (Just-In-Time) runner configuration via the GitHub API
145609+ async function getJitRunnerConfig(label) {
145610+ const octokit = github.getOctokit(config.input.githubToken);
145611+
145612+ try {
145613+ const response = await octokit.request(
145614+ 'POST /repos/{owner}/{repo}/actions/runners/generate-jitconfig',
145615+ {
145616+ ...config.githubContext,
145617+ name: `ec2-${label}`,
145618+ runner_group_id: config.input.runnerGroupId,
145619+ labels: [label],
145620+ work_folder: '_work',
145621+ }
145622+ );
145623+
145624+ core.info('GitHub JIT runner configuration is received');
145625+ return {
145626+ runnerId: response.data.runner.id,
145627+ encodedJitConfig: response.data.encoded_jit_config,
145628+ };
145629+ } catch (error) {
145630+ core.error('GitHub JIT runner configuration generation error');
145631+ throw error;
145632+ }
145633+ }
145634+
145542145635async function removeRunner() {
145543145636 const runner = await getRunner(config.input.label);
145544145637 const octokit = github.getOctokit(config.input.githubToken);
@@ -145604,6 +145697,7 @@ async function waitForRunnerRegistered(label, onPollCallback) {
145604145697
145605145698module.exports = {
145606145699 getRegistrationToken,
145700+ getJitRunnerConfig,
145607145701 removeRunner,
145608145702 waitForRunnerRegistered,
145609145703};
@@ -147590,8 +147684,19 @@ function setOutput(label, ec2InstanceId, region) {
147590147684
147591147685async function start() {
147592147686 const label = config.input.label ? config.input.label : config.generateUniqueLabel();
147593- const githubRegistrationToken = await gh.getRegistrationToken();
147594- const result = await aws.startEc2Instance(label, githubRegistrationToken);
147687+
147688+ let githubRegistrationToken = null;
147689+ let encodedJitConfig = null;
147690+
147691+ if (config.input.useJit) {
147692+ const jitConfig = await gh.getJitRunnerConfig(label);
147693+ encodedJitConfig = jitConfig.encodedJitConfig;
147694+ core.info(`JIT runner created with runner ID: ${jitConfig.runnerId}`);
147695+ } else {
147696+ githubRegistrationToken = await gh.getRegistrationToken();
147697+ }
147698+
147699+ const result = await aws.startEc2Instance(label, githubRegistrationToken, encodedJitConfig);
147595147700 const ec2InstanceId = result.ec2InstanceId;
147596147701 const region = result.region;
147597147702
@@ -147623,7 +147728,12 @@ async function start() {
147623147728
147624147729async function stop() {
147625147730 await aws.terminateEc2Instance();
147626- await gh.removeRunner();
147731+
147732+ if (config.input.useJit) {
147733+ core.info('JIT runner auto-deregisters after job completion. Skipping runner removal.');
147734+ } else {
147735+ await gh.removeRunner();
147736+ }
147627147737}
147628147738
147629147739(async function () {
0 commit comments