diff --git a/.github/workflows/release-psdocs-azure.yml b/.github/workflows/release-psdocs-azure.yml index e0ae56d0..3280b6c6 100644 --- a/.github/workflows/release-psdocs-azure.yml +++ b/.github/workflows/release-psdocs-azure.yml @@ -4,6 +4,17 @@ on: push: tags: - 'psdocs-azure-v*' + workflow_dispatch: + inputs: + tag: + description: 'Tag to simulate (e.g. psdocs-azure-v0.5.0 or psdocs-azure-v0.5.0-preview.1)' + required: true + type: string + dry_run: + description: 'Skip Publish-Module and gh release create (validate + build only).' + required: false + type: boolean + default: true permissions: {} @@ -29,19 +40,50 @@ jobs: Set-PSRepository -Name PSGallery -InstallationPolicy Trusted Install-Module -Name InvokeBuild -MinimumVersion 5.4.0 -Scope CurrentUser -Force - - name: Extract version from tag + - name: Parse tag and detect channel id: version + shell: pwsh run: | - VERSION=${GITHUB_REF#refs/tags/psdocs-azure-v} - echo "version=$VERSION" >> $GITHUB_OUTPUT - + $tagRef = if ('${{ github.event_name }}' -eq 'workflow_dispatch') { '${{ inputs.tag }}' } else { '${{ github.ref_name }}' } + $full = $tagRef -replace '^psdocs-azure-v', '' + $base = ($full -split '-', 2)[0] + $isPrerelease = $full.Contains('-') + $channel = if ($isPrerelease) { 'preview' } else { 'stable' } + "tag=$tagRef" | Out-File -FilePath $env:GITHUB_OUTPUT -Append + "full=$full" | Out-File -FilePath $env:GITHUB_OUTPUT -Append + "base=$base" | Out-File -FilePath $env:GITHUB_OUTPUT -Append + "channel=$channel" | Out-File -FilePath $env:GITHUB_OUTPUT -Append + "is_prerelease=$($isPrerelease.ToString().ToLower())" | Out-File -FilePath $env:GITHUB_OUTPUT -Append + Write-Host "Tag: $tagRef -> full=$full base=$base channel=$channel" + + - name: Validate version matches manifest + shell: pwsh + run: | + $manifestPath = './packages/psdocs-azure/src/PSDocs.Azure/PSDocs.Azure.psd1' + $manifest = Import-PowerShellDataFile -Path $manifestPath + $expected = '${{ steps.version.outputs.base }}' + if ($manifest.ModuleVersion -ne $expected) { + Write-Error "Manifest ModuleVersion '$($manifest.ModuleVersion)' does not match tag base version '$expected'. Update $manifestPath before tagging." + exit 1 + } + Write-Host "Manifest ModuleVersion '$($manifest.ModuleVersion)' matches tag base '$expected'." + + - name: Extract release notes + shell: pwsh + run: | + ./scripts/extract-release-notes.ps1 ` + -ChangelogPath ./CHANGELOG.md ` + -Version '${{ steps.version.outputs.full }}' ` + -OutputPath ./release-notes.md + - name: Build shell: pwsh working-directory: packages/psdocs-azure run: | - Invoke-Build Build -File ./pipeline.build.ps1 -Build '${{ steps.version.outputs.version }}' + Invoke-Build Build -File ./pipeline.build.ps1 -Build '${{ steps.version.outputs.full }}' - name: Publish to PSGallery + if: ${{ github.event_name != 'workflow_dispatch' || inputs.dry_run != true }} shell: pwsh env: PSGALLERY_API_KEY: ${{ secrets.PSGALLERY_API_KEY }} @@ -49,9 +91,22 @@ jobs: Publish-Module -Path packages/psdocs-azure/out/modules/PSDocs.Azure -NuGetApiKey $env:PSGALLERY_API_KEY - name: Create GitHub Release + if: ${{ github.event_name != 'workflow_dispatch' || inputs.dry_run != true }} env: GH_TOKEN: ${{ github.token }} + PRERELEASE_FLAG: ${{ steps.version.outputs.is_prerelease == 'true' && '--prerelease' || '' }} + run: | + gh release create "${{ steps.version.outputs.tag }}" \ + --title "PSDocs.Azure v${{ steps.version.outputs.full }}" \ + --notes-file ./release-notes.md \ + $PRERELEASE_FLAG + + - name: Dry-run summary + if: ${{ github.event_name == 'workflow_dispatch' && inputs.dry_run == true }} + shell: pwsh run: | - gh release create "${{ github.ref_name }}" \ - --title "PSDocs.Azure v${{ steps.version.outputs.version }}" \ - --notes-file CHANGELOG.md + Write-Host '=== DRY RUN ===' + Write-Host "Would publish PSDocs.Azure v${{ steps.version.outputs.full }} (channel=${{ steps.version.outputs.channel }}) to PSGallery." + Write-Host "Would create GitHub Release for tag ${{ steps.version.outputs.tag }}." + Write-Host '--- release notes ---' + Get-Content ./release-notes.md diff --git a/.github/workflows/release-psdocs.yml b/.github/workflows/release-psdocs.yml index 4fdf542d..5164bea9 100644 --- a/.github/workflows/release-psdocs.yml +++ b/.github/workflows/release-psdocs.yml @@ -4,6 +4,17 @@ on: push: tags: - 'psdocs-v*' + workflow_dispatch: + inputs: + tag: + description: 'Tag to simulate (e.g. psdocs-v0.10.0 or psdocs-v0.10.0-preview.1)' + required: true + type: string + dry_run: + description: 'Skip Publish-Module and gh release create (validate + build only).' + required: false + type: boolean + default: true permissions: {} @@ -29,19 +40,51 @@ jobs: Set-PSRepository -Name PSGallery -InstallationPolicy Trusted Install-Module -Name InvokeBuild -MinimumVersion 5.4.0 -Scope CurrentUser -Force - - name: Extract version from tag + - name: Parse tag and detect channel id: version + shell: pwsh run: | - VERSION=${GITHUB_REF#refs/tags/psdocs-v} - echo "version=$VERSION" >> $GITHUB_OUTPUT - + $tagRef = if ('${{ github.event_name }}' -eq 'workflow_dispatch') { '${{ inputs.tag }}' } else { '${{ github.ref_name }}' } + $full = $tagRef -replace '^psdocs-v', '' + $base = ($full -split '-', 2)[0] + $isPrerelease = $full.Contains('-') + $channel = if ($isPrerelease) { 'preview' } else { 'stable' } + "tag=$tagRef" | Out-File -FilePath $env:GITHUB_OUTPUT -Append + "full=$full" | Out-File -FilePath $env:GITHUB_OUTPUT -Append + "base=$base" | Out-File -FilePath $env:GITHUB_OUTPUT -Append + "channel=$channel" | Out-File -FilePath $env:GITHUB_OUTPUT -Append + "is_prerelease=$($isPrerelease.ToString().ToLower())" | Out-File -FilePath $env:GITHUB_OUTPUT -Append + Write-Host "Tag: $tagRef -> full=$full base=$base channel=$channel" + + - name: Validate version matches manifest + shell: pwsh + run: | + $manifestPath = './packages/psdocs/src/PSDocs/PSDocs.psd1' + $manifest = Import-PowerShellDataFile -Path $manifestPath + $expected = '${{ steps.version.outputs.base }}' + if ($manifest.ModuleVersion -ne $expected) { + Write-Error "Manifest ModuleVersion '$($manifest.ModuleVersion)' does not match tag base version '$expected'. Update $manifestPath before tagging." + exit 1 + } + Write-Host "Manifest ModuleVersion '$($manifest.ModuleVersion)' matches tag base '$expected'." + + - name: Extract release notes + id: notes + shell: pwsh + run: | + ./scripts/extract-release-notes.ps1 ` + -ChangelogPath ./packages/psdocs/CHANGELOG.md ` + -Version '${{ steps.version.outputs.full }}' ` + -OutputPath ./release-notes.md + - name: Build shell: pwsh working-directory: packages/psdocs run: | - Invoke-Build Build -File ./pipeline.build.ps1 -Build '${{ steps.version.outputs.version }}' + Invoke-Build Build -File ./pipeline.build.ps1 -Build '${{ steps.version.outputs.full }}' - name: Publish to PSGallery + if: ${{ github.event_name != 'workflow_dispatch' || inputs.dry_run != true }} shell: pwsh env: PSGALLERY_API_KEY: ${{ secrets.PSGALLERY_API_KEY }} @@ -49,9 +92,22 @@ jobs: Publish-Module -Path packages/psdocs/out/modules/PSDocs -NuGetApiKey $env:PSGALLERY_API_KEY - name: Create GitHub Release + if: ${{ github.event_name != 'workflow_dispatch' || inputs.dry_run != true }} env: GH_TOKEN: ${{ github.token }} + PRERELEASE_FLAG: ${{ steps.version.outputs.is_prerelease == 'true' && '--prerelease' || '' }} + run: | + gh release create "${{ steps.version.outputs.tag }}" \ + --title "PSDocs v${{ steps.version.outputs.full }}" \ + --notes-file ./release-notes.md \ + $PRERELEASE_FLAG + + - name: Dry-run summary + if: ${{ github.event_name == 'workflow_dispatch' && inputs.dry_run == true }} + shell: pwsh run: | - gh release create "${{ github.ref_name }}" \ - --title "PSDocs v${{ steps.version.outputs.version }}" \ - --notes-file packages/psdocs/CHANGELOG.md + Write-Host '=== DRY RUN ===' + Write-Host "Would publish PSDocs v${{ steps.version.outputs.full }} (channel=${{ steps.version.outputs.channel }}) to PSGallery." + Write-Host "Would create GitHub Release for tag ${{ steps.version.outputs.tag }}." + Write-Host '--- release notes ---' + Get-Content ./release-notes.md diff --git a/.github/workflows/release-vscode.yml b/.github/workflows/release-vscode.yml index 54ffc1fa..885bf634 100644 --- a/.github/workflows/release-vscode.yml +++ b/.github/workflows/release-vscode.yml @@ -1,5 +1,10 @@ -# Release VS Code Extension (Stable) -# Triggered by vscode-v* tags for stable releases +# Release VS Code Extension +# Stable: tag `vscode-vX.Y.Z` -> extension `vicperdana.psdocs-vscode` +# Preview: tag `vscode-preview-vX.Y.Z` -> extension `vicperdana.psdocs-vscode-preview` (separate Marketplace listing) +# +# VS Code Marketplace does not accept SemVer prerelease suffixes (e.g. -preview.1) in +# the package version, so the channel is encoded in the tag prefix and routed to the +# build pipeline via -Channel. Both channels use plain X.Y.Z versions in package.json. name: Release VS Code Extension @@ -7,13 +12,25 @@ on: push: tags: - 'vscode-v*' + - 'vscode-preview-v*' + workflow_dispatch: + inputs: + tag: + description: 'Tag to simulate (e.g. vscode-v0.4.0 or vscode-preview-v0.4.0)' + required: true + type: string + dry_run: + description: 'Skip vsce publish and gh release create (validate + build only).' + required: false + type: boolean + default: true permissions: contents: write jobs: release: - name: Release Stable + name: Release runs-on: ubuntu-latest environment: release @@ -34,22 +51,61 @@ jobs: Set-PSRepository -Name PSGallery -InstallationPolicy Trusted Install-Module -Name InvokeBuild -MinimumVersion 5.4.0 -Scope CurrentUser -Force - - name: Extract version from tag + - name: Parse tag and detect channel id: version + shell: pwsh run: | - VERSION=${GITHUB_REF#refs/tags/vscode-v} - echo "version=$VERSION" >> $GITHUB_OUTPUT - echo "Releasing version: $VERSION" + $tag = if ('${{ github.event_name }}' -eq 'workflow_dispatch') { '${{ inputs.tag }}' } else { '${{ github.ref_name }}' } + if ($tag -match '^vscode-preview-v(.+)$') { + $version = $Matches[1] + $channel = 'preview' + $isPrerelease = $true + } elseif ($tag -match '^vscode-v(.+)$') { + $version = $Matches[1] + $channel = 'stable' + $isPrerelease = $false + } else { + Write-Error "Tag '$tag' does not match expected pattern vscode-v* or vscode-preview-v*" + exit 1 + } + if ($version -notmatch '^\d+\.\d+\.\d+$') { + Write-Error "Version '$version' must be plain SemVer (X.Y.Z) — VS Code Marketplace does not accept prerelease suffixes." + exit 1 + } + "tag=$tag" | Out-File -FilePath $env:GITHUB_OUTPUT -Append + "version=$version" | Out-File -FilePath $env:GITHUB_OUTPUT -Append + "channel=$channel" | Out-File -FilePath $env:GITHUB_OUTPUT -Append + "is_prerelease=$($isPrerelease.ToString().ToLower())" | Out-File -FilePath $env:GITHUB_OUTPUT -Append + Write-Host "Tag: $tag -> version=$version channel=$channel" + + - name: Validate version matches package.json + shell: pwsh + run: | + $pkg = Get-Content ./packages/vscode-extension/package.json -Raw | ConvertFrom-Json + $expected = '${{ steps.version.outputs.version }}' + if ($pkg.version -ne $expected) { + Write-Error "package.json version '$($pkg.version)' does not match tag version '$expected'. Update packages/vscode-extension/package.json before tagging." + exit 1 + } + Write-Host "package.json version '$($pkg.version)' matches tag '$expected'." + + - name: Extract release notes + shell: pwsh + run: | + ./scripts/extract-release-notes.ps1 ` + -ChangelogPath ./packages/vscode-extension/CHANGELOG.md ` + -Version '${{ steps.version.outputs.version }}' ` + -OutputPath ./release-notes.md - name: Install dependencies working-directory: packages/vscode-extension run: npm ci - - name: Build stable channel + - name: Build (${{ steps.version.outputs.channel }}) shell: pwsh working-directory: packages/vscode-extension run: | - Invoke-Build Build -Channel 'stable' -Build '${{ steps.version.outputs.version }}' + Invoke-Build Build -Channel '${{ steps.version.outputs.channel }}' -Build '${{ steps.version.outputs.version }}' - name: Find VSIX file id: vsix @@ -65,17 +121,33 @@ jobs: echo "Found VSIX: $VSIX_FILE" - name: Publish to VS Marketplace + if: ${{ github.event_name != 'workflow_dispatch' || inputs.dry_run != true }} working-directory: packages/vscode-extension/out/package env: VSCE_PAT: ${{ secrets.VSCE_PAT }} + PRE_RELEASE_FLAG: ${{ steps.version.outputs.is_prerelease == 'true' && '--pre-release' || '' }} run: | - npx @vscode/vsce publish --packagePath "${{ steps.vsix.outputs.file }}" --pat "$VSCE_PAT" + npx @vscode/vsce publish --packagePath "${{ steps.vsix.outputs.file }}" --pat "$VSCE_PAT" $PRE_RELEASE_FLAG - name: Create GitHub Release + if: ${{ github.event_name != 'workflow_dispatch' || inputs.dry_run != true }} env: GH_TOKEN: ${{ github.token }} + PRERELEASE_FLAG: ${{ steps.version.outputs.is_prerelease == 'true' && '--prerelease' || '' }} + TITLE_SUFFIX: ${{ steps.version.outputs.is_prerelease == 'true' && ' (Preview)' || '' }} run: | - gh release create "${{ github.ref_name }}" \ - --title "VS Code Extension v${{ steps.version.outputs.version }}" \ - --notes-file packages/vscode-extension/CHANGELOG.md \ + gh release create "${{ steps.version.outputs.tag }}" \ + --title "VS Code Extension v${{ steps.version.outputs.version }}$TITLE_SUFFIX" \ + --notes-file ./release-notes.md \ + $PRERELEASE_FLAG \ "${{ steps.vsix.outputs.path }}" + + - name: Dry-run summary + if: ${{ github.event_name == 'workflow_dispatch' && inputs.dry_run == true }} + shell: pwsh + run: | + Write-Host '=== DRY RUN ===' + Write-Host "Would publish VS Code extension v${{ steps.version.outputs.version }} (channel=${{ steps.version.outputs.channel }}) to VS Marketplace." + Write-Host "Would create GitHub Release for tag ${{ steps.version.outputs.tag }} with VSIX ${{ steps.vsix.outputs.file }}." + Write-Host '--- release notes ---' + Get-Content ./release-notes.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c2fc6bec..dc70a4f3 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -57,7 +57,8 @@ Check out the links below to get started. ### Release Process -Refer to [release process](docs/release.md) +See [RELEASING.md](RELEASING.md) for the full release runbook covering all three packages +(PSDocs, PSDocs.Azure, VS Code extension), tag conventions, and the pre-release flow. ## Thank You! diff --git a/MONOREPO_MIGRATION.md b/MONOREPO_MIGRATION.md index ad0bf355..7ccd1f71 100644 --- a/MONOREPO_MIGRATION.md +++ b/MONOREPO_MIGRATION.md @@ -145,6 +145,20 @@ git subtree pull --prefix=packages/psdocs https://github.com/microsoft/PSDocs.gi git subtree pull --prefix=packages/vscode-extension https://github.com/microsoft/PSDocs-vscode.git main --squash ``` +> **After any subtree pull**, audit the result and remove anything we don't +> want to inherit from upstream: +> +> - `packages/*/.azure-pipelines/` and `packages/*/azure-pipelines*.yaml` — +> the monorepo uses GitHub Actions (`/.github/workflows/`); upstream ADO +> pipelines are not active here and should be deleted to avoid confusion. +> - `packages/*/.github/workflows/` — workflows nested inside packages do +> not run on GitHub. Either delete them or, if a workflow is genuinely +> wanted, move and adapt it under root `.github/workflows/` with +> appropriate path filters. +> - Any upstream `.github/dependabot.yml` / `.github/CODEOWNERS` etc. — +> monorepo policy lives at the repo root and should not be shadowed by +> per-package copies. + ## Workflow Migration The following legacy workflows were removed as part of the monorepo migration, replaced by new workflows with path-based filtering: diff --git a/README.md b/README.md index 7db77f6e..969e423e 100644 --- a/README.md +++ b/README.md @@ -339,6 +339,8 @@ For a list of module changes please see the [change log](CHANGELOG.md). > Pre-release versions should be considered experimental. > Modules and change log details for pre-releases will be removed as standard releases are made available. +For maintainers cutting a release, see [RELEASING.md](RELEASING.md). + ## Contributing This project welcomes contributions and suggestions. diff --git a/RELEASING.md b/RELEASING.md new file mode 100644 index 00000000..c8880b70 --- /dev/null +++ b/RELEASING.md @@ -0,0 +1,256 @@ +# Releasing + +This monorepo ships three independently-versioned packages. Each has its own +release workflow, tag convention, and publish destination. + +| Package | Tag (stable) | Tag (preview) | Publishes to | +| ----------------- | -------------------------- | ------------------------------------ | ------------------------------------------- | +| PSDocs | `psdocs-vX.Y.Z` | `psdocs-vX.Y.Z-preview.N` | PowerShell Gallery (`PSDocs`) | +| PSDocs.Azure | `psdocs-azure-vX.Y.Z` | `psdocs-azure-vX.Y.Z-preview.N` | PowerShell Gallery (`PSDocs.Azure`) | +| VS Code Extension | `vscode-vX.Y.Z` | `vscode-preview-vX.Y.Z` | VS Marketplace (`vicperdana.psdocs-vscode` / `…-preview`) | + +All three workflows also create a GitHub Release on this repo with notes +extracted from the relevant `CHANGELOG.md` and (for VS Code) the `.vsix` +attached. + +> **VS Code is special.** The Marketplace does not accept SemVer prerelease +> suffixes (`-preview.N`) in the package version. Channel is encoded in the +> **tag prefix** (`vscode-preview-v…`) and routed via the build pipeline's +> `-Channel preview` parameter, which produces a separate extension ID +> (`vicperdana.psdocs-vscode-preview`). Both channels publish plain `X.Y.Z` +> versions. + +## One-time prerequisites + +### 1. PowerShell Gallery API key + +1. Sign in at with an account that is a + co-owner of `PSDocs` and `PSDocs.Azure`. +2. **Profile → API Keys → Create**: + - Glob pattern: `PSDocs;PSDocs.Azure` + - Scopes: `Push new packages and package versions` + `Unlist packages` + - Expiration: 365 days (calendar a renewal reminder). +3. Copy the key. +4. In this repo: **Settings → Secrets and variables → Actions → New repository + secret**, name `PSGALLERY_API_KEY`. + +### 2. VS Marketplace personal access token + +1. At (the Azure DevOps org backing the + `vicperdana` Marketplace publisher), open **User settings → Personal access + tokens → New Token**: + - Organization: **All accessible organizations** + - Scopes: **Custom defined → Marketplace → Manage** + - Expiration: 365 days max. +2. Copy the token. +3. In this repo: add it as a secret named `VSCE_PAT`. + +### 3. `release` GitHub Environment (manual approval gate) + +1. **Settings → Environments → New environment** named `release`. +2. **Required reviewers**: add yourself (and optionally a co-maintainer). +3. **Deployment branches and tags**: select **Selected branches and tags**, add + three tag rules: + - `psdocs-v*` + - `psdocs-azure-v*` + - `vscode-v*` + - `vscode-preview-v*` +4. Confirm — both `PSGALLERY_API_KEY` and `VSCE_PAT` are accessible to jobs + that target this environment. + +## Pre-flight: dry-run via workflow_dispatch + +Each release workflow accepts a manual trigger with two inputs: + +- **tag** — the tag you intend to push (e.g. `psdocs-azure-v0.5.0` or `vscode-preview-v0.4.0`). +- **dry_run** — defaults to `true`. When `true`, the workflow does *everything* except `Publish-Module` / `vsce publish` / `gh release create`. Use this to: + - Confirm the manifest version matches the planned tag. + - Confirm `scripts/extract-release-notes.ps1` finds and renders the right CHANGELOG section. + - Confirm the build artifact is produced (and, for VS Code, that the VSIX is named correctly). + +To run a dry-run: **Actions → Release {Package} → Run workflow → fill in `tag`, leave `dry_run` checked → Run**. + +Always do a dry-run before pushing the real tag for first-of-its-kind cases (first release after a manifest restructure, first time after upgrading a build dep, etc.). + +## Cutting a release + +The same shape applies to all three packages. The differences are: which +files get bumped, which CHANGELOG you update, and which tag prefix you push. + +### Step 1 — Bump version + +| Package | Edit | +| ----------------- | --------------------------------------------------------------------- | +| PSDocs | `packages/psdocs/src/PSDocs/PSDocs.psd1` → `ModuleVersion = 'X.Y.Z'` | +| PSDocs.Azure | `packages/psdocs-azure/src/PSDocs.Azure/PSDocs.Azure.psd1` → `ModuleVersion = 'X.Y.Z'` | +| VS Code Extension | `packages/vscode-extension/package.json` → `"version": "X.Y.Z"` | + +The release workflow validates the manifest version against the tag and +**fails fast on mismatch**. + +### Step 2 — Update CHANGELOG + +Add a new section at the top of the relevant CHANGELOG immediately after the +`## Unreleased` heading: + +```markdown +## vX.Y.Z + +What's changed since vA.B.C: + +- Category: + - Description. [#PR](link) +``` + +| Package | CHANGELOG path | +| ----------------- | ---------------------------------------------------- | +| PSDocs | `packages/psdocs/CHANGELOG.md` | +| PSDocs.Azure | `CHANGELOG.md` (repo root) | +| VS Code Extension | `packages/vscode-extension/CHANGELOG.md` | + +The release workflow uses `scripts/extract-release-notes.ps1` to pull this +section verbatim into the GitHub Release notes. Keep the heading exactly as +`## vX.Y.Z` (a date in parentheses is also accepted, e.g. `## v0.4.0 (15 May 2026)`). + +### Step 3 — Commit on a release branch and merge + +```bash +git checkout -b release/psdocs-azure-v0.5.0 +git add packages/psdocs-azure/src/PSDocs.Azure/PSDocs.Azure.psd1 CHANGELOG.md +git commit -m "release: PSDocs.Azure v0.5.0" +git push -u origin release/psdocs-azure-v0.5.0 +gh pr create --fill --base main +# review + merge +``` + +### Step 4 — Tag and push + +After merge, on `main` at the merge commit: + +```bash +git checkout main +git pull --ff-only +git tag -a psdocs-azure-v0.5.0 -m "PSDocs.Azure v0.5.0" +git push origin psdocs-azure-v0.5.0 +``` + +### Step 5 — Approve the release in GitHub Actions + +1. Open **Actions → Release {Package}** — the run is paused at the `release` + environment gate. +2. Click **Review deployments → Approve and deploy**. +3. Watch the run complete. + +### Step 6 — Verify + +| Package | Verification | +| ------------ | ------------------------------------------------------------------------------- | +| PSDocs | `Find-Module PSDocs -Repository PSGallery` shows the new version | +| PSDocs.Azure | `Find-Module PSDocs.Azure -Repository PSGallery` shows the new version | +| VS Code | | +| All three | shows the new release | + +For VS Code, also confirm the `.vsix` is attached to the GitHub Release. + +## Pre-release flow + +### PSDocs / PSDocs.Azure + +The PowerShell Gallery accepts SemVer 2.0 prerelease suffixes. Tag with the +suffix appended: + +```bash +git tag -a psdocs-azure-v0.5.0-preview.1 -m "PSDocs.Azure v0.5.0-preview.1" +git push origin psdocs-azure-v0.5.0-preview.1 +``` + +The workflow strips `-preview.1` from the tag, validates the *base* version +(`0.5.0`) against the manifest's `ModuleVersion`, then `Update-ModuleManifest` +sets the `PSData.Prerelease` field to `preview1` (the pipeline removes the dot +to satisfy the gallery's prerelease character rules) before publish. The +GitHub Release is marked as a pre-release. + +> **`-preview.N` increments**: Each preview iteration must increment `N`. You +> cannot republish the same version (e.g. `…-preview.1` twice) — PSGallery +> returns 409. + +### VS Code Extension + +Use the dedicated tag prefix: + +```bash +git tag -a vscode-preview-v0.5.0 -m "VS Code Extension v0.5.0 (preview)" +git push origin vscode-preview-v0.5.0 +``` + +The workflow routes this to `Invoke-Build -Channel preview`, which sets the +extension ID to `vicperdana.psdocs-vscode-preview`, sets `package.preview = +true`, publishes with `vsce publish --pre-release`, and marks the GitHub +Release as a pre-release. + +> Preview and stable extensions are **separate Marketplace listings**. Users +> install one or the other; they do not auto-cross-upgrade. + +## Troubleshooting + +### `Manifest ModuleVersion 'A.B.C' does not match tag base version 'X.Y.Z'` + +You forgot Step 1 (bump the manifest). Either re-tag after fixing the manifest +on `main`, or delete the offending tag: + +```bash +git tag -d psdocs-azure-v0.5.0 +git push origin :refs/tags/psdocs-azure-v0.5.0 +# bump manifest, commit, then re-tag +``` + +### `Section 'vX.Y.Z' is empty in CHANGELOG.md` + +The CHANGELOG section heading exists but has no body. Add release notes +under the heading (Step 2). If you genuinely want to publish with no notes, +re-run with `-AllowEmpty` by editing the workflow temporarily — but this is +strongly discouraged. + +### PSGallery `409 Conflict` + +You're trying to republish an existing version. PSGallery never allows this. +Bump to the next patch version (or next `-preview.N`) and re-tag. + +### `vsce publish` returns `Extension version 'X.Y.Z' is not greater than the published version` + +The Marketplace already has this version. Bump `package.json` and the tag. + +### `vsce` rejects the package because `engines.vscode > @types/vscode` + +Ensure `@types/vscode` in `package.json` is `~` pinned to the same minor as +`engines.vscode`. See . + +### `release` environment never approves automatically + +By design — manual approval is the safety gate. If you're the only reviewer +and need to ship outside business hours, the approval link is in the workflow +run page. + +## Rollback + +> **Never republish an already-published version.** Always roll forward. + +| Registry | Action | +| ------------------ | ------------------------------------------------------------ | +| PowerShell Gallery | Sign in → package page → **Manage Owners** / **Unlist** | +| VS Marketplace | `npx @vscode/vsce unpublish vicperdana.psdocs-vscode@X.Y.Z` | +| GitHub Release | `gh release delete vscode-vX.Y.Z` then `git push origin :refs/tags/vscode-vX.Y.Z` | + +Then ship a new version with the fix. + +## Quick reference + +```text +PSDocs tag: psdocs-vX.Y.Z (preview: psdocs-vX.Y.Z-preview.N) +PSDocs.Azure tag: psdocs-azure-vX.Y.Z (preview: psdocs-azure-vX.Y.Z-preview.N) +VS Code tag: vscode-vX.Y.Z (preview: vscode-preview-vX.Y.Z) + +Secrets: PSGALLERY_API_KEY, VSCE_PAT +Env: release (with reviewers + tag-restricted) +Notes: auto-extracted via scripts/extract-release-notes.ps1 +``` diff --git a/docs/publish/devops-wiki.md b/docs/publish/devops-wiki.md index b74cd051..3ab65f0a 100644 --- a/docs/publish/devops-wiki.md +++ b/docs/publish/devops-wiki.md @@ -1,8 +1,35 @@ -# Publish to Azure DevOps Wiki +# Publish to a wiki -Markdown generated with PSDocs for Azure can be published as wiki content for viewing with Azure DevOps. +Markdown generated with PSDocs for Azure can be published as wiki content for +viewing on Azure DevOps Wiki, GitHub Wiki, or any other Markdown-friendly +documentation host. !!! Abstract - This topic covers using a pipeline to publish markdown generated by PSDocs for Azure into an Azure DevOps Wiki. + This topic covers using a pipeline to publish markdown generated by + PSDocs for Azure to either an Azure DevOps Wiki or a GitHub Wiki / GitHub + Pages site. -_Coming soon_. +_Coming soon_ — detailed pipeline samples for both options. + +In the meantime, the general approach is: + +=== "Azure Pipelines" + + 1. Run `Invoke-PSDocument` against your templates in a build job, writing + Markdown to a known output directory. + 2. Publish the output as a pipeline artifact. + 3. In a release stage, `git clone` the wiki repo (each Azure DevOps Wiki + is backed by a Git repository), copy the artifact's Markdown into the + wiki repo, commit, and push. + +=== "GitHub Actions" + + 1. Run `Invoke-PSDocument` against your templates in a workflow job, + writing Markdown to a known output directory. + 2. Either: + - Push the Markdown to the repository's GitHub Wiki (the `*.wiki.git` + repo) using a workflow such as + [`Andrew-Chen-Wang/github-wiki-action`](https://github.com/Andrew-Chen-Wang/github-wiki-action), **or** + - Publish the Markdown to GitHub Pages using the standard + [`actions/deploy-pages`](https://github.com/actions/deploy-pages) + flow with a static-site generator like MkDocs or Jekyll. diff --git a/docs/release.md b/docs/release.md index 67a47c68..d1160055 100644 --- a/docs/release.md +++ b/docs/release.md @@ -1,13 +1,15 @@ -# Release process for PSDocs for Azure - -The following sections describe the process for making a new release. - -- Update [CHANGELOG](../CHANGELOG.md) with all changes since the last release (including all pre-release versions) -- Submit a Pull Request for review -- Navigate to [Releases](https://github.com/Azure/PSDocs.Azure/releases) and select **Draft a new release** -- [SemVer](https://semver.org/) is used to version the releases. To use the correct release/tag the following options are used: - - A stable release: increment from the previous release *e.g. v0.2.0* - - A pre-release: select the **This is a pre-release** option and select the build version from the previous build in Azure DevOps Pipelines. This version can be found from previous build run. *E.g., for the recent Pull Request go to Pull Request -> Checks -> Analyze -> Azure Pipelines -> PSDocs.Azure-CI Build **[#0.2.0-B2103003](https://github.com/Azure/PSDocs.Azure/pull/52/checks?check_run_id=2066087539)*** - - Wait for the deployment to be released in [PowerShell Gallery](https://www.powershellgallery.com/packages/PSDocs.Azure/) -- After a successful release, create another PR updating: - - [.azure-pipelines/azure-pipelines.yaml](https://github.com/Azure/PSDocs.Azure/blob/main/.azure-pipelines/azure-pipelines.yaml) with the next version increment *e.g. if the newly released version is 0.2.0, set this to `version: '0.3.0'`* +# Release process + +The release process for all packages in this monorepo (PSDocs, PSDocs.Azure, +VS Code extension) is documented in the repo-root [RELEASING.md][releasing] +runbook. + +That runbook covers: + +- One-time setup of GitHub Actions secrets and the `release` environment. +- Tag conventions per package (stable and preview). +- Step-by-step instructions for cutting a release. +- Pre-release flow for both PowerShell Gallery and VS Marketplace. +- Troubleshooting and rollback. + +[releasing]: https://github.com/Azure/PSDocs.Azure/blob/main/RELEASING.md diff --git a/packages/psdocs/src/PSDocs/PSDocs.psd1 b/packages/psdocs/src/PSDocs/PSDocs.psd1 index b2285128..74ceb3f9 100644 --- a/packages/psdocs/src/PSDocs/PSDocs.psd1 +++ b/packages/psdocs/src/PSDocs/PSDocs.psd1 @@ -120,16 +120,16 @@ PrivateData = @{ Tags = @('Markdown', 'PSDocs', 'DevOps', 'CI') # A URL to the license for this module. - LicenseUri = 'https://github.com/Microsoft/PSDocs/blob/main/LICENSE' + LicenseUri = 'https://github.com/Azure/PSDocs.Azure/blob/main/LICENSE' # A URL to the main website for this project. - ProjectUri = 'https://github.com/Microsoft/PSDocs' + ProjectUri = 'https://github.com/Azure/PSDocs.Azure/tree/main/packages/psdocs' # A URL to an icon representing this module. # IconUri = '' # ReleaseNotes of this module - ReleaseNotes = 'https://github.com/Microsoft/PSDocs/releases' + ReleaseNotes = 'https://github.com/Azure/PSDocs.Azure/blob/main/packages/psdocs/CHANGELOG.md' } # End of PSData hashtable diff --git a/scripts/extract-release-notes.ps1 b/scripts/extract-release-notes.ps1 new file mode 100644 index 00000000..5dfb6992 --- /dev/null +++ b/scripts/extract-release-notes.ps1 @@ -0,0 +1,126 @@ +#Requires -Version 7.0 + +<# +.SYNOPSIS + Extract a per-version section from a CHANGELOG.md file. + +.DESCRIPTION + Locates a heading of the form `## v{Version}` (optionally followed by + additional text in parentheses or whitespace) and emits every line up to + but not including the next `## ` heading. The heading line itself is + excluded from the output. Used by the release-* GitHub Actions workflows + to produce the --notes-file content for `gh release create` and the + ReleaseNotes value passed to PSGallery. + +.PARAMETER ChangelogPath + Path to the CHANGELOG.md file to read. + +.PARAMETER Version + The version string to look up, without the leading 'v'. Examples: + '0.10.0', '0.5.0-preview.1', '0.4.0-B2107030'. + +.PARAMETER OutputPath + Optional. When provided, writes the extracted notes to this file in + addition to STDOUT. + +.PARAMETER AllowEmpty + When set, an empty body for the matched section emits a warning instead + of failing. + +.EXAMPLE + pwsh ./scripts/extract-release-notes.ps1 -ChangelogPath ./CHANGELOG.md -Version 0.5.0 + +.EXAMPLE + pwsh ./scripts/extract-release-notes.ps1 ` + -ChangelogPath ./packages/vscode-extension/CHANGELOG.md ` + -Version 0.4.0 -OutputPath ./release-notes.md + +.NOTES + Exits with a non-zero exit code on any of: file not found, version not + found, or empty body without -AllowEmpty. +#> + +[CmdletBinding()] +param( + [Parameter(Mandatory)] + [string] $ChangelogPath, + + [Parameter(Mandatory)] + [string] $Version, + + [Parameter()] + [string] $OutputPath, + + [Parameter()] + [switch] $AllowEmpty +) + +$ErrorActionPreference = 'Stop' + +if (-not (Test-Path -LiteralPath $ChangelogPath -PathType Leaf)) { + Write-Error "CHANGELOG file not found: $ChangelogPath" + exit 2 +} + +$lines = Get-Content -LiteralPath $ChangelogPath + +# Match `## v{Version}` exactly, where the version may be followed by +# end-of-line, whitespace, or '(' (e.g. `## v0.3.3 (27 May 2024)`). +$escaped = [regex]::Escape($Version) +$headingPattern = "^##\s+v$escaped(\s|\(|$)" +$nextHeadingPattern = '^##\s+' + +$startIndex = -1 +for ($i = 0; $i -lt $lines.Count; $i++) { + if ($lines[$i] -match $headingPattern) { + $startIndex = $i + break + } +} + +if ($startIndex -lt 0) { + Write-Error "Version 'v$Version' not found in $ChangelogPath. Looked for heading matching: $headingPattern" + exit 3 +} + +$endIndex = $lines.Count +for ($i = $startIndex + 1; $i -lt $lines.Count; $i++) { + if ($lines[$i] -match $nextHeadingPattern) { + $endIndex = $i + break + } +} + +# Body is everything between (exclusive) the matched heading and the next +# `## ` heading (or EOF). Trim leading/trailing blank lines. +$body = if (($endIndex - $startIndex - 1) -gt 0) { + $lines[($startIndex + 1)..($endIndex - 1)] +} else { + @() +} + +# Trim leading blanks +while ($body.Count -gt 0 -and [string]::IsNullOrWhiteSpace($body[0])) { + $body = $body[1..($body.Count - 1)] +} +# Trim trailing blanks +while ($body.Count -gt 0 -and [string]::IsNullOrWhiteSpace($body[-1])) { + $body = $body[0..($body.Count - 2)] +} + +if ($body.Count -eq 0) { + if ($AllowEmpty) { + Write-Warning "Section 'v$Version' is empty in $ChangelogPath" + } else { + Write-Error "Section 'v$Version' is empty in $ChangelogPath. Pass -AllowEmpty to permit." + exit 4 + } +} + +$output = ($body -join [Environment]::NewLine) + +if ($OutputPath) { + $output | Set-Content -LiteralPath $OutputPath -NoNewline -Encoding utf8 +} + +Write-Output $output