Skip to content

Commit c1dfc83

Browse files
authored
Add more PowerShell scripts for automating releases (#302)
1 parent 00c679e commit c1dfc83

5 files changed

Lines changed: 428 additions & 17 deletions

File tree

.github/copilot-instructions.md

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ These instructions define how GitHub Copilot should assist with this project. Th
2424
> Notable `.editorconfig` rules: C/C++ files use 4-space indentation, `crlf` line endings, and `latin1` charset — avoid non-ASCII characters in source files.
2525
- **Documentation**: The project provides documentation on [Microsoft Learn](https://learn.microsoft.com/windows/win32/dxmath/directxmath-portal) with additional wiki pages available on [GitHub](https://github.com/microsoft/DirectXMath/wiki/). The project does **not** use Doxygen.
2626
- **Error Handling**: The majority of functions have no error conditions and do not throw C++ exceptions which is why they are marked `noexcept`. A few functions have `bool` results to indicate success or failure.
27-
- **Testing**: Unit tests for this project are implemented in this repository [Test Suite](https://github.com/walbourn/directxmathtest/) and can be run using CTest per the instructions at [Test Documentation](https://github.com/walbourn/directxmathtest/wiki). See [test copilot instructions](https://github.com/walbourn/directxmathtest/blob/main/.github/copilot-instructions.md) for additional information on the tests.
27+
- **Testing**: Unit tests for this project are implemented in a separate repository [Test Suite](https://github.com/walbourn/directxmathtest/) and can be run using CTest per the instructions at [Test Documentation](https://github.com/walbourn/directxmathtest/wiki). See [test copilot instructions](https://github.com/walbourn/directxmathtest/blob/main/.github/copilot-instructions.md) for additional information on the tests.
2828
- **Security**: This project uses secure coding practices from the Microsoft Secure Coding Guidelines, and is subject to the `SECURITY.md` file in the root of the repository.
2929
- **Dependencies**: The project has minimal dependencies, primarily relying on compiler intrinsics. It is designed to be self-contained and portable across different platforms and toolsets.
3030
- **Continuous Integration**: This project has 18 GitHub Actions workflows covering MSVC, Clang/LLVM, GCC (WSL), ARM64, Address Sanitizer, CodeQL, and super-linter. Workflows are in `.github/workflows/` and include compiler-specific builds (`msvc.yml`, `clangcl.yml`, `cxx.yml`), platform-specific builds (`arm64.yml`, `wsl.yml`), extension tests (`shmath.yml`, `xdsp.yml`), and static analysis (`codeql.yml`, `lint.yml`, `asan.yml`). Azure DevOps pipeline configurations are in `.azuredevops/`.
@@ -438,7 +438,7 @@ These were legacy types originally from xboxmath on the Xbox 360 which no longer
438438

439439
- [Source git repository on GitHub](https://github.com/microsoft/DirectXMath.git)
440440
- [DirectXMath wiki git repository on GitHub](https://github.com/microsoft/DirectXMath.wiki.git)
441-
- [DirectXMath test suite git repository on GitHub](https://github.com/walbourn/directxmathtest.wiki.git).
441+
- [DirectXMath test suite git repository on GitHub](https://github.com/walbourn/directxmathtest.git)
442442
- [C++ Core Guidelines](https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines)
443443
- [Microsoft Secure Coding Guidelines](https://learn.microsoft.com/en-us/security/develop/secure-coding-guidelines)
444444
- [CMake Documentation](https://cmake.org/documentation/)
@@ -493,7 +493,7 @@ Use these established guards — do not invent new ones:
493493
| `_M_ARM64EC` | ARM64EC ABI (ARM64 code with x64 interop using ARM-NEON) for MSVC |
494494
| `__aarch64__` / `__x86_64__` / `__i386__` / `__powerpc64__` | Additional architecture-specific symbols for MinGW/GNUC (`#if`) |
495495

496-
> `_M_ARM`/ `__arm__` is legacy 32-bit ARM which is deprecated.
496+
> `_M_ARM` / `__arm__` is legacy 32-bit ARM which is deprecated.
497497
498498
## Code Review Instructions
499499

@@ -511,3 +511,27 @@ When reviewing code, focus on the following aspects:
511511
- Ensure that all public functions and classes are covered by unit tests located on [GitHub](https://github.com/walbourn/directxmathtest.git) where applicable. Report any gaps in test coverage.
512512
- Check for performance implications, especially in geometry processing algorithms.
513513
- Provide brutally honest feedback on code quality, design, and potential improvements as needed.
514+
515+
## Release Process
516+
517+
1. Ensure all changes are merged into the `main` branch and that all tests pass.
518+
2. Git pull the local repository to ensure it is up to date with the `main` branch.
519+
3. Run the PowerShell script `build\preparerelease.ps1` which will generate a topic branch for the release, update the version number in `CMakeLists.txt`, the `README.md` file, the release notes in the nuspec files, and create a stub in the `CHANGELOG.md` file for the new release.
520+
4. Edit the `CHANGELOG.md` file to update it with a summary of changes.
521+
5. Submit the topic branch for review and merge into `main` once approved. Allow the GitHub Actions workflows and the Azure DevOps pipelines to complete successfully before proceeding.
522+
6. Run the PowerShell script `build\completerelease.ps1` which will set a tag on the project repo and the test repo, and create a release on GitHub with the release notes from `CHANGELOG.md`. Ensure you have set up GPG signing for your GitHub account so that the tags will be verified.
523+
7. Git pull the local repository to ensure it is up to date with the `main` branch. Be sure to include `--tags`.
524+
8. Push the `main` branch to the MSCodeHub mirror repository. Be sure to include `--tags`.
525+
9. Create a PR on MSCodeHub from the `main` branch to the `release` branch.
526+
10. Merge the PR on MSCodeHub to update the release branch, which will trigger the Azure DevOps pipeline to build the NuGet package.
527+
11. Download the GitHub source .zip archive from the release. Unzip and compare to the local repo to ensure it matches — keep in mind there may be some CR/LF differences. Run minisign on the .zip to generate a signature file, and upload the signature file to the release assets.
528+
12. Run the PowerShell script `build\promotenuget.ps1` with the `-Release` parameter to promote the version to the Release view on the project-scoped ADO feed.
529+
13. Run the MSCodeHub pipeline to publish the NuGet package to nuget.org. The pipeline will automatically push the most recent package promoted to the Release view to nuget.org.
530+
14. Git pull a local repository of VCPKG to `d:\vcpkg` in sync with the `main` branch of the VCPKG repository.
531+
15. Run the PowerShell script `build\updatevcpkg.ps1` to update the DirectXMath port in VCPKG with the new release version. This will edit the files in `ports\directxmath`.
532+
16. Test the VCPKG port using all appropriate triplets and features.
533+
17. Run `.\vcpkg --x-add-version directxmath` to update the VCPKG versioning history.
534+
18. Submit a PR to the VCPKG GitHub repository to update the DirectXMath port. The PR will be reviewed and merged by the VCPKG maintainers.
535+
19. For the DirectXMath release to be included in the next Windows SDK, prepare a PR for the MSCodeHub project from the `main` branch to the `ms_sdk_release` branch. When the PR is complete, the Azure DevOps pipeline will automatically build vpack and submit a PR for further review.
536+
537+
> When fully completed, be sure to update the GitHub release with links to the matching NuGet packages and the VCPKG port.
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# PSScriptAnalyzerSettings.psd1
22
@{
33
Severity=@('Error','Warning')
4-
ExcludeRules=@('PSAvoidUsingWriteHost')
4+
ExcludeRules=@('PSAvoidUsingWriteHost', 'PSUseShouldProcessForStateChangingFunctions')
55
}

build/completerelease.ps1

Lines changed: 275 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,275 @@
1+
<#
2+
3+
.NOTES
4+
Copyright (c) Microsoft Corporation.
5+
Licensed under the MIT License.
6+
7+
.SYNOPSIS
8+
Completes the release process by creating verified GitHub tags and a release.
9+
10+
.DESCRIPTION
11+
Creates GPG-signed annotated tags on both the DirectXMath repository and the
12+
DirectXMath test suite repository (Tests/), then publishes a GitHub release on
13+
the DirectXMath repository using the signed tag.
14+
15+
Tags are signed locally with 'git tag -s', which requires GPG signing to be
16+
configured in git and the signing key to be registered with GitHub. This
17+
produces the Verified badge in the GitHub UI.
18+
19+
Run this script after the release PR (prepared by preparerelease.ps1) has been
20+
merged into the main branch.
21+
22+
.PARAMETER PAT
23+
GitHub Personal Access Token with 'repo' scope, used to publish the GitHub
24+
release on microsoft/DirectXMath. Can also be provided via the GITHUB_TOKEN
25+
environment variable. If neither is provided, the script attempts to obtain a
26+
token from the 'gh' CLI.
27+
28+
.PARAMETER SkipTestRepo
29+
If set, skips creating a tag on the test suite repository (Tests/).
30+
31+
.PARAMETER WhatIf
32+
Shows what would happen without creating tags, pushing, or publishing a release.
33+
34+
.LINK
35+
https://github.com/microsoft/DirectXMath/wiki
36+
37+
#>
38+
39+
[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingEmptyCatchBlock', '')]
40+
param(
41+
[string]$PAT = "",
42+
[switch]$SkipTestRepo,
43+
[switch]$WhatIf
44+
)
45+
46+
$reporoot = Split-Path -Path $PSScriptRoot -Parent
47+
$readme = Join-Path $reporoot "README.md"
48+
$history = Join-Path $reporoot "CHANGELOG.md"
49+
$testsroot = Join-Path $reporoot "Tests"
50+
51+
#--- Validate script location ---
52+
53+
if ((-Not (Test-Path $readme)) -Or (-Not (Test-Path $history))) {
54+
Write-Error "ERROR: Unexpected location of script file!" -ErrorAction Stop
55+
}
56+
57+
#--- Validate local repo state ---
58+
59+
$branch = git -C $reporoot branch --show-current
60+
if ($branch -ne "main") {
61+
Write-Error "ERROR: Must be on the 'main' branch (currently on '$branch')!" -ErrorAction Stop
62+
}
63+
64+
Write-Host "Fetching from origin..."
65+
git -C $reporoot fetch -q origin
66+
if ($LastExitCode -ne 0) {
67+
Write-Error "ERROR: Failed to fetch from origin!" -ErrorAction Stop
68+
}
69+
70+
$headHash = git -C $reporoot rev-parse HEAD
71+
$remoteHash = git -C $reporoot rev-parse "origin/main"
72+
if ($headHash -ne $remoteHash) {
73+
Write-Error "ERROR: Local 'main' is not in sync with origin. Run 'git pull' first." -ErrorAction Stop
74+
}
75+
76+
#--- Derive release info from README.md ---
77+
78+
$rawreleasedate = $(Get-Content $readme) | Select-String -Pattern "^## [A-Z][a-z]+ (?:\d+,?\s+)?\d{4}" | Select-Object -First 1
79+
if ([string]::IsNullOrEmpty($rawreleasedate)) {
80+
Write-Error "ERROR: Failed to find a release date header in README.md!" -ErrorAction Stop
81+
}
82+
83+
$releasename = ($rawreleasedate.ToString() -replace '^## ', '').Trim()
84+
85+
try {
86+
$releaseDateTime = [datetime]::Parse($releasename)
87+
}
88+
catch {
89+
Write-Error "ERROR: Failed to parse release date '$releasename': $_" -ErrorAction Stop
90+
}
91+
92+
$releasetag = (Get-Date -Date $releaseDateTime -Format "MMMyyyy").ToLower()
93+
94+
Write-Host " Release Name: $releasename"
95+
Write-Host " Release Tag: $releasetag"
96+
97+
#--- Extract release notes from CHANGELOG.md ---
98+
99+
$changelog = Get-Content $history
100+
$notesStart = -1
101+
$notesEnd = $changelog.Count - 1
102+
103+
for ($i = 0; $i -lt $changelog.Count; $i++) {
104+
if ($changelog[$i] -match "^### $([regex]::Escape($releasename))") {
105+
$notesStart = $i + 1
106+
}
107+
elseif ($notesStart -ge 0 -and $changelog[$i] -match "^### ") {
108+
$notesEnd = $i - 1
109+
break
110+
}
111+
}
112+
113+
if ($notesStart -lt 0) {
114+
Write-Error "ERROR: Could not find release notes for '$releasename' in CHANGELOG.md!" -ErrorAction Stop
115+
}
116+
117+
$releaseNotes = (($changelog[$notesStart..$notesEnd] | Where-Object { $_ -ne "" }) -join "`n").Trim()
118+
119+
Write-Host "Release Notes:"
120+
Write-Host $releaseNotes
121+
Write-Host ""
122+
123+
#--- Acquire GitHub token ---
124+
125+
if ($PAT.Length -eq 0) {
126+
$PAT = [string]$env:GITHUB_TOKEN
127+
}
128+
129+
if ($PAT.Length -eq 0) {
130+
try {
131+
$ghToken = & gh auth token 2>$null
132+
if ($LASTEXITCODE -eq 0 -and -not [string]::IsNullOrWhiteSpace($ghToken)) {
133+
$PAT = $ghToken.Trim()
134+
Write-Host "Using token from 'gh' CLI."
135+
}
136+
}
137+
catch {
138+
# gh CLI not available
139+
}
140+
}
141+
142+
if ($PAT.Length -eq 0) {
143+
Write-Error "ERROR: No GitHub token found. Provide -PAT, set GITHUB_TOKEN, or sign in with 'gh auth login'." -ErrorAction Stop
144+
}
145+
146+
$apiHeaders = @{
147+
"Accept" = "application/vnd.github+json"
148+
"Authorization" = "Bearer $PAT"
149+
"X-GitHub-Api-Version" = "2022-11-28"
150+
}
151+
152+
#--- Helper: create a GPG-signed tag locally and push it ---
153+
154+
function Push-SignedTag {
155+
param(
156+
[Parameter(Mandatory)] [string]$RepoPath,
157+
[Parameter(Mandatory)] [string]$TagName,
158+
[Parameter(Mandatory)] [string]$Message
159+
)
160+
161+
# Check whether the tag already exists locally
162+
$existing = git -C $RepoPath tag -l $TagName
163+
if (-not [string]::IsNullOrEmpty($existing)) {
164+
Write-Error "ERROR: Tag '$TagName' already exists in '$RepoPath'!" -ErrorAction Stop
165+
}
166+
167+
if ($WhatIf) {
168+
Write-Host "[WhatIf] Would create signed tag '$TagName' in '$RepoPath' and push to origin"
169+
return
170+
}
171+
172+
Write-Host "Creating signed tag '$TagName'..."
173+
git -C $RepoPath tag -s $TagName -m $Message
174+
if ($LastExitCode -ne 0) {
175+
Write-Error "ERROR: Failed to create signed tag '$TagName'. Ensure GPG signing is configured." -ErrorAction Stop
176+
}
177+
178+
Write-Host "Pushing tag '$TagName' to origin..."
179+
git -C $RepoPath push origin $TagName
180+
if ($LastExitCode -ne 0) {
181+
git -C $RepoPath tag -d $TagName 2>$null
182+
Write-Error "ERROR: Failed to push tag '$TagName' to origin." -ErrorAction Stop
183+
}
184+
}
185+
186+
#--- Helper: create a GitHub release ---
187+
188+
function New-GitHubRelease {
189+
param(
190+
[Parameter(Mandatory)] [string]$Owner,
191+
[Parameter(Mandatory)] [string]$Repo,
192+
[Parameter(Mandatory)] [string]$TagName,
193+
[Parameter(Mandatory)] [string]$ReleaseName,
194+
[Parameter(Mandatory)] [string]$ReleaseBody
195+
)
196+
197+
# Check whether a release already exists for this tag
198+
$checkUri = "https://api.github.com/repos/$Owner/$Repo/releases/tags/$TagName"
199+
$releaseExists = $false
200+
201+
try {
202+
$null = Invoke-RestMethod -Uri $checkUri -Method Get -Headers $apiHeaders -ErrorAction Stop
203+
$releaseExists = $true
204+
}
205+
catch {
206+
$sc = $null
207+
try { $sc = [int]$_.Exception.Response.StatusCode } catch { }
208+
if ($sc -ne 404) {
209+
Write-Error "ERROR: Failed to check for existing release '$TagName' on ${Owner}/${Repo}: $_" -ErrorAction Stop
210+
}
211+
# 404 = no release exists yet, which is expected
212+
}
213+
214+
if ($releaseExists) {
215+
Write-Error "ERROR: Release '$TagName' already exists on ${Owner}/${Repo}!" -ErrorAction Stop
216+
}
217+
218+
if ($WhatIf) {
219+
Write-Host "[WhatIf] Would create release '$TagName' on ${Owner}/${Repo}"
220+
return
221+
}
222+
223+
$payload = @{
224+
tag_name = $TagName
225+
name = $ReleaseName
226+
body = $ReleaseBody
227+
draft = $false
228+
prerelease = $false
229+
make_latest = "true"
230+
} | ConvertTo-Json
231+
232+
Write-Host "Creating release '$TagName' on ${Owner}/${Repo}..."
233+
234+
try {
235+
$result = Invoke-RestMethod -Uri "https://api.github.com/repos/$Owner/$Repo/releases" `
236+
-Method Post -Headers $apiHeaders -Body $payload -ContentType "application/json" -ErrorAction Stop
237+
Write-Host " Created: $($result.html_url)"
238+
}
239+
catch {
240+
Write-Error "ERROR: Failed to create release '$TagName' on ${Owner}/${Repo}: $_" -ErrorAction Stop
241+
}
242+
}
243+
244+
#--- Create verified tag and release on microsoft/DirectXMath ---
245+
246+
Push-SignedTag -RepoPath $reporoot -TagName $releasetag -Message $releasename
247+
248+
New-GitHubRelease `
249+
-Owner "microsoft" `
250+
-Repo "DirectXMath" `
251+
-TagName $releasetag `
252+
-ReleaseName $releasename `
253+
-ReleaseBody $releaseNotes
254+
255+
#--- Create verified tag on walbourn/directxmathtest ---
256+
257+
if (-Not $SkipTestRepo) {
258+
if (-Not (Test-Path $testsroot)) {
259+
Write-Warning "WARNING: Tests/ folder not found at '$testsroot'. Skipping test suite tag."
260+
}
261+
else {
262+
Push-SignedTag -RepoPath $testsroot -TagName $releasetag -Message $releasename
263+
}
264+
}
265+
266+
#--- Done ---
267+
268+
if (-Not $WhatIf) {
269+
Write-Host ""
270+
Write-Host "Release complete. Sync the new tags locally with:"
271+
Write-Host " git pull --tags"
272+
if (-Not $SkipTestRepo) {
273+
Write-Host " git -C Tests pull --tags"
274+
}
275+
}

0 commit comments

Comments
 (0)