Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
139 changes: 135 additions & 4 deletions .github/workflows/dependabot-vendor.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@ on:
workflows: [Tests]
types: [completed]

permissions:
contents: write
pull-requests: read
actions: write
# Each job declares only what it needs. Both run the trusted default-branch
# workflow and scripts; Dependabot content is checked out only after the gate
# validates it and is never executed beyond the pinned generators.
permissions: {}

jobs:
refresh-vendor:
Expand All @@ -19,6 +19,10 @@ jobs:
github.event.workflow_run.head_repository.full_name == github.repository &&
startsWith(github.event.workflow_run.head_branch, 'dependabot/npm_and_yarn/')
runs-on: ubuntu-24.04
permissions:
contents: write
pull-requests: read
actions: write

steps:
- name: Checkout trusted default branch
Expand Down Expand Up @@ -49,6 +53,7 @@ jobs:
gh api "repos/${GITHUB_REPOSITORY}/actions/runs/${RUN_ID}/jobs?filter=latest&per_page=100" \
> "$RUNNER_TEMP/jobs.json"
python scripts/dependabot_vendor.py validate \
--ecosystem npm \
--event "$GITHUB_EVENT_PATH" \
--pull-request "$RUNNER_TEMP/pull-request.json" \
--files "$RUNNER_TEMP/files.json" \
Expand Down Expand Up @@ -92,6 +97,7 @@ jobs:
test "$(git rev-parse HEAD)" = "$HEAD_SHA"
rsync --archive --delete "$RUNNER_TEMP/vendor/" static/vendor/
python "$RUNNER_TEMP/dependabot_vendor.py" stage \
--ecosystem npm \
--root . \
--github-output "$GITHUB_OUTPUT"

Expand All @@ -117,3 +123,128 @@ jobs:
HEAD_REF: ${{ steps.context.outputs.head_ref }}
EXPECTED_SHA: ${{ steps.commit.outputs.generated_sha }}
run: gh workflow run tests.yml --ref "$HEAD_REF" -f expected_sha="$EXPECTED_SHA"

# Dependabot's pip updater rewrites the lock files without the repository
# lock script, dropping the --require-hashes header and universal markers.
refresh-python-locks:
if: >-
github.event.workflow_run.event == 'pull_request' &&
github.event.workflow_run.conclusion == 'failure' &&
github.event.workflow_run.actor.login == 'dependabot[bot]' &&
github.event.workflow_run.head_repository.full_name == github.repository &&
startsWith(github.event.workflow_run.head_branch, 'dependabot/pip/')
runs-on: ubuntu-24.04
permissions:
contents: write
pull-requests: read
actions: write

steps:
- name: Checkout trusted default branch
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
persist-credentials: false

- name: Set up Python
uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0
with:
python-version: '3.11'

# The resolver version comes from the trusted checkout. No cache is
# restored or saved, so this job cannot seed caches for main.
- name: Set up uv
uses: astral-sh/setup-uv@68559e99cc807c1eb7e9238fd43a15bba460eec2 # v10.3.3
with:
version-file: uv.toml
enable-cache: false

- name: Fetch and validate Dependabot context
id: context
shell: bash
env:
GH_TOKEN: ${{ github.token }}
PR_NUMBER: ${{ github.event.workflow_run.pull_requests[0].number }}
RUN_ID: ${{ github.event.workflow_run.id }}
run: |
set -euo pipefail
[[ "$PR_NUMBER" =~ ^[1-9][0-9]*$ ]]
[[ "$RUN_ID" =~ ^[1-9][0-9]*$ ]]
gh api "repos/${GITHUB_REPOSITORY}/pulls/${PR_NUMBER}" \
> "$RUNNER_TEMP/pull-request.json"
gh api "repos/${GITHUB_REPOSITORY}/pulls/${PR_NUMBER}/files?per_page=100" \
> "$RUNNER_TEMP/files.json"
gh api "repos/${GITHUB_REPOSITORY}/actions/runs/${RUN_ID}/jobs?filter=latest&per_page=100" \
> "$RUNNER_TEMP/jobs.json"
python scripts/dependabot_vendor.py validate \
--ecosystem pip \
--event "$GITHUB_EVENT_PATH" \
--pull-request "$RUNNER_TEMP/pull-request.json" \
--files "$RUNNER_TEMP/files.json" \
--jobs "$RUNNER_TEMP/jobs.json" \
--github-output "$GITHUB_OUTPUT"

- name: Checkout validated Dependabot head
shell: bash
env:
HEAD_REF: ${{ steps.context.outputs.head_ref }}
HEAD_SHA: ${{ steps.context.outputs.head_sha }}
run: |
set -euo pipefail
sha256sum scripts/lock_requirements.ps1 scripts/dependabot_vendor.py uv.toml \
> "$RUNNER_TEMP/trusted-script-checksums"
git fetch --no-tags origin "refs/heads/${HEAD_REF}"
test "$(git rev-parse FETCH_HEAD)" = "$HEAD_SHA"
git checkout --detach FETCH_HEAD
test "$(git rev-parse HEAD)" = "$HEAD_SHA"
sha256sum --check "$RUNNER_TEMP/trusted-script-checksums"
cp scripts/dependabot_vendor.py "$RUNNER_TEMP/dependabot_vendor.py"

# UV_NO_BUILD keeps resolution metadata-only: no source distribution
# build backend, and therefore no package code, runs in this job.
- name: Regenerate Python locks with the repository lock script
shell: bash
env:
UV_NO_BUILD: '1'
run: |
set -euo pipefail
python "$RUNNER_TEMP/dependabot_vendor.py" check-python-inputs --root .
pwsh -NoProfile -NonInteractive -File scripts/lock_requirements.ps1
pwsh -NoProfile -NonInteractive -File scripts/lock_requirements.ps1 -Check

- name: Commit regenerated locks to the validated branch
id: commit
shell: bash
env:
GH_TOKEN: ${{ github.token }}
HEAD_REF: ${{ steps.context.outputs.head_ref }}
HEAD_SHA: ${{ steps.context.outputs.head_sha }}
run: |
set -euo pipefail
test "$(git rev-parse HEAD)" = "$HEAD_SHA"
python "$RUNNER_TEMP/dependabot_vendor.py" stage \
--ecosystem pip \
--root . \
--github-output "$GITHUB_OUTPUT"

if [ "$(tail -n 1 "$GITHUB_OUTPUT")" = "changed=false" ]; then
exit 0
fi

git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git commit -m "Regenerate Python locks with the repository lock script [dependabot skip]"
generated_sha="$(git rev-parse HEAD)"

test "$(git ls-remote origin "refs/heads/${HEAD_REF}" | cut -f1)" = "$HEAD_SHA"
gh auth setup-git
git push origin "HEAD:refs/heads/${HEAD_REF}"
echo "generated_sha=${generated_sha}" >> "$GITHUB_OUTPUT"

- name: Run tests for the regenerated locks
if: steps.commit.outputs.changed == 'true'
shell: bash
env:
GH_TOKEN: ${{ github.token }}
HEAD_REF: ${{ steps.context.outputs.head_ref }}
EXPECTED_SHA: ${{ steps.commit.outputs.generated_sha }}
run: gh workflow run tests.yml --ref "$HEAD_REF" -f expected_sha="$EXPECTED_SHA"
4 changes: 3 additions & 1 deletion docs/wiki/Development-and-Testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,9 @@ Verify locks without modifying them:
.\scripts\lock_requirements.ps1 -Check
```

Use `-Upgrade` for an intentional refresh of all allowed resolutions; the default preserves existing pins where possible. `-Check` and `-Upgrade` cannot be combined. Do not hand-edit generated lock files. A dependency change must update the input, all applicable lock outputs, and relevant compatibility tests. Preserve the Python 3.11 marker branches when newer Python versions select different packages.
Use `-Upgrade` for an intentional refresh of all allowed resolutions; the default preserves existing pins where possible. `-Check` and `-Upgrade` cannot be combined. Do not hand-edit generated lock files. A dependency change must update the input, all applicable lock outputs, and relevant compatibility tests. Preserve the Python 3.11 marker branches when newer Python versions select different packages. Existing lock files only seed version preferences; their hash lines are discarded and every hash is taken from the package index, so `-Check` also rejects added or duplicated hashes.

Dependabot's pip updater rewrites the lock files without this script. When the `dependency-locks` check of a Dependabot pip pull request fails, the `Dependabot vendor refresh` workflow validates the pull request, reruns the lock script with the pinned uv and pushes only the three lock files as a follow-up commit. It accepts only Dependabot-authored `dependabot/pip/*` branches that change nothing but the requirement inputs and locks, and lock inputs limited to plain version specifiers.

## Python tests

Expand Down
Loading
Loading