diff --git a/.github/workflows/maint-sync-action-versions.yml b/.github/workflows/maint-sync-action-versions.yml new file mode 100644 index 000000000..ba8f392a1 --- /dev/null +++ b/.github/workflows/maint-sync-action-versions.yml @@ -0,0 +1,155 @@ +name: Maint Sync Action Versions + +# Sync GitHub Action versions from .github/workflows to templates +# after Dependabot merges action version updates. +# +# This ensures templates stay in sync with the latest action versions +# that Dependabot updates in the main workflows. + +on: + push: + branches: [main] + paths: + - '.github/workflows/*.yml' + workflow_dispatch: + +permissions: + contents: write + pull-requests: write + +jobs: + sync-versions: + name: Sync action versions to templates + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Extract action versions from workflows + id: extract + run: | + set -euo pipefail + + # Extract unique action versions from .github/workflows/ + declare -A versions + + for file in .github/workflows/*.yml; do + while IFS= read -r line; do + # Match 'uses: owner/action@version' + if [[ "$line" =~ uses:[[:space:]]*([^[:space:]]+)@(v[0-9]+) ]]; then + action="${BASH_REMATCH[1]}" + version="${BASH_REMATCH[2]}" + # Use numeric comparison for versions + if [[ -z "${versions[$action]:-}" ]]; then + versions["$action"]="$version" + else + new_num="${version#v}" + current_num="${versions[$action]#v}" + if (( new_num > current_num )); then + versions["$action"]="$version" + fi + fi + fi + done < "$file" + done + + # Output versions for key actions (grouped redirects for SC2129) + { + echo "checkout=${versions[actions/checkout]:-v4}" + echo "github_script=${versions[actions/github-script]:-v7}" + echo "upload_artifact=${versions[actions/upload-artifact]:-v4}" + echo "download_artifact=${versions[actions/download-artifact]:-v4}" + echo "cache=${versions[actions/cache]:-v4}" + } >> "$GITHUB_OUTPUT" + + echo "Detected versions:" + for action in "${!versions[@]}"; do + echo " $action: ${versions[$action]}" + done + + - name: Update templates with synced versions + id: update + run: | + set -euo pipefail + + checkout="${{ steps.extract.outputs.checkout }}" + github_script="${{ steps.extract.outputs.github_script }}" + upload_artifact="${{ steps.extract.outputs.upload_artifact }}" + download_artifact="${{ steps.extract.outputs.download_artifact }}" + cache="${{ steps.extract.outputs.cache }}" + + # Update all YAML files in templates/ (using find -exec for SC2044) + find templates/ -name "*.yml" -type f -exec sh -c ' + for file do + orig_hash=$(md5sum "$file" | cut -d" " -f1) + + sed -i \ + -e "s|actions/checkout@v[0-9]\+|actions/checkout@'"${checkout}"'|g" \ + -e "s|actions/github-script@v[0-9]\+|actions/github-script@'"${github_script}"'|g" \ + -e "s|actions/upload-artifact@v[0-9]\+|actions/upload-artifact@'"${upload_artifact}"'|g" \ + -e "s|actions/download-artifact@v[0-9]\+|actions/download-artifact@'"${download_artifact}"'|g" \ + -e "s|actions/cache@v[0-9]\+|actions/cache@'"${cache}"'|g" \ + "$file" + + new_hash=$(md5sum "$file" | cut -d" " -f1) + if [ "$orig_hash" != "$new_hash" ]; then + echo "Updated: $file" + fi + done + ' sh {} + + + - name: Check for changes + id: check + run: | + if git diff --quiet templates/; then + echo "has_changes=false" >> "$GITHUB_OUTPUT" + echo "No changes to templates" + else + echo "has_changes=true" >> "$GITHUB_OUTPUT" + echo "Changes detected:" + git diff --stat templates/ + fi + + - name: Create PR if changes exist + if: steps.check.outputs.has_changes == 'true' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + set -euo pipefail + + branch="auto/sync-action-versions-$(date +%Y%m%d%H%M%S)" + + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + + git checkout -b "$branch" + git add templates/ + git commit -m "ci(deps): sync action versions to templates + + Automated sync from .github/workflows/ to templates/ + + Updated versions: + - actions/checkout: ${{ steps.extract.outputs.checkout }} + - actions/github-script: ${{ steps.extract.outputs.github_script }} + - actions/upload-artifact: ${{ steps.extract.outputs.upload_artifact }} + - actions/download-artifact: ${{ steps.extract.outputs.download_artifact }} + - actions/cache: ${{ steps.extract.outputs.cache }}" + + git push origin "$branch" + + gh pr create \ + --title "ci(deps): sync action versions to templates" \ + --body "Automated PR to sync GitHub Action versions from \`.github/workflows/\` to \`templates/\`. + + This ensures templates stay in sync with Dependabot updates. + + **Updated versions:** + - actions/checkout: \`${{ steps.extract.outputs.checkout }}\` + - actions/github-script: \`${{ steps.extract.outputs.github_script }}\` + - actions/upload-artifact: \`${{ steps.extract.outputs.upload_artifact }}\` + - actions/download-artifact: \`${{ steps.extract.outputs.download_artifact }}\` + - actions/cache: \`${{ steps.extract.outputs.cache }}\`" \ + --label "dependencies" \ + --label "github-actions" diff --git a/.workflows-lib b/.workflows-lib index 5a7ab0492..19f6bd0af 160000 --- a/.workflows-lib +++ b/.workflows-lib @@ -1 +1 @@ -Subproject commit 5a7ab0492b27501f6ba46fcda452ab24207633d8 +Subproject commit 19f6bd0afb904487be5fd3b63497d1d84d14b082 diff --git a/docs/ci/WORKFLOWS.md b/docs/ci/WORKFLOWS.md index 6bfb1463c..86ade5179 100644 --- a/docs/ci/WORKFLOWS.md +++ b/docs/ci/WORKFLOWS.md @@ -98,6 +98,7 @@ The gate uses the shared `.github/scripts/detect-changes.js` helper to decide wh * [`reusable-20-pr-meta.yml`](../../.github/workflows/reusable-20-pr-meta.yml) detects keepalive round-marker comments in PRs, dispatches the orchestrator when detected, and manages PR body section updates for consumer repositories using the dual-checkout pattern. * [`maint-45-cosmetic-repair.yml`](../../.github/workflows/maint-45-cosmetic-repair.yml) invokes the reusable autofix pipeline on a schedule to keep cosmetic issues in check. * [`maint-47-disable-legacy-workflows.yml`](../../.github/workflows/maint-47-disable-legacy-workflows.yml) sweeps the repository to make sure archived GitHub workflows remain disabled in the Actions UI. +* [`maint-sync-action-versions.yml`](../../.github/workflows/maint-sync-action-versions.yml) syncs action version pins from `.github/workflows` into the workflow templates after Dependabot updates land. * [`maint-50-tool-version-check.yml`](../../.github/workflows/maint-50-tool-version-check.yml) checks PyPI weekly for new versions of CI/autofix tools (black, ruff, mypy, pytest) and creates an issue when updates are available. * [`maint-51-dependency-refresh.yml`](../../.github/workflows/maint-51-dependency-refresh.yml) regenerates `requirements.lock` using `uv pip compile`, validates tool-pin alignment, and opens a refresh pull request when dependency updates are detected (dry-run friendly). * [`maint-sync-env-from-pyproject.yml`](../../.github/workflows/maint-sync-env-from-pyproject.yml) syncs dev tool version pins from `pyproject.toml` to `autofix-versions.env` after Dependabot updates land. diff --git a/docs/ci/WORKFLOW_SYSTEM.md b/docs/ci/WORKFLOW_SYSTEM.md index 2dd0b5d1f..7bb96fb0e 100644 --- a/docs/ci/WORKFLOW_SYSTEM.md +++ b/docs/ci/WORKFLOW_SYSTEM.md @@ -511,6 +511,9 @@ Keep this table handy when you are triaging automation: it confirms which workfl - **Maint 47 Disable Legacy Workflows** – `.github/workflows/maint-47-disable-legacy-workflows.yml` runs on-demand and disables archived workflows still listed as active in the Actions UI. +- **Maint Sync Action Versions** – `.github/workflows/maint-sync-action-versions.yml` + syncs action version pins from `.github/workflows` into workflow templates after + Dependabot updates land. - **Maint 50 Tool Version Check** – `.github/workflows/maint-50-tool-version-check.yml` runs weekly (Mondays 8:00 AM UTC) to check PyPI for new versions of CI/autofix tools (black, ruff, mypy, pytest, etc.) and creates an issue when updates are available. diff --git a/scripts/sync_action_versions.sh b/scripts/sync_action_versions.sh new file mode 100755 index 000000000..31663011b --- /dev/null +++ b/scripts/sync_action_versions.sh @@ -0,0 +1,67 @@ +#!/usr/bin/env bash +# Sync GitHub Action versions from .github/workflows/ to templates/ +# Run this after Dependabot updates are merged to keep templates in sync. +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(dirname "$SCRIPT_DIR")" + +cd "$REPO_ROOT" + +# Extract versions from .github/workflows/ +declare -A versions + +echo "Extracting versions from .github/workflows/..." +for file in .github/workflows/*.yml; do + while IFS= read -r line; do + if [[ "$line" =~ uses:[[:space:]]*([^[:space:]]+)@(v[0-9]+) ]]; then + action="${BASH_REMATCH[1]}" + version="${BASH_REMATCH[2]}" + # Use numeric comparison for versions + if [[ -z "${versions[$action]:-}" ]]; then + versions["$action"]="$version" + else + new_num="${version#v}" + current_num="${versions[$action]#v}" + if (( new_num > current_num )); then + versions["$action"]="$version" + fi + fi + fi + done < "$file" +done + +echo "" +echo "Detected versions:" +for action in "${!versions[@]}"; do + echo " $action: ${versions[$action]}" +done + +checkout="${versions[actions/checkout]:-v4}" +github_script="${versions[actions/github-script]:-v7}" +upload_artifact="${versions[actions/upload-artifact]:-v4}" +download_artifact="${versions[actions/download-artifact]:-v4}" +cache="${versions[actions/cache]:-v4}" + +echo "" +echo "Updating templates/..." + +# Update templates +find templates/ -name "*.yml" -type f | while read -r file; do + orig=$(cat "$file") + + sed -i \ + -e "s|actions/checkout@v[0-9]\+|actions/checkout@${checkout}|g" \ + -e "s|actions/github-script@v[0-9]\+|actions/github-script@${github_script}|g" \ + -e "s|actions/upload-artifact@v[0-9]\+|actions/upload-artifact@${upload_artifact}|g" \ + -e "s|actions/download-artifact@v[0-9]\+|actions/download-artifact@${download_artifact}|g" \ + -e "s|actions/cache@v[0-9]\+|actions/cache@${cache}|g" \ + "$file" + + if [[ "$(cat "$file")" != "$orig" ]]; then + echo " Updated: $file" + fi +done + +echo "" +echo "Done. Run 'git diff templates/' to see changes." diff --git a/templates/consumer-repo/.github/workflows/agents-autofix-loop.yml b/templates/consumer-repo/.github/workflows/agents-autofix-loop.yml index 23a343272..e24cc312a 100644 --- a/templates/consumer-repo/.github/workflows/agents-autofix-loop.yml +++ b/templates/consumer-repo/.github/workflows/agents-autofix-loop.yml @@ -492,7 +492,7 @@ jobs: PY - name: Upload metrics artifact - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v6 with: name: agents-autofix-metrics path: autofix-metrics.ndjson diff --git a/templates/consumer-repo/.github/workflows/agents-keepalive-loop.yml b/templates/consumer-repo/.github/workflows/agents-keepalive-loop.yml index 6dcebe889..e63c93eac 100644 --- a/templates/consumer-repo/.github/workflows/agents-keepalive-loop.yml +++ b/templates/consumer-repo/.github/workflows/agents-keepalive-loop.yml @@ -419,7 +419,7 @@ jobs: echo "$metrics_json" >> keepalive-metrics.ndjson - name: Upload keepalive metrics artifact - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v6 with: name: keepalive-metrics path: keepalive-metrics.ndjson diff --git a/templates/consumer-repo/.github/workflows/agents-pr-meta.yml b/templates/consumer-repo/.github/workflows/agents-pr-meta.yml index 58300b0c5..cb4e17c23 100644 --- a/templates/consumer-repo/.github/workflows/agents-pr-meta.yml +++ b/templates/consumer-repo/.github/workflows/agents-pr-meta.yml @@ -59,7 +59,7 @@ jobs: steps: - name: Resolve PR context id: resolve - uses: actions/github-script@v8 + uses: actions/github-script@v7 with: script: | const pr = context.payload.issue; diff --git a/templates/consumer-repo/.github/workflows/autofix.yml b/templates/consumer-repo/.github/workflows/autofix.yml index 11850fff8..eb60edd47 100644 --- a/templates/consumer-repo/.github/workflows/autofix.yml +++ b/templates/consumer-repo/.github/workflows/autofix.yml @@ -49,7 +49,7 @@ jobs: steps: - name: Resolve PR context id: context - uses: actions/github-script@v8 + uses: actions/github-script@v7 with: script: | const pr = context.payload.pull_request; diff --git a/templates/consumer-repo/.github/workflows/maint-coverage-guard.yml b/templates/consumer-repo/.github/workflows/maint-coverage-guard.yml index bb033bc03..a993ec038 100644 --- a/templates/consumer-repo/.github/workflows/maint-coverage-guard.yml +++ b/templates/consumer-repo/.github/workflows/maint-coverage-guard.yml @@ -79,7 +79,7 @@ jobs: - name: Download coverage trend artifact if: ${{ steps.discover.outputs.run_id }} - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v7 continue-on-error: true with: name: gate-coverage-trend @@ -89,7 +89,7 @@ jobs: - name: Download coverage artifact if: ${{ steps.discover.outputs.run_id }} - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v7 continue-on-error: true with: pattern: gate-coverage-* diff --git a/tests/workflows/fixtures/keepalive/harness.js b/tests/workflows/fixtures/keepalive/harness.js index 076cd8a66..e11e98d11 100755 --- a/tests/workflows/fixtures/keepalive/harness.js +++ b/tests/workflows/fixtures/keepalive/harness.js @@ -343,7 +343,6 @@ async function runScenario(scenario) { }, }; - const originalEnv = {}; const envOverrides = { ACTIONS_BOT_PAT: 'dummy-token', SERVICE_BOT_PAT: 'service-token', @@ -352,9 +351,19 @@ async function runScenario(scenario) { actions_bot_pat: '', ...(scenario.env || {}), }; + const tokenKeys = new Set([ + 'ACTIONS_BOT_PAT', + 'SERVICE_BOT_PAT', + 'GH_TOKEN', + 'gh_token', + 'actions_bot_pat', + ]); + const env = {}; for (const [key, value] of Object.entries(envOverrides)) { - originalEnv[key] = process.env[key]; - process.env[key] = String(value); + if (value === '' && tokenKeys.has(key)) { + continue; + } + env[key] = String(value); } const originalNow = Date.now; @@ -365,16 +374,9 @@ async function runScenario(scenario) { try { const { runKeepalive } = loadKeepaliveRunner(); - await runKeepalive({ core, github, context, env: process.env }); + await runKeepalive({ core, github, context, env }); } finally { Date.now = originalNow; - for (const [key, value] of Object.entries(originalEnv)) { - if (value === undefined) { - delete process.env[key]; - } else { - process.env[key] = value; - } - } } return { diff --git a/tests/workflows/test_workflow_naming.py b/tests/workflows/test_workflow_naming.py index dc13091dc..b5bb2c4e4 100644 --- a/tests/workflows/test_workflow_naming.py +++ b/tests/workflows/test_workflow_naming.py @@ -193,6 +193,7 @@ def test_workflow_display_names_are_unique(): "maint-47-disable-legacy-workflows.yml": "Maint 47 Disable Legacy Workflows", "maint-50-tool-version-check.yml": "Maint 50 Tool Version Check", "maint-51-dependency-refresh.yml": "Maint 51 Dependency Refresh", + "maint-sync-action-versions.yml": "Maint Sync Action Versions", "maint-sync-env-from-pyproject.yml": "Maint - Sync versions.env from pyproject.toml", "maint-52-validate-workflows.yml": "Maint 52 Validate Workflows", "maint-52-sync-dev-versions.yml": "Maint 52 Sync Dev Versions",