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
75 changes: 4 additions & 71 deletions .github/workflows/maint-50-tool-version-check.yml
Original file line number Diff line number Diff line change
@@ -1,21 +1,16 @@
name: Maint 50 Tool Version Check

# Check for new versions of CI/autofix tools and create an issue if updates are available
# Runs weekly on Mondays at 8:00 AM UTC
# Read-only freshness evidence for CI/autofix tools.
# Source proposals belong to maint-auto-update-pypi-versions.yml, which owns
# the weekly batch window and any explicit security override.

on:
schedule:
- cron: '0 8 * * 1' # Weekly on Mondays
workflow_dispatch:
inputs:
force_issue:
description: 'Force issue creation even if versions are current'
required: false
default: 'false'

permissions:
contents: read
issues: write

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
Expand Down Expand Up @@ -85,71 +80,9 @@ jobs:
run: |
python scripts/maint_tool_version_check.py compare

- name: Create or update issue
if: steps.compare.outputs.has_updates == 'true' || github.event.inputs.force_issue == 'true'
uses: actions/github-script@v9
with:
github-token: ${{ github.token }}
script: |
const fs = require('fs');
const retryHelperPath = './.github/scripts/github-api-with-retry.js';
const retryHelpers = fs.existsSync(retryHelperPath)
? require(retryHelperPath)
: {
withRetry: (fn) => fn(),
paginateWithRetry: (githubInstance, method, params) =>
githubInstance.paginate(method, params),
};
const { withRetry } = retryHelpers;
const body = fs.readFileSync('tool-version-issue-body.md', 'utf8');

const title = '🔧 CI/Autofix Tool Updates Available';

// Search for existing open issues with this title
const issues = await withRetry(() =>
github.rest.issues.listForRepo({
owner: context.repo.owner,
repo: context.repo.repo,
state: 'open',
labels: 'maintenance,dependencies',
per_page: 100,
})
);

const existingIssue = issues.data.find(issue => issue.title === title);

if (existingIssue) {
// Update existing issue
await withRetry(() => github.rest.issues.update({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: existingIssue.number,
body: body
}));

await withRetry(() => github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: existingIssue.number,
body: `🔄 Updated with latest version check results.`
}));

core.notice(`Updated existing issue #${existingIssue.number}`);
} else {
// Create new issue
const issue = await withRetry(() => github.rest.issues.create({
owner: context.repo.owner,
repo: context.repo.repo,
title: title,
body: body,
labels: ['maintenance', 'dependencies']
}));

core.notice(`Created issue #${issue.data.number}`);
}

- name: Summary
run: |
echo "Source proposals belong to maint-auto-update-pypi-versions.yml." >> "$GITHUB_STEP_SUMMARY"
cat <<'EOF' >> "$GITHUB_STEP_SUMMARY"
${{ steps.compare.outputs.report }}
EOF
22 changes: 18 additions & 4 deletions .github/workflows/maint-52-sync-dev-versions.yml
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ jobs:
outputs:
repos: ${{ steps.repos.outputs.matrix }}
versions_hash: ${{ steps.hash.outputs.hash }}
canonical_source_sha: ${{ steps.source.outputs.sha }}
steps:
- name: Checkout Workflows
uses: actions/checkout@v7
Expand Down Expand Up @@ -104,6 +105,11 @@ jobs:
echo "Current versions:"
grep -v '^#' .github/workflows/autofix-versions.env | grep '='

- name: Record settled canonical source commit
id: source
run: |
echo "sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"

- name: Build repo matrix
id: repos
run: |
Expand Down Expand Up @@ -275,6 +281,7 @@ jobs:
DELIVERY_PLAN_ID: dev-tool-${{ needs.prepare.outputs.versions_hash }}
DELIVERY_GENERATION: ${{ needs.prepare.outputs.versions_hash }}
DELIVERY_REPOSITORY: ${{ matrix.repo }}
CANONICAL_SOURCE_SHA: ${{ needs.prepare.outputs.canonical_source_sha }}
run: |
cd consumer

Expand Down Expand Up @@ -349,7 +356,7 @@ jobs:
--arg generation "$DELIVERY_GENERATION" \
--arg repository "$DELIVERY_REPOSITORY" \
--arg desired_tree_hash "$desired_tree_hash" \
--arg source_commit "$GITHUB_SHA" \
--arg source_commit "$CANONICAL_SOURCE_SHA" \
--arg lease_expires_at "$lease_expires_at" \
'{schema:$schema,durable_issue_url:$durable_issue_url,plan_id:$plan_id,generation:$generation,repository:$repository,desired_tree_hash:$desired_tree_hash,source_commit:$source_commit,lease_expires_at:$lease_expires_at,predecessor_prs:[],successor_prs:[]}')
pr_body="## Dev Tool Version Sync
Expand All @@ -369,6 +376,7 @@ jobs:

---
**Source:** \`.github/workflows/autofix-versions.env\`
**Settled source commit:** \`$CANONICAL_SOURCE_SHA\`

<!-- sync-pr-delivery-record:v1 $delivery_marker -->"

Expand Down Expand Up @@ -406,15 +414,21 @@ jobs:
if: always()
steps:
- name: Generate summary
env:
VERSIONS_HASH: ${{ needs.prepare.outputs.versions_hash }}
CANONICAL_SOURCE_SHA: ${{ needs.prepare.outputs.canonical_source_sha }}
DRY_RUN: ${{ inputs.dry_run || 'false' }}
REPOS_JSON: ${{ needs.prepare.outputs.repos }}
run: |
{
echo "## Dev Version Sync Summary"
echo ""
echo "**Versions Hash:** ${{ needs.prepare.outputs.versions_hash }}"
echo "**Dry Run:** ${{ inputs.dry_run || 'false' }}"
echo "**Versions Hash:** $VERSIONS_HASH"
echo "**Settled source commit:** $CANONICAL_SOURCE_SHA"
echo "**Dry Run:** $DRY_RUN"
echo ""
echo "### Repos Processed"
echo '${{ needs.prepare.outputs.repos }}' | jq -r '.repo[]' | while read -r repo; do
printf '%s\n' "$REPOS_JSON" | jq -r '.repo[]' | while read -r repo; do
echo "- $repo"
done
} >> "$GITHUB_STEP_SUMMARY"
80 changes: 63 additions & 17 deletions .github/workflows/maint-auto-update-pypi-versions.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Auto-update dev tool versions from PyPI
#
# This workflow ensures autofix-versions.env stays current with PyPI releases.
# It runs daily and creates a PR if any versions are outdated.
# Routine proposals run once each Monday and create at most one source PR.
#
# CRITICAL: This workflow MUST run before maint-52-sync-dev-versions.yml
# to ensure we never ship stale versions to consumer repos.
Expand All @@ -10,14 +10,18 @@ name: Maint Auto-Update PyPI Versions

on:
schedule:
# Daily at 03:00 UTC (before the weekly sync at 05:00)
- cron: '0 3 * * *'
# Monday 03:00 UTC, before the consumer propagation window.
- cron: '0 3 * * 1'
Comment thread
stranske marked this conversation as resolved.
workflow_dispatch:
inputs:
dry_run:
description: 'Preview changes without creating PR'
type: boolean
default: false
security_override:
description: 'Allow a reviewed security update outside the routine weekly window'
type: boolean
default: false

permissions:
contents: write
Expand All @@ -42,6 +46,19 @@ jobs:
with:
python-version: '3.14'

- name: Enforce the canonical source proposal policy
id: policy
env:
SECURITY_OVERRIDE: ${{ inputs.security_override }}
run: |
set -euo pipefail
args=()
if [ "$SECURITY_OVERRIDE" = "true" ]; then
args+=(--security-override)
fi
python scripts/dev_tool_update_policy.py "${args[@]}" | tee /tmp/source_policy.txt
cat /tmp/source_policy.txt >> "$GITHUB_OUTPUT"

Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
- name: Check for outdated versions
id: check
run: |
Expand All @@ -58,7 +75,10 @@ jobs:

- name: Update versions
id: update
if: steps.check.outputs.has_updates == 'true' && inputs.dry_run != true
if: >-
steps.check.outputs.has_updates == 'true'
&& steps.policy.outputs.should_propose == 'true'
&& inputs.dry_run != true
run: |
echo "📦 Updating autofix-versions.env with latest PyPI versions..."
python scripts/update_versions_from_pypi.py --apply 2>&1 | tee /tmp/update_output.txt
Expand All @@ -71,14 +91,20 @@ jobs:
} >> "$GITHUB_OUTPUT"

- name: Sync pyproject, lockfile, and templates
if: steps.check.outputs.has_updates == 'true' && inputs.dry_run != true
if: >-
steps.check.outputs.has_updates == 'true'
&& steps.policy.outputs.should_propose == 'true'
&& inputs.dry_run != true
run: |
echo "🔄 Syncing pyproject.toml + requirements.lock + templates"
python scripts/sync_tool_versions.py --apply
python scripts/sync_dev_dependencies.py --apply --lockfile

- name: Create PR
if: steps.check.outputs.has_updates == 'true' && inputs.dry_run != true
if: >-
steps.check.outputs.has_updates == 'true'
&& steps.policy.outputs.should_propose == 'true'
&& inputs.dry_run != true
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
Expand All @@ -97,9 +123,19 @@ jobs:
exit 0
fi

# Create branch
branch="auto/update-pypi-versions-$(date +%Y%m%d)"
git checkout -b "$branch"
# One mutable source PR per weekly window. A security override uses
# the same branch if the routine window already has a proposal.
branch="auto/weekly-dev-tool-update-$(date +%G-W%V)"
existing_pr=$(gh pr list --head "$branch" --state open --json number -q '.[0].number' || true)
# A prior run can leave the weekly branch behind without an open PR.
# Capture its remote tip when present so the force-with-lease below
# is both retry-safe and able to refresh that branch.
git fetch origin "$branch:refs/remotes/origin/$branch" 2>/dev/null || true
expected_sha=""
if git show-ref --verify --quiet "refs/remotes/origin/$branch"; then
expected_sha=$(git rev-parse "refs/remotes/origin/$branch")
fi
git checkout -B "$branch"
Comment thread
stranske marked this conversation as resolved.

# Commit changes
git add \
Expand All @@ -114,12 +150,14 @@ jobs:

${{ steps.update.outputs.summary }}"

# Push and create PR
git push origin "$branch"
# Push and create or refresh the one source PR for this window.
if [ -n "$expected_sha" ]; then
git push --force-with-lease="refs/heads/$branch:$expected_sha" -u origin "$branch"
else
git push --force-with-lease -u origin "$branch"
fi

gh pr create \
--title "chore: update dev tool versions from PyPI" \
--body "## Summary
pr_body="## Summary

This PR updates the pinned dev tool versions in \`autofix-versions.env\`.
It matches the latest releases on PyPI.
Expand All @@ -136,9 +174,17 @@ jobs:
- Version drift between repos is minimized

---
*Auto-generated by the maint-auto-update-pypi-versions workflow*" \
--label "dependencies" \
--label "automation"
*Auto-generated by the maint-auto-update-pypi-versions workflow*"

if [ -n "$existing_pr" ]; then
gh pr edit "$existing_pr" --title "chore: update dev tool versions from PyPI" --body "$pr_body"
else
gh pr create \
--title "chore: update dev tool versions from PyPI" \
--body "$pr_body" \
--label "dependencies" \
--label "automation"
fi
Comment thread
stranske marked this conversation as resolved.

- name: Dry run summary
if: inputs.dry_run == true
Expand Down
4 changes: 2 additions & 2 deletions docs/WORKFLOW_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ _Inline Gate helper_
- **`maint-45-cosmetic-repair.yml`** — Manual dispatch utility that runs `pytest -q`, applies guard-gated cosmetic fixes via `scripts/ci_cosmetic_repair.py`, and (when not in dry-run mode) opens a labelled PR with the default workflow token—no extra GitHub App mint required.
- **`maint-46-post-ci.yml`** — Post-CI recovery watcher triggered by `workflow_run` on Gate completion. It inspects the Gate summary job before touching the repo, and only checks out helpers / installs the token-balanced API client when the summary leg actually failed, keeping the default token pool free unless recovery is required.
- **`maint-47-disable-legacy-workflows.yml`** — Manual dispatch utility to disable retired workflows that still appear in the Actions UI (with a dry-run preview + allowlist overrides); now relies solely on the default workflow token because the helper script never leaves the repository.
- **`maint-50-tool-version-check.yml`** — Weekly/manual tool-version audit that reads `autofix-versions.env`, hits PyPI to detect drifts, and files/refreshes the maintenance issue via the default token + load-balanced helper (no extra App mint).
- **`maint-50-tool-version-check.yml`** — Weekly/manual read-only tool-version audit that reports PyPI freshness for the canonical pin set; it never opens or comments on a competing update issue or PR.
- **`maint-52-sync-dev-versions.yml`** — Fans out to each registered consumer repo (or a supplied subset), reports `autofix-versions.env` freshness for visibility, then exact-pins matching `pyproject.toml` entries and updates direct pins in `requirements.lock` and `requirements-dev.lock` when present; its wave ID covers both the pin set and propagation script so implementation repairs create replacement PRs. It uses the PAT provided via `REPO_TOKEN`, reuses `scripts/list_registered_consumer_repos.py`, and avoids redundant GitHub App token mints.
- **`maint-52-validate-workflows.yml`** — PR/push workflow that dry-parses every workflow file with `yq`, runs actionlint with the repo allowlist, and caches both binaries; no extra GitHub App token is minted because the job never leaves the repository.
- **`maint-60-release.yml`** — Tag-triggered release workflow that publishes notes with `softprops/action-gh-release` when a `v*` tag is pushed; only the default workflow token is needed, so no extra App mint runs. (Retains a legacy floating-`v1` tag step for any `v1.*` push, but consumers ride `@main` — the single supported pin — so the floating tag is no longer part of normal operation.)
Expand All @@ -67,7 +67,7 @@ _Inline Gate helper_
- **`maint-71-merge-sync-prs.yml`** — Scans each registered consumer repo for open `sync/workflows-*` PRs, closes stale duplicates, deletes leftover same-repo sync branches tied to closed/merged sync PRs, and (optionally) auto-merges passing PRs using the shared repo helper + PATs. It emits plan-bound canary evidence with the PR/check/review state for Maint 68 promotion. PRs carrying runtime-AC labels are not merged by this external lane; they must pass through the local Orchestrator runtime AC guard.
- **`maint-72-fix-pr-body-conflicts.yml`** — Periodically removes stray `pr_body.md` files from consumer repos and ensures `.gitignore` blocks them, reusing the registered-repo helper + PAT discovery so cleanups only run when push access is available.
- **`maint-74-ledger-base-sync.yml`** — Keeps `.agents` ledger base entries aligned with the repo’s default branch by running `scripts/ledger_migrate_base.py` and opening a helper PR (no extra App token mint needed).
- **`maint-auto-update-pypi-versions.yml`** — Daily PyPI watcher that updates `autofix-versions.env`, regenerates supporting files, and opens a PR when new tool versions land (runs entirely with the default token + GH CLI).
- **`maint-auto-update-pypi-versions.yml`** — Monday 03:00 UTC canonical source lane that batches routine PyPI pin updates into one mutable source PR; an explicit security override may run outside that window.
- **`maint-coverage-guard.yml`** — Scheduled coverage baseline monitor with a rate-limit gate that now relies solely on the shared API client + default token.
- **`maint-coverage-guard.yml`** — Daily cron + dispatch workflow that monitors Gate coverage artifacts and maintains the rolling coverage baseline breach issue.
- **`health-40-sweep.yml`** — Weekly sweep that fans out to Actionlint and branch-protection verification. Pull requests trigger the Actionlint leg (paths-filter gated) while schedule/manual runs execute both checks to keep the enforcement snapshots fresh. Manual dispatchers can now pass `run_branch_protection=false` to skip the API-heavy branch guard when they only need the workflow lint pass.
Expand Down
Loading
Loading