Skip to content
Closed
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
175 changes: 175 additions & 0 deletions .github/workflows/_job_uv_audit.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
# SPDX-FileCopyrightText: 2025 Contributors to the OpenSTEF project <openstef@lfenergy.org>
#
# SPDX-License-Identifier: MPL-2.0

name: uv Audit and Targeted Upgrade
on:
workflow_call:
inputs:
force:
description: "Skip the manual-edit guard and overwrite the automated branch/PR."
type: boolean
required: false
default: false
rebase:
description: "Skip the manual-edit guard and replay manual commits on top of a freshly regenerated fix, instead of discarding them."
type: boolean
required: false
default: false
secrets:
token:
description: "Token used to push the fix branch and open the PR. Defaults to GITHUB_TOKEN."
required: false

jobs:
uv-audit:
name: uv Audit
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
steps:
- name: Checkout # Must be done before using composite actions
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
show-progress: false # very verbose for not much
token: ${{ secrets.token || secrets.GITHUB_TOKEN }}

- name: Install uv
uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
with:
activate-environment: true
enable-cache: true

- name: Run uv audit
id: audit
shell: bash
run: |
uv audit --preview-features audit,json-output --output-format json > uv_audit_report.json || true

UPGRADE=$(jq -r '[.vulnerabilities[]? | select(.fix_versions | length > 0) | .dependency.name] | unique | .[]' uv_audit_report.json)
NO_FIX=$(jq -r '[.vulnerabilities[]? | select(.fix_versions | length == 0) | .dependency.name] | unique | .[]' uv_audit_report.json)

echo "Upgrade: $UPGRADE"
echo "No fix available: $NO_FIX"

if [ -z "$UPGRADE" ]; then
echo "No packages to upgrade."
echo "changed=false" >> "$GITHUB_OUTPUT"
exit 0
fi

ARGS=()
for pkg in $UPGRADE; do
ARGS+=(--upgrade-package "$pkg")
done

uv lock "${ARGS[@]}" 2>&1 | tee uv_audit_upgrade.txt

if git diff --quiet -- uv.lock; then
echo "changed=false" >> "$GITHUB_OUTPUT"
else
echo "changed=true" >> "$GITHUB_OUTPUT"
fi

- name: Check for manual edits on automated branch
id: guard
if: steps.audit.outputs.changed == 'true'
run: |
# Fixed branch name: reused/force-pushed each run so we update one PR instead of piling up new ones.
BRANCH="automated/uv-audit-fix"
echo "BRANCH=$BRANCH" >> "$GITHUB_ENV"

if [ "${{ inputs.force }}" = "true" ] || [ "${{ inputs.rebase }}" = "true" ]; then
echo "Force/rebase requested; skipping the manual-edit check."
echo "skip=false" >> "$GITHUB_OUTPUT"
exit 0
fi

git fetch origin "$BRANCH" 2>/dev/null || true

if git rev-parse --verify "origin/$BRANCH" >/dev/null 2>&1; then
LAST_AUTHOR=$(git log -1 --format='%ae' "origin/$BRANCH")
if [ "$LAST_AUTHOR" != "github-actions[bot]@users.noreply.github.com" ]; then
echo "::warning::$BRANCH was manually edited (last commit by $LAST_AUTHOR); skipping automated update so the manual changes aren't overwritten. Merge or close the PR to resume automation."
echo "skip=true" >> "$GITHUB_OUTPUT"
exit 0
fi
fi

echo "skip=false" >> "$GITHUB_OUTPUT"

- name: Build PR body
if: steps.audit.outputs.changed == 'true' && steps.guard.outputs.skip != 'true'
run: |
{
echo "## uv audit — targeted dependency fixes"
if [ -f uv_audit_upgrade.txt ]; then
echo "### uv lock output"
echo '```'
cat uv_audit_upgrade.txt
echo '```'
fi
echo "### Commands"
echo "This PR is regenerated automatically and safe to edit — if you push a commit here, automation pauses so your changes aren't overwritten. Comment \`/uv-audit rebase\` to replay your edits on top of a refreshed fix, or \`/uv-audit recreate\` to discard your edits and let automation take over again."
} > pr_body.md

- name: Commit and push branch
if: steps.audit.outputs.changed == 'true' && steps.guard.outputs.skip != 'true' && inputs.rebase != true
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git checkout -B "$BRANCH"
git add uv.lock
git commit -m "deps(security): patch vulnerable packages found by uv audit"
git push --force origin "$BRANCH"

- name: Rebase manual edits onto refreshed fix
if: steps.audit.outputs.changed == 'true' && steps.guard.outputs.skip != 'true' && inputs.rebase == true
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"

# The default checkout is shallow; rebase needs the branch's full history.
git fetch --unshallow origin 2>/dev/null || git fetch origin
git fetch origin "$BRANCH"

git checkout -b uv-audit-fix-new
git add uv.lock
git commit -m "deps(security): patch vulnerable packages found by uv audit"
NEW_COMMIT=$(git rev-parse HEAD)

OLD_COMMIT=$(git log "origin/$BRANCH" --author='github-actions[bot]' -1 --format=%H)
if [ -z "$OLD_COMMIT" ]; then
echo "::error::No prior automated commit found on $BRANCH to rebase onto. Comment /uv-audit recreate instead."
exit 1
fi

git checkout -B "$BRANCH" "origin/$BRANCH"
if ! git rebase --onto "$NEW_COMMIT" "$OLD_COMMIT" "$BRANCH"; then
git rebase --abort
echo "::error::Rebase failed: your edits conflict with the refreshed fix. Resolve manually, or comment /uv-audit recreate to discard your edits."
exit 1
fi

git push --force origin "$BRANCH"

- name: Create pull request
if: steps.audit.outputs.changed == 'true' && steps.guard.outputs.skip != 'true'
env:
GH_TOKEN: ${{ secrets.token || secrets.GITHUB_TOKEN }}
run: |
PR_NUMBER=$(gh pr list --head "$BRANCH" --state open --json number --jq '.[0].number // empty')

if [ -n "$PR_NUMBER" ]; then
gh pr edit "$PR_NUMBER" \
--title "deps(security): uv audit targeted dependency fixes" \
--body-file pr_body.md
else
gh pr create \
--title "deps(security): uv audit targeted dependency fixes" \
--body-file pr_body.md \
--base "${{ github.ref_name }}" \
--head "$BRANCH" \
--label dependencies
fi
51 changes: 51 additions & 0 deletions .github/workflows/uv-audit-rebase.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# SPDX-FileCopyrightText: 2025 Contributors to the OpenSTEF project <openstef@lfenergy.org>
#
# SPDX-License-Identifier: MPL-2.0

name: uv Audit Rebase

on:
issue_comment:
types: [created]

permissions:
contents: write
pull-requests: write

jobs:
rebase:
name: Resume uv audit automation
# Only maintainers can trigger this, since it force-pushes and re-runs a job with write access.
if: >
github.event.issue.pull_request != null &&
contains(github.event.comment.body, '/uv-audit rebase') &&
contains(fromJSON('["OWNER", "MEMBER", "COLLABORATOR"]'), github.event.comment.author_association)
runs-on: ubuntu-latest
permissions:
pull-requests: read
issues: write
steps:
- name: Verify comment targets the automated branch
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
HEAD_REF=$(gh pr view "${{ github.event.issue.number }}" --repo "${{ github.repository }}" --json headRefName --jq .headRefName)
if [ "$HEAD_REF" != "automated/uv-audit-fix" ]; then
echo "::error::/uv-audit rebase only works on the automated uv-audit PR (head ref was '$HEAD_REF')."
exit 1
fi

- name: Acknowledge
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
gh api "repos/${{ github.repository }}/issues/comments/${{ github.event.comment.id }}/reactions" \
-f content='+1' --silent

uv-audit:
name: uv Audit
needs: rebase
uses: ./.github/workflows/_job_uv_audit.yaml
with:
rebase: true
secrets: inherit
51 changes: 51 additions & 0 deletions .github/workflows/uv-audit-recreate.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# SPDX-FileCopyrightText: 2025 Contributors to the OpenSTEF project <openstef@lfenergy.org>
#
# SPDX-License-Identifier: MPL-2.0

name: uv Audit Recreate

on:
issue_comment:
types: [created]

permissions:
contents: write
pull-requests: write

jobs:
recreate:
name: Resume uv audit automation
# Only maintainers can trigger this, since it force-pushes and re-runs a job with write access.
if: >
github.event.issue.pull_request != null &&
contains(github.event.comment.body, '/uv-audit recreate') &&
contains(fromJSON('["OWNER", "MEMBER", "COLLABORATOR"]'), github.event.comment.author_association)
runs-on: ubuntu-latest
permissions:
pull-requests: read
issues: write
steps:
- name: Verify comment targets the automated branch
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
HEAD_REF=$(gh pr view "${{ github.event.issue.number }}" --repo "${{ github.repository }}" --json headRefName --jq .headRefName)
if [ "$HEAD_REF" != "automated/uv-audit-fix" ]; then
echo "::error::/uv-audit recreate only works on the automated uv-audit PR (head ref was '$HEAD_REF')."
exit 1
fi

- name: Acknowledge
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
gh api "repos/${{ github.repository }}/issues/comments/${{ github.event.comment.id }}/reactions" \
-f content='+1' --silent

uv-audit:
name: uv Audit
needs: recreate
uses: ./.github/workflows/_job_uv_audit.yaml
with:
force: true
secrets: inherit
23 changes: 23 additions & 0 deletions .github/workflows/uv-audit.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# SPDX-FileCopyrightText: 2025 Contributors to the OpenSTEF project <openstef@lfenergy.org>
#
# SPDX-License-Identifier: MPL-2.0

name: uv Audit

on:
schedule:
- cron: '0 6 * * *' # Every day at 06:00 UTC
workflow_dispatch:
push: # TEMPORARY: lets us trigger real runs from this PR branch, since workflow_dispatch only works once merged to the default branch. Remove before merging.
branches:
- ci/uv-audit-workflow

permissions:
contents: write
pull-requests: write

jobs:
uv-audit:
name: uv Audit
uses: ./.github/workflows/_job_uv_audit.yaml
secrets: inherit
12 changes: 6 additions & 6 deletions uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading