-
Notifications
You must be signed in to change notification settings - Fork 353
Add automated per-plugin versioning (NBGV) with /version-bump + weekly backstop #813
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
Merged
Merged
Changes from 8 commits
Commits
Show all changes
10 commits
Select commit
Hold shift + click to select a range
462cf7d
Add automated per-plugin versioning (NBGV) with /version-bump + weekl…
AbhitejJohn 8c2b8c9
Merge branch 'main' into abhitejjohn/per-plugin-version-strategy
AbhitejJohn 766115f
Address Copilot review feedback
AbhitejJohn 22133dc
Harden version automation per maintainer review
AbhitejJohn 259c663
Document that manifest-only edits don't bump the version
AbhitejJohn 284695d
Merge branch 'main' into abhitejjohn/per-plugin-version-strategy
AbhitejJohn ebd957e
Align plugin version bases after main merge; harden enumeration
AbhitejJohn ebfc958
Address Copilot re-review: unstage overlay, non-fatal cleanup, codex …
AbhitejJohn 65d79ce
Address Copilot re-review: stamp optional .claude-plugin manifest, ha…
AbhitejJohn 068eaa2
Address Copilot re-review: stage optional .claude-plugin manifest in …
AbhitejJohn File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| { | ||
| "version": 1, | ||
| "isRoot": true, | ||
| "tools": { | ||
| "nbgv": { | ||
| "version": "3.10.85", | ||
| "commands": [ | ||
| "nbgv" | ||
| ], | ||
| "rollForward": false | ||
| } | ||
|
AbhitejJohn marked this conversation as resolved.
|
||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,193 @@ | ||
| # Admin-triggered version stamping. A maintainer comments `/version-bump` on a | ||
| # PR; this computes each changed plugin's post-merge version with NBGV and writes | ||
| # it into both manifests on the PR branch, then pushes the commit. | ||
| # | ||
| # Scope: same-repo (in-repo) branches only — the workflow cannot push to a fork's | ||
| # branch. Fork PRs are deferred to the weekly backstop (weekly-version-sync.yml). | ||
| # | ||
| # NOTE: issue_comment always runs the workflow YAML from the default branch, so | ||
| # edits here only take effect once merged to main. | ||
| name: version-bump-command | ||
|
|
||
| on: | ||
| issue_comment: | ||
| types: [created] | ||
|
|
||
| concurrency: | ||
| group: version-bump-${{ github.event.issue.number }} | ||
| cancel-in-progress: false | ||
|
|
||
| permissions: | ||
| contents: write | ||
| pull-requests: write | ||
| issues: write | ||
|
|
||
| jobs: | ||
| bump: | ||
| if: >- | ||
| github.event.issue.pull_request && | ||
| startsWith(github.event.comment.body, '/version-bump') | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| - name: Check actor permissions | ||
| id: perms | ||
| env: | ||
| GH_TOKEN: ${{ github.token }} | ||
| ACTOR: ${{ github.event.comment.user.login }} | ||
| run: | | ||
| PERMISSION=$(gh api "repos/${{ github.repository }}/collaborators/${ACTOR}/permission" --jq '.permission') | ||
| echo "Actor ${ACTOR} has permission: $PERMISSION" | ||
| if [[ "$PERMISSION" != "admin" && "$PERMISSION" != "write" && "$PERMISSION" != "maintain" ]]; then | ||
| echo "::error::/version-bump requires write access" | ||
| gh api --method POST "repos/${{ github.repository }}/issues/${{ github.event.issue.number }}/comments" \ | ||
| -f body="@${ACTOR} \`/version-bump\` requires write access to this repository." >/dev/null | ||
| exit 1 | ||
| fi | ||
|
AbhitejJohn marked this conversation as resolved.
Outdated
|
||
|
|
||
| - name: Resolve PR | ||
| id: pr | ||
| env: | ||
| GH_TOKEN: ${{ github.token }} | ||
| run: | | ||
| PR=$(gh api "repos/${{ github.repository }}/pulls/${{ github.event.issue.number }}") | ||
| HEAD_REPO=$(echo "$PR" | jq -r '.head.repo.full_name') | ||
| BASE_REPO=$(echo "$PR" | jq -r '.base.repo.full_name') | ||
| HEAD_REF=$(echo "$PR" | jq -r '.head.ref') | ||
| HEAD_SHA=$(echo "$PR" | jq -r '.head.sha') | ||
| BASE_SHA=$(echo "$PR" | jq -r '.base.sha') | ||
| { | ||
| echo "head_ref=$HEAD_REF" | ||
| echo "head_sha=$HEAD_SHA" | ||
| echo "base_sha=$BASE_SHA" | ||
| } >> "$GITHUB_OUTPUT" | ||
| if [[ "$HEAD_REPO" != "$BASE_REPO" ]]; then | ||
| echo "is_fork=true" >> "$GITHUB_OUTPUT" | ||
| else | ||
| echo "is_fork=false" >> "$GITHUB_OUTPUT" | ||
| fi | ||
|
|
||
| - name: Reject fork PRs | ||
| if: steps.pr.outputs.is_fork == 'true' | ||
| env: | ||
| GH_TOKEN: ${{ github.token }} | ||
| run: | | ||
| gh api --method POST "repos/${{ github.repository }}/issues/${{ github.event.issue.number }}/comments" \ | ||
| -f body="\`/version-bump\` only works on branches in this repository. This is a fork PR — the weekly version sync will stamp it after merge." >/dev/null | ||
| echo "Fork PR — skipping." | ||
|
|
||
| - name: Checkout PR branch | ||
| if: steps.pr.outputs.is_fork == 'false' | ||
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 | ||
| with: | ||
| # Pin to the exact head SHA that was authorized, not the mutable branch | ||
| # name — a race-push after the maintainer comments cannot change what runs. | ||
| ref: ${{ steps.pr.outputs.head_sha }} | ||
| fetch-depth: 0 | ||
| persist-credentials: true | ||
|
|
||
| # SECURITY: this job runs in the privileged default-branch context with a | ||
| # contents:write token. Never execute tooling that the PR author controls. | ||
| # Overlay the version tooling, the tool manifest, the SDK pin, and the locked | ||
| # NuGet config from main so only trusted code runs and tools can only be | ||
| # restored from nuget.org; the PR's plugin *content* is consumed as data. | ||
| # These overlays are reverted before the commit step so the PR's own copies | ||
| # of these files are never modified. | ||
| - name: Pin trusted tooling from main | ||
| if: steps.pr.outputs.is_fork == 'false' | ||
| run: | | ||
| # NOTE: do NOT pass --depth here. A shallow fetch writes .git/shallow and flips the | ||
| # WHOLE repo to shallow (even though checkout used fetch-depth: 0), and NBGV refuses to | ||
| # compute version height on a shallow repo. fetch-depth: 0 already fetched main, so this | ||
| # full-depth fetch is cheap and keeps the repo non-shallow. | ||
| git fetch --no-tags origin main | ||
| git checkout FETCH_HEAD -- \ | ||
| eng/version/Sync-PluginVersions.ps1 \ | ||
| eng/version/nuget.config \ | ||
| .config/dotnet-tools.json \ | ||
| global.json | ||
|
AbhitejJohn marked this conversation as resolved.
|
||
| # `git checkout <ref> -- <paths>` writes the overlay into the index as well as the working | ||
| # tree, i.e. it STAGES these files. Unstage them immediately so the index only ever holds | ||
| # HEAD's copies: the tooling still runs from the overlaid working tree, but a later commit | ||
| # can never include the main-overlaid tooling even if the cleanup step below is skipped or | ||
| # fails. This is the actual guarantee that trusted tooling isn't committed to the PR branch. | ||
| git reset -q HEAD -- \ | ||
| eng/version/Sync-PluginVersions.ps1 \ | ||
| eng/version/nuget.config \ | ||
| .config/dotnet-tools.json \ | ||
| global.json | ||
|
|
||
| - name: Setup .NET SDK | ||
| if: steps.pr.outputs.is_fork == 'false' | ||
| uses: actions/setup-dotnet@c2fa09f4bde5ebb9d1777cf28262a3eb3db3ced7 # v5 | ||
| with: | ||
| # nbgv targets net8.0; global.json pins an SDK that ships only a preview runtime, onto | ||
| # which a release-targeted tool will not roll forward. Install an 8.0.x runtime so nbgv | ||
| # has a runtime to execute on, alongside the global.json SDK. | ||
| dotnet-version: '8.0.x' | ||
| global-json-file: global.json | ||
|
|
||
| - name: Restore tools | ||
| if: steps.pr.outputs.is_fork == 'false' | ||
| run: dotnet tool restore --configfile eng/version/nuget.config | ||
|
|
||
| - name: Stamp plugin versions | ||
| if: steps.pr.outputs.is_fork == 'false' | ||
| id: stamp | ||
| shell: pwsh | ||
| env: | ||
| BASE_SHA: ${{ steps.pr.outputs.base_sha }} | ||
| run: | | ||
| $head = git rev-parse HEAD | ||
| $mergeBase = git merge-base $env:BASE_SHA $head | ||
|
AbhitejJohn marked this conversation as resolved.
|
||
| if (-not $mergeBase) { | ||
| throw "Could not determine a merge base between base ($env:BASE_SHA) and head ($head). Ensure main is fully fetched (non-shallow)." | ||
| } | ||
|
|
||
| $report = & "$PWD/eng/version/Sync-PluginVersions.ps1" ` | ||
| -BaseCommit $mergeBase -HeadCommit $head -PredictSquashMerge -OnlyChanged -Write | | ||
| ConvertFrom-Json | ||
|
|
||
| if (-not $report -or $report.Count -eq 0) { | ||
| "stamped=false" >> $env:GITHUB_OUTPUT | ||
| "summary=No plugin content needs a version bump in this PR." >> $env:GITHUB_OUTPUT | ||
| exit 0 | ||
| } | ||
|
|
||
| $rows = ($report | ForEach-Object { "- ``$($_.plugin)``: $($_.current) → **$($_.computed)**" }) -join "`n" | ||
| "stamped=true" >> $env:GITHUB_OUTPUT | ||
| # Multi-line outputs need a heredoc-style delimiter. | ||
| $delim = "EOF_SUMMARY_$(Get-Random)" | ||
| "summary<<$delim" >> $env:GITHUB_OUTPUT | ||
| "Stamped plugin versions:`n$rows" >> $env:GITHUB_OUTPUT | ||
| "$delim" >> $env:GITHUB_OUTPUT | ||
|
|
||
| - name: Commit and push | ||
| if: steps.pr.outputs.is_fork == 'false' && steps.stamp.outputs.stamped == 'true' | ||
| env: | ||
| HEAD_REF: ${{ steps.pr.outputs.head_ref }} | ||
| run: | | ||
| git config user.name "github-actions[bot]" | ||
| git config user.email "41898282+github-actions[bot]@users.noreply.github.com" | ||
| # Worktree hygiene: restore HEAD's copies of the overlaid tooling. The overlay was already | ||
| # unstaged when it was checked out, so this is NOT what prevents a leak (the index is already | ||
| # clean); it just tidies the working tree. It is non-fatal (`|| true`) so an older PR branch | ||
| # that predates some of these files (e.g. has no global.json in its head commit) doesn't fail | ||
| # the whole command on a "pathspec did not match" error. | ||
| git checkout HEAD -- \ | ||
| eng/version/Sync-PluginVersions.ps1 \ | ||
| eng/version/nuget.config \ | ||
| .config/dotnet-tools.json \ | ||
| global.json || true | ||
| git add plugins/*/plugin.json plugins/*/.codex-plugin/plugin.json | ||
| git commit -m "Stamp plugin versions via /version-bump" | ||
| git push origin "HEAD:${HEAD_REF}" | ||
|
|
||
| - name: Comment result | ||
| if: steps.pr.outputs.is_fork == 'false' && always() | ||
| env: | ||
| GH_TOKEN: ${{ github.token }} | ||
| SUMMARY: ${{ steps.stamp.outputs.summary }} | ||
| run: | | ||
| if [[ -z "$SUMMARY" ]]; then SUMMARY="Could not compute plugin versions — see the workflow run for details."; fi | ||
| gh api --method POST "repos/${{ github.repository }}/issues/${{ github.event.issue.number }}/comments" \ | ||
| -f body="$SUMMARY" >/dev/null | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,118 @@ | ||
| # Weekly backstop. Computes every plugin's authoritative version on `main` with | ||
| # NBGV and, for any plugin whose content changed without a matching version bump, | ||
| # opens ONE pull request that stamps the correct versions and explains why. | ||
| # | ||
| # This is the safety net that makes per-PR bumping optional: anything a | ||
| # contributor or the /version-bump command missed is reconciled here. Running on | ||
| # main (post-merge) is the single source of truth, so the computed heights can | ||
| # never collide across PRs. | ||
| name: weekly-version-sync | ||
|
|
||
| on: | ||
| schedule: | ||
| - cron: '0 9 * * 1' # Mondays 09:00 UTC | ||
| workflow_dispatch: | ||
|
|
||
| concurrency: | ||
| group: weekly-version-sync | ||
| cancel-in-progress: false | ||
|
|
||
| permissions: | ||
| contents: write | ||
| pull-requests: write | ||
|
|
||
| jobs: | ||
| sync: | ||
| # Never run on forks (avoids burning a fork owner's Actions minutes). | ||
| if: github.repository == 'dotnet/skills' | ||
| runs-on: ubuntu-latest | ||
| env: | ||
| SYNC_BRANCH: bot/weekly-version-sync | ||
| steps: | ||
| - name: Checkout main | ||
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 | ||
| with: | ||
| fetch-depth: 0 | ||
| persist-credentials: true | ||
|
|
||
| - name: Setup .NET SDK | ||
| uses: actions/setup-dotnet@c2fa09f4bde5ebb9d1777cf28262a3eb3db3ced7 # v5 | ||
| with: | ||
| # nbgv targets net8.0; global.json pins an SDK that ships only a preview runtime, onto | ||
| # which a release-targeted tool will not roll forward. Install an 8.0.x runtime so nbgv | ||
| # has a runtime to execute on, alongside the global.json SDK. | ||
| dotnet-version: '8.0.x' | ||
| global-json-file: global.json | ||
|
|
||
| - name: Restore tools | ||
| run: dotnet tool restore --configfile eng/version/nuget.config | ||
|
|
||
| - name: Compute and stamp versions | ||
| id: stamp | ||
| shell: pwsh | ||
| run: | | ||
| # Authoritative compute on main HEAD; write drifted plugins in place. | ||
| $report = & "$PWD/eng/version/Sync-PluginVersions.ps1" -OnlyChanged -Write | ConvertFrom-Json | ||
|
|
||
| if (-not $report -or $report.Count -eq 0) { | ||
| "has_changes=false" >> $env:GITHUB_OUTPUT | ||
| "No plugin versions drifted — nothing to sync." >> $env:GITHUB_STEP_SUMMARY | ||
| exit 0 | ||
| } | ||
|
|
||
| # Build the PR body, explaining each bump with the commits that caused it. | ||
| $sections = foreach ($r in $report) { | ||
| $name = $r.plugin | ||
| $lastStamp = git log -1 --format='%H' -- "plugins/$name/plugin.json" | ||
| $range = if ($lastStamp) { "$lastStamp..HEAD" } else { 'HEAD' } | ||
| $commits = git log --format='- %h %s' $range -- "plugins/$name" ` | ||
| ":(exclude)plugins/$name/plugin.json" ` | ||
| ":(exclude)plugins/$name/.codex-plugin/plugin.json" | ||
| $why = if ($commits) { ($commits -join "`n") } else { '- (no attributable commits found)' } | ||
| @( | ||
| "### ``$name``: $($r.current) → $($r.computed)" | ||
| "" | ||
| "Changes since the last version bump:" | ||
| "" | ||
| $why | ||
| "" | ||
| ) -join "`n" | ||
| } | ||
|
|
||
| $body = @( | ||
| "Automated weekly version sync. The following plugins changed since their" | ||
| "last version bump; this PR stamps the NBGV-computed version into their" | ||
| "``plugin.json`` and ``.codex-plugin/plugin.json``." | ||
|
AbhitejJohn marked this conversation as resolved.
Outdated
|
||
| "" | ||
| "Patch numbers are derived from git history — review the reasons below, then merge." | ||
| "" | ||
| ($sections -join "`n") | ||
| ) -join "`n" | ||
|
|
||
| Set-Content -Path pr-body.md -Value $body | ||
| "has_changes=true" >> $env:GITHUB_OUTPUT | ||
| $body >> $env:GITHUB_STEP_SUMMARY | ||
|
|
||
| - name: Open or update sync PR | ||
| if: steps.stamp.outputs.has_changes == 'true' | ||
| env: | ||
| GH_TOKEN: ${{ github.token }} | ||
| run: | | ||
| git config user.name "github-actions[bot]" | ||
| git config user.email "41898282+github-actions[bot]@users.noreply.github.com" | ||
| git switch -c "$SYNC_BRANCH" | ||
| git add plugins/*/plugin.json plugins/*/.codex-plugin/plugin.json | ||
|
AbhitejJohn marked this conversation as resolved.
|
||
| git commit -m "Weekly plugin version sync" | ||
| git push --force origin "$SYNC_BRANCH" | ||
|
|
||
| EXISTING=$(gh pr list --head "$SYNC_BRANCH" --state open --json number --jq '.[0].number // empty') | ||
| if [[ -n "$EXISTING" ]]; then | ||
| gh pr edit "$EXISTING" --body-file pr-body.md | ||
| echo "Updated existing PR #$EXISTING" | ||
| else | ||
| gh pr create \ | ||
| --base main \ | ||
| --head "$SYNC_BRANCH" \ | ||
| --title "Weekly plugin version sync" \ | ||
| --body-file pr-body.md | ||
| fi | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.