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
155 changes: 155 additions & 0 deletions .github/workflows/maint-sync-action-versions.yml
Original file line number Diff line number Diff line change
@@ -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"
2 changes: 1 addition & 1 deletion .workflows-lib
Submodule .workflows-lib updated from 5a7ab0 to 19f6bd
1 change: 1 addition & 0 deletions docs/ci/WORKFLOWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
3 changes: 3 additions & 0 deletions docs/ci/WORKFLOW_SYSTEM.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
67 changes: 67 additions & 0 deletions scripts/sync_action_versions.sh
Original file line number Diff line number Diff line change
@@ -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."
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion templates/consumer-repo/.github/workflows/autofix.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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-*
Expand Down
24 changes: 13 additions & 11 deletions tests/workflows/fixtures/keepalive/harness.js
Original file line number Diff line number Diff line change
Expand Up @@ -343,7 +343,6 @@ async function runScenario(scenario) {
},
};

const originalEnv = {};
const envOverrides = {
ACTIONS_BOT_PAT: 'dummy-token',
SERVICE_BOT_PAT: 'service-token',
Expand All @@ -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;
Expand All @@ -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 {
Expand Down
1 change: 1 addition & 0 deletions tests/workflows/test_workflow_naming.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Loading