Skip to content

docs: prefer Protocol over ProtocolBase in cross-references, per the owner's #1103 ruling #11490

docs: prefer Protocol over ProtocolBase in cross-references, per the owner's #1103 ruling

docs: prefer Protocol over ProtocolBase in cross-references, per the owner's #1103 ruling #11490

Workflow file for this run

name: Project Status

Check warning on line 1 in .github/workflows/project-status.yml

View workflow run for this annotation

GitHub Actions / Project Status

Workflow execution policy warning (evaluate mode)

On December 2, 2026, GitHub will restrict `pull_request_target` on public repositories by default. To continue allowing the event trigger, configure an Actions policy. Learn more: https://gh.io/securely-using-pull_request_target#default-policy-for-pull_request_target
# Keeps the project board's Status field (users/JarryShaw/projects/2) in line with
# each item's state labels, which no built-in board workflow observes. The mapping
# is `status_for` in util/project_status.py; it and the PROJECT_TOKEN setup are
# documented under "Milestones and the Project Board" in
# docs/source/contributing/conventions/process.rst.
#
# SECURITY. `pull_request_target` is used only because a fork's `pull_request` run
# gets no secrets, and PROJECT_TOKEN is one. So this runs with a secret on events a
# stranger can cause, and must never execute anything the pull request supplies --
# the classic "pwn request". The job below:
#
# * never checks out the repository, at any ref, and has no `uses:` step at all;
# * fetches util/project_status.py, and util/_actions.py, which it imports
# (#2047), through the API at `github.sha`, which under
# `pull_request_target` is the tip of the *base* branch, never the PR head --
# so editing either in a pull request changes nothing about its own run;
# * takes nothing from the event payload but the item number, passed through
# `env:` rather than interpolated into the script, and re-reads labels and
# state from the API.
#
# tests/project/test_project_status.py asserts each of these over this file.
#
# `workflow_dispatch` is not a fork boundary: it runs the selected branch's file
# with PROJECT_TOKEN in scope. Only write access can dispatch, and write access can
# already change any workflow, so this trusts collaborators rather than forks --
# the same trust the rest of the repository's secrets rely on.
on:
issues:
types: [opened, reopened, labeled, unlabeled, closed]
pull_request_target:
types: [opened, reopened, labeled, unlabeled, closed, ready_for_review]
schedule:
# Nightly backstop for events this workflow missed -- a run dropped from a
# full concurrency queue, a GitHub outage, or a label changed while the
# secret was absent. 00:37 UTC is clear of the Saturday jobs (#1967) and off
# the hour, which GitHub's scheduler queues most heavily.
- cron: '37 0 * * *'
workflow_dispatch:
# The job uses PROJECT_TOKEN only, never GITHUB_TOKEN.
permissions: {}
# Event runs are keyed on the item number, so the `unlabeled` and `labeled` of a
# label swap, fired within the same second, serialize on one item. Issues and pull
# requests share one number space, so keys cannot collide across the two.
#
# Nothing is cancelled: a cancelled run stays on the PR head as a failed Sync
# Status check even after a newer run has fixed the board. Omitting
# `cancel-in-progress` is not enough, because by default a group holds one pending
# run and cancels it when a newer one arrives; `queue: max` lets up to 100 wait,
# and only a run past that is cancelled.
# GitHub documents both, and that the two keys together are a validation error:
# https://docs.github.com/en/actions/how-tos/write-workflows/choose-when-workflows-run/control-workflow-concurrency
#
# Queue order is not guaranteed, but runs on one item never overlap and each
# re-reads the labels, so the last to execute writes the final state. The cost is
# one run per event, not one per burst. Scheduled and manual reconciles share a
# separate group, so a reconcile can write an older Status over a fresher one; that
# self-repairs on the item's next event or the next nightly run, while one global
# group would let a reconcile hold every event run.
concurrency:
group: project-status-${{ (github.event.issue.number || github.event.pull_request.number) && format('item-{0}', github.event.issue.number || github.event.pull_request.number) || 'reconcile' }}
queue: max
jobs:
status:
name: Sync Status
runs-on: ubuntu-latest
# A single item is a handful of API calls; a full reconcile is one search
# page per 50 items plus one mutation per item that changed.
timeout-minutes: 10
steps:
- name: Sync the project board
env:
GH_TOKEN: ${{ secrets.PROJECT_TOKEN }}
ITEM: ${{ github.event.issue.number || github.event.pull_request.number }}
SCRIPT_REF: ${{ github.sha }}
run: |
if [ -z "$GH_TOKEN" ]; then
echo '::notice title=Project Status::The PROJECT_TOKEN secret is not set, so the project board was not updated.'
exit 0
fi
# The base branch's copy of the script, and of util/_actions.py, which
# it imports from beside it (#2047) -- see the security note above.
for script in project_status.py _actions.py; do
gh api -H 'Accept: application/vnd.github.raw' \
"repos/${GITHUB_REPOSITORY}/contents/util/${script}?ref=${SCRIPT_REF}" \
> "${RUNNER_TEMP}/${script}"
done
if [ -n "$ITEM" ]; then
python3 "${RUNNER_TEMP}/project_status.py" --item "$ITEM"
else
python3 "${RUNNER_TEMP}/project_status.py" --reconcile
fi