Skip to content

Commit 335ecbc

Browse files
authored
Add Preview workflows and enforce versioning rules (#1088)
* Add prerelease infrastructure (workflows + docs) - release.yml triggers on push to both main and release/** - ci-build.yml triggers on PRs to both main and release/** - GitHub Releases conditionally marked as prerelease via NBGV.PrereleaseVersion - Added RELEASING.md documenting the branching model and cutover sequence This is part 1 of 2. version.json is unchanged; main continues to publish 9.4.N stable until the follow-up PR bumps it to 9.5-preview.{height}. Order of operations: 1. Merge this PR. 2. Cut release/9.x from main HEAD (it now has correct workflows). 3. Merge the follow-up version.json bump PR to start main pre-releases. * Bump main to 9.5-preview.{height} for pre-release publishing This is part 2 of 2. After this commit, main publishes 9.5.0-preview.N on every PR merge. The release/<major>.x branches continue to publish stable packages. Do NOT merge this until release/9.x has been cut from main (with the workflows from part 1 in place). * Add release automation workflows Adds three workflow_dispatch actions that handle all version.json edits and release-branch creation, so maintainers never have to edit version.json by hand: - promote-minor.yml: promotes main to a new minor on release/<major>.x. Opens two PRs (promotion + main bump to next preview). - cut-major.yml: creates a new release/<major>.x branch at <major>.0 stable and opens a PR advancing main to the next preview. - bump-major-preview.yml: bumps main to next major preview ahead of breaking changes. Adds two passive guards: - pr-version-check.yml: blocks breaking-change PRs when main hasn't been bumped past the latest stable major. - release.yml: refuses to publish a prerelease for major.minor when a stable tag for that major.minor already exists (catches the 'main forgot to bump after promotion' regression). RELEASING.md is updated to document the automated flows and notes the GITHUB_TOKEN limitation (bot PRs don't trigger downstream CI by default). * Enforce exactly-one-greater rule for major bumps Prevents accidentally skipping a major version (e.g., bumping from 9.x stable to 11.0-preview, leaving 10 unshipped). - pr-version-check.yml: a breaking-change PR now fails unless main's major is exactly latest_stable_major + 1. Previously allowed any major > latest_stable_major. - bump-major-preview.yml: rejects next_major inputs that aren't exactly latest_stable_major + 1. Workflow input validation makes this fail-fast at dispatch time. - RELEASING.md: notes the strict sequential-major rule. Skipping a major is still possible by editing version.json by hand (both checks reference workflow names, not version.json content). * Update versionHeightOffset for cutover after 9.4.37 ship main has shipped 9.4.37 since this branch was opened. The previously documented versionHeightOffset of 36 would produce 9.4.37 again (collision). Updated to 37 so the first build on release/9.x produces 9.4.38, continuing sequentially. * Modernize release workflow - Replace archived actions/create-release@v1.1.4 with softprops/action-gh-release@v2. The release_name input was renamed to name; GITHUB_TOKEN is now implicit via the contents:write permission on the job. - Add --skip-duplicate to dotnet nuget push so workflow re-runs are idempotent when a package version was already uploaded. - Standardize productNamespacePrefix to DynamicData (was inconsistently 'ReactiveMarbles' in release.yml vs 'DynamicData' in ci-build.yml). - Add explicit permissions: contents: write at the job level, required by softprops/action-gh-release to create the GitHub release and tag. * Address Copilot review feedback - version.json publicReleaseRefSpec: tighten 'release/.+' to 'release/\\d+\\.x' so accidental branches like 'release/test' don't get treated as public releases. - release.yml branch trigger: narrow from 'release/**' to 'release/*.x' and add an explicit branch-name validation step that refuses to publish from anything other than main or 'release/<digits>.x'. - ci-build.yml and pr-version-check.yml: same narrower trigger for consistency (no behavioural impact, just stops them firing on malformed release branch names). - promote-minor.yml: validate that stable_version matches main's current preview major.minor, so 'stable_version=9.6' while main is '9.5-preview' is rejected instead of silently mislabeling code. - promote-minor.yml: when setting the new stable version, also remove any existing versionHeightOffset so the new minor starts fresh at X.Y.1 instead of inheriting the previous minor's offset (which would otherwise produce 9.5.38 on the first 9.5 build given the cutover offset of 37). - cut-major.yml: tighten next_main_version validation. After cutting major X, the next main version must be either X.<minor>=1+ (same major continuation) or (X+1).0 (next sequential major). Skips like cutting 10.0 with next_main_version=12.0 are rejected. - cut-major.yml: also strip versionHeightOffset on the new release branch, defensively (main shouldn't have it, but in case it does). Verified the versionHeightOffset removal regex produces valid JSON in all three cases: offset in middle, offset at end, offset absent. * Address multi-agent adversarial review findings Reviewers: 6 (Claude Opus xhigh x2, Claude Sonnet x2, GPT-5.5 x2). ~99 raw findings consolidated; filtered to those with consensus or high-impact failure scenarios. Security and correctness: - All workflow_dispatch inputs now route through env: instead of being interpolated as inputs expressions into PowerShell single-quoted strings. Prevents shell injection via a quote-in-input. - Every pwsh block begins with $ErrorActionPreference='Stop' and $PSNativeCommandUseErrorActionPreference=$true so native command failures (gh, git) actually propagate. - 'git fetch ... 2>$null' replaced with explicit $LASTEXITCODE checks so a transient fetch failure no longer silently bypasses tag-based security guards (sequential-major check, prerelease regression guard). - 'gh pr create' output captured via Select-Object -Last 1 and trimmed; failure on empty or non-URL output. Previously a partial gh failure could leave PR_URL='' and the workflow would report success. - Input regex tightened from ^\d+$ to ^(0|[1-9]\d*)$ so leading zeros (e.g. '010') are rejected before being written to version.json. - Integer round-trip on parsed inputs to normalize string form. Workflow logic: - promote-minor uses 'git merge -X theirs origin/main', so the unavoidable conflict on the SECOND promotion (release branch's prior stable vs main's preview) resolves automatically. Verified empirically against the conflict case. - cut-major reordered: bump-main PR is opened FIRST, then the release branch is created and pushed. Previously the release branch was pushed before the bump PR, so any failure between them shipped stable but never advanced main. - cut-major now dispatches release.yml via 'gh workflow run' after pushing the new release branch. GITHUB_TOKEN-driven pushes do not trigger workflow_run events, so a bare push would never publish. - promote-minor and cut-major capture main's HEAD SHA at validation time and re-verify it before destructive ops, closing the TOCTOU window where main moves between validate and bump. - bot/* branch names include github.run_id so retries after a partial failure don't collide on the previous run's pushed branch. - All branch pushes use --force-with-lease for safer idempotency. - versionHeightOffset stripper regex now allows end-of-string in addition to \r?\n, so a missing trailing newline doesn't leave the offset in place after promotion. - bump-major-preview now also strips versionHeightOffset (consistency with promote-minor/cut-major). release.yml hardening: - Concurrency group per ref so two pushes to main don't race on tag creation. - NuGet push moved BEFORE GitHub Release creation. Previously a GitHub release/tag could exist for a version that never reached nuget.org. - New regression guard: release/*.x branches refuse to publish any prerelease (those branches must only ship stable). - workflow_dispatch trigger added so cut-major can manually invoke it. - Validate-publish-branch regex tightened to ^release/(0|[1-9]\d*)\.x$ - dotnet/nbgv pinned to v0.5.1 (was master, supply chain risk). - softprops/action-gh-release pinned to v2.6.2. - 'Build' step renamed to 'Pack' (it runs dotnet pack). - Dead 'productNamespacePrefix' env var removed. - Stable-tag regex escapes dots properly. - NuGet push glob narrowed from **/*.nupkg to src/**/bin/Release/*.nupkg. ci-build.yml: actions/upload-artifact pinned to v7 (was master). pr-version-check.yml: - Early exit when base ref is not 'main' (the check is main-only; PRs to release/*.x previously failed with a confusing error). - Validates the PR HEAD's version.json instead of the base branch, so a PR that reverts main's version is also caught. - Fail-closed when no stable tags exist for a breaking-change PR (previously silently passed). - 'semver:major' label support dropped; standardized on 'breaking-change'. - Error message now tells the maintainer to push an empty commit to re-trigger after the bump PR merges. RELEASING.md: - Corrected '9.5.0 stable' to 'first 9.5.x stable (9.5.1)' to match actual NBGV behavior (the version-bump commit is at height 1). - Documents that promotion PRs are NOT mechanical and need review. - Documents recommended branch protection for the version check. - Documents manual re-trigger requirement for breaking-change PRs. - Documents recovery paths for partial cut-major / promote-minor runs. - Shows the exact version.json shape after the initial cutover edit. Findings deliberately not addressed (false positives, low ROI, or out of scope): - ConvertFrom-Json on JSONC: verified working on PowerShell 7.5 (matches GH runners), no issue. - actions/checkout@v6 existence: verified, latest is v6.0.2. - NBGV height=0 vs 1 for cutover offset: verified, height=1 at the version-change commit, so offset of 37 produces 9.4.38 correctly. - Unicode digit injection in inputs: requires malicious maintainer with workflow_dispatch access; extremely improbable. - v-prefixed tag style: not used in this repo. - Fork PR label visibility: edge case, repo doesn't actively manage fork PRs through this label flow. - Per-step permissions scoping: requires major restructure for marginal benefit. * Address round-2 adversarial review findings Round 2 had 4 reviewers (Opus codereview + general, Sonnet codereview, GPT-5 general). They found that round 1's fixes introduced new bugs and that round 1 missed several issues. Pattern fix: PowerShell error handling The round-1 pattern of $PSNativeCommandUseErrorActionPreference=$true made every subsequent explicit $LASTEXITCODE check unreachable: a non-zero native exit threw before the check could run. Reverted to just $ErrorActionPreference='Stop' and explicit $LASTEXITCODE checks after each native command. Curated error messages now actually fire. For 'gh pr create' specifically, the call is wrapped in try/catch so the curated message survives the strict-error throw path. Promote-minor merge strategy Replaced 'git merge -X theirs' with a plain merge that aborts on any conflict outside version.json. The -X theirs strategy would silently overwrite a release-branch-only hotfix that was never back-merged to main, dropping the fix without any conflict marker. The new strategy fails loudly on any non-version.json conflict and only auto-resolves the version.json overwrite (which the next step would overwrite anyway). cut-major step reordering Moved 'Verify main hasn't moved' BEFORE 'Open main bump PR'. Round 1 had the verify after the PR was already opened, leaving an orphan PR with stale content if main moved during the run. Cross-workflow concurrency All three main-mutating workflows (bump-major-preview, cut-major, promote-minor) now share a 'main-version-mutator' concurrency group. Two of these workflows can no longer race; the queue serializes them. Round 1 had per-workflow groups which only protected against self-races. TOCTOU coverage bump-major-preview now captures and re-verifies the main SHA the same way cut-major and promote-minor do. promote-minor now also captures and re-verifies the release-branch SHA (previously only main was checked). versionHeightOffset stripper Regex extended to tolerate trailing JSONC '//' comments on the offset line. Added a post-strip ConvertFrom-Json validation that throws if the field is somehow still present after the regex pass. Belt and suspenders: regex catches common cases, parse validation catches anything unusual. cut-major release.yml dispatch reliability Added a retry loop around 'gh workflow run release.yml' (up to 6 attempts, 5s apart) because GitHub's API can briefly fail to find a newly pushed branch. After dispatch, polls 'gh run list' to confirm a run actually queued and surfaces the run URL in the workflow summary so the maintainer can monitor. release.yml asymmetric guard fixed Added a symmetric check: stable versions can't be published from main. Round 1 only blocked prereleases from release branches. A manual edit that accidentally set main's version to a non-prerelease form would have published stable from main, bypassing the entire branching model. release.yml runs tests before pack Pack and push now follow 'dotnet test'. Previously the release pipeline ran restore -> pack -> push with no test coverage on the merge commit. Pack uses --no-build to avoid double-compilation. release.yml NuGet glob Replaced shell-expanded '**/*.nupkg' with explicit pwsh Get-ChildItem under src/**/bin/Release. Eliminates the risk of pushing 3rd-party packages restored into the workspace. Each package is pushed via an explicit $env: read of the API key (no expression interpolation inside the command string). Action pinning glennawatson/ChangeLog now pinned to commit SHA (was @v1 floating). Round 1 missed this third-party action while pinning the others. pr-version-check additions - Unconditional check: if a PR changes version.json's major, it MUST be labeled 'breaking-change'. Catches unlabeled major edits that would otherwise slip through. - Fail-closed when no stable tags exist now allows the legitimate first-breaking-change case (head major == base major + 1) on a brand-new repo without forcing the user to remove the label. - Error message tells the user to rebase onto main (not push an empty commit). The check reads version.json from the PR head; an empty commit on a stale fork point doesn't change version.json. Idempotency Bot branch names now include GITHUB_RUN_ATTEMPT in addition to GITHUB_RUN_ID, so a workflow rerun produces a fresh branch instead of failing to force-push over the prior attempt's branch. cut-major next_main_version safety When advancing main to a NEW major (next major from cut), now also verifies no stable tags already exist for that major. Prevents advancing main to a preview line that has already shipped stable. Documentation - 'push an empty commit' replaced with 'rebase onto main' (the check reads PR head; an empty commit doesn't help). - promote and cut docs emphasize merging the main-bump PR IMMEDIATELY after the stable promotion ships, because the prerelease regression guard fails between those two events. - cut-major step summary now links to the dispatched release run when available. Findings deliberately not addressed - actions/checkout@v6 and actions/setup-dotnet@v5.0.1 SHA pinning: pre-existing, scope creep, GitHub-owned (lower risk). - gh-release run for tag-already-exists recovery: rare; manual recovery is acceptable. - 'cut-major opens PR before TOCTOU': partially addressed by moving verify to between push and PR-create; the bump branch itself is still pushed before the verify, but the PR is not. Recovery is documented (delete the branch and re-run). * Address round-3 adversarial review findings Round 3 (2 reviewers) found 4 real bugs introduced by round-2 fixes: pr-version-check: bot bump PRs trigger the unconditional check The new unconditional major-change check would block exactly the PRs that bump-major-preview and cut-major open: they legitimately change the major but aren't labeled 'breaking-change' (they're preparation for breaking, not breaking themselves). Added a bypass for PRs authored by github-actions[bot] with a head branch matching bot/bump-*. The bypass is narrow: it doesn't open a hole for arbitrary bot-authored PRs, just the specific automation pattern. pr-version-check: no-stable-tags branch off-by-one The fail-closed path required head_major == base_major + 1 even in the post-bump steady state where both are already advanced. This would block every breaking-change PR after bump-major-preview merged on any project with no prior stable tags. Now allows head == base (post-bump steady state) OR head == base + 1 (PR does its own bump). promote-minor merge: empty-conflicts case mishandled If 'git merge --no-commit' failed for a reason other than file conflicts (transport error, corrupted object), the code fell through to 'git checkout --theirs version.json' with nothing to resolve, producing a misleading error. Now explicitly detects the empty- conflicts case, aborts the merge cleanly, and rethrows with a diagnostic that points at the real cause. promote-minor verify step: missing $LASTEXITCODE checks The 'Verify branch and main still at validated SHAs' step had two 'git rev-parse' calls without exit-code checks. If either failed transiently, .Trim() returned '' and the comparison would fire the 'moved' error with empty SHAs, misdiagnosing the actual problem. Added exit-code checks consistent with the sibling workflows. * Address PR review feedback - Stable releases now publish X.Y.0 (semver convention) instead of X.Y.1 via versionHeightOffset: -1 written by cut-major/promote-minor on release branches. Previews unchanged (preview.1 first, per .NET ecosystem convention). - pr-version-check rejects any human-authored PR that modifies version.json unless labeled manual-version-edit. Bot PRs (bot/bump-*, bot/promote-*) are exempted. - Add 'Cherry-picking a fix from main to a release branch' to Day-to-day flows. - Remove branch-protection recommendation from RELEASING.md (one-time admin task; belongs elsewhere). - Remove initial cutover section from RELEASING.md (one-time task; PR description has it). - Drop redundant int-then-string cycling after regex validation in cut-major and bump-major-preview (caught by review). * Harden SemVer regex against version-last-property edge case The previous insertion logic relied on a trailing comma being present on the version line. If a future reorganization placed 'version' as the LAST property in version.json (no trailing comma), the trailing-comma cleanup would strip the comma between version and the inserted versionHeightOffset, producing invalid JSON. New logic absorbs any existing comma in the version-line match and emits both properties with their own commas; the cleanup then drops a trailing comma only if it lands before a closing brace. Verified against 7 edge cases including last-property, idempotency, CRLF, tabs, and existing-offset replacement. * Address PR review: orphan guard, drop PR-build pack/upload ci-build.yml: drop the Pack and Create NuGet Artifacts steps. PR builds gate on compile and test; producing and uploading nupkgs on every PR adds runtime, costs storage, and exposes a known intermittent silent-failure in upload-artifact@v7 (actions/upload-artifact#806). release.yml still packs and pushes on merge to main/release/*.x, which is when artifacts actually matter. bump-major-preview.yml: add pre-flight orphan check. If main is on X.Y-preview and release/X.x does not exist (or is on an older minor), the workflow now refuses to bump main to the next major unless the operator passes discard_current_preview=true. Prevents silently abandoning an in-progress preview line. RELEASING.md: expand workflow reference with a quick decision table and a full inputs/prereqs/output table. * Restore Pack + upload-artifact on PR build, pin to v6.0.0 Reverting the previous drop. Pack-on-PR is a real correctness check (catches packaging-only failures: invalid PackageLicenseExpression, missing nuspec metadata, broken Pack='true' inclusions) that would otherwise blow up only at release time, after merge. Artifact upload kept for 'try this PR' downstream testing workflows and SLSA-style build trail transparency. Pinned to actions/upload-artifact@v6.0.0 (not v7) to avoid the ESM-rewrite silent-failure regression seen on the previous PR build (actions/upload-artifact#806). v6 runs on Node 24 so it doesn't sunset with Node 20 in September. Matches the repo convention of tag-pinning trusted publishers (compare actions/setup-dotnet@v5.0.1). * Pin actions/upload-artifact to v4.6.2 Avoids the v7 ESM silent-failure regression seen on the previous PR build (actions/upload-artifact#806). v4 runs on Node 20, which is being removed from the Actions runner on Sept 16, 2026. Tracked by #1089.
1 parent 28adb4f commit 335ecbc

8 files changed

Lines changed: 1106 additions & 29 deletions

File tree

Lines changed: 214 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
1+
name: Bump main to next major preview
2+
3+
on:
4+
workflow_dispatch:
5+
inputs:
6+
next_major:
7+
description: 'Next major version (e.g., 10) for the new preview line on main'
8+
required: true
9+
type: string
10+
discard_current_preview:
11+
description: 'Allow orphaning the current major''s preview work (no release branch will ever ship for it). Check this only when intentionally abandoning the current X.Y-preview line.'
12+
required: false
13+
type: boolean
14+
default: false
15+
16+
permissions:
17+
contents: write
18+
pull-requests: write
19+
20+
concurrency:
21+
group: main-version-mutator
22+
cancel-in-progress: false
23+
24+
jobs:
25+
bump:
26+
runs-on: ubuntu-latest
27+
env:
28+
NEXT_MAJOR_INPUT: ${{ inputs.next_major }}
29+
DISCARD_PREVIEW_INPUT: ${{ inputs.discard_current_preview }}
30+
steps:
31+
- name: Checkout
32+
uses: actions/checkout@v6
33+
with:
34+
ref: main
35+
fetch-depth: 0
36+
37+
- name: Configure git
38+
run: |
39+
git config user.email "github-actions[bot]@users.noreply.github.com"
40+
git config user.name "github-actions[bot]"
41+
42+
- name: Validate inputs
43+
shell: pwsh
44+
run: |
45+
$ErrorActionPreference = 'Stop'
46+
47+
$next = $env:NEXT_MAJOR_INPUT
48+
if ($next -notmatch '^(0|[1-9]\d*)$') {
49+
throw "next_major must be a positive integer with no leading zeros (got: '$next')"
50+
}
51+
$nextInt = [int]$next
52+
53+
git fetch --tags --force origin
54+
if ($LASTEXITCODE -ne 0) { throw "git fetch --tags failed (exit $LASTEXITCODE); cannot validate sequential-major rule." }
55+
56+
$mainHeadSha = (git rev-parse origin/main).Trim()
57+
if ($LASTEXITCODE -ne 0 -or -not $mainHeadSha) { throw "git rev-parse origin/main failed (exit $LASTEXITCODE)." }
58+
59+
$versionJson = git show "${mainHeadSha}:version.json" | ConvertFrom-Json
60+
if ($LASTEXITCODE -ne 0) { throw "Failed to read version.json from origin/main." }
61+
$ver = $versionJson.version
62+
if ($ver -notmatch '^(0|[1-9]\d*)\.(0|[1-9]\d*)-preview') {
63+
throw "main's version is '$ver' but should be '<major>.<minor>-preview.{height}'"
64+
}
65+
$currentMajor = [int]$matches[1]
66+
$currentMinor = [int]$matches[2]
67+
if ($nextInt -le $currentMajor) {
68+
throw "next_major ($nextInt) must be greater than current major ($currentMajor)"
69+
}
70+
71+
# Pre-flight: detect whether the current major's preview work would be orphaned.
72+
# The only workflow that creates release branches is cut-major.yml, and it requires
73+
# main to be on <major>.0-preview. If main has moved past .0-preview without the
74+
# major ever being cut (or has accumulated minor work past the existing release
75+
# branch's stable minor), bumping to the next major silently loses that work.
76+
$discardOverride = $env:DISCARD_PREVIEW_INPUT -eq 'true'
77+
$releaseBranch = "release/$currentMajor.x"
78+
$releaseExists = git ls-remote --heads origin "refs/heads/$releaseBranch"
79+
if ($LASTEXITCODE -ne 0) { throw "git ls-remote for $releaseBranch failed (exit $LASTEXITCODE); cannot verify orphan state." }
80+
81+
if (-not $releaseExists) {
82+
$msg = "main is on $currentMajor.$currentMinor-preview but '$releaseBranch' does not exist. Bumping main to $nextInt would orphan ALL '$currentMajor.x' work (no stable '$currentMajor.*' would ever ship). To ship $currentMajor.0 stable first, run 'Cut major release' with major_version=$currentMajor and next_main_version=$nextInt.0. To intentionally discard the $currentMajor preview line, re-run this workflow with 'discard_current_preview' checked."
83+
if (-not $discardOverride) { throw $msg }
84+
Write-Host "WARNING (discard_current_preview=true): release/$currentMajor.x does not exist. All $currentMajor.x work is being orphaned."
85+
} else {
86+
$releaseVerJson = git show "origin/${releaseBranch}:version.json" | ConvertFrom-Json
87+
if ($LASTEXITCODE -ne 0) { throw "Failed to read version.json from origin/$releaseBranch." }
88+
$releaseVer = $releaseVerJson.version
89+
if ($releaseVer -notmatch '^(0|[1-9]\d*)\.(0|[1-9]\d*)$') {
90+
throw "'$releaseBranch' version is '$releaseVer'; expected stable '<major>.<minor>' form. Cannot verify orphan state."
91+
}
92+
$releaseMinor = [int]$matches[2]
93+
if ($currentMinor -gt $releaseMinor) {
94+
$msg = "main is on $currentMajor.$currentMinor-preview but '$releaseBranch' is at $currentMajor.$releaseMinor stable. Bumping main to $nextInt would orphan the $currentMajor.$currentMinor preview work (never shipped as stable). To ship $currentMajor.$currentMinor stable first, run 'Promote main to stable minor' with target_release_branch=$releaseBranch and stable_version=$currentMajor.$currentMinor. To intentionally discard $currentMajor.$currentMinor preview, re-run this workflow with 'discard_current_preview' checked."
95+
if (-not $discardOverride) { throw $msg }
96+
Write-Host "WARNING (discard_current_preview=true): orphaning $currentMajor.$currentMinor preview work (release/$currentMajor.x is at $currentMajor.$releaseMinor)."
97+
} else {
98+
Write-Host "OK: $releaseBranch is at $currentMajor.$releaseMinor stable and main is on $currentMajor.$currentMinor-preview ($currentMinor <= $releaseMinor); no orphaning."
99+
}
100+
}
101+
102+
$latestStable = git tag --list --sort=-v:refname |
103+
Where-Object { $_ -match '^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$' } |
104+
Select-Object -First 1
105+
106+
if ($latestStable) {
107+
if ($latestStable -notmatch '^(\d+)\.') {
108+
throw "Could not parse latest stable tag: '$latestStable'"
109+
}
110+
$latestStableMajor = [int]$matches[1]
111+
$expectedMajor = $latestStableMajor + 1
112+
if ($nextInt -ne $expectedMajor) {
113+
throw "next_major ($nextInt) must be exactly one greater than the latest stable major ($latestStableMajor; tag '$latestStable'). Expected: $expectedMajor. Major versions must be incremented sequentially."
114+
}
115+
Write-Host "OK: next_major ($nextInt) is exactly one greater than latest stable major ($latestStableMajor)."
116+
} else {
117+
$expectedMajor = $currentMajor + 1
118+
if ($nextInt -ne $expectedMajor) {
119+
throw "No stable tags found; falling back to comparing against main's current major ($currentMajor). next_major ($nextInt) must be exactly $expectedMajor."
120+
}
121+
Write-Host "No stable tags found; next_major ($nextInt) matches currentMajor+1 ($expectedMajor)."
122+
}
123+
124+
$newVersion = "$next.0-preview.{height}"
125+
$runId = $env:GITHUB_RUN_ID
126+
$runAttempt = $env:GITHUB_RUN_ATTEMPT
127+
Add-Content -Path $env:GITHUB_ENV -Value "NEW_VERSION=$newVersion"
128+
Add-Content -Path $env:GITHUB_ENV -Value "NEXT_MAJOR=$next"
129+
Add-Content -Path $env:GITHUB_ENV -Value "BUMP_BRANCH=bot/bump-major-$next-$runId-$runAttempt"
130+
Add-Content -Path $env:GITHUB_ENV -Value "MAIN_HEAD_SHA=$mainHeadSha"
131+
132+
- name: Create bump branch (from validated main SHA)
133+
shell: pwsh
134+
run: |
135+
$ErrorActionPreference = 'Stop'
136+
git checkout "$env:MAIN_HEAD_SHA"
137+
if ($LASTEXITCODE -ne 0) { throw "git checkout failed (exit $LASTEXITCODE)." }
138+
git checkout -b "$env:BUMP_BRANCH"
139+
if ($LASTEXITCODE -ne 0) { throw "git checkout -b failed (exit $LASTEXITCODE)." }
140+
141+
- name: Bump version.json
142+
shell: pwsh
143+
run: |
144+
$ErrorActionPreference = 'Stop'
145+
$path = 'version.json'
146+
$content = [System.IO.File]::ReadAllText($path)
147+
$content = [regex]::Replace($content, '"version":\s*"[^"]+"', "`"version`": `"$env:NEW_VERSION`"")
148+
$content = [regex]::Replace($content, '(?m)^\s*"versionHeightOffset"\s*:\s*-?\d+\s*,?\s*(//[^\r\n]*)?\s*(\r?\n|$)', '')
149+
$content = [regex]::Replace($content, ',(\s*[}\]])', '$1')
150+
[System.IO.File]::WriteAllText($path, $content)
151+
$obj = $content | ConvertFrom-Json
152+
if ($obj.PSObject.Properties.Name -contains 'versionHeightOffset') {
153+
throw "Failed to strip versionHeightOffset from version.json. Manual edit required."
154+
}
155+
156+
- name: Commit and push
157+
shell: pwsh
158+
run: |
159+
$ErrorActionPreference = 'Stop'
160+
git add version.json
161+
if ($LASTEXITCODE -ne 0) { throw "git add failed (exit $LASTEXITCODE)." }
162+
git commit -m "Bump main to $env:NEW_VERSION (next major preview line)"
163+
if ($LASTEXITCODE -ne 0) { throw "git commit failed (exit $LASTEXITCODE)." }
164+
git push --force-with-lease -u origin "$env:BUMP_BRANCH"
165+
if ($LASTEXITCODE -ne 0) { throw "git push failed (exit $LASTEXITCODE)." }
166+
167+
- name: Verify main hasn't moved since validation
168+
shell: pwsh
169+
run: |
170+
$ErrorActionPreference = 'Stop'
171+
git fetch origin main --quiet
172+
if ($LASTEXITCODE -ne 0) { throw "git fetch failed (exit $LASTEXITCODE)." }
173+
$currentMainSha = (git rev-parse origin/main).Trim()
174+
if ($currentMainSha -ne $env:MAIN_HEAD_SHA) {
175+
throw "main moved during bump-major-preview (was $env:MAIN_HEAD_SHA, now $currentMainSha). The bump branch was pushed but no PR was created. Delete the bump branch and re-run."
176+
}
177+
Write-Host "OK: main is still at $env:MAIN_HEAD_SHA."
178+
179+
- name: Open PR
180+
env:
181+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
182+
shell: pwsh
183+
run: |
184+
$ErrorActionPreference = 'Stop'
185+
$nextMajor = $env:NEXT_MAJOR
186+
$body = @"
187+
Bumps ``main`` from its current preview to ``$env:NEW_VERSION`` ahead of upcoming breaking changes for ``${nextMajor}.0``.
188+
189+
Merge this **before** any PR labeled ``breaking-change`` so the prereleases are correctly versioned as ``${nextMajor}.0.0-preview.N``.
190+
"@
191+
try {
192+
$url = (gh pr create `
193+
--base main `
194+
--head "$env:BUMP_BRANCH" `
195+
--title "Bump main to $env:NEW_VERSION (next major preview)" `
196+
--body $body | Select-Object -Last 1).Trim()
197+
} catch {
198+
throw "gh pr create failed: $($_.Exception.Message)"
199+
}
200+
if ($LASTEXITCODE -ne 0 -or -not $url -or $url -notmatch '^https?://') {
201+
throw "gh pr create did not return a valid URL (exit $LASTEXITCODE; got: '$url')"
202+
}
203+
Add-Content -Path $env:GITHUB_ENV -Value "PR_URL=$url"
204+
205+
- name: Summary
206+
shell: pwsh
207+
run: |
208+
$summary = @"
209+
## Major preview bump
210+
211+
- **PR**: $env:PR_URL
212+
- Merge this before landing the first breaking-change PR.
213+
"@
214+
Add-Content -Path $env:GITHUB_STEP_SUMMARY -Value $summary

.github/workflows/ci-build.yml

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,10 @@ name: Build
22

33
on:
44
pull_request:
5-
branches: [ main ]
5+
branches: [ main, 'release/*.x' ]
66

77
env:
88
configuration: Release
9-
productNamespacePrefix: "DynamicData"
109

1110
jobs:
1211
build:
@@ -40,7 +39,7 @@ jobs:
4039
4140
- name: NBGV
4241
id: nbgv
43-
uses: dotnet/nbgv@master
42+
uses: dotnet/nbgv@v0.5.1
4443
with:
4544
setAllVars: true
4645

@@ -61,7 +60,7 @@ jobs:
6160
working-directory: src
6261

6362
- name: Create NuGet Artifacts
64-
uses: actions/upload-artifact@master
63+
uses: actions/upload-artifact@v4.6.2
6564
with:
6665
name: nuget
67-
path: '**/*.nupkg'
66+
path: 'src/**/bin/Release/*.nupkg'

0 commit comments

Comments
 (0)