Skip to content

Commit 0fdd401

Browse files
authored
feat: Phase 4 (retry) — non-root runner + --ephemeral + hardcoded checksum table (#26)
Closes #20. Supersedes the reverted #18 / #19 / #21. Implements the full Phase 4 bootstrap hardening from issue #10, with the root-cause fix from #20 baked in. Key differences from the earlier failed attempts: ## The fix for the actual failure Previous attempts died at: curl -fsSL <tarball>.sha256 | awk '{print }' with a 404 (actions/runner doesn't publish per-tarball sidecar files, empirically confirmed via aws ec2 get-console-output on a probe instance — see #20). This PR replaces that with a hardcoded table of expected hashes in src/runner-checksums.js, keyed by 'arch-version'. Two x86_64 / arm64 entries for the currently-pinned v2.333.1, sourced from the release body at github.com/actions/runner/releases/tag/v2.333.1. CI enforces table-vs-upstream consistency on every PR (see pr.yml). ## Everything else from Phase 4 - Non-root 'runner' user (useradd -m, sudo -u runner -H bash heredoc). RUNNER_ALLOW_RUNASROOT=1 escape hatch removed. - New 'runner-version' input in action.yml (default '2.333.1'). To override, add matching x64+arm64 SHAs to runner-checksums.js in the same PR — verify-runner-url CI will reject the change if the hashes don't match upstream. - --ephemeral --unattended --disableupdate on config.sh. GitHub auto-deregisters the runner after its job; disableupdate keeps the binary stable during the short ephemeral session. - set -euo pipefail on both the outer and inner (runner-user) shells. The earlier fatal failure under set -e was the .sha256 404, which no longer exists. - Paramaterized RUNNER_VERSION / TARBALL / BASE bash vars. ## Tests tests/runner-checksums.test.js — 6 new cases covering the table shape, hex format, x64+arm64 parity per version, lookup returns for known/unknown keys. tests/config.test.js — 2 new cases for the runner-version input (default fallback + override). Total: 36 -> 44 tests. ## CI: verify-runner-url overhaul The job now parses the runner-version from action.yml, then: 1. HEADs the Linux x64 release asset (unchanged). 2. Fetches the release body via 'gh api'. 3. Greps the BEGIN SHA linux-x64 / linux-arm64 HTML comments. 4. Cross-checks against the values lookup() returns from src/runner-checksums.js. Drift between the hardcoded table and upstream fails CI at code- review time, not at runtime. ## Dogfood plan (MUCH more careful this time) Provider SHA-pin rotation after merge, same pattern as prior phases. This time I have full EC2 console-output diagnostic capability via the recipe saved in my notes — any new bootstrap failure should be trivially diagnosable rather than opaque. Closing #20 on merge. Signed-off-by: yuriyryabikov <22548029+kurok@users.noreply.github.com>
1 parent fd15768 commit 0fdd401

8 files changed

Lines changed: 339 additions & 23 deletions

File tree

.github/workflows/pr.yml

Lines changed: 43 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -63,25 +63,63 @@ jobs:
6363
run: npm test
6464

6565
verify-runner-url:
66-
name: Verify pinned actions/runner release exists
66+
name: Verify pinned actions/runner release + checksum table
6767
runs-on: ubuntu-latest
6868
steps:
6969
- name: Checkout
7070
uses: actions/checkout@v4
71-
- name: Extract runner version from src/aws.js
71+
- name: Extract default runner version from action.yml
7272
id: extract
7373
run: |
74-
version=$(grep -oE 'actions/runner/releases/download/v[0-9]+\.[0-9]+\.[0-9]+' src/aws.js | head -1 | sed 's|.*/v||')
74+
# action.yml declares:
75+
# runner-version:
76+
# ...
77+
# default: '2.333.1'
78+
version=$(awk '/^ runner-version:/{found=1} found && /^ default:/{gsub(/[^0-9.]/, "", $2); print $2; exit}' action.yml)
7579
if [ -z "$version" ]; then
76-
echo "::error::Could not locate the pinned actions/runner version in src/aws.js"
80+
echo "::error::Could not locate the default runner-version in action.yml"
7781
exit 1
7882
fi
7983
echo "version=$version" >> "$GITHUB_OUTPUT"
80-
echo "Pinned actions/runner: v$version"
84+
echo "Default actions/runner: v$version"
8185
- name: HEAD check the Linux x64 release asset
8286
env:
8387
VERSION: ${{ steps.extract.outputs.version }}
8488
run: |
8589
url="https://github.com/actions/runner/releases/download/v${VERSION}/actions-runner-linux-x64-${VERSION}.tar.gz"
8690
echo "Checking $url"
8791
curl -fsSLI -o /dev/null "$url"
92+
- name: Cross-check src/runner-checksums.js against release body
93+
env:
94+
VERSION: ${{ steps.extract.outputs.version }}
95+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
96+
run: |
97+
# Pull the release body once.
98+
body=$(gh api "/repos/actions/runner/releases/tags/v${VERSION}" --jq .body)
99+
100+
# Extract upstream hashes from HTML-comment-wrapped markdown like:
101+
# <!-- BEGIN SHA linux-x64 -->hex<!-- END SHA linux-x64 -->
102+
upstream_x64=$(printf '%s' "$body" | grep -oE 'BEGIN SHA linux-x64 -->[a-f0-9]+' | cut -d'>' -f2)
103+
upstream_arm64=$(printf '%s' "$body" | grep -oE 'BEGIN SHA linux-arm64 -->[a-f0-9]+' | cut -d'>' -f2)
104+
105+
if [ -z "$upstream_x64" ] || [ -z "$upstream_arm64" ]; then
106+
echo "::error::Could not parse linux-x64 / linux-arm64 SHA from release body"
107+
exit 1
108+
fi
109+
110+
# Extract committed hashes from src/runner-checksums.js by loading
111+
# it as a Node module. The module exports { CHECKSUMS, lookup(...) }.
112+
committed_x64=$(node -e "console.log(require('./src/runner-checksums').lookup('x64', process.env.VERSION) || '')")
113+
committed_arm64=$(node -e "console.log(require('./src/runner-checksums').lookup('arm64', process.env.VERSION) || '')")
114+
115+
ok=true
116+
if [ "$upstream_x64" != "$committed_x64" ]; then
117+
echo "::error::runner-checksums.js x64-$VERSION ($committed_x64) != upstream ($upstream_x64)"
118+
ok=false
119+
fi
120+
if [ "$upstream_arm64" != "$committed_arm64" ]; then
121+
echo "::error::runner-checksums.js arm64-$VERSION ($committed_arm64) != upstream ($upstream_arm64)"
122+
ok=false
123+
fi
124+
$ok
125+
echo "Checksums verified for v$VERSION (x64 + arm64)."

action.yml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,15 @@ inputs:
7070
IAM Role Name to attach to the created EC2 instance.
7171
This requires additional permissions on the AWS role used to launch instances.
7272
required: false
73+
runner-version:
74+
description: >-
75+
Version of the actions/runner binary to download and register.
76+
Must be one of the versions for which an entry exists in
77+
src/runner-checksums.js (the action verifies the downloaded
78+
tarball's SHA-256 against that table before extraction). To
79+
override, add the corresponding hash to the table in a PR.
80+
required: false
81+
default: '2.333.1'
7382
http-tokens:
7483
description: >-
7584
Instance Metadata Service (IMDS) token mode. Accepted values:

dist/index.js

Lines changed: 121 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -87857,6 +87857,7 @@ const config = __nccwpck_require__(1283);
8785787857
const log = __nccwpck_require__(7223);
8785887858
const { withRetry } = __nccwpck_require__(6759);
8785987859
const { sortByCreationDate } = __nccwpck_require__(5804);
87860+
const checksums = __nccwpck_require__(2874);
8786087861

8786187862
// EC2Client reads region + credentials from the environment (set by
8786287863
// aws-actions/configure-aws-credentials or by the instance profile on
@@ -87914,20 +87915,88 @@ async function resolveImageId(client) {
8791487915
async function startEc2Instance(label, githubRegistrationToken) {
8791587916
const client = ec2Client();
8791687917

87917-
// User data scripts are run as the root user.
87918-
// Docker and git are necessary for GitHub runner and should be pre-installed on the AMI.
87918+
// Bootstrap design notes (fix-forward after ec2-github-runner#18/#19/#20):
87919+
//
87920+
// - Hashes for the runner tarball come from src/runner-checksums.js
87921+
// (hardcoded table, cross-checked against the release body in CI).
87922+
// The earlier `curl -fsSL <tarball>.sha256` approach died because
87923+
// actions/runner doesn't publish per-tarball .sha256 sidecars.
87924+
//
87925+
// - Dedicated 'runner' user via useradd + sudo -u. The old
87926+
// RUNNER_ALLOW_RUNASROOT=1 escape hatch is gone. Runner has its
87927+
// own home under /home/runner/ and writes config.sh state there.
87928+
//
87929+
// - --ephemeral --unattended --disableupdate on config.sh: one-job
87930+
// runner, no interactive prompts, no runtime self-update during
87931+
// the session. GitHub auto-deregisters ephemeral runners after
87932+
// their job, making the removeRunner() API call in gh.js become
87933+
// belt-and-braces rather than the primary deregister path.
87934+
//
87935+
// - set -euo pipefail across both the outer and inner (runner-user)
87936+
// shells so ANY failure kills the bootstrap immediately. Made
87937+
// failures diagnosable in the Phase 4.b attempt (see #20 for the
87938+
// `aws ec2 get-console-output --latest` recipe).
87939+
const runnerVersion = config.input.runnerVersion;
87940+
const owner = config.githubContext.owner;
87941+
const repo = config.githubContext.repo;
87942+
const shaX64 = checksums.lookup('x64', runnerVersion);
87943+
const shaArm64 = checksums.lookup('arm64', runnerVersion);
87944+
if (!shaX64 || !shaArm64) {
87945+
throw new Error(
87946+
`No SHA-256 entry in src/runner-checksums.js for runner-version ${runnerVersion}. ` +
87947+
'Add the x64 + arm64 hashes from the release body at ' +
87948+
`https://github.com/actions/runner/releases/tag/v${runnerVersion}`,
87949+
);
87950+
}
87951+
8791987952
const userData = [
8792087953
'#!/bin/bash',
87954+
'set -euo pipefail',
87955+
'',
87956+
'# Root-required setup.',
8792187957
'mount -o remount,size=1G /tmp',
87922-
'yum install -y libicu make',
87923-
'mkdir actions-runner && cd actions-runner',
87924-
'case $(uname -m) in aarch64) ARCH="arm64" ;; amd64|x86_64) ARCH="x64" ;; esac && export RUNNER_ARCH=${ARCH}',
87925-
'curl -O -L https://github.com/actions/runner/releases/download/v2.333.1/actions-runner-linux-${RUNNER_ARCH}-2.333.1.tar.gz',
87926-
'tar xzf ./actions-runner-linux-${RUNNER_ARCH}-2.333.1.tar.gz',
87927-
'export RUNNER_ALLOW_RUNASROOT=1',
87958+
'yum install -y libicu make sudo',
87959+
'',
87960+
'# Create the non-root runner user (idempotent).',
87961+
'if ! id runner >/dev/null 2>&1; then',
87962+
' useradd -m -s /bin/bash runner',
87963+
'fi',
87964+
'',
87965+
'# Drop to the runner user for download + configure + run.',
87966+
"sudo -u runner -H bash <<'RUNNER_BOOTSTRAP'",
87967+
'set -euo pipefail',
87968+
'cd "$HOME"',
87969+
'mkdir -p actions-runner && cd actions-runner',
87970+
'',
87971+
'case "$(uname -m)" in',
87972+
' aarch64) RUNNER_ARCH="arm64" ;;',
87973+
' amd64|x86_64) RUNNER_ARCH="x64" ;;',
87974+
' *) echo "unsupported arch: $(uname -m)" >&2; exit 1 ;;',
87975+
'esac',
87976+
'',
87977+
`RUNNER_VERSION="${runnerVersion}"`,
87978+
'TARBALL="actions-runner-linux-${RUNNER_ARCH}-${RUNNER_VERSION}.tar.gz"',
87979+
'BASE="https://github.com/actions/runner/releases/download/v${RUNNER_VERSION}"',
87980+
'',
87981+
'curl -fsSLo "$TARBALL" "$BASE/$TARBALL"',
87982+
'',
87983+
'# SHA-256 verification against the hash baked into the action at',
87984+
'# build time (src/runner-checksums.js). The table is kept in sync',
87985+
'# with upstream by the verify-runner-url CI job on every PR.',
87986+
'case "$RUNNER_ARCH" in',
87987+
` x64) EXPECTED_SHA="${shaX64}" ;;`,
87988+
` arm64) EXPECTED_SHA="${shaArm64}" ;;`,
87989+
' *) echo "no checksum for arch $RUNNER_ARCH" >&2; exit 1 ;;',
87990+
'esac',
87991+
'echo "$EXPECTED_SHA $TARBALL" | sha256sum -c -',
87992+
'',
87993+
'tar xzf "$TARBALL"',
87994+
'',
8792887995
'export DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1',
87929-
`./config.sh --url https://github.com/${config.githubContext.owner}/${config.githubContext.repo} --token ${githubRegistrationToken} --labels ${label}`,
87996+
`./config.sh --url "https://github.com/${owner}/${repo}" --token "${githubRegistrationToken}" --labels "${label}" --ephemeral --unattended --disableupdate`,
8793087997
'./run.sh',
87998+
'RUNNER_BOOTSTRAP',
87999+
'',
8793188000
];
8793288001

8793388002
config.input.ec2ImageId = await resolveImageId(client);
@@ -88042,6 +88111,7 @@ class Config {
8804288111
label: core.getInput('label'),
8804388112
ec2InstanceId: core.getInput('ec2-instance-id'),
8804488113
iamRoleName: core.getInput('iam-role-name'),
88114+
runnerVersion: core.getInput('runner-version') || '2.333.1',
8804588115
httpTokens: core.getInput('http-tokens') || 'required',
8804688116
debug: core.getInput('debug') || 'false',
8804788117
};
@@ -88368,6 +88438,48 @@ module.exports = {
8836888438
};
8836988439

8837088440

88441+
/***/ }),
88442+
88443+
/***/ 2874:
88444+
/***/ ((module) => {
88445+
88446+
// Expected SHA-256 sums for actions/runner tarballs, keyed by
88447+
// `${arch}-${version}`.
88448+
//
88449+
// Why hardcoded: actions/runner doesn't publish per-tarball .sha256
88450+
// sidecar files (see ec2-github-runner#20 for the empirical proof —
88451+
// `curl -fsSL <tarball>.sha256` returns 404 and killed the Phase 4
88452+
// bootstrap under `set -euo pipefail`). Hashes ARE published in the
88453+
// release body as HTML-comment-wrapped markdown, but parsing that
88454+
// at boot time means an api.github.com round trip on every runner
88455+
// start and hits the unauth rate limit quickly at org scale.
88456+
//
88457+
// Maintenance: whenever the `runner-version` default bumps in
88458+
// action.yml, add the matching hashes here from the release body at
88459+
// https://github.com/actions/runner/releases/tag/v<version>. The
88460+
// `verify-runner-url` CI job cross-checks every entry against the
88461+
// live release body on every PR, so a drift between this table and
88462+
// upstream is caught at code-review time, not at runtime.
88463+
//
88464+
// Sources:
88465+
// https://github.com/actions/runner/releases/tag/v2.333.1
88466+
88467+
const CHECKSUMS = {
88468+
// v2.333.1 — pinned default as of 2026-04-21.
88469+
'x64-2.333.1': '18f8f68ed1892854ff2ab1bab4fcaa2f5abeedc98093b6cb13638991725cab74',
88470+
'arm64-2.333.1': '69ac7e5692f877189e7dddf4a1bb16cbbd6425568cd69a0359895fac48b9ad3b',
88471+
};
88472+
88473+
function lookup(arch, version) {
88474+
return CHECKSUMS[`${arch}-${version}`] || null;
88475+
}
88476+
88477+
module.exports = {
88478+
CHECKSUMS,
88479+
lookup,
88480+
};
88481+
88482+
8837188483
/***/ }),
8837288484

8837388485
/***/ 5804:

src/aws.js

Lines changed: 78 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ const config = require('./config');
1111
const log = require('./log');
1212
const { withRetry } = require('./retry');
1313
const { sortByCreationDate } = require('./utils');
14+
const checksums = require('./runner-checksums');
1415

1516
// EC2Client reads region + credentials from the environment (set by
1617
// aws-actions/configure-aws-credentials or by the instance profile on
@@ -68,20 +69,88 @@ async function resolveImageId(client) {
6869
async function startEc2Instance(label, githubRegistrationToken) {
6970
const client = ec2Client();
7071

71-
// User data scripts are run as the root user.
72-
// Docker and git are necessary for GitHub runner and should be pre-installed on the AMI.
72+
// Bootstrap design notes (fix-forward after ec2-github-runner#18/#19/#20):
73+
//
74+
// - Hashes for the runner tarball come from src/runner-checksums.js
75+
// (hardcoded table, cross-checked against the release body in CI).
76+
// The earlier `curl -fsSL <tarball>.sha256` approach died because
77+
// actions/runner doesn't publish per-tarball .sha256 sidecars.
78+
//
79+
// - Dedicated 'runner' user via useradd + sudo -u. The old
80+
// RUNNER_ALLOW_RUNASROOT=1 escape hatch is gone. Runner has its
81+
// own home under /home/runner/ and writes config.sh state there.
82+
//
83+
// - --ephemeral --unattended --disableupdate on config.sh: one-job
84+
// runner, no interactive prompts, no runtime self-update during
85+
// the session. GitHub auto-deregisters ephemeral runners after
86+
// their job, making the removeRunner() API call in gh.js become
87+
// belt-and-braces rather than the primary deregister path.
88+
//
89+
// - set -euo pipefail across both the outer and inner (runner-user)
90+
// shells so ANY failure kills the bootstrap immediately. Made
91+
// failures diagnosable in the Phase 4.b attempt (see #20 for the
92+
// `aws ec2 get-console-output --latest` recipe).
93+
const runnerVersion = config.input.runnerVersion;
94+
const owner = config.githubContext.owner;
95+
const repo = config.githubContext.repo;
96+
const shaX64 = checksums.lookup('x64', runnerVersion);
97+
const shaArm64 = checksums.lookup('arm64', runnerVersion);
98+
if (!shaX64 || !shaArm64) {
99+
throw new Error(
100+
`No SHA-256 entry in src/runner-checksums.js for runner-version ${runnerVersion}. ` +
101+
'Add the x64 + arm64 hashes from the release body at ' +
102+
`https://github.com/actions/runner/releases/tag/v${runnerVersion}`,
103+
);
104+
}
105+
73106
const userData = [
74107
'#!/bin/bash',
108+
'set -euo pipefail',
109+
'',
110+
'# Root-required setup.',
75111
'mount -o remount,size=1G /tmp',
76-
'yum install -y libicu make',
77-
'mkdir actions-runner && cd actions-runner',
78-
'case $(uname -m) in aarch64) ARCH="arm64" ;; amd64|x86_64) ARCH="x64" ;; esac && export RUNNER_ARCH=${ARCH}',
79-
'curl -O -L https://github.com/actions/runner/releases/download/v2.333.1/actions-runner-linux-${RUNNER_ARCH}-2.333.1.tar.gz',
80-
'tar xzf ./actions-runner-linux-${RUNNER_ARCH}-2.333.1.tar.gz',
81-
'export RUNNER_ALLOW_RUNASROOT=1',
112+
'yum install -y libicu make sudo',
113+
'',
114+
'# Create the non-root runner user (idempotent).',
115+
'if ! id runner >/dev/null 2>&1; then',
116+
' useradd -m -s /bin/bash runner',
117+
'fi',
118+
'',
119+
'# Drop to the runner user for download + configure + run.',
120+
"sudo -u runner -H bash <<'RUNNER_BOOTSTRAP'",
121+
'set -euo pipefail',
122+
'cd "$HOME"',
123+
'mkdir -p actions-runner && cd actions-runner',
124+
'',
125+
'case "$(uname -m)" in',
126+
' aarch64) RUNNER_ARCH="arm64" ;;',
127+
' amd64|x86_64) RUNNER_ARCH="x64" ;;',
128+
' *) echo "unsupported arch: $(uname -m)" >&2; exit 1 ;;',
129+
'esac',
130+
'',
131+
`RUNNER_VERSION="${runnerVersion}"`,
132+
'TARBALL="actions-runner-linux-${RUNNER_ARCH}-${RUNNER_VERSION}.tar.gz"',
133+
'BASE="https://github.com/actions/runner/releases/download/v${RUNNER_VERSION}"',
134+
'',
135+
'curl -fsSLo "$TARBALL" "$BASE/$TARBALL"',
136+
'',
137+
'# SHA-256 verification against the hash baked into the action at',
138+
'# build time (src/runner-checksums.js). The table is kept in sync',
139+
'# with upstream by the verify-runner-url CI job on every PR.',
140+
'case "$RUNNER_ARCH" in',
141+
` x64) EXPECTED_SHA="${shaX64}" ;;`,
142+
` arm64) EXPECTED_SHA="${shaArm64}" ;;`,
143+
' *) echo "no checksum for arch $RUNNER_ARCH" >&2; exit 1 ;;',
144+
'esac',
145+
'echo "$EXPECTED_SHA $TARBALL" | sha256sum -c -',
146+
'',
147+
'tar xzf "$TARBALL"',
148+
'',
82149
'export DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1',
83-
`./config.sh --url https://github.com/${config.githubContext.owner}/${config.githubContext.repo} --token ${githubRegistrationToken} --labels ${label}`,
150+
`./config.sh --url "https://github.com/${owner}/${repo}" --token "${githubRegistrationToken}" --labels "${label}" --ephemeral --unattended --disableupdate`,
84151
'./run.sh',
152+
'RUNNER_BOOTSTRAP',
153+
'',
85154
];
86155

87156
config.input.ec2ImageId = await resolveImageId(client);

src/config.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ class Config {
1616
label: core.getInput('label'),
1717
ec2InstanceId: core.getInput('ec2-instance-id'),
1818
iamRoleName: core.getInput('iam-role-name'),
19+
runnerVersion: core.getInput('runner-version') || '2.333.1',
1920
httpTokens: core.getInput('http-tokens') || 'required',
2021
debug: core.getInput('debug') || 'false',
2122
};

src/runner-checksums.js

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
// Expected SHA-256 sums for actions/runner tarballs, keyed by
2+
// `${arch}-${version}`.
3+
//
4+
// Why hardcoded: actions/runner doesn't publish per-tarball .sha256
5+
// sidecar files (see ec2-github-runner#20 for the empirical proof —
6+
// `curl -fsSL <tarball>.sha256` returns 404 and killed the Phase 4
7+
// bootstrap under `set -euo pipefail`). Hashes ARE published in the
8+
// release body as HTML-comment-wrapped markdown, but parsing that
9+
// at boot time means an api.github.com round trip on every runner
10+
// start and hits the unauth rate limit quickly at org scale.
11+
//
12+
// Maintenance: whenever the `runner-version` default bumps in
13+
// action.yml, add the matching hashes here from the release body at
14+
// https://github.com/actions/runner/releases/tag/v<version>. The
15+
// `verify-runner-url` CI job cross-checks every entry against the
16+
// live release body on every PR, so a drift between this table and
17+
// upstream is caught at code-review time, not at runtime.
18+
//
19+
// Sources:
20+
// https://github.com/actions/runner/releases/tag/v2.333.1
21+
22+
const CHECKSUMS = {
23+
// v2.333.1 — pinned default as of 2026-04-21.
24+
'x64-2.333.1': '18f8f68ed1892854ff2ab1bab4fcaa2f5abeedc98093b6cb13638991725cab74',
25+
'arm64-2.333.1': '69ac7e5692f877189e7dddf4a1bb16cbbd6425568cd69a0359895fac48b9ad3b',
26+
};
27+
28+
function lookup(arch, version) {
29+
return CHECKSUMS[`${arch}-${version}`] || null;
30+
}
31+
32+
module.exports = {
33+
CHECKSUMS,
34+
lookup,
35+
};

0 commit comments

Comments
 (0)