Skip to content
Open
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
13 changes: 13 additions & 0 deletions .config/dotnet-tools.json
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
Comment thread
AbhitejJohn marked this conversation as resolved.
}
Comment thread
AbhitejJohn marked this conversation as resolved.
}
}
180 changes: 180 additions & 0 deletions .github/workflows/version-bump-command.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
# 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

- 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

- 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
Comment thread
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"
# Drop the main-overlaid tooling so only the regenerated manifests are committed;
# the PR's own versions of these files are restored to the working tree + index.
git checkout HEAD -- \
eng/version/Sync-PluginVersions.ps1 \
eng/version/nuget.config \
.config/dotnet-tools.json \
global.json
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
118 changes: 118 additions & 0 deletions .github/workflows/weekly-version-sync.yml
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``."
""
"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
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
31 changes: 31 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ To create a new plugin:

1. Add `plugins/<plugin-name>/plugin.json` and a `skills/` directory beneath it.
2. Add a matching entry in `.github/plugin/marketplace.json`, `.claude-plugin/marketplace.json`, `.cursor-plugin/marketplace.json`, and `.agents/plugins/marketplace.json`. Keep plugin entries consistent across all marketplace manifests (including `plugins[].source` format) to reduce drift and make future updates safer.
Also add a `plugins/<plugin-name>/version.json` (copy an existing one) so the plugin participates in automated versioning. Start its `plugin.json` version at `0.1.0`.
3. Add a CODEOWNERS entry for the new plugin and its tests (see [Code ownership](#code-ownership)).
4. Add the plugin to the **What's Included** table in the root `README.md`.
5. Create a `tests/<plugin-name>/` directory for skill tests.
Expand All @@ -68,6 +69,36 @@ Skills in `dotnet-experimental`:

Place experimental skills under `plugins/dotnet-experimental/skills/` with matching tests in `tests/dotnet-experimental/`.

## Plugin versioning

Each plugin is versioned independently. The same version is duplicated in two manifest files —
`plugins/<plugin>/plugin.json` and `plugins/<plugin>/.codex-plugin/plugin.json` — and consumers
(Copilot CLI, Claude, Codex, Cursor) read it directly from this repository.

Versioning is automated with [Nerdbank.GitVersioning](https://github.com/dotnet/Nerdbank.GitVersioning).
A per-plugin `plugins/<plugin>/version.json` scopes the git height to that plugin's subtree, so the
**patch** number is derived from history — you do not edit it by hand. The two generated manifests
(`plugin.json`, `.codex-plugin/plugin.json`) and `version.json` itself are excluded from that height
Comment on lines +78 to +81
via the `pathFilters`, so editing only manifest metadata (anything other than a deliberate base bump
in `version.json`) does **not** change the patch number and is **not** picked up by `/version-bump` or
the weekly sync. Touch a skill or other plugin content to bump the version.

What this means when you contribute:

- **Don't hand-edit the `version` field** in either `plugin.json`. The patch number is computed and
stamped automatically, and a manual edit will be overwritten.
- **The only version field you may change is the base** (`"version"`) in `plugins/<plugin>/version.json`,
and only to declare a deliberate **minor or major** release of that plugin (e.g. `0.1` → `0.2` or `1.0`).
Changing the base resets the patch number to `0`.
- After a PR changes a plugin's content, bumping its version is optional:
- A maintainer can comment **`/version-bump`** on a same-repo PR to stamp the new version onto the branch.
- Otherwise the **weekly version sync** opens a PR that stamps any plugin whose content changed without a
version bump, explaining each change. Nothing is ever missed.

Patch numbers are predicted from git history, so two PRs bumped concurrently can land the same patch
number for a plugin; the weekly sync recomputes the authoritative height on `main` and reconciles any
collision. Version-only changes do not trigger skill evaluations.

## Before you start

- Search existing issues and pull requests to avoid duplicates.
Expand Down
Loading