From 9fcb4ed591fa625c546b4a0e9a20604daea3a160 Mon Sep 17 00:00:00 2001 From: kurok <22548029+kurok@users.noreply.github.com> Date: Mon, 6 Jul 2026 15:59:58 +0100 Subject: [PATCH] fix: correct sed delimiter in GH_REPO_URL extraction (#63) buildReusableUserData()'s per-boot registerScript extracted GH_REPO_URL with sed using "#" as the delimiter, while GH_TOKEN and GH_LABEL used "/". Because the pattern ends in the sed end-of-line anchor "$" immediately followed by "#", and the whole sed argument sits inside a double-quoted shell string, bash expands "$#" (number of positional arguments, always 0 for this script) before sed ever runs. That corrupts the sed script and makes it error out with "unterminated 's' command". Since the assignment isn't guarded with `|| true` and the script runs under `set -euo pipefail`, the failure aborted the whole registerScript at the "configuring" phone-home step, so every `reuse: stop` warm-pool launch failed registration before config.sh was ever invoked. Switch the delimiter to "," which isn't a bash special-parameter suffix, matching the fix verified in the issue against both BSD and GNU sed. Adds a regression test that extracts the real GH_REPO_URL sed line out of the generated user-data and executes it in bash, so it fails against the old "#" delimiter and passes with the fix. Fixes #63 Signed-off-by: kurok <22548029+kurok@users.noreply.github.com> --- dist/index.js | 2 +- src/aws.js | 2 +- tests/warmpool.test.js | 33 +++++++++++++++++++++++++++++++++ 3 files changed, 35 insertions(+), 2 deletions(-) diff --git a/dist/index.js b/dist/index.js index 94773fa0..4af9a575 100644 --- a/dist/index.js +++ b/dist/index.js @@ -105018,7 +105018,7 @@ function buildReusableUserData({ runnerVersion, owner, repo, label, githubRegist 'UD=$(curl -fsS -H "X-aws-ec2-metadata-token: $IMDS_TOKEN" "http://169.254.169.254/latest/user-data" 2>/dev/null || true)', 'GH_TOKEN=$(printf \'%s\\n\' "$UD" | sed -n "s/^GH_TOKEN=\'\\(.*\\)\'$/\\1/p" | head -n1)', 'GH_LABEL=$(printf \'%s\\n\' "$UD" | sed -n "s/^GH_LABEL=\'\\(.*\\)\'$/\\1/p" | head -n1)', - 'GH_REPO_URL=$(printf \'%s\\n\' "$UD" | sed -n "s#^GH_REPO_URL=\'\\(.*\\)\'$#\\1#p" | head -n1)', + 'GH_REPO_URL=$(printf \'%s\\n\' "$UD" | sed -n "s,^GH_REPO_URL=\'\\(.*\\)\'$,\\1,p" | head -n1)', 'cd /home/runner/actions-runner', 'rm -f .runner .credentials .credentials_rsaparams', 'gh_runner_phone_home configuring', diff --git a/src/aws.js b/src/aws.js index 47646298..683af1df 100644 --- a/src/aws.js +++ b/src/aws.js @@ -368,7 +368,7 @@ function buildReusableUserData({ runnerVersion, owner, repo, label, githubRegist 'UD=$(curl -fsS -H "X-aws-ec2-metadata-token: $IMDS_TOKEN" "http://169.254.169.254/latest/user-data" 2>/dev/null || true)', 'GH_TOKEN=$(printf \'%s\\n\' "$UD" | sed -n "s/^GH_TOKEN=\'\\(.*\\)\'$/\\1/p" | head -n1)', 'GH_LABEL=$(printf \'%s\\n\' "$UD" | sed -n "s/^GH_LABEL=\'\\(.*\\)\'$/\\1/p" | head -n1)', - 'GH_REPO_URL=$(printf \'%s\\n\' "$UD" | sed -n "s#^GH_REPO_URL=\'\\(.*\\)\'$#\\1#p" | head -n1)', + 'GH_REPO_URL=$(printf \'%s\\n\' "$UD" | sed -n "s,^GH_REPO_URL=\'\\(.*\\)\'$,\\1,p" | head -n1)', 'cd /home/runner/actions-runner', 'rm -f .runner .credentials .credentials_rsaparams', 'gh_runner_phone_home configuring', diff --git a/tests/warmpool.test.js b/tests/warmpool.test.js index 83ef7da5..dd169642 100644 --- a/tests/warmpool.test.js +++ b/tests/warmpool.test.js @@ -1,6 +1,8 @@ // Tests for warm-pool (reuse: stop) support: findStoppedPoolInstance strict // matching, warmStartInstance ordering, stop/cycle helpers, and the per-boot // re-registration hook in the generated user-data. +const { execFileSync } = require('child_process'); + const mockSend = jest.fn(); jest.mock('@aws-sdk/client-ec2', () => ({ EC2Client: jest.fn(() => ({ send: mockSend })), @@ -185,4 +187,35 @@ describe('buildUserData — reuse: stop variant', () => { expect(ud).not.toContain('gh-runner.service'); expect(ud).not.toContain('/opt/gh-runner-register.sh'); }); + + test('GH_REPO_URL extraction survives the sed-delimiter/$# collision (regression, #63)', () => { + const ud = aws.buildUserData({ ...args, reuse: 'stop' }); + + // Pull the real GH_REPO_URL=$(... sed ...) line straight out of the + // generated script, so this test exercises the actual shipped code + // rather than a hand-reimplementation of it. + const marker = 'GH_REPO_URL=$(printf'; + const start = ud.indexOf(marker); + expect(start).toBeGreaterThan(-1); + const end = ud.indexOf('\n', start); + expect(end).toBeGreaterThan(start); + const extractionLine = ud.slice(start, end); + + // Run that exact line under 'set -euo pipefail' with a controlled IMDS + // user-data value, the same shape the real registerScript reads via + // `UD=$(curl ... /latest/user-data)`. If the sed delimiter collides with + // bash's `$#` special parameter (as `#` does), the sed invocation is + // malformed, the pipeline fails, and the assignment aborts the script + // under `set -e` before GH_REPO_URL is ever printed. + const script = [ + '#!/bin/bash', + 'set -euo pipefail', + "UD=\"GH_REPO_URL='https://github.com/foo/bar'\"", + extractionLine, + 'printf \'%s\' "$GH_REPO_URL"', + ].join('\n'); + + const result = execFileSync('bash', ['-c', script]).toString(); + expect(result).toBe('https://github.com/foo/bar'); + }); });