mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-01 15:35:12 +01:00
## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work. > - PR CI selects affected suites and test files to reduce feedback time. > - A selected test inherited the parent selection when it launched an independent runner fixture. > - Ordinary source filenames containing config or setup also forced broad coverage. > - AWS runner access must depend on current CI CODEOWNERS, not only a stored numeric roster. > - This pull request fixes both selection problems and verifies owners before AWS allocation. > - Contributors retain GitHub-hosted CI. Authorized maintainers retain AWS test and final verification jobs. ## Linked Issues or Issue Description Refs #18362 and #18348. The selected server run for #18369 failed in `vitest-chat-shards.test.ts` because its temporary fixture inherited `PAPERCLIP_CI_TEST_SELECTION` and attempted `git rev-parse HEAD` outside a Git repository. #18366 could not use graph selection because `session-config-metadata.test.ts` matched the broad filename rule. The AWS setting was already enabled. The reviewed outsider PRs correctly used hosted runners; a maintainer PR used AWS, including final verification. This change strengthens that boundary rather than enabling AWS for outsiders. ## What Changed - Remove selection settings from Vitest child environments after the wrapper validates the selected shard. - Match conventional Vite/Vitest configuration filenames rather than any occurrence of config or setup. Keep graph tracking for actual setup and configuration dependencies. - Bind rerun actor lookup to the exact run attempt. Require the author, event sender and rerun actor to be current CI CODEOWNERS and members of the existing numeric allowlist. Read the resolver and CODEOWNERS from one protected master revision. - Run metadata authorization on GitHub-hosted capacity before any AWS allocation. Keep authorized test and final verification placement on the AWS fleet. - Add a real selected-server regression that launches nested chat fixtures, negative authorization cases, and qualification for a real config-named test. - Update the developer and profiling guides. - Retire eight legacy PR workflow SHA authorizations and one deleted qualification-branch reference from the live `paperclip-public-pr` runner group. The remaining three references are the trusted PR workflow and the existing full-stack/protocol evaluation workflows on master. No active PR run referenced the retired versions. ## Verification - Selection regression tests: 13 passed, including the real nested chat runner. - Routing, planner and profiling tests: 100 passed, including 69 routing cases. - `actionlint` passed for both changed workflows. `git diff --check` passed. - Confirmed protected master still requires CODEOWNER review. A live lookup found four CI owners, with three of six numeric roster entries eligible and three denied by the new owner check. - `pnpm -r typecheck` and `pnpm build` passed. The full local Vitest suite was stopped after repeated embedded PostgreSQL initialization failures; it did not pass. On final revision `0b40ae15b7996603e405edd4157a5c337e0405c7`, both Linux qualification jobs and all required CI gates passed. The main PR workflow completed with 48 successful jobs and one intentional skip, including final verification. [CI run](https://github.com/paperclipai/paperclip/actions/runs/41126881716). Greptile reviewed the final revision at 5/5 with no inline findings. - Shard and workflow contracts: 44 passed. CI first caught an outdated assertion that required the routing job to use AWS; it now requires hosted authorization, and the policy job passes. - A read-only live gate exercise preserved the existing merge-freshness denial when master advanced beyond the available merge ref. It allocated no runner and was not a successful admission proof. ## Risks - The short metadata authorization job now depends on GitHub-hosted queue capacity. This prevents a stale roster from allocating AWS before current CODEOWNERS are checked. Authorized test and final verification jobs still use AWS. - Identity/API failures fall back to hosted CI. No caller can override the trusted owner list with PR contents. - Configuration dependencies still require conservative graph handling. Existing setup dependency, opaque I/O, stale manifest and wrong-lane tests remain in place. - The runner group is restricted to selected repositories and selected workflows. Its workflow allowlist was verified and narrowed through the GitHub settings UI. Legacy pinned callers must update to the current trusted workflow before using AWS; active PR runs used master references. The current PR workflow authorization and evaluation workflow authorizations remain in place. ## Model Used - OpenAI Codex, based on GPT-6. The exact serving variant and context limit are not exposed in this session. Used reasoning, repository inspection, code execution and GitHub tools. ## Checklist - [x] I have included a thinking path that traces from project context to this change - [x] I have specified the model used (with version and capability details) - [x] I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work - [x] I have searched GitHub for duplicate or related PRs and linked them above - [x] I have either (a) linked existing issues with `Fixes: #` / `Closes #` / `Refs #` OR (b) described the issue in-PR following the relevant issue template - [x] I have not referenced internal/instance-local Paperclip issues or links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip` URLs) - [x] My branch name describes the change (e.g. `docs/...`, `fix/...`) and contains no internal Paperclip ticket id or instance-derived details - [x] I have run tests locally and they pass — targeted suites passed; the full local suite hit the PostgreSQL startup limitation documented above, and full Linux CI passed - [x] I have added or updated tests where applicable - [x] I have updated relevant documentation to reflect my changes - [x] I have considered and documented any risks above - [x] All Paperclip CI gates are green - [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups - [x] I will address all Greptile and reviewer comments before requesting merge --------- Co-authored-by: Paperclip <noreply@paperclip.ing>
409 lines
20 KiB
YAML
409 lines
20 KiB
YAML
name: CI routing
|
|
|
|
on:
|
|
workflow_dispatch:
|
|
workflow_call:
|
|
outputs:
|
|
runner:
|
|
value: ${{ jobs.gate.outputs.runner }}
|
|
full_ci:
|
|
value: ${{ jobs.gate.outputs.full_ci }}
|
|
plan:
|
|
value: ${{ jobs.gate.outputs.plan }}
|
|
profile_ci:
|
|
value: ${{ jobs.gate.outputs.profile_ci }}
|
|
|
|
permissions:
|
|
actions: read
|
|
contents: read
|
|
pull-requests: read
|
|
|
|
jobs:
|
|
gate:
|
|
name: Select trusted runner
|
|
# No AWS capacity is allocated until current CI CODEOWNERS are verified.
|
|
# A cached numeric allowlist alone can outlive removal from CODEOWNERS.
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 5
|
|
outputs:
|
|
plan: ${{ steps.plan.outputs.plan }}
|
|
runner: ${{ steps.route.outputs.runner }}
|
|
full_ci: ${{ steps.scope.outputs.full_ci }}
|
|
profile_ci: ${{ steps.profile.outputs.profile_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_LABEL_NAME: ${{ github.event.label.name }}
|
|
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_ATTEMPT: ${{ github.run_attempt }}
|
|
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" --argjson owners "$owner_ids" \
|
|
'index($user_id) != null and ($owners | 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|labeled)$ ]] || fail_closed 'unsupported pull_request action'
|
|
if [[ "$EVENT_ACTION" == 'labeled' && "${EVENT_LABEL_NAME:-}" != 'profile-ci' ]]; then
|
|
fail_closed 'only the profile-ci label can request a profiling run'
|
|
fi
|
|
|
|
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'
|
|
|
|
# Both the resolver and CODEOWNERS come from the same protected master
|
|
# revision. Never use the PR's CODEOWNERS or execute its source here.
|
|
trusted_sha="$(gh api /repos/paperclipai/paperclip/git/ref/heads/master --jq .object.sha)" \
|
|
|| fail_closed 'could not resolve trusted CODEOWNERS revision'
|
|
is_commit_sha "$trusted_sha" || fail_closed 'invalid trusted CODEOWNERS revision'
|
|
owner_helper="$RUNNER_TEMP/aws-ci-owners.mjs"
|
|
if ! gh api "/repos/paperclipai/paperclip/contents/scripts/ci-profile/authorize.mjs?ref=$trusted_sha" \
|
|
--jq '.content' > "$owner_helper.base64" ||
|
|
! base64 --decode < "$owner_helper.base64" > "$owner_helper"; then
|
|
fail_closed 'could not load trusted CODEOWNERS resolver'
|
|
fi
|
|
owner_ids="$(node --input-type=module - "$owner_helper" "$trusted_sha" <<'NODE'
|
|
import { pathToFileURL } from 'node:url';
|
|
const { trustedOwnerIds } = await import(pathToFileURL(process.argv[2]));
|
|
console.log(JSON.stringify(trustedOwnerIds(undefined, process.argv[3])));
|
|
NODE
|
|
)" || fail_closed 'could not verify current CI CODEOWNERS'
|
|
|
|
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 an allowlisted CI CODEOWNER"
|
|
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'
|
|
|
|
is_positive_integer "$RUN_ID" || fail_closed 'invalid workflow run ID'
|
|
is_positive_integer "$RUN_ATTEMPT" || fail_closed 'invalid workflow run attempt'
|
|
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/attempts/$RUN_ATTEMPT" 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 an allowlisted CI CODEOWNER"
|
|
|
|
jq -e \
|
|
--argjson repository_id 1170821064 \
|
|
--argjson run_attempt "$RUN_ATTEMPT" \
|
|
--arg event_name "$EVENT_NAME" '
|
|
.repository.id == $repository_id and
|
|
.run_attempt == $run_attempt 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: Authorize CODEOWNERS CI profiling
|
|
id: profile
|
|
shell: bash
|
|
env:
|
|
GH_TOKEN: ${{ github.token }}
|
|
PROFILE_REQUESTED: ${{ contains(github.event.pull_request.labels.*.name, 'profile-ci') }}
|
|
run: |
|
|
set -euo pipefail
|
|
if [[ "$PROFILE_REQUESTED" != 'true' ]]; then
|
|
echo 'profile_ci=false' >> "$GITHUB_OUTPUT"
|
|
exit 0
|
|
fi
|
|
# Fetch only from canonical master, never the PR ref. Pin both the
|
|
# authorization program and CODEOWNERS to the same trusted revision.
|
|
if ! TRUSTED_PROFILE_SHA="$(gh api /repos/paperclipai/paperclip/git/ref/heads/master --jq .object.sha)" ||
|
|
[[ ! "$TRUSTED_PROFILE_SHA" =~ ^[0-9a-f]{40}$ ]] ||
|
|
! gh api -H 'Accept: application/vnd.github.raw+json' \
|
|
"/repos/paperclipai/paperclip/contents/scripts/ci-profile/authorize.mjs?ref=$TRUSTED_PROFILE_SHA" \
|
|
> "$RUNNER_TEMP/ci-profile-authorize.mjs"; then
|
|
echo 'profile_ci=false' >> "$GITHUB_OUTPUT"
|
|
exit 0
|
|
fi
|
|
export TRUSTED_PROFILE_SHA
|
|
node "$RUNNER_TEMP/ci-profile-authorize.mjs"
|
|
|
|
- name: Select PR CI scope
|
|
id: scope
|
|
shell: bash
|
|
env:
|
|
GH_TOKEN: ${{ github.token }}
|
|
EVENT_NAME: ${{ github.event_name }}
|
|
EVENT_BASE_SHA: ${{ github.event.pull_request.base.sha }}
|
|
EVENT_HEAD_SHA: ${{ github.event.pull_request.head.sha }}
|
|
PROFILE_CI: ${{ steps.profile.outputs.profile_ci }}
|
|
FORCE_FULL_CI: ${{ contains(github.event.pull_request.labels.*.name, 'full-ci') }}
|
|
CI_INCREMENTAL_MODE: ${{ vars.CI_INCREMENTAL_MODE || 'enabled' }}
|
|
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'
|
|
# True when the PR's own diff is everything it adds to the stack's
|
|
# base branch: no stack, or the lowest unmerged PR in one. Above
|
|
# that, the diff omits lower PRs' code that this run must still test.
|
|
diff_covers_stack='true'
|
|
|
|
# Release notes PRs touch only releases/v*.md or releases/beta/v*.md,
|
|
# which no build, test, or e2e lane reads. The diff is pinned to the
|
|
# event's base and head SHAs, so a later push cannot change the
|
|
# decision for this run. Any lookup failure keeps full CI.
|
|
release_notes_only() {
|
|
[[ "${EVENT_NAME:-}" == 'pull_request' ]] || return 1
|
|
[[ "${EVENT_BASE_SHA:-}" =~ ^[0-9a-f]{40}$ ]] || return 1
|
|
[[ "${EVENT_HEAD_SHA:-}" =~ ^[0-9a-f]{40}$ ]] || return 1
|
|
|
|
local comparison
|
|
comparison="$(gh api \
|
|
-H 'Accept: application/vnd.github+json' \
|
|
-H 'X-GitHub-Api-Version: 2022-11-28' \
|
|
"/repos/paperclipai/paperclip/compare/$EVENT_BASE_SHA...$EVENT_HEAD_SHA" 2>/dev/null)" \
|
|
|| return 1
|
|
|
|
# The compare API lists at most 300 files, so a full page may be
|
|
# truncated. Renames count both the old and the new path.
|
|
jq -e '
|
|
(.files | type == "array") and
|
|
(.files | length > 0 and length < 300) and
|
|
all(.files[];
|
|
(.filename | type == "string") and
|
|
([.filename, .previous_filename] | map(select(. != null)) |
|
|
all(.[]; type == "string" and test("^releases/(beta/)?v[^/]+\\.md$"))))
|
|
' <<< "$comparison" >/dev/null 2>&1
|
|
}
|
|
|
|
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 [[ -z "$stack_base_ref" || "$stack_base_ref" != "$PR_BASE_REF" ]]; then
|
|
diff_covers_stack='false'
|
|
fi
|
|
|
|
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'
|
|
diff_covers_stack='false'
|
|
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
|
|
|
|
if [[ "$full_ci" == 'true' && "$diff_covers_stack" == 'true' ]] &&
|
|
release_notes_only; then
|
|
full_ci='false'
|
|
reason='release notes only'
|
|
fi
|
|
|
|
if [[ "${PROFILE_CI:-false}" == 'true' || "${FORCE_FULL_CI:-false}" == 'true' || "${CI_INCREMENTAL_MODE:-enabled}" == 'full' ]]; then
|
|
full_ci='true'
|
|
reason='profile-ci, full-ci or full mode requests the complete PR suite'
|
|
fi
|
|
|
|
echo "full_ci=$full_ci" >> "$GITHUB_OUTPUT"
|
|
echo "::notice title=PR CI scope::$reason; full_ci=$full_ci"
|
|
|
|
|
|
- name: Plan affected CI tasks from trusted definitions
|
|
id: plan
|
|
shell: bash
|
|
env:
|
|
GH_TOKEN: ${{ github.token }}
|
|
FULL_CI: ${{ steps.scope.outputs.full_ci }}
|
|
PROFILE_CI: ${{ steps.profile.outputs.profile_ci }}
|
|
FORCE_FULL_CI: ${{ contains(github.event.pull_request.labels.*.name, 'full-ci') }}
|
|
CI_INCREMENTAL_MODE: ${{ vars.CI_INCREMENTAL_MODE || 'enabled' }}
|
|
STACK_JSON: ${{ toJSON(github.event.pull_request.stack) }}
|
|
EVENT_HEAD_SHA: ${{ github.event.pull_request.head.sha }}
|
|
EVENT_MERGE_SHA: ${{ github.sha }}
|
|
run: |
|
|
set -euo pipefail
|
|
fallback() { echo 'plan=' >> "$GITHUB_OUTPUT"; exit 0; }
|
|
[[ "$FULL_CI" == 'true' ]] || fallback
|
|
[[ "$EVENT_HEAD_SHA" =~ ^[0-9a-f]{40}$ && "$EVENT_MERGE_SHA" =~ ^[0-9a-f]{40}$ ]] || fallback
|
|
trusted="$(gh api /repos/paperclipai/paperclip/git/ref/heads/master --jq .object.sha)" || fallback
|
|
[[ "$trusted" =~ ^[0-9a-f]{40}$ ]] || fallback
|
|
directory="$(mktemp -d)"
|
|
trap 'rm -rf "$directory"' EXIT
|
|
for file in .github/scripts/ci-plan.mjs .github/scripts/ci-inputs.mjs .github/scripts/ci-diff.mjs .github/ci-task-matrix.json turbo.json; do
|
|
gh api -H 'Accept: application/vnd.github.raw+json' \
|
|
"/repos/paperclipai/paperclip/contents/$file?ref=$trusted" \
|
|
> "$directory/${file##*/}" || fallback
|
|
done
|
|
# Read the exact tested merge and both parents. The event's base SHA can
|
|
# lag behind its actual first parent. A bare tree diff has no API file cap.
|
|
git init --bare "$directory/repository.git" >/dev/null || fallback
|
|
git -C "$directory/repository.git" fetch --quiet --no-tags --filter=blob:none --depth=2 \
|
|
https://github.com/paperclipai/paperclip.git "$EVENT_MERGE_SHA" || fallback
|
|
if [[ "$PROFILE_CI" == 'true' || "$FORCE_FULL_CI" == 'true' ]]; then
|
|
export CI_FORCE_REASON='Full suite requested by label'
|
|
elif [[ "$STACK_JSON" != 'null' && -n "$STACK_JSON" ]]; then
|
|
export CI_FORCE_REASON='Stack verification includes lower pull requests'
|
|
fi
|
|
export CI_TASK_CONFIG="$directory/turbo.json"
|
|
export CI_TASK_MATRIX="$directory/ci-task-matrix.json"
|
|
export CI_DIFF_REPOSITORY="$directory/repository.git"
|
|
node "$directory/ci-plan.mjs" || fallback
|