Skip to content

Split prize index into adoption-first vs legacy schemes #506

Split prize index into adoption-first vs legacy schemes

Split prize index into adoption-first vs legacy schemes #506

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, 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]
permissions:
contents: read
pull-requests: write
# Cancel an in-flight run for the same PR when a new commit is pushed.
concurrency:
group: validate-submission-${{ github.event.pull_request.number }}
cancel-in-progress: true
jobs:
validate:
runs-on: ubuntu-latest
steps:
- name: Checkout base branch (trusted)
uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.base.sha }}
path: base
# 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
id: changed
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PR_NUMBER: ${{ github.event.pull_request.number }}
run: |
# API is used instead of `git diff` to avoid shallow-checkout merge-base issues.
files=$(gh api --paginate \
-H "Accept: application/vnd.github+json" \
"/repos/${{ github.repository }}/pulls/${PR_NUMBER}/files" \
--jq '.[].filename')
{
echo "files<<__LP_EOF__"
echo "$files"
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:
PR_TITLE: ${{ github.event.pull_request.title }}
PR_REPO: ${{ github.event.pull_request.head.repo.full_name }}
BASE_REPO: ${{ github.event.pull_request.base.repo.full_name }}
CHANGED_FILES: ${{ steps.changed.outputs.files }}
run: bash base/.github/scripts/validate-submission.sh
- name: Comment on PR
if: always()
uses: actions/github-script@v7
with:
script: |
const fs = require('fs');
let body;
try {
body = fs.readFileSync('/tmp/validation-comment.md', 'utf8');
} catch {
body = '⚠️ Validation script did not produce output.';
}
const marker = '<!-- lambda-prize-validation -->';
body = marker + '\n' + body;
const { data: comments } = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
});
const existing = comments.find(c => (c.body || '').includes(marker));
if (existing) {
await github.rest.issues.updateComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: existing.id,
body,
});
} else {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body,
});
}