mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-04 19:28:46 +01:00
merge: compose Claude behavior with canonical master reconciliation
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
commit
304e95c320
431 files changed
+22586
-15387
No files matched your search
@@ -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"
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
@@ -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/
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in new issue
Block a user