Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
105 changes: 98 additions & 7 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,97 @@ inputs:
runs:
using: composite
steps:
# client_payload arrives as plain JSON, base64(gzip), or a reference to a server-stashed payload.
# Later steps read fields off it in YAML expressions, which evaluate before the engine runs, so
# resolve them here. Must stay first. CLIENT_PAYLOAD itself is passed through untouched.
- name: Resolve payload fields
id: payload-fields
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
env:
PAYLOAD_ARG: ${{ inputs.client_payload }}
RESOLVER_URL_ARG: ${{ inputs.resolver_url }}
with:
script: |
const { gunzipSync } = require('zlib');
const PAYLOAD_FETCH_TIMEOUT_MS = 10000;
const OVERSIZED_PAYLOAD_REFERENCE = 'oversized-payload-reference';
// Bounds a decompression bomb: gzip is asymmetric, so a ~66KB input can inflate to 50MB and
// exhaust the runner. The largest payload seen in production is ~1.4MB, so this leaves ample
// room while keeping the allocation finite.
const MAX_INFLATED_PAYLOAD_BYTES = 32 * 1024 * 1024;
const inflate = (value) => {
const buffer = Buffer.from(value, 'base64');
if (buffer.length < 2 || buffer[0] !== 0x1f || buffer[1] !== 0x8b) {
return null;
}
try {
return gunzipSync(buffer, { maxOutputLength: MAX_INFLATED_PAYLOAD_BYTES }).toString('utf8');
} catch (err) {
if (err.code === 'ERR_BUFFER_TOO_LARGE') {
throw new Error(`payload inflates beyond ${MAX_INFLATED_PAYLOAD_BYTES} bytes; refusing to expand it`);
}
throw new Error(`gzip decompression failed: ${err.message}`);
}
};

const parsePayload = (value) => {
const parsed = JSON.parse(value);
return typeof parsed === 'string' ? JSON.parse(parsed) : parsed;
};

const resolve = async (raw) => {
if (raw.includes(OVERSIZED_PAYLOAD_REFERENCE)) {
const reference = parsePayload(raw);
if (reference && reference.type === OVERSIZED_PAYLOAD_REFERENCE) {
// The stash is served by the same host as the resolver, so require that origin rather
// than fetching whatever the payload names. Without this a crafted client_payload could
// aim the runner at an internal address, which matters most on self-hosted runners.
const expectedOrigin = new URL(process.env.RESOLVER_URL_ARG || '').origin;
Comment thread
yeelali14 marked this conversation as resolved.
const payloadOrigin = new URL(reference.payloadUrl).origin;
if (payloadOrigin !== expectedOrigin) {
throw new Error(`refusing to fetch stashed payload from ${payloadOrigin}; expected ${expectedOrigin}`);
}
core.setSecret(reference.resolverToken);
const response = await fetch(reference.payloadUrl, {
headers: { Authorization: `Bearer ${reference.resolverToken}` },
signal: AbortSignal.timeout(PAYLOAD_FETCH_TIMEOUT_MS),
});
Comment thread
yeelali14 marked this conversation as resolved.
if (!response.ok) {
throw new Error(`stashed payload fetch returned ${response.status}`);
}
const body = await response.text();
return { mode: 'reference', payload: parsePayload(inflate(body) ?? body) };
}
}
const inflated = inflate(raw);
if (inflated !== null) {
return { mode: 'compressed', payload: parsePayload(inflated) };
}
return { mode: 'plain', payload: parsePayload(raw) };
};

try {
const { mode, payload } = await resolve(process.env.PAYLOAD_ARG || '');
core.info(`client_payload mode=${mode}`);

// The installation token rides inside client_payload, so mask it before it reaches an
// output or a later step's env dump.
const githubToken = payload.githubToken || '';
if (githubToken) {
core.setSecret(githubToken);
}
const hasCmRepo = payload.hasCmRepo === true;
core.setOutput('github_token', githubToken);
core.setOutput('url', payload.headHttpUrl || payload.repoUrl || '');
core.setOutput('has_cm_repo', String(hasCmRepo));
core.setOutput('cm_repository', hasCmRepo ? `${payload.owner}/${payload.cmRepo}` : '');
core.setOutput('cm_repo_ref', payload.cmRepoRef || '');
core.setOutput('has_cm_org', String(payload.hasCmOrg === true));
core.setOutput('cm_org_ref', payload.cmOrgRef || '');
} catch (err) {
core.setFailed(`Failed resolving client payload: ${err}`);
}

- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
with:
node-version: 20.12.2
Expand All @@ -47,7 +138,7 @@ runs:
repository: ${{ inputs.full_repository }}
ref: ${{ inputs.base_ref }}
path: gitstream/repo/
token: ${{ fromJSON(fromJSON(inputs.client_payload)).githubToken || github.token }}
token: ${{ steps.payload-fields.outputs.github_token || github.token }}

- name: Escape single quotes
id: safe-strings
Expand All @@ -56,7 +147,7 @@ runs:
BASE_REF_ARG: ${{ inputs.base_ref }}
HEAD_REF_ARG: ${{ inputs.head_ref }}
PAYLOAD_ARG: ${{ inputs.client_payload }}
URL_ARG: ${{ fromJSON(fromJSON(inputs.client_payload)).headHttpUrl || fromJSON(fromJSON(inputs.client_payload)).repoUrl }}
URL_ARG: ${{ steps.payload-fields.outputs.url }}
with:
script: |
try {
Expand Down Expand Up @@ -97,19 +188,19 @@ runs:

- name: Checkout cm repo
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
if: ${{ fromJSON(fromJSON(inputs.client_payload)).hasCmRepo == true }}
if: ${{ steps.payload-fields.outputs.has_cm_repo == 'true' }}
with:
repository: '${{ fromJSON(fromJSON(inputs.client_payload)).owner }}/${{ fromJSON(fromJSON(inputs.client_payload)).cmRepo }}'
ref: ${{ fromJSON(fromJSON(inputs.client_payload)).cmRepoRef }}
repository: ${{ steps.payload-fields.outputs.cm_repository }}
ref: ${{ steps.payload-fields.outputs.cm_repo_ref }}
path: gitstream/cm/
fetch-depth: 1

- name: Checkout cm org
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
if: ${{ fromJSON(fromJSON(inputs.client_payload)).hasCmOrg == true }}
if: ${{ steps.payload-fields.outputs.has_cm_org == 'true' }}
with:
repository: 'cm/cm'
ref: ${{ fromJSON(fromJSON(inputs.client_payload)).cmOrgRef }}
ref: ${{ steps.payload-fields.outputs.cm_org_ref }}
path: gitstream/cm/
fetch-depth: 1

Expand Down