Skip to content

Commit 53fc6a2

Browse files
authored
feat(release): channel-aware workflows + RELEASING runbook (Phase 1) (#412)
* feat(release): channel-aware workflows + RELEASING runbook Phase 1 of the release-strategy work. - Add scripts/extract-release-notes.ps1: shared CHANGELOG section extractor consumed by all three release workflows. - release-psdocs.yml / release-psdocs-azure.yml: - Parse tag into base version + optional preview suffix. - Validate base version against PSDocs.psd1 / PSDocs.Azure.psd1 ModuleVersion (fail fast on mismatch). - Extract per-version notes from CHANGELOG.md and pass via --notes-file to gh release create. - Mark GitHub Release as --prerelease when tag carries a SemVer prerelease suffix. - release-vscode.yml: - Two-tag-prefix design (vscode-v* stable + vscode-preview-v* preview) because VS Marketplace does not accept SemVer prerelease suffixes in package.json version. - Validate version against package.json. - Route channel through the build's existing -Channel param so preview publishes as vicperdana.psdocs-vscode-preview. - Add RELEASING.md: end-to-end runbook covering one-time secret + environment setup (PSGALLERY_API_KEY, VSCE_PAT, release env with required reviewers), per-package release steps, pre-release flow, troubleshooting, and rollback. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * docs: link RELEASING runbook + clean up legacy ADO release doc Phase 1 finish + Phase 2 of the release-strategy work. - README.md: add 'see RELEASING.md' line under 'Changes and versioning'. - CONTRIBUTING.md: replace stale 'docs/release.md' link with link to RELEASING.md. - docs/release.md: replace ADO-flavored release doc (referenced obsolete Azure DevOps build numbers like 0.2.0-B2103003) with a stub pointing to RELEASING.md. - docs/publish/devops-wiki.md: page was a 'Coming soon' stub; expand it with both Azure Pipelines and GitHub Actions tabs covering the same scenario, matching the convention used elsewhere in the docs. - MONOREPO_MIGRATION.md: under 'Future Subtree Updates', add a callout reminding maintainers to delete upstream .azure-pipelines/ and nested .github/ directories after each subtree pull (the monorepo uses GH Actions only at the repo root). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * feat(release): dry-run trigger + PSDocs manifest URIs to monorepo Phase 3 code-side work for the release-strategy. - Add workflow_dispatch trigger to all three release-*.yml workflows with two inputs: 'tag' (string, required) and 'dry_run' (boolean, default true). When dry_run=true the workflows still parse the tag, validate the manifest, extract CHANGELOG notes, and build the package, but skip Publish-Module / vsce publish / gh release create and instead print a summary step. Lets maintainers sanity-check a release end-to-end without producing any artifacts on PSGallery, the VS Marketplace, or GitHub Releases. - Replace 'github.ref_name' with 'inputs.tag || github.ref_name' so push and dispatch paths share one code path. - packages/psdocs/src/PSDocs/PSDocs.psd1: redirect ProjectUri, LicenseUri, and ReleaseNotes from microsoft/PSDocs to the new monorepo (Azure/PSDocs.Azure/tree/main/packages/psdocs and the package's CHANGELOG). PSDocs.Azure.psd1 was already correct. - RELEASING.md: add a 'Pre-flight: dry-run' section that explains when and how to use the new dispatch trigger.
1 parent 5fa6504 commit 53fc6a2

11 files changed

Lines changed: 661 additions & 50 deletions

File tree

.github/workflows/release-psdocs-azure.yml

Lines changed: 63 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,17 @@ on:
44
push:
55
tags:
66
- 'psdocs-azure-v*'
7+
workflow_dispatch:
8+
inputs:
9+
tag:
10+
description: 'Tag to simulate (e.g. psdocs-azure-v0.5.0 or psdocs-azure-v0.5.0-preview.1)'
11+
required: true
12+
type: string
13+
dry_run:
14+
description: 'Skip Publish-Module and gh release create (validate + build only).'
15+
required: false
16+
type: boolean
17+
default: true
718

819
permissions: {}
920

@@ -29,29 +40,73 @@ jobs:
2940
Set-PSRepository -Name PSGallery -InstallationPolicy Trusted
3041
Install-Module -Name InvokeBuild -MinimumVersion 5.4.0 -Scope CurrentUser -Force
3142
32-
- name: Extract version from tag
43+
- name: Parse tag and detect channel
3344
id: version
45+
shell: pwsh
3446
run: |
35-
VERSION=${GITHUB_REF#refs/tags/psdocs-azure-v}
36-
echo "version=$VERSION" >> $GITHUB_OUTPUT
37-
47+
$tagRef = if ('${{ github.event_name }}' -eq 'workflow_dispatch') { '${{ inputs.tag }}' } else { '${{ github.ref_name }}' }
48+
$full = $tagRef -replace '^psdocs-azure-v', ''
49+
$base = ($full -split '-', 2)[0]
50+
$isPrerelease = $full.Contains('-')
51+
$channel = if ($isPrerelease) { 'preview' } else { 'stable' }
52+
"tag=$tagRef" | Out-File -FilePath $env:GITHUB_OUTPUT -Append
53+
"full=$full" | Out-File -FilePath $env:GITHUB_OUTPUT -Append
54+
"base=$base" | Out-File -FilePath $env:GITHUB_OUTPUT -Append
55+
"channel=$channel" | Out-File -FilePath $env:GITHUB_OUTPUT -Append
56+
"is_prerelease=$($isPrerelease.ToString().ToLower())" | Out-File -FilePath $env:GITHUB_OUTPUT -Append
57+
Write-Host "Tag: $tagRef -> full=$full base=$base channel=$channel"
58+
59+
- name: Validate version matches manifest
60+
shell: pwsh
61+
run: |
62+
$manifestPath = './packages/psdocs-azure/src/PSDocs.Azure/PSDocs.Azure.psd1'
63+
$manifest = Import-PowerShellDataFile -Path $manifestPath
64+
$expected = '${{ steps.version.outputs.base }}'
65+
if ($manifest.ModuleVersion -ne $expected) {
66+
Write-Error "Manifest ModuleVersion '$($manifest.ModuleVersion)' does not match tag base version '$expected'. Update $manifestPath before tagging."
67+
exit 1
68+
}
69+
Write-Host "Manifest ModuleVersion '$($manifest.ModuleVersion)' matches tag base '$expected'."
70+
71+
- name: Extract release notes
72+
shell: pwsh
73+
run: |
74+
./scripts/extract-release-notes.ps1 `
75+
-ChangelogPath ./CHANGELOG.md `
76+
-Version '${{ steps.version.outputs.full }}' `
77+
-OutputPath ./release-notes.md
78+
3879
- name: Build
3980
shell: pwsh
4081
working-directory: packages/psdocs-azure
4182
run: |
42-
Invoke-Build Build -File ./pipeline.build.ps1 -Build '${{ steps.version.outputs.version }}'
83+
Invoke-Build Build -File ./pipeline.build.ps1 -Build '${{ steps.version.outputs.full }}'
4384
4485
- name: Publish to PSGallery
86+
if: ${{ github.event_name != 'workflow_dispatch' || inputs.dry_run != true }}
4587
shell: pwsh
4688
env:
4789
PSGALLERY_API_KEY: ${{ secrets.PSGALLERY_API_KEY }}
4890
run: |
4991
Publish-Module -Path packages/psdocs-azure/out/modules/PSDocs.Azure -NuGetApiKey $env:PSGALLERY_API_KEY
5092
5193
- name: Create GitHub Release
94+
if: ${{ github.event_name != 'workflow_dispatch' || inputs.dry_run != true }}
5295
env:
5396
GH_TOKEN: ${{ github.token }}
97+
PRERELEASE_FLAG: ${{ steps.version.outputs.is_prerelease == 'true' && '--prerelease' || '' }}
98+
run: |
99+
gh release create "${{ steps.version.outputs.tag }}" \
100+
--title "PSDocs.Azure v${{ steps.version.outputs.full }}" \
101+
--notes-file ./release-notes.md \
102+
$PRERELEASE_FLAG
103+
104+
- name: Dry-run summary
105+
if: ${{ github.event_name == 'workflow_dispatch' && inputs.dry_run == true }}
106+
shell: pwsh
54107
run: |
55-
gh release create "${{ github.ref_name }}" \
56-
--title "PSDocs.Azure v${{ steps.version.outputs.version }}" \
57-
--notes-file CHANGELOG.md
108+
Write-Host '=== DRY RUN ==='
109+
Write-Host "Would publish PSDocs.Azure v${{ steps.version.outputs.full }} (channel=${{ steps.version.outputs.channel }}) to PSGallery."
110+
Write-Host "Would create GitHub Release for tag ${{ steps.version.outputs.tag }}."
111+
Write-Host '--- release notes ---'
112+
Get-Content ./release-notes.md

.github/workflows/release-psdocs.yml

Lines changed: 64 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,17 @@ on:
44
push:
55
tags:
66
- 'psdocs-v*'
7+
workflow_dispatch:
8+
inputs:
9+
tag:
10+
description: 'Tag to simulate (e.g. psdocs-v0.10.0 or psdocs-v0.10.0-preview.1)'
11+
required: true
12+
type: string
13+
dry_run:
14+
description: 'Skip Publish-Module and gh release create (validate + build only).'
15+
required: false
16+
type: boolean
17+
default: true
718

819
permissions: {}
920

@@ -29,29 +40,74 @@ jobs:
2940
Set-PSRepository -Name PSGallery -InstallationPolicy Trusted
3041
Install-Module -Name InvokeBuild -MinimumVersion 5.4.0 -Scope CurrentUser -Force
3142
32-
- name: Extract version from tag
43+
- name: Parse tag and detect channel
3344
id: version
45+
shell: pwsh
3446
run: |
35-
VERSION=${GITHUB_REF#refs/tags/psdocs-v}
36-
echo "version=$VERSION" >> $GITHUB_OUTPUT
37-
47+
$tagRef = if ('${{ github.event_name }}' -eq 'workflow_dispatch') { '${{ inputs.tag }}' } else { '${{ github.ref_name }}' }
48+
$full = $tagRef -replace '^psdocs-v', ''
49+
$base = ($full -split '-', 2)[0]
50+
$isPrerelease = $full.Contains('-')
51+
$channel = if ($isPrerelease) { 'preview' } else { 'stable' }
52+
"tag=$tagRef" | Out-File -FilePath $env:GITHUB_OUTPUT -Append
53+
"full=$full" | Out-File -FilePath $env:GITHUB_OUTPUT -Append
54+
"base=$base" | Out-File -FilePath $env:GITHUB_OUTPUT -Append
55+
"channel=$channel" | Out-File -FilePath $env:GITHUB_OUTPUT -Append
56+
"is_prerelease=$($isPrerelease.ToString().ToLower())" | Out-File -FilePath $env:GITHUB_OUTPUT -Append
57+
Write-Host "Tag: $tagRef -> full=$full base=$base channel=$channel"
58+
59+
- name: Validate version matches manifest
60+
shell: pwsh
61+
run: |
62+
$manifestPath = './packages/psdocs/src/PSDocs/PSDocs.psd1'
63+
$manifest = Import-PowerShellDataFile -Path $manifestPath
64+
$expected = '${{ steps.version.outputs.base }}'
65+
if ($manifest.ModuleVersion -ne $expected) {
66+
Write-Error "Manifest ModuleVersion '$($manifest.ModuleVersion)' does not match tag base version '$expected'. Update $manifestPath before tagging."
67+
exit 1
68+
}
69+
Write-Host "Manifest ModuleVersion '$($manifest.ModuleVersion)' matches tag base '$expected'."
70+
71+
- name: Extract release notes
72+
id: notes
73+
shell: pwsh
74+
run: |
75+
./scripts/extract-release-notes.ps1 `
76+
-ChangelogPath ./packages/psdocs/CHANGELOG.md `
77+
-Version '${{ steps.version.outputs.full }}' `
78+
-OutputPath ./release-notes.md
79+
3880
- name: Build
3981
shell: pwsh
4082
working-directory: packages/psdocs
4183
run: |
42-
Invoke-Build Build -File ./pipeline.build.ps1 -Build '${{ steps.version.outputs.version }}'
84+
Invoke-Build Build -File ./pipeline.build.ps1 -Build '${{ steps.version.outputs.full }}'
4385
4486
- name: Publish to PSGallery
87+
if: ${{ github.event_name != 'workflow_dispatch' || inputs.dry_run != true }}
4588
shell: pwsh
4689
env:
4790
PSGALLERY_API_KEY: ${{ secrets.PSGALLERY_API_KEY }}
4891
run: |
4992
Publish-Module -Path packages/psdocs/out/modules/PSDocs -NuGetApiKey $env:PSGALLERY_API_KEY
5093
5194
- name: Create GitHub Release
95+
if: ${{ github.event_name != 'workflow_dispatch' || inputs.dry_run != true }}
5296
env:
5397
GH_TOKEN: ${{ github.token }}
98+
PRERELEASE_FLAG: ${{ steps.version.outputs.is_prerelease == 'true' && '--prerelease' || '' }}
99+
run: |
100+
gh release create "${{ steps.version.outputs.tag }}" \
101+
--title "PSDocs v${{ steps.version.outputs.full }}" \
102+
--notes-file ./release-notes.md \
103+
$PRERELEASE_FLAG
104+
105+
- name: Dry-run summary
106+
if: ${{ github.event_name == 'workflow_dispatch' && inputs.dry_run == true }}
107+
shell: pwsh
54108
run: |
55-
gh release create "${{ github.ref_name }}" \
56-
--title "PSDocs v${{ steps.version.outputs.version }}" \
57-
--notes-file packages/psdocs/CHANGELOG.md
109+
Write-Host '=== DRY RUN ==='
110+
Write-Host "Would publish PSDocs v${{ steps.version.outputs.full }} (channel=${{ steps.version.outputs.channel }}) to PSGallery."
111+
Write-Host "Would create GitHub Release for tag ${{ steps.version.outputs.tag }}."
112+
Write-Host '--- release notes ---'
113+
Get-Content ./release-notes.md

.github/workflows/release-vscode.yml

Lines changed: 85 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,36 @@
1-
# Release VS Code Extension (Stable)
2-
# Triggered by vscode-v* tags for stable releases
1+
# Release VS Code Extension
2+
# Stable: tag `vscode-vX.Y.Z` -> extension `vicperdana.psdocs-vscode`
3+
# Preview: tag `vscode-preview-vX.Y.Z` -> extension `vicperdana.psdocs-vscode-preview` (separate Marketplace listing)
4+
#
5+
# VS Code Marketplace does not accept SemVer prerelease suffixes (e.g. -preview.1) in
6+
# the package version, so the channel is encoded in the tag prefix and routed to the
7+
# build pipeline via -Channel. Both channels use plain X.Y.Z versions in package.json.
38

49
name: Release VS Code Extension
510

611
on:
712
push:
813
tags:
914
- 'vscode-v*'
15+
- 'vscode-preview-v*'
16+
workflow_dispatch:
17+
inputs:
18+
tag:
19+
description: 'Tag to simulate (e.g. vscode-v0.4.0 or vscode-preview-v0.4.0)'
20+
required: true
21+
type: string
22+
dry_run:
23+
description: 'Skip vsce publish and gh release create (validate + build only).'
24+
required: false
25+
type: boolean
26+
default: true
1027

1128
permissions:
1229
contents: write
1330

1431
jobs:
1532
release:
16-
name: Release Stable
33+
name: Release
1734
runs-on: ubuntu-latest
1835
environment: release
1936

@@ -34,22 +51,61 @@ jobs:
3451
Set-PSRepository -Name PSGallery -InstallationPolicy Trusted
3552
Install-Module -Name InvokeBuild -MinimumVersion 5.4.0 -Scope CurrentUser -Force
3653
37-
- name: Extract version from tag
54+
- name: Parse tag and detect channel
3855
id: version
56+
shell: pwsh
3957
run: |
40-
VERSION=${GITHUB_REF#refs/tags/vscode-v}
41-
echo "version=$VERSION" >> $GITHUB_OUTPUT
42-
echo "Releasing version: $VERSION"
58+
$tag = if ('${{ github.event_name }}' -eq 'workflow_dispatch') { '${{ inputs.tag }}' } else { '${{ github.ref_name }}' }
59+
if ($tag -match '^vscode-preview-v(.+)$') {
60+
$version = $Matches[1]
61+
$channel = 'preview'
62+
$isPrerelease = $true
63+
} elseif ($tag -match '^vscode-v(.+)$') {
64+
$version = $Matches[1]
65+
$channel = 'stable'
66+
$isPrerelease = $false
67+
} else {
68+
Write-Error "Tag '$tag' does not match expected pattern vscode-v* or vscode-preview-v*"
69+
exit 1
70+
}
71+
if ($version -notmatch '^\d+\.\d+\.\d+$') {
72+
Write-Error "Version '$version' must be plain SemVer (X.Y.Z) — VS Code Marketplace does not accept prerelease suffixes."
73+
exit 1
74+
}
75+
"tag=$tag" | Out-File -FilePath $env:GITHUB_OUTPUT -Append
76+
"version=$version" | Out-File -FilePath $env:GITHUB_OUTPUT -Append
77+
"channel=$channel" | Out-File -FilePath $env:GITHUB_OUTPUT -Append
78+
"is_prerelease=$($isPrerelease.ToString().ToLower())" | Out-File -FilePath $env:GITHUB_OUTPUT -Append
79+
Write-Host "Tag: $tag -> version=$version channel=$channel"
80+
81+
- name: Validate version matches package.json
82+
shell: pwsh
83+
run: |
84+
$pkg = Get-Content ./packages/vscode-extension/package.json -Raw | ConvertFrom-Json
85+
$expected = '${{ steps.version.outputs.version }}'
86+
if ($pkg.version -ne $expected) {
87+
Write-Error "package.json version '$($pkg.version)' does not match tag version '$expected'. Update packages/vscode-extension/package.json before tagging."
88+
exit 1
89+
}
90+
Write-Host "package.json version '$($pkg.version)' matches tag '$expected'."
91+
92+
- name: Extract release notes
93+
shell: pwsh
94+
run: |
95+
./scripts/extract-release-notes.ps1 `
96+
-ChangelogPath ./packages/vscode-extension/CHANGELOG.md `
97+
-Version '${{ steps.version.outputs.version }}' `
98+
-OutputPath ./release-notes.md
4399
44100
- name: Install dependencies
45101
working-directory: packages/vscode-extension
46102
run: npm ci
47103

48-
- name: Build stable channel
104+
- name: Build (${{ steps.version.outputs.channel }})
49105
shell: pwsh
50106
working-directory: packages/vscode-extension
51107
run: |
52-
Invoke-Build Build -Channel 'stable' -Build '${{ steps.version.outputs.version }}'
108+
Invoke-Build Build -Channel '${{ steps.version.outputs.channel }}' -Build '${{ steps.version.outputs.version }}'
53109
54110
- name: Find VSIX file
55111
id: vsix
@@ -65,17 +121,33 @@ jobs:
65121
echo "Found VSIX: $VSIX_FILE"
66122
67123
- name: Publish to VS Marketplace
124+
if: ${{ github.event_name != 'workflow_dispatch' || inputs.dry_run != true }}
68125
working-directory: packages/vscode-extension/out/package
69126
env:
70127
VSCE_PAT: ${{ secrets.VSCE_PAT }}
128+
PRE_RELEASE_FLAG: ${{ steps.version.outputs.is_prerelease == 'true' && '--pre-release' || '' }}
71129
run: |
72-
npx @vscode/vsce publish --packagePath "${{ steps.vsix.outputs.file }}" --pat "$VSCE_PAT"
130+
npx @vscode/vsce publish --packagePath "${{ steps.vsix.outputs.file }}" --pat "$VSCE_PAT" $PRE_RELEASE_FLAG
73131
74132
- name: Create GitHub Release
133+
if: ${{ github.event_name != 'workflow_dispatch' || inputs.dry_run != true }}
75134
env:
76135
GH_TOKEN: ${{ github.token }}
136+
PRERELEASE_FLAG: ${{ steps.version.outputs.is_prerelease == 'true' && '--prerelease' || '' }}
137+
TITLE_SUFFIX: ${{ steps.version.outputs.is_prerelease == 'true' && ' (Preview)' || '' }}
77138
run: |
78-
gh release create "${{ github.ref_name }}" \
79-
--title "VS Code Extension v${{ steps.version.outputs.version }}" \
80-
--notes-file packages/vscode-extension/CHANGELOG.md \
139+
gh release create "${{ steps.version.outputs.tag }}" \
140+
--title "VS Code Extension v${{ steps.version.outputs.version }}$TITLE_SUFFIX" \
141+
--notes-file ./release-notes.md \
142+
$PRERELEASE_FLAG \
81143
"${{ steps.vsix.outputs.path }}"
144+
145+
- name: Dry-run summary
146+
if: ${{ github.event_name == 'workflow_dispatch' && inputs.dry_run == true }}
147+
shell: pwsh
148+
run: |
149+
Write-Host '=== DRY RUN ==='
150+
Write-Host "Would publish VS Code extension v${{ steps.version.outputs.version }} (channel=${{ steps.version.outputs.channel }}) to VS Marketplace."
151+
Write-Host "Would create GitHub Release for tag ${{ steps.version.outputs.tag }} with VSIX ${{ steps.vsix.outputs.file }}."
152+
Write-Host '--- release notes ---'
153+
Get-Content ./release-notes.md

CONTRIBUTING.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,8 @@ Check out the links below to get started.
5757

5858
### Release Process
5959

60-
Refer to [release process](docs/release.md)
60+
See [RELEASING.md](RELEASING.md) for the full release runbook covering all three packages
61+
(PSDocs, PSDocs.Azure, VS Code extension), tag conventions, and the pre-release flow.
6162

6263
## Thank You!
6364

MONOREPO_MIGRATION.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,20 @@ git subtree pull --prefix=packages/psdocs https://github.com/microsoft/PSDocs.gi
145145
git subtree pull --prefix=packages/vscode-extension https://github.com/microsoft/PSDocs-vscode.git main --squash
146146
```
147147

148+
> **After any subtree pull**, audit the result and remove anything we don't
149+
> want to inherit from upstream:
150+
>
151+
> - `packages/*/.azure-pipelines/` and `packages/*/azure-pipelines*.yaml`
152+
> the monorepo uses GitHub Actions (`/.github/workflows/`); upstream ADO
153+
> pipelines are not active here and should be deleted to avoid confusion.
154+
> - `packages/*/.github/workflows/` — workflows nested inside packages do
155+
> not run on GitHub. Either delete them or, if a workflow is genuinely
156+
> wanted, move and adapt it under root `.github/workflows/` with
157+
> appropriate path filters.
158+
> - Any upstream `.github/dependabot.yml` / `.github/CODEOWNERS` etc. —
159+
> monorepo policy lives at the repo root and should not be shadowed by
160+
> per-package copies.
161+
148162
## Workflow Migration
149163

150164
The following legacy workflows were removed as part of the monorepo migration, replaced by new workflows with path-based filtering:

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -339,6 +339,8 @@ For a list of module changes please see the [change log](CHANGELOG.md).
339339
> Pre-release versions should be considered experimental.
340340
> Modules and change log details for pre-releases will be removed as standard releases are made available.
341341

342+
For maintainers cutting a release, see [RELEASING.md](RELEASING.md).
343+
342344
## Contributing
343345

344346
This project welcomes contributions and suggestions.

0 commit comments

Comments
 (0)