Repository navigation
chore(renovate): P4a — durable lock-regen (fleet pip-compile manager + auto-lock backstop) #2406
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -1,6 +1,14 @@ | ||||||
| name: Dependabot Auto-Lock | ||||||
| # Automatically regenerate requirements.lock when dependabot updates any | ||||||
| # declared lock input file so the checked-in lock stays in sync. | ||||||
| # Backstop that regenerates requirements.lock when a dependency bot updates a | ||||||
| # declared lock input file, so the checked-in lock stays in sync. | ||||||
| # | ||||||
| # Primary mechanism is now the fleet Renovate preset's pip-compile manager | ||||||
| # (renovate-presets/fleet.json), which regenerates the lock IN the same PR by | ||||||
| # re-running the command recorded in the lock header. This workflow is a | ||||||
| # Workflows-local backstop for when that in-PR regeneration is unavailable | ||||||
| # (e.g. Dependabot PRs, or a Renovate run that could not run uv). To avoid | ||||||
| # fighting Renovate it re-runs the SAME header command verbatim, producing | ||||||
| # byte-identical output. (Not synced to consumers; consumers rely on the preset.) | ||||||
|
|
||||||
| on: | ||||||
| pull_request: | ||||||
|
|
@@ -10,6 +18,7 @@ on: | |||||
| - 'tools/requirements-llm.txt' | ||||||
| branches: | ||||||
| - 'dependabot/**' | ||||||
| - 'renovate/**' | ||||||
|
Comment on lines
19
to
+21
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: cat -n .github/workflows/maint-dependabot-auto-lock.yml | head -50Repository: stranske/Workflows Length of output: 1926 🌐 Web query:
💡 Result: In GitHub Actions, the Citations:
🏁 Script executed: cat -n .github/workflows/maint-dependabot-auto-lock.yml | sed -n '50,80p'Repository: stranske/Workflows Length of output: 1624 🏁 Script executed: head -n 3 requirements.lockRepository: stranske/Template Length of output: 236
The Use Suggested fix on:
pull_request:
paths:
- 'pyproject.toml'
- 'requirements.txt'
- 'tools/requirements-llm.txt'
- branches:
- - 'dependabot/**'
- - 'renovate/**'- if: github.actor == 'dependabot[bot]' || github.actor == 'renovate[bot]'
+ if: >
+ startsWith(github.head_ref, 'dependabot/') || startsWith(github.head_ref, 'renovate/')🤖 Prompt for AI Agents |
||||||
|
|
||||||
| permissions: | ||||||
| contents: write | ||||||
|
|
@@ -23,7 +32,7 @@ jobs: | |||||
| regenerate-lock: | ||||||
| name: Regenerate requirements.lock | ||||||
| runs-on: ubuntu-latest | ||||||
| if: github.actor == 'dependabot[bot]' | ||||||
| if: github.actor == 'dependabot[bot]' || github.actor == 'renovate[bot]' | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
# First, find and inspect the workflow file
find . -name "maint-dependabot-auto-lock.yml" -type f | head -5Repository: stranske/Workflows Length of output: 113 🏁 Script executed: #!/bin/bash
# Read the entire workflow file with line numbers
cat -n .github/workflows/maint-dependabot-auto-lock.ymlRepository: stranske/Workflows Length of output: 4483 🌐 Web query:
💡 Result: The github.actor context in GitHub Actions is considered spoofable and insecure for gatekeeping privileged operations [1][2][3]. This is because it refers to the user who triggered the specific event that initiated the current workflow run [4], not necessarily the original creator of a pull request or the author of the code being executed [5][3]. Attackers can exploit this behavior, particularly in workflows using events like pull_request_target, by performing actions—such as commenting on a pull request or triggering a re-run—that cause the workflow to re-trigger [4]. If an attacker controls an event that triggers a re-run, they can manipulate the workflow into executing with a github.actor value that appears to be a trusted account (e.g., 'dependabot[bot]') [4][6]. Relying on this value to grant permissions or execute code can lead to a Confused Deputy attack, where a privileged process is tricked into performing malicious actions [5]. To securely validate actors, you should avoid relying on github.actor for security-critical decisions [2][3]. Instead, use immutable, event-specific contexts that represent the true initiator or author of the event [1][7][6]: 1. For pull request events, use github.event.pull_request.user.login to verify the original PR author [1][7]. 2. For comment-based triggers, use github.event.comment.user.login [1]. 3. For general security-critical validation, consider comparing against the actor's unique numeric ID (github.actor_id or event-specific IDs) rather than their username, which can be impersonated or registered by attackers [1][8]. 4. Always implement additional defense-in-depth measures for workflows that handle secrets or privileged operations, such as explicitly checking that the pull request does not originate from a fork (github.event.pull_request.head.repo.fork == false) [7][9]. Citations:
🏁 Script executed: #!/bin/bash
# Search for any comments, issues, or discussions about github.actor in workflows
rg "github\.actor|github\.event\.pull_request\.user" --type yaml -C 2Repository: stranske/Workflows Length of output: 44 🏁 Script executed: #!/bin/bash
# Check if other workflows in this repo use the safer pattern
find .github/workflows -name "*.yml" -o -name "*.yaml" | xargs grep -l "if:" | head -5 | xargs cat | grep -A 2 "github.event.pull_request"Repository: stranske/Workflows Length of output: 251 Use PR author identity for bot gating, not Line 35 uses Suggested fix- if: github.actor == 'dependabot[bot]' || github.actor == 'renovate[bot]'
+ if: contains(fromJSON('["dependabot[bot]","renovate[bot]"]'), github.event.pull_request.user.login)📝 Committable suggestion
Suggested change
🤖 Prompt for AI AgentsSource: Coding guidelines |
||||||
|
|
||||||
| steps: | ||||||
| - uses: actions/checkout@v6 | ||||||
|
|
@@ -39,61 +48,44 @@ jobs: | |||||
| - name: Install uv | ||||||
| run: pip install uv | ||||||
|
|
||||||
| - name: Build compile arguments | ||||||
| id: compile_args | ||||||
| - name: Regenerate requirements.lock from its recorded header command | ||||||
| id: check | ||||||
| run: | | ||||||
| python - <<'PY' | ||||||
| import shlex | ||||||
| import tomllib | ||||||
| from pathlib import Path | ||||||
|
|
||||||
| pyproject_path = Path("pyproject.toml") | ||||||
| pyproject = tomllib.loads(pyproject_path.read_text(encoding="utf-8")) | ||||||
| extras = sorted(pyproject.get("project", {}).get("optional-dependencies", {})) | ||||||
|
|
||||||
| args = ["--python-version", "3.12", "pyproject.toml"] | ||||||
| for extra in extras: | ||||||
| args.extend(["--extra", extra]) | ||||||
|
|
||||||
| for candidate in ("requirements.txt", "tools/requirements-llm.txt"): | ||||||
| if Path(candidate).is_file(): | ||||||
| args.append(candidate) | ||||||
|
|
||||||
| args.extend(["--universal", "--output-file"]) | ||||||
| set -euo pipefail | ||||||
| if [ ! -f requirements.lock ]; then | ||||||
| echo "needs_update=false" >> "$GITHUB_OUTPUT" | ||||||
| echo "No requirements.lock present; nothing to regenerate." | ||||||
| exit 0 | ||||||
| fi | ||||||
|
|
||||||
| github_output = Path(__import__("os").environ["GITHUB_OUTPUT"]) | ||||||
| with github_output.open("a", encoding="utf-8") as fh: | ||||||
| fh.write(f"compile_args={shlex.join(args)}\n") | ||||||
| fh.write(f"extras={','.join(extras)}\n") | ||||||
| PY | ||||||
| # Re-run the EXACT command recorded in the lock header. Renovate's | ||||||
| # pip-compile manager regenerates the lock the same way (it reads this | ||||||
| # header), so producing byte-identical output keeps this backstop from | ||||||
| # fighting Renovate with flip-flop commits. The header carries only | ||||||
| # flags/paths (including the --output-file / -o target), so the | ||||||
| # unquoted word-splitting below is intentional. | ||||||
| args=$(sed -n 's/^#[[:space:]]*uv pip compile[[:space:]]\{1,\}\(.*\)$/\1/p' requirements.lock | head -n1) | ||||||
| if [ -z "${args}" ]; then | ||||||
| echo "::error::Could not parse a 'uv pip compile' command from the requirements.lock header." | ||||||
| exit 1 | ||||||
| fi | ||||||
| echo "Recorded command: uv pip compile ${args}" | ||||||
|
|
||||||
| - name: Check if lock file needs update | ||||||
| id: check | ||||||
| run: | | ||||||
| out=requirements.check | ||||||
| log=compile.log | ||||||
| compile_args="${{ steps.compile_args.outputs.compile_args }}" | ||||||
| uv pip compile ${compile_args} "$out" 2>&1 | tee "$log" || { | ||||||
| # shellcheck disable=SC2086 | ||||||
| uv pip compile ${args} 2>&1 | tee compile.log || { | ||||||
|
Comment on lines
+67
to
+75
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
# Confirm the file exists and check the exact code at lines 67-75
echo "=== Checking file existence and content ==="
if [ -f ".github/workflows/maint-dependabot-auto-lock.yml" ]; then
echo "File found"
echo "Lines 67-75:"
sed -n '67,75p' .github/workflows/maint-dependabot-auto-lock.yml
else
echo "File not found"
fi
# Check if requirements.lock exists
echo -e "\n=== Checking for requirements.lock ==="
find . -name "requirements.lock" -type f | head -20Repository: stranske/Workflows Length of output: 638 🏁 Script executed: #!/bin/bash
# Examine actual requirements.lock files to understand the header format
echo "=== Sample requirements.lock headers ==="
find . -name "requirements.lock" -type f | while read -r file; do
echo "--- $file ---"
head -n 3 "$file"
echo ""
done | head -50Repository: stranske/Workflows Length of output: 367 🏁 Script executed: #!/bin/bash
# Check the full context around lines 60-80 to understand the workflow better
echo "=== Full context of the workflow (lines 60-85) ==="
sed -n '60,85p' .github/workflows/maint-dependabot-auto-lock.ymlRepository: stranske/Workflows Length of output: 1372 🏁 Script executed: #!/bin/bash
# Check Template's requirements.lock header format to understand what gets stored
echo "=== Template requirements.lock header ==="
if [ -f "requirements.lock" ]; then
head -n 5 requirements.lock
else
echo "No requirements.lock found"
fiRepository: stranske/Template Length of output: 307 Harden header-command replay against shell injection. Lines 67-75 parse command text from Suggested fix- args=$(sed -n 's/^#[[:space:]]*uv pip compile[[:space:]]\{1,\}\(.*\)$/\1/p' requirements.lock | head -n1)
- if [ -z "${args}" ]; then
+ args=$(sed -n 's/^#[[:space:]]*uv pip compile[[:space:]]\{1,\}\(.*\)$/\1/p' requirements.lock | head -n1)
+ if [ -z "${args}" ]; then
echo "::error::Could not parse a 'uv pip compile' command from the requirements.lock header."
exit 1
fi
- echo "Recorded command: uv pip compile ${args}"
+ if printf '%s' "${args}" | grep -Eq '[;&|`$()<>]'; then
+ echo "::error::Unsafe token detected in lock header command."
+ exit 1
+ fi
+ read -r -a uv_args <<< "${args}"
+ echo "Recorded command: uv pip compile ${args}"
# shellcheck disable=SC2086
- uv pip compile ${args} 2>&1 | tee compile.log || {
+ uv pip compile "${uv_args[@]}" 2>&1 | tee compile.log || {
echo "❌ Compilation failed:"
cat compile.log
exit 1
}As per coding guidelines, ".github/workflows/**: Flag template-injection, unpinned third-party actions, and spoofable bot-actor checks — this workflow YAML is synced to 9 consumer repos, so one bug replicates fleet-wide." 🤖 Prompt for AI AgentsSource: Coding guidelines |
||||||
| echo "❌ Compilation failed:" | ||||||
| cat "$log" | ||||||
| cat compile.log | ||||||
| exit 1 | ||||||
| } | ||||||
|
|
||||||
| if git diff --exit-code requirements.lock requirements.check > /dev/null 2>&1; then | ||||||
| if git diff --exit-code requirements.lock > /dev/null 2>&1; then | ||||||
| echo "needs_update=false" >> "$GITHUB_OUTPUT" | ||||||
| echo "✓ Lock file is already up to date" | ||||||
| else | ||||||
| echo "needs_update=true" >> "$GITHUB_OUTPUT" | ||||||
| echo "⚠ Lock file needs regeneration" | ||||||
| echo "⚠ Lock file regenerated; committing the refreshed lock" | ||||||
| fi | ||||||
|
|
||||||
| - name: Regenerate lock file | ||||||
| if: steps.check.outputs.needs_update == 'true' | ||||||
| run: | | ||||||
| compile_args="${{ steps.compile_args.outputs.compile_args }}" | ||||||
| uv pip compile ${compile_args} requirements.lock | ||||||
| echo "✓ Generated new lock file" | ||||||
|
|
||||||
| - name: Commit updated lock file | ||||||
| if: steps.check.outputs.needs_update == 'true' | ||||||
| run: | | ||||||
|
|
@@ -102,6 +94,6 @@ jobs: | |||||
| git add requirements.lock | ||||||
| git commit \ | ||||||
| -m "chore(deps): regenerate requirements.lock" \ | ||||||
| -m "Includes all optional dependencies" | ||||||
| -m "Backstop regeneration via the command recorded in the lock header." | ||||||
| git push | ||||||
| echo "✓ Committed and pushed updated lock file" | ||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For
pull_request, thebranchesfilter matches the PR target/base branch, not the source branch (GitHub docs describe this as PRs that target matching branches). Normal Renovate PRs come fromrenovate/...intomain, so this new pattern prevents the workflow from being queued before the job-level actor check can run, and the advertised Renovate backstop never runs when Renovate fails to regenerate the lock. Use a base branch filter such asmain(or remove it) and gate source branches withgithub.head_refin the jobif.Useful? React with 👍 / 👎.