merge: compose Claude behavior with canonical master reconciliation

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip committed 2026-10-12 20:42:52 -05:00
commit 304e95c320
431 files changed
+22586 -15387

No files matched your search

+194
View File
@@ -0,0 +1,194 @@
import assert from "node:assert/strict";
import { chmodSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { spawnSync } from "node:child_process";
import { tmpdir } from "node:os";
import path from "node:path";
import test from "node:test";
import vm from "node:vm";
const workflow = readFileSync(new URL("../../workflows/ci-routing.yml", import.meta.url), "utf8");
const jobs = workflow.split(/^jobs:\n/m)[1];
test("routing uses reserved capacity and runs no PR code or actions", () => {
assert.deepEqual([...jobs.matchAll(/^ {2}([a-z_]+):$/gm)].map((match) => match[1]), ["gate"]);
const placement = jobs.match(/^ {4}runs-on: (.+)$/m)?.[1];
assert.ok(placement, "routing placement must be defined");
assert.doesNotMatch(placement, /fromJSON/);
assert.ok(placement.includes("vars.AWS_CI_TRUSTED_USER_IDS == vars.AWS_CI_ROUTING_USER_IDS_SOURCE"));
assert.ok(placement.includes("github.event.pull_request.user.id"));
assert.ok(placement.includes("github.event.sender.id"));
assert.ok(placement.endsWith("&& 'runs-on/fleet=paperclip-ci-routing-x64/env=public-ci' || 'ubuntu-latest' }}"));
assert.doesNotMatch(jobs, /uses:|actions\/checkout|secrets\./);
assert.doesNotMatch(workflow, /^ {2}(?:actions|contents|pull-requests): write$/m);
});
test("routing exports both placement and stack scope to callers", () => {
assert.match(workflow, /^ {6}runner:\n {8}value: \$\{\{ jobs\.gate\.outputs\.runner \}\}$/m);
assert.match(workflow, /^ {6}full_ci:\n {8}value: \$\{\{ jobs\.gate\.outputs\.full_ci \}\}$/m);
assert.match(jobs, /^ {6}runner: \$\{\{ steps\.route\.outputs\.runner \}\}$/m);
assert.match(jobs, /^ {6}full_ci: \$\{\{ steps\.scope\.outputs\.full_ci \}\}$/m);
});
const githubStub = "#!/usr/bin/env bash\nset -Eeuo pipefail\n\nendpoint=\"${*: -1}\"\ncase \"$endpoint\" in\n */pulls/*)\n jq -cn \\\n --argjson author_id \"${TEST_PR_AUTHOR_ID:-34892728}\" \\\n --argjson repository_id \"${TEST_REPOSITORY_ID:-1170821064}\" \\\n --arg base_ref \"${TEST_BASE_REF:-master}\" \\\n --arg base_sha \"${TEST_PR_BASE_SHA:-$EVENT_BASE_SHA}\" \\\n --arg head_sha \"${TEST_PR_HEAD_SHA:-1111111111111111111111111111111111111111}\" \\\n --arg merge_sha \"${TEST_PR_MERGE_SHA:-4444444444444444444444444444444444444444}\" '{\n state: \"open\",\n user: {id: $author_id},\n base: {repo: {id: $repository_id}, ref: $base_ref, sha: $base_sha},\n head: {sha: $head_sha},\n merge_commit_sha: $merge_sha\n }'\n ;;\n */git/ref/heads/*)\n jq -cn \\\n --arg sha \"${TEST_LIVE_BASE_REF_SHA:-$EVENT_BASE_SHA}\" '{object: {sha: $sha}}'\n ;;\n */compare/*)\n jq -cn \\\n --arg merge_base_sha \"${TEST_BASE_COMPARISON_MERGE_BASE_SHA:-$EVENT_BASE_SHA}\" \\\n --arg status \"${TEST_BASE_COMPARISON_STATUS:-ahead}\" '{\n merge_base_commit: {sha: $merge_base_sha},\n status: $status\n }'\n ;;\n */git/commits/*)\n requested_sha=\"${endpoint##*/}\"\n live_merge_sha=\"${TEST_PR_MERGE_SHA:-4444444444444444444444444444444444444444}\"\n synthetic_base_merge_sha=\"${TEST_SYNTHETIC_BASE_MERGE_SHA:-}\"\n if [[ \"$requested_sha\" == \"$EVENT_MERGE_SHA\" ]]; then\n tree_sha=\"${TEST_EVENT_MERGE_TREE_SHA:-5555555555555555555555555555555555555555}\"\n base_sha=\"${TEST_EVENT_MERGE_BASE_SHA:-$EVENT_BASE_SHA}\"\n head_sha=\"${TEST_EVENT_MERGE_HEAD_SHA:-$EVENT_HEAD_SHA}\"\n elif [[ \"$requested_sha\" == \"$live_merge_sha\" ]]; then\n tree_sha=\"${TEST_LIVE_MERGE_TREE_SHA:-5555555555555555555555555555555555555555}\"\n base_sha=\"${TEST_LIVE_MERGE_BASE_SHA:-$EVENT_BASE_SHA}\"\n head_sha=\"${TEST_LIVE_MERGE_HEAD_SHA:-$EVENT_HEAD_SHA}\"\n elif [[ -n \"$synthetic_base_merge_sha\" && \"$requested_sha\" == \"$synthetic_base_merge_sha\" ]]; then\n tree_sha=\"${TEST_SYNTHETIC_BASE_MERGE_TREE_SHA:-8888888888888888888888888888888888888888}\"\n base_sha=\"${TEST_SYNTHETIC_MASTER_PARENT_SHA:-9999999999999999999999999999999999999999}\"\n head_sha=\"${TEST_SYNTHETIC_BASE_REF_PARENT_SHA:-${TEST_LIVE_BASE_REF_SHA:-$EVENT_BASE_SHA}}\"\n else\n echo \"unexpected merge commit: $requested_sha\" >&2\n exit 1\n fi\n jq -cn \\\n --arg sha \"$requested_sha\" \\\n --arg tree_sha \"$tree_sha\" \\\n --arg base_sha \"$base_sha\" \\\n --arg head_sha \"$head_sha\" '{\n sha: $sha,\n tree: {sha: $tree_sha},\n parents: [{sha: $base_sha}, {sha: $head_sha}]\n }'\n ;;\n */actions/runs/*)\n jq -cn \\\n --argjson actor_id \"${TEST_TRIGGERING_ACTOR_ID:-34892728}\" \\\n --argjson repository_id \"${TEST_REPOSITORY_ID:-1170821064}\" \\\n --arg event_name \"${TEST_EVENT_NAME:-pull_request}\" '{\n triggering_actor: {id: $actor_id},\n repository: {id: $repository_id},\n event: $event_name\n }'\n ;;\n *)\n echo \"unexpected gh endpoint: $endpoint\" >&2\n exit 1\n ;;\nesac";
function stepScript(name) {
const start = workflow.indexOf(` - name: ${name}\n`);
assert.ok(start >= 0, `missing step: ${name}`);
const body = workflow.slice(start);
const runStart = body.indexOf(" run: |\n");
assert.ok(runStart >= 0, `missing script: ${name}`);
const script = body.slice(runStart + " run: |\n".length).split("\n - name:")[0];
return script.split("\n").map((line) => line.replace(/^ {10}/, "")).join("\n");
}
function executeStep(name, overrides = {}) {
const directory = mkdtempSync(path.join(tmpdir(), "paperclip-routing-test-"));
const output = path.join(directory, "output");
try {
const gh = path.join(directory, "gh");
writeFileSync(gh, githubStub);
chmodSync(gh, 0o700);
const result = spawnSync("bash", ["-c", stepScript(name)], {
encoding: "utf8",
env: {
...process.env,
PATH: `${directory}:${process.env.PATH}`,
GITHUB_OUTPUT: output,
AWS_CI_ENABLED: "true",
TRUSTED_USER_IDS: "[34892728]",
EVENT_NAME: "pull_request",
EVENT_REPOSITORY: "paperclipai/paperclip",
EVENT_REPOSITORY_ID: "1170821064",
EVENT_ACTION: "synchronize",
EVENT_PR_NUMBER: "12350",
EVENT_PR_AUTHOR_ID: "34892728",
EVENT_SENDER_ID: "34892728",
EVENT_BASE_REPOSITORY_ID: "1170821064",
EVENT_BASE_REF: "master",
EVENT_BASE_SHA: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
EVENT_HEAD_SHA: "1111111111111111111111111111111111111111",
EVENT_MERGE_SHA: "2222222222222222222222222222222222222222",
RUN_ID: "123456789",
...overrides,
},
});
assert.equal(result.status, 0, result.stderr);
const lines = readFileSync(output, "utf8").trim().split("\n");
assert.equal(lines.length, 1, "each step must emit exactly one decision");
return Object.fromEntries(lines.map((line) => {
const separator = line.indexOf("=");
return [line.slice(0, separator), line.slice(separator + 1)];
}));
} finally {
rmSync(directory, { recursive: true, force: true });
}
}
const awsRunner = "runs-on/fleet=paperclip-public-pr-x64/env=public-ci";
const stackedBase = {
EVENT_BASE_REF: "trusted-stack-base",
TEST_BASE_REF: "trusted-stack-base",
TEST_PR_BASE_SHA: "7777777777777777777777777777777777777777",
TEST_LIVE_BASE_REF_SHA: "7777777777777777777777777777777777777777",
TEST_SYNTHETIC_BASE_MERGE_SHA: "8888888888888888888888888888888888888888",
TEST_EVENT_MERGE_BASE_SHA: "8888888888888888888888888888888888888888",
TEST_LIVE_MERGE_BASE_SHA: "8888888888888888888888888888888888888888",
};
const routeCases = [
["allowlisted PR", {}, awsRunner],
["validated stacked base", stackedBase, awsRunner],
["lagging PR API base snapshot", { TEST_PR_BASE_SHA: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" }, awsRunner],
["disabled AWS", { AWS_CI_ENABLED: "false" }],
["missing AWS variables", { AWS_CI_ENABLED: "", TRUSTED_USER_IDS: "" }],
["unlisted author", { EVENT_PR_AUTHOR_ID: "99999999" }],
["unlisted sender", { EVENT_SENDER_ID: "99999999" }],
["unlisted rerun actor", { TEST_TRIGGERING_ACTOR_ID: "99999999" }],
["missing base", { EVENT_BASE_REF: "" }],
["wrong base repository", { EVENT_BASE_REPOSITORY_ID: "999999999" }],
["replaced base ancestry", { TEST_BASE_COMPARISON_MERGE_BASE_SHA: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" }],
["stale head", { TEST_PR_HEAD_SHA: "3333333333333333333333333333333333333333" }],
["changed merge head", { TEST_LIVE_MERGE_HEAD_SHA: "3333333333333333333333333333333333333333" }],
["stacked base with wrong parent", { ...stackedBase, TEST_SYNTHETIC_BASE_REF_PARENT_SHA: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" }],
["changed merge tree", { TEST_LIVE_MERGE_TREE_SHA: "6666666666666666666666666666666666666666" }],
["wrong repository", { EVENT_REPOSITORY_ID: "999999999" }],
["malformed allowlist", { TRUSTED_USER_IDS: '["34892728"]' }],
["manual dispatch", { EVENT_NAME: "workflow_dispatch" }],
["unsupported PR action", { EVENT_ACTION: "closed" }],
["malformed commit SHA", { EVENT_HEAD_SHA: "invalid" }],
];
for (const [name, env, expected = "ubuntu-latest"] of routeCases) {
test(`runner decision: ${name}`, () => {
assert.equal(executeStep("Validate PR identity and select runner", env).runner, expected);
});
}
const scopeCases = [
["ordinary PR", null, "master", "true"],
["top stack PR", { position: 11, size: 11, base: { ref: "master" } }, "stack-10", "true"],
["lowest unmerged PR", { position: 1, size: 11, base: { ref: "master" } }, "master", "true"],
["middle stack PR", { position: 6, size: 11, base: { ref: "master" } }, "stack-5", "false"],
["malformed position", { position: "invalid", size: 11, base: { ref: "master" } }, "stack-5", "true"],
["out-of-range position", { position: 12, size: 11, base: { ref: "master" } }, "stack-5", "true"],
["missing stack base", { position: 6, size: 11 }, "stack-5", "true"],
["empty metadata", {}, "master", "true"],
];
for (const [name, stack, base, expected] of scopeCases) {
test(`scope decision: ${name}`, () => {
assert.equal(executeStep("Select stacked PR CI scope", {
STACK_JSON: JSON.stringify(stack), PR_BASE_REF: base,
}).full_ci, expected);
});
}
function routingPlacement(vars = {}, github = {}) {
const source = "[34892728]";
const expression = jobs.match(/^ {4}runs-on: \$\{\{ (.+) \}\}$/m)?.[1];
assert.ok(expression, "routing expression is missing");
return vm.runInNewContext(expression, {
vars: {
AWS_CI_ENABLED: "true",
AWS_CI_TRUSTED_USER_IDS: source,
AWS_CI_ROUTING_USER_IDS_SOURCE: source,
AWS_CI_ROUTING_USER_IDS: "|34892728|",
...vars,
},
github: {
repository: "paperclipai/paperclip",
repository_id: "1170821064",
event_name: "pull_request",
ref: "refs/heads/master",
actor_id: "34892728",
event: { pull_request: { user: { id: 34892728 } }, sender: { id: 34892728 } },
...github,
},
contains: (search, item) => String(search).toLowerCase().includes(String(item).toLowerCase()),
format: (template, ...args) => template.replace(/\{(\d+)\}/g, (_, index) => String(args[Number(index)])),
}, { timeout: 1000 });
}
const placementCases = [
["approved PR", {}, {}, "runs-on/fleet=paperclip-ci-routing-x64/env=public-ci"],
["unapproved author", {}, { event: { pull_request: { user: { id: 999 } }, sender: { id: 34892728 } } }],
["unapproved sender", {}, { event: { pull_request: { user: { id: 34892728 } }, sender: { id: 999 } } }],
["partial ID cannot match", {}, { event: { pull_request: { user: { id: 4892728 } }, sender: { id: 34892728 } } }],
["invalid JSON keeps hosted fallback", { AWS_CI_TRUSTED_USER_IDS: "[34892728,]" }, {}],
["stale snapshot", { AWS_CI_TRUSTED_USER_IDS: "[34892728,999]" }, {}],
["missing snapshot", { AWS_CI_ROUTING_USER_IDS_SOURCE: "" }, {}],
["missing admission members", { AWS_CI_ROUTING_USER_IDS: "" }, {}],
["disabled routing", { AWS_CI_ENABLED: "false" }, {}],
["different repository", {}, { repository: "other/repo" }],
["different repository ID", {}, { repository_id: "999" }],
["approved manual master canary", {}, { event_name: "workflow_dispatch" }, "runs-on/fleet=paperclip-ci-routing-x64/env=public-ci"],
["unapproved manual actor", {}, { event_name: "workflow_dispatch", actor_id: "999" }],
["manual branch canary", {}, { event_name: "workflow_dispatch", ref: "refs/heads/other" }],
];
for (const [name, vars, github, expected = "ubuntu-latest"] of placementCases) {
test(`placement before allocation: ${name}`, () => {
assert.equal(routingPlacement(vars, github), expected);
});
}
+252
View File
@@ -0,0 +1,252 @@
name: CI routing
on:
workflow_dispatch:
workflow_call:
outputs:
runner:
value: ${{ jobs.gate.outputs.runner }}
full_ci:
value: ${{ jobs.gate.outputs.full_ci }}
permissions:
actions: read
contents: read
pull-requests: read
jobs:
gate:
name: Select trusted runner
runs-on: ${{ vars.AWS_CI_ENABLED == 'true' && vars.AWS_CI_ROUTING_USER_IDS_SOURCE != '' && vars.AWS_CI_TRUSTED_USER_IDS == vars.AWS_CI_ROUTING_USER_IDS_SOURCE && github.repository == 'paperclipai/paperclip' && github.repository_id == '1170821064' && ((github.event_name == 'pull_request' && contains(vars.AWS_CI_ROUTING_USER_IDS, format('|{0}|', github.event.pull_request.user.id)) && contains(vars.AWS_CI_ROUTING_USER_IDS, format('|{0}|', github.event.sender.id))) || (github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/master' && contains(vars.AWS_CI_ROUTING_USER_IDS, format('|{0}|', github.actor_id)))) && 'runs-on/fleet=paperclip-ci-routing-x64/env=public-ci' || 'ubuntu-latest' }}
timeout-minutes: 5
outputs:
runner: ${{ steps.route.outputs.runner }}
full_ci: ${{ steps.scope.outputs.full_ci }}
steps:
- name: Validate PR identity and select runner
id: route
shell: bash
env:
GH_TOKEN: ${{ github.token }}
AWS_CI_ENABLED: ${{ vars.AWS_CI_ENABLED }}
TRUSTED_USER_IDS: ${{ vars.AWS_CI_TRUSTED_USER_IDS }}
EVENT_NAME: ${{ github.event_name }}
EVENT_REPOSITORY: ${{ github.repository }}
EVENT_REPOSITORY_ID: ${{ github.repository_id }}
EVENT_ACTION: ${{ github.event.action }}
EVENT_PR_NUMBER: ${{ github.event.pull_request.number }}
EVENT_PR_AUTHOR_ID: ${{ github.event.pull_request.user.id }}
EVENT_SENDER_ID: ${{ github.event.sender.id }}
EVENT_BASE_REPOSITORY_ID: ${{ github.event.pull_request.base.repo.id }}
EVENT_BASE_REF: ${{ github.event.pull_request.base.ref }}
EVENT_BASE_SHA: ${{ github.event.pull_request.base.sha }}
EVENT_HEAD_SHA: ${{ github.event.pull_request.head.sha }}
EVENT_MERGE_SHA: ${{ github.sha }}
RUN_ID: ${{ github.run_id }}
run: |
set -u
github_runner='ubuntu-latest'
aws_runner='runs-on/fleet=paperclip-public-pr-x64/env=public-ci'
fail_closed() {
echo "runner=$github_runner" >> "$GITHUB_OUTPUT"
echo "::notice title=AWS CI routing::Using GitHub-hosted runner: $1"
exit 0
}
is_positive_integer() {
[[ "$1" =~ ^[1-9][0-9]*$ ]]
}
is_commit_sha() {
[[ "$1" =~ ^[0-9a-f]{40}$ ]]
}
is_allowed() {
local user_id="$1"
jq -e --argjson user_id "$user_id" 'index($user_id) != null' \
<<< "$TRUSTED_USER_IDS" >/dev/null
}
[[ "$AWS_CI_ENABLED" == 'true' ]] || fail_closed 'AWS_CI_ENABLED is not true'
[[ "$EVENT_NAME" == 'pull_request' ]] || fail_closed 'event is not pull_request'
[[ "$EVENT_REPOSITORY" == 'paperclipai/paperclip' ]] || fail_closed 'unexpected repository'
[[ "$EVENT_REPOSITORY_ID" == '1170821064' ]] || fail_closed 'unexpected repository ID'
[[ "$EVENT_BASE_REPOSITORY_ID" == '1170821064' ]] || fail_closed 'unexpected base repository ID'
[[ -n "$EVENT_BASE_REF" ]] || fail_closed 'missing base branch'
[[ "$EVENT_ACTION" =~ ^(opened|reopened|synchronize)$ ]] || fail_closed 'unsupported pull_request action'
jq -e '
type == "array" and
length > 0 and
all(.[]; type == "number" and . > 0 and floor == .)
' <<< "$TRUSTED_USER_IDS" >/dev/null 2>&1 || fail_closed 'trusted user ID list is malformed'
for user_id in "$EVENT_PR_AUTHOR_ID" "$EVENT_SENDER_ID"; do
is_positive_integer "$user_id" || fail_closed 'event contains a malformed user ID'
is_allowed "$user_id" || fail_closed "GitHub user ID $user_id is not allowlisted"
done
for commit_sha in "$EVENT_BASE_SHA" "$EVENT_HEAD_SHA" "$EVENT_MERGE_SHA"; do
is_commit_sha "$commit_sha" || fail_closed 'event contains a malformed commit SHA'
done
pr_json="$(gh api \
-H 'Accept: application/vnd.github+json' \
-H 'X-GitHub-Api-Version: 2022-11-28' \
"/repos/paperclipai/paperclip/pulls/$EVENT_PR_NUMBER" 2>/dev/null)" \
|| fail_closed 'could not refresh pull request state'
jq -e \
--argjson repository_id 1170821064 \
--argjson author_id "$EVENT_PR_AUTHOR_ID" \
--arg base_ref "$EVENT_BASE_REF" \
--arg head_sha "$EVENT_HEAD_SHA" \
'
.state == "open" and
.user.id == $author_id and
.base.repo.id == $repository_id and
.base.ref == $base_ref and
.head.sha == $head_sha
' <<< "$pr_json" >/dev/null 2>&1 \
|| fail_closed 'current pull request state does not match the triggering event'
live_merge_sha="$(jq -r '.merge_commit_sha // empty' <<< "$pr_json")"
is_commit_sha "$live_merge_sha" || fail_closed 'current pull request has no valid merge SHA'
base_ref_json="$(gh api \
-H 'Accept: application/vnd.github+json' \
-H 'X-GitHub-Api-Version: 2022-11-28' \
"/repos/paperclipai/paperclip/git/ref/heads/$EVENT_BASE_REF" 2>/dev/null)" \
|| fail_closed 'could not inspect the current base branch'
live_base_ref_sha="$(jq -r '.object.sha // empty' <<< "$base_ref_json")"
is_commit_sha "$live_base_ref_sha" || fail_closed 'current base branch has no valid commit SHA'
base_comparison="$(gh api \
-H 'Accept: application/vnd.github+json' \
-H 'X-GitHub-Api-Version: 2022-11-28' \
"/repos/paperclipai/paperclip/compare/$EVENT_BASE_SHA...$live_base_ref_sha" 2>/dev/null)" \
|| fail_closed 'could not compare the triggering and current base branches'
jq -e \
--arg event_base_sha "$EVENT_BASE_SHA" '
.merge_base_commit.sha == $event_base_sha and
(.status == "ahead" or .status == "identical")
' <<< "$base_comparison" >/dev/null 2>&1 \
|| fail_closed 'current base branch does not descend from the triggering base snapshot'
event_merge_json="$(gh api \
-H 'Accept: application/vnd.github+json' \
-H 'X-GitHub-Api-Version: 2022-11-28' \
"/repos/paperclipai/paperclip/git/commits/$EVENT_MERGE_SHA" 2>/dev/null)" \
|| fail_closed 'could not inspect the event merge commit'
live_merge_json="$(gh api \
-H 'Accept: application/vnd.github+json' \
-H 'X-GitHub-Api-Version: 2022-11-28' \
"/repos/paperclipai/paperclip/git/commits/$live_merge_sha" 2>/dev/null)" \
|| fail_closed 'could not inspect the current merge commit'
validate_merge_commit() {
local merge_json="$1"
local expected_sha="$2"
jq -e \
--arg expected_sha "$expected_sha" \
--arg head_sha "$EVENT_HEAD_SHA" '
.sha == $expected_sha and
(.parents | length) == 2 and
.parents[1].sha == $head_sha and
(.parents[0].sha | test("^[0-9a-f]{40}$")) and
(.tree.sha | test("^[0-9a-f]{40}$"))
' <<< "$merge_json" >/dev/null 2>&1
}
validate_merge_commit "$event_merge_json" "$EVENT_MERGE_SHA" \
|| fail_closed 'event merge commit does not match the current base and head'
validate_merge_commit "$live_merge_json" "$live_merge_sha" \
|| fail_closed 'current merge commit does not match the triggering base and head'
event_merge_parent="$(jq -r '.parents[0].sha' <<< "$event_merge_json")"
live_merge_parent="$(jq -r '.parents[0].sha' <<< "$live_merge_json")"
[[ "$event_merge_parent" == "$live_merge_parent" ]] \
|| fail_closed 'current merge commit uses a different base merge parent'
if [[ "$event_merge_parent" != "$live_base_ref_sha" ]]; then
base_merge_json="$(gh api \
-H 'Accept: application/vnd.github+json' \
-H 'X-GitHub-Api-Version: 2022-11-28' \
"/repos/paperclipai/paperclip/git/commits/$event_merge_parent" 2>/dev/null)" \
|| fail_closed 'could not inspect the stacked base merge commit'
jq -e \
--arg expected_sha "$event_merge_parent" \
--arg live_base_ref_sha "$live_base_ref_sha" '
.sha == $expected_sha and
(.parents | length) == 2 and
any(.parents[]; .sha == $live_base_ref_sha) and
(.tree.sha | test("^[0-9a-f]{40}$"))
' <<< "$base_merge_json" >/dev/null 2>&1 \
|| fail_closed 'stacked base merge commit does not contain the current base branch'
fi
event_merge_tree="$(jq -r '.tree.sha' <<< "$event_merge_json")"
live_merge_tree="$(jq -r '.tree.sha' <<< "$live_merge_json")"
[[ "$event_merge_tree" == "$live_merge_tree" ]] \
|| fail_closed 'current merge tree differs from the triggering merge tree'
run_json="$(gh api \
-H 'Accept: application/vnd.github+json' \
-H 'X-GitHub-Api-Version: 2022-11-28' \
"/repos/paperclipai/paperclip/actions/runs/$RUN_ID" 2>/dev/null)" \
|| fail_closed 'could not refresh workflow run state'
triggering_actor_id="$(jq -r '.triggering_actor.id // empty' <<< "$run_json")"
is_positive_integer "$triggering_actor_id" || fail_closed 'workflow run has no valid triggering actor ID'
is_allowed "$triggering_actor_id" || fail_closed "triggering GitHub user ID $triggering_actor_id is not allowlisted"
jq -e \
--argjson repository_id 1170821064 \
--arg event_name "$EVENT_NAME" '
.repository.id == $repository_id and
.event == $event_name
' <<< "$run_json" >/dev/null 2>&1 \
|| fail_closed 'current workflow run does not match the expected repository event'
echo "runner=$aws_runner" >> "$GITHUB_OUTPUT"
echo '::notice title=AWS CI routing::Using an ephemeral RunsOn Fleet runner'
- name: Select stacked PR CI scope
id: scope
shell: bash
env:
STACK_JSON: ${{ toJSON(github.event.pull_request.stack) }}
PR_BASE_REF: ${{ github.event.pull_request.base.ref }}
run: |
set -euo pipefail
full_ci='true'
reason='ordinary pull request'
if jq -e 'type == "object"' <<< "$STACK_JSON" >/dev/null 2>&1; then
stack_position="$(jq -r '.position // empty' <<< "$STACK_JSON")"
stack_size="$(jq -r '.size // empty' <<< "$STACK_JSON")"
stack_base_ref="$(jq -r '.base.ref // empty' <<< "$STACK_JSON")"
if [[ ! "$stack_position" =~ ^[1-9][0-9]*$ ]] ||
[[ ! "$stack_size" =~ ^[1-9][0-9]*$ ]] ||
(( stack_position > stack_size )) ||
[[ -z "$stack_base_ref" ]]; then
reason='malformed stack metadata; defaulting to full CI'
elif (( stack_position == stack_size )); then
reason='top pull request in stack'
elif [[ "$stack_base_ref" == "$PR_BASE_REF" ]]; then
reason='lowest unmerged pull request in stack'
else
full_ci='false'
reason='middle pull request in stack'
fi
fi
echo "full_ci=$full_ci" >> "$GITHUB_OUTPUT"
echo "::notice title=Stacked PR CI scope::$reason; full_ci=$full_ci"
+1 -235
View File
@@ -17,241 +17,7 @@ concurrency:
jobs:
gate:
name: Select trusted runner
runs-on: ubuntu-latest
timeout-minutes: 5
outputs:
runner: ${{ steps.route.outputs.runner }}
full_ci: ${{ steps.scope.outputs.full_ci }}
steps:
- name: Validate PR identity and select runner
id: route
shell: bash
env:
GH_TOKEN: ${{ github.token }}
AWS_CI_ENABLED: ${{ vars.AWS_CI_ENABLED }}
TRUSTED_USER_IDS: ${{ vars.AWS_CI_TRUSTED_USER_IDS }}
EVENT_NAME: ${{ github.event_name }}
EVENT_REPOSITORY: ${{ github.repository }}
EVENT_REPOSITORY_ID: ${{ github.repository_id }}
EVENT_ACTION: ${{ github.event.action }}
EVENT_PR_NUMBER: ${{ github.event.pull_request.number }}
EVENT_PR_AUTHOR_ID: ${{ github.event.pull_request.user.id }}
EVENT_SENDER_ID: ${{ github.event.sender.id }}
EVENT_BASE_REPOSITORY_ID: ${{ github.event.pull_request.base.repo.id }}
EVENT_BASE_REF: ${{ github.event.pull_request.base.ref }}
EVENT_BASE_SHA: ${{ github.event.pull_request.base.sha }}
EVENT_HEAD_SHA: ${{ github.event.pull_request.head.sha }}
EVENT_MERGE_SHA: ${{ github.sha }}
RUN_ID: ${{ github.run_id }}
run: |
set -u
github_runner='ubuntu-latest'
aws_runner='runs-on/fleet=paperclip-public-pr-x64/env=public-ci'
fail_closed() {
echo "runner=$github_runner" >> "$GITHUB_OUTPUT"
echo "::notice title=AWS CI routing::Using GitHub-hosted runner: $1"
exit 0
}
is_positive_integer() {
[[ "$1" =~ ^[1-9][0-9]*$ ]]
}
is_commit_sha() {
[[ "$1" =~ ^[0-9a-f]{40}$ ]]
}
is_allowed() {
local user_id="$1"
jq -e --argjson user_id "$user_id" 'index($user_id) != null' \
<<< "$TRUSTED_USER_IDS" >/dev/null
}
[[ "$AWS_CI_ENABLED" == 'true' ]] || fail_closed 'AWS_CI_ENABLED is not true'
# Reusable workflows retain the caller's github context and event
# payload, so a pull_request caller must still report pull_request.
[[ "$EVENT_NAME" == 'pull_request' ]] || fail_closed 'event is not pull_request'
[[ "$EVENT_REPOSITORY" == 'paperclipai/paperclip' ]] || fail_closed 'unexpected repository'
[[ "$EVENT_REPOSITORY_ID" == '1170821064' ]] || fail_closed 'unexpected repository ID'
[[ "$EVENT_BASE_REPOSITORY_ID" == '1170821064' ]] || fail_closed 'unexpected base repository ID'
[[ -n "$EVENT_BASE_REF" ]] || fail_closed 'missing base branch'
[[ "$EVENT_ACTION" =~ ^(opened|reopened|synchronize)$ ]] || fail_closed 'unsupported pull_request action'
jq -e '
type == "array" and
length > 0 and
all(.[]; type == "number" and . > 0 and floor == .)
' <<< "$TRUSTED_USER_IDS" >/dev/null 2>&1 || fail_closed 'trusted user ID list is malformed'
for user_id in "$EVENT_PR_AUTHOR_ID" "$EVENT_SENDER_ID"; do
is_positive_integer "$user_id" || fail_closed 'event contains a malformed user ID'
is_allowed "$user_id" || fail_closed "GitHub user ID $user_id is not allowlisted"
done
for commit_sha in "$EVENT_BASE_SHA" "$EVENT_HEAD_SHA" "$EVENT_MERGE_SHA"; do
is_commit_sha "$commit_sha" || fail_closed 'event contains a malformed commit SHA'
done
pr_json="$(gh api \
-H 'Accept: application/vnd.github+json' \
-H 'X-GitHub-Api-Version: 2022-11-28' \
"/repos/paperclipai/paperclip/pulls/$EVENT_PR_NUMBER" 2>/dev/null)" \
|| fail_closed 'could not refresh pull request state'
jq -e \
--argjson repository_id 1170821064 \
--argjson author_id "$EVENT_PR_AUTHOR_ID" \
--arg base_ref "$EVENT_BASE_REF" \
--arg head_sha "$EVENT_HEAD_SHA" \
'
.state == "open" and
.user.id == $author_id and
.base.repo.id == $repository_id and
.base.ref == $base_ref and
.head.sha == $head_sha
' <<< "$pr_json" >/dev/null 2>&1 \
|| fail_closed 'current pull request state does not match the triggering event'
live_merge_sha="$(jq -r '.merge_commit_sha // empty' <<< "$pr_json")"
is_commit_sha "$live_merge_sha" || fail_closed 'current pull request has no valid merge SHA'
base_ref_json="$(gh api \
-H 'Accept: application/vnd.github+json' \
-H 'X-GitHub-Api-Version: 2022-11-28' \
"/repos/paperclipai/paperclip/git/ref/heads/$EVENT_BASE_REF" 2>/dev/null)" \
|| fail_closed 'could not inspect the current base branch'
live_base_ref_sha="$(jq -r '.object.sha // empty' <<< "$base_ref_json")"
is_commit_sha "$live_base_ref_sha" || fail_closed 'current base branch has no valid commit SHA'
base_comparison="$(gh api \
-H 'Accept: application/vnd.github+json' \
-H 'X-GitHub-Api-Version: 2022-11-28' \
"/repos/paperclipai/paperclip/compare/$EVENT_BASE_SHA...$live_base_ref_sha" 2>/dev/null)" \
|| fail_closed 'could not compare the triggering and current base branches'
jq -e \
--arg event_base_sha "$EVENT_BASE_SHA" '
.merge_base_commit.sha == $event_base_sha and
(.status == "ahead" or .status == "identical")
' <<< "$base_comparison" >/dev/null 2>&1 \
|| fail_closed 'current base branch does not descend from the triggering base snapshot'
event_merge_json="$(gh api \
-H 'Accept: application/vnd.github+json' \
-H 'X-GitHub-Api-Version: 2022-11-28' \
"/repos/paperclipai/paperclip/git/commits/$EVENT_MERGE_SHA" 2>/dev/null)" \
|| fail_closed 'could not inspect the event merge commit'
live_merge_json="$(gh api \
-H 'Accept: application/vnd.github+json' \
-H 'X-GitHub-Api-Version: 2022-11-28' \
"/repos/paperclipai/paperclip/git/commits/$live_merge_sha" 2>/dev/null)" \
|| fail_closed 'could not inspect the current merge commit'
validate_merge_commit() {
local merge_json="$1"
local expected_sha="$2"
jq -e \
--arg expected_sha "$expected_sha" \
--arg head_sha "$EVENT_HEAD_SHA" '
.sha == $expected_sha and
(.parents | length) == 2 and
.parents[1].sha == $head_sha and
(.parents[0].sha | test("^[0-9a-f]{40}$")) and
(.tree.sha | test("^[0-9a-f]{40}$"))
' <<< "$merge_json" >/dev/null 2>&1
}
validate_merge_commit "$event_merge_json" "$EVENT_MERGE_SHA" \
|| fail_closed 'event merge commit does not match the current base and head'
validate_merge_commit "$live_merge_json" "$live_merge_sha" \
|| fail_closed 'current merge commit does not match the triggering base and head'
event_merge_parent="$(jq -r '.parents[0].sha' <<< "$event_merge_json")"
live_merge_parent="$(jq -r '.parents[0].sha' <<< "$live_merge_json")"
[[ "$event_merge_parent" == "$live_merge_parent" ]] \
|| fail_closed 'current merge commit uses a different base merge parent'
if [[ "$event_merge_parent" != "$live_base_ref_sha" ]]; then
base_merge_json="$(gh api \
-H 'Accept: application/vnd.github+json' \
-H 'X-GitHub-Api-Version: 2022-11-28' \
"/repos/paperclipai/paperclip/git/commits/$event_merge_parent" 2>/dev/null)" \
|| fail_closed 'could not inspect the stacked base merge commit'
jq -e \
--arg expected_sha "$event_merge_parent" \
--arg live_base_ref_sha "$live_base_ref_sha" '
.sha == $expected_sha and
(.parents | length) == 2 and
any(.parents[]; .sha == $live_base_ref_sha) and
(.tree.sha | test("^[0-9a-f]{40}$"))
' <<< "$base_merge_json" >/dev/null 2>&1 \
|| fail_closed 'stacked base merge commit does not contain the current base branch'
fi
event_merge_tree="$(jq -r '.tree.sha' <<< "$event_merge_json")"
live_merge_tree="$(jq -r '.tree.sha' <<< "$live_merge_json")"
[[ "$event_merge_tree" == "$live_merge_tree" ]] \
|| fail_closed 'current merge tree differs from the triggering merge tree'
run_json="$(gh api \
-H 'Accept: application/vnd.github+json' \
-H 'X-GitHub-Api-Version: 2022-11-28' \
"/repos/paperclipai/paperclip/actions/runs/$RUN_ID" 2>/dev/null)" \
|| fail_closed 'could not refresh workflow run state'
triggering_actor_id="$(jq -r '.triggering_actor.id // empty' <<< "$run_json")"
is_positive_integer "$triggering_actor_id" || fail_closed 'workflow run has no valid triggering actor ID'
is_allowed "$triggering_actor_id" || fail_closed "triggering GitHub user ID $triggering_actor_id is not allowlisted"
jq -e \
--argjson repository_id 1170821064 \
--arg event_name "$EVENT_NAME" '
.repository.id == $repository_id and
.event == $event_name
' <<< "$run_json" >/dev/null 2>&1 \
|| fail_closed 'current workflow run does not match the expected repository event'
echo "runner=$aws_runner" >> "$GITHUB_OUTPUT"
echo '::notice title=AWS CI routing::Using an ephemeral RunsOn Fleet runner'
- name: Select stacked PR CI scope
id: scope
shell: bash
env:
STACK_JSON: ${{ toJSON(github.event.pull_request.stack) }}
PR_BASE_REF: ${{ github.event.pull_request.base.ref }}
run: |
set -euo pipefail
full_ci='true'
reason='ordinary pull request'
if jq -e 'type == "object"' <<< "$STACK_JSON" >/dev/null 2>&1; then
stack_position="$(jq -r '.position // empty' <<< "$STACK_JSON")"
stack_size="$(jq -r '.size // empty' <<< "$STACK_JSON")"
stack_base_ref="$(jq -r '.base.ref // empty' <<< "$STACK_JSON")"
if [[ ! "$stack_position" =~ ^[1-9][0-9]*$ ]] ||
[[ ! "$stack_size" =~ ^[1-9][0-9]*$ ]] ||
(( stack_position > stack_size )) ||
[[ -z "$stack_base_ref" ]]; then
reason='malformed stack metadata; defaulting to full CI'
elif (( stack_position == stack_size )); then
reason='top pull request in stack'
elif [[ "$stack_base_ref" == "$PR_BASE_REF" ]]; then
reason='lowest unmerged pull request in stack'
else
full_ci='false'
reason='middle pull request in stack'
fi
fi
echo "full_ci=$full_ci" >> "$GITHUB_OUTPUT"
echo "::notice title=Stacked PR CI scope::$reason; full_ci=$full_ci"
uses: paperclipai/paperclip/.github/workflows/ci-routing.yml@master
policy:
needs: [gate]
+11 -12
View File
@@ -227,17 +227,17 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 45
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
with:
ref: ${{ inputs.qualification_source_sha }}
persist-credentials: false
- uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6
- uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86
with:
version: 9.15.4
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020
with:
node-version: 24
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093
with:
pattern: ${{ needs.qualification_daemons.outputs.runner_artifact_prefix }}-*
path: ${{ runner.temp }}/release-runner-inputs
@@ -275,7 +275,7 @@ jobs:
fi
- name: Retain completed package assembly even after a later Linux failure
if: ${{ !cancelled() && steps.pack_identity.outputs.available == 'true' }}
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02
with:
name: ${{ inputs.artifact_name }}-public-packages
path: ${{ runner.temp }}/public-runner-packs/
@@ -292,17 +292,17 @@ jobs:
fail-fast: false
matrix: ${{ fromJSON(needs.qualification_daemons.outputs.native_consumer_matrix) }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
with:
ref: ${{ inputs.qualification_source_sha }}
persist-credentials: false
- uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6
- uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86
with:
version: 9.15.4
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020
with:
node-version: 24
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093
with:
name: ${{ inputs.artifact_name }}-public-packages
path: ${{ runner.temp }}/public-runner-packs
@@ -310,9 +310,8 @@ jobs:
run: pnpm install --ignore-scripts --no-frozen-lockfile
- name: Checkout immutable ARM qualification tooling
if: ${{ matrix.target == 'linux-arm64' }}
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
with:
# job.workflow_* identifies this reusable workflow, not its caller.
repository: ${{ job.workflow_repository }}
ref: ${{ job.workflow_sha }}
path: .qualification-tooling
@@ -363,7 +362,7 @@ jobs:
if [[ "$TARGET" == linux-arm64 ]]; then mode=--linux-arm64-consumer; fi
node scripts/verify-grok-npm-install.mjs "$mode" \
"$RUNNER_TEMP/public-runner-packs" "$SOURCE_SHA" "$RUNNER_TEMP/native-public-install.json"
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02
if: always()
with:
name: ${{ inputs.artifact_name }}-${{ matrix.target }}
+7 -14
View File
@@ -527,11 +527,11 @@ jobs:
matrix: ${{ steps.targets.outputs.matrix }}
native_consumers: ${{ steps.targets.outputs.native_consumers }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
with:
ref: ${{ inputs.ref }}
persist-credentials: false
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020
with:
node-version: 24
- name: Preserve the immutable source's platform contract
@@ -564,8 +564,6 @@ jobs:
&& Object.keys(contract).sort().join() === 'schema,targets', 'Unknown exact-source daemon platform contract');
declared = contract.targets;
} else {
// Frozen pre-contract sources used a literal export. Read that one
// declaration as JSON data; never import or evaluate preview code.
const legacy = readBlob('packages/paperclip-runner/scripts/stage-release-runner-binaries.mjs');
const declarations = [...(legacy ?? '').matchAll(/^[ \t]*export const RELEASE_RUNNER_TARGETS = (?:Object\.freeze\()?(\[[^\r\n]*\])\)?;[ \t]*$/gm)];
assert.equal(declarations.length, 1, 'Unknown exact-source daemon platform contract');
@@ -596,14 +594,14 @@ jobs:
outputs:
artifact_prefix: ${{ steps.identity.outputs.artifact_prefix }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
with:
ref: ${{ inputs.ref }}
persist-credentials: false
- uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6
- uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86
with:
version: 9.15.4
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020
with:
node-version: 24
- name: Bind immutable producer source and target
@@ -640,18 +638,13 @@ jobs:
fi
rustup toolchain list
- name: Cache native Rust dependencies
uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6
with:
workspaces: ${{ steps.native_rust_workspace.outputs.path }} -> target
# Compiler/environment and Cargo lock hashes are added by the action.
# Also bind generated contracts outside the Rust workspace, while
# always recompiling workspace code and recording actual new bytes.
shared-key: release-native-v2-${{ matrix.host }}-${{ matrix.target }}-${{ hashFiles('packages/paperclip-runner/protocol/**', 'packages/paperclip-runner/src/**', 'packages/paperclip-runner/*.json') }}
cache-workspace-crates: false
cache-bin: false
cache-on-failure: false
# A master workflow can verify an unmerged preview ref. Those jobs
# restore only; they never write master's trusted dependency cache.
save-if: ${{ github.repository == 'paperclipai/paperclip' && github.repository_id == '1170821064' && github.event_name == 'push' && github.ref == 'refs/heads/master' && inputs.ref == github.sha }}
- name: Install dependencies
run: pnpm install --no-frozen-lockfile
@@ -694,7 +687,7 @@ jobs:
run: |
node packages/paperclip-runner/scripts/stage-release-runner-binaries.mjs record "$SOURCE_SHA" "$TARGET" \
packages/paperclip-runner/dist/bin/paperclip-runnerd "$RUNNER_TEMP/runner-release/$TARGET"
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02
with:
name: ${{ steps.identity.outputs.artifact_prefix }}-${{ matrix.target }}
path: ${{ runner.temp }}/runner-release/
+6 -8
View File
@@ -341,8 +341,6 @@ jobs:
run: node scripts/cloud-source-verification.mjs "$SOURCE_SHA"
release_runner_source:
# Old promotions retain their already-shipped packaging contract. New
# sources opt into source-declared native target assembly in their release.sh.
needs: [verify_canary, select_nightly, select_beta, preflight_stable]
if: >-
!cancelled() && (
@@ -357,7 +355,7 @@ jobs:
sha: ${{ steps.source.outputs.sha }}
required: ${{ steps.source.outputs.required }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
with:
ref: ${{ needs.preflight_stable.outputs.sha || needs.select_beta.outputs.sha || needs.select_nightly.outputs.sha || github.sha }}
persist-credentials: false
@@ -427,7 +425,7 @@ jobs:
- name: Download exact-source release daemon receipts
if: needs.release_runner_source.outputs.required == 'true'
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093
with:
pattern: ${{ needs.release_runner_binaries.outputs.runner_artifact_prefix }}-*
path: ${{ runner.temp }}/release-runner-inputs
@@ -724,7 +722,7 @@ jobs:
- name: Download exact-source release daemon receipts
if: needs.release_runner_source.outputs.required == 'true'
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093
with:
pattern: ${{ needs.release_runner_binaries.outputs.runner_artifact_prefix }}-*
path: ${{ runner.temp }}/release-runner-inputs
@@ -1003,7 +1001,7 @@ jobs:
- name: Download exact-source release daemon receipts
if: needs.release_runner_source.outputs.required == 'true'
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093
with:
pattern: ${{ needs.release_runner_binaries.outputs.runner_artifact_prefix }}-*
path: ${{ runner.temp }}/release-runner-inputs
@@ -1384,7 +1382,7 @@ jobs:
- name: Download exact-source release daemon receipts
if: needs.release_runner_source.outputs.required == 'true'
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093
with:
pattern: ${{ needs.release_runner_binaries.outputs.runner_artifact_prefix }}-*
path: ${{ runner.temp }}/release-runner-inputs
@@ -1442,7 +1440,7 @@ jobs:
- name: Download exact-source release daemon receipts
if: needs.release_runner_source.outputs.required == 'true'
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093
with:
pattern: ${{ needs.release_runner_binaries.outputs.runner_artifact_prefix }}-*
path: ${{ runner.temp }}/release-runner-inputs