mirror of
https://github.com/logos-co/lambda-prize.git
synced 2026-08-27 09:31:13 +00:00
ci: fetch submission files instead of checking out the fork
actions/checkout v4.4.0 refuses to check out fork PR code from a pull_request_target workflow, so every submission PR from a fork now dies at the checkout step and the validator never runs. Rather than opt back in with allow-unsafe-pr-checkout, drop the fork checkout entirely. The validator reads exactly one thing from the PR -- the submission markdown -- so those blobs are fetched by path from the contents API instead. The fork's working tree never lands on the runner, which removes the pwn-request exposure the guard exists for rather than declaring it acceptable, and leaves nothing to re-audit when this job grows a build or lint step later. Also: - Read the prize spec from base/ rather than the PR. It is repo-owned, so the base branch's copy is authoritative; the fork's is a stale snapshot from whenever it branched, which also made the closed/draft check read outdated status. - persist-credentials: false on the base checkout. Nothing here pushes. - Bound the submitter-supplied external clone and the one full-content scan of it with timeouts, no submodules, and no auth prompts.
This commit is contained in:
@@ -4,8 +4,11 @@
|
||||
#
|
||||
# Runtime contract (from validate-submission.yml):
|
||||
# - Runs on ubuntu-latest (GNU grep / sed / coreutils assumed).
|
||||
# - `base/` contains the trusted base-branch checkout.
|
||||
# - `pr/` contains the PR-head checkout, treated as untrusted data only.
|
||||
# - `base/` contains the trusted base-branch checkout, and is the only
|
||||
# source for repo-owned files such as the prize specs.
|
||||
# - `pr/solutions/` contains just the submission markdown the PR changed,
|
||||
# fetched by path from the API and treated as untrusted data only. The
|
||||
# fork's working tree is never checked out.
|
||||
# - Env: PR_TITLE, PR_REPO, BASE_REPO, CHANGED_FILES.
|
||||
set -euo pipefail
|
||||
|
||||
@@ -230,7 +233,9 @@ if $IS_SOLUTION; then
|
||||
# -------------------------------------------------------------------------
|
||||
# 3h. Prize exists and is open
|
||||
# -------------------------------------------------------------------------
|
||||
PRIZE_FILE="pr/prizes/${PRIZE_ID}.md"
|
||||
# Read from base/: the prize spec is repo-owned, so the authoritative copy
|
||||
# is the base branch's, not whatever stale revision the fork branched from.
|
||||
PRIZE_FILE="base/prizes/${PRIZE_ID}.md"
|
||||
if [[ ! -f "$PRIZE_FILE" ]]; then
|
||||
err "Prize \`${PRIZE_ID}\` not found in \`prizes/\`. Check the ID."
|
||||
else
|
||||
@@ -261,7 +266,12 @@ if $IS_SOLUTION; then
|
||||
|
||||
CLONE_DIR="/tmp/submission-repo"
|
||||
rm -rf "$CLONE_DIR"
|
||||
if git clone --depth=1 "$REPO_URL" "$CLONE_DIR" 2>/dev/null; then
|
||||
# `REPO_URL` is submitter-controlled, so the clone is bounded: no auth
|
||||
# prompts, no submodules, one shallow branch, and a wall-clock ceiling so
|
||||
# an oversized or slow repo cannot consume the whole job.
|
||||
if GIT_TERMINAL_PROMPT=0 timeout "${LP_CLONE_TIMEOUT:-300}" \
|
||||
git clone --depth=1 --single-branch --no-tags \
|
||||
--no-recurse-submodules "$REPO_URL" "$CLONE_DIR" 2>/dev/null; then
|
||||
|
||||
# 4a. AI workspace artifacts in the external repo
|
||||
AI_ARTIFACTS=()
|
||||
@@ -336,7 +346,9 @@ if $IS_SOLUTION; then
|
||||
|
||||
# Logos Messaging / Waku integration
|
||||
if echo "$PRIZE_CONTENT" | grep -qi 'Logos Messaging\|Logos Chat\|Waku'; then
|
||||
waku_ref=$(grep -ril 'waku\|logos.messaging\|logos.chat' "$CLONE_DIR" \
|
||||
# Time-bounded: this is the one full-content scan of the clone.
|
||||
waku_ref=$(timeout "${LP_SCAN_TIMEOUT:-60}" \
|
||||
grep -ril 'waku\|logos.messaging\|logos.chat' "$CLONE_DIR" \
|
||||
--include='*.rs' --include='*.go' --include='*.ts' --include='*.js' \
|
||||
--include='*.toml' --include='*.json' 2>/dev/null | head -1 || true)
|
||||
if [[ -z "$waku_ref" ]]; then
|
||||
|
||||
@@ -2,8 +2,8 @@ name: Validate Submission
|
||||
|
||||
# `pull_request_target` is used so the workflow can comment on PRs from forks
|
||||
# (which is how solutions are submitted). It runs on the base branch with
|
||||
# write perms; the PR-head checkout is treated as untrusted data only and
|
||||
# never executed.
|
||||
# write perms, so the fork's tree is never checked out here: the only
|
||||
# submission content the validator reads is fetched by path as data.
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [opened, synchronize, reopened, edited]
|
||||
@@ -26,13 +26,8 @@ jobs:
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.base.sha }}
|
||||
path: base
|
||||
|
||||
- name: Checkout PR head (untrusted data, never executed)
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
repository: ${{ github.event.pull_request.head.repo.full_name }}
|
||||
path: pr
|
||||
# Nothing in this job pushes, so the token has no reason to sit in
|
||||
# base/.git/config for the life of the run.
|
||||
persist-credentials: false
|
||||
|
||||
- name: Get changed files
|
||||
@@ -52,6 +47,31 @@ jobs:
|
||||
echo "__LP_EOF__"
|
||||
} >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Fetch changed solution files (untrusted data, never executed)
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
REPO: ${{ github.repository }}
|
||||
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
|
||||
CHANGED_FILES: ${{ steps.changed.outputs.files }}
|
||||
run: |
|
||||
# The validator reads exactly one thing from the PR: the submission
|
||||
# markdown. Fetching those blobs by path keeps the fork's working
|
||||
# tree off the runner entirely, so there is nothing here to execute
|
||||
# and no `allow-unsafe-pr-checkout` opt-in to carry. Paths come from
|
||||
# the PR files API and are constrained to a single markdown file
|
||||
# directly under `solutions/`.
|
||||
mkdir -p pr/solutions
|
||||
solution_files=$(printf '%s\n' "$CHANGED_FILES" \
|
||||
| grep -E '^solutions/[A-Za-z0-9._-]+\.md$' || true)
|
||||
# Unquoted on purpose: the pattern above admits no spaces or globs.
|
||||
for f in $solution_files; do
|
||||
if ! gh api -H "Accept: application/vnd.github.raw" \
|
||||
"/repos/${REPO}/contents/${f}?ref=${HEAD_SHA}" > "pr/${f}"; then
|
||||
rm -f "pr/${f}"
|
||||
echo "::warning::Could not fetch ${f} at ${HEAD_SHA}."
|
||||
fi
|
||||
done
|
||||
|
||||
- name: Run validation
|
||||
id: validate
|
||||
env:
|
||||
|
||||
Reference in New Issue
Block a user