Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 59 additions & 14 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ on:

permissions:
contents: write
actions: read

jobs:
release:
Expand All @@ -36,11 +37,9 @@ jobs:

steps:
- name: Checkout repository
uses: actions/checkout@v6
uses: actions/checkout@v4
Comment thread
blindzero marked this conversation as resolved.
with:
fetch-depth: 0
# Always checkout the workflow ref (branch/tag selected in the UI or the pushed tag ref).
# Never treat the "tag" input as a git ref.
ref: ${{ github.ref }}

- name: Show PowerShell version
Expand Down Expand Up @@ -71,23 +70,70 @@ jobs:
shell: pwsh
run: |
$tag = '${{ steps.tag.outputs.value }}'
# Stable tags: vMAJOR.MINOR.PATCH (e.g. v0.7.0)
$isStable = $tag -match '^v\d+\.\d+\.\d+$'
# Pre-release tags: vMAJOR.MINOR.PATCH-<label> (e.g. v0.7.0-preview.1)
$isPrerelease = $tag -match '^v\d+\.\d+\.\d+-[0-9A-Za-z][0-9A-Za-z\.\-]*$'

"is_stable=$($isStable.ToString().ToLowerInvariant())" | Out-File -FilePath $env:GITHUB_OUTPUT -Append -Encoding utf8
"is_prerelease=$($isPrerelease.ToString().ToLowerInvariant())" | Out-File -FilePath $env:GITHUB_OUTPUT -Append -Encoding utf8

- name: Require tag to point to main HEAD
if: ${{ startsWith(github.ref, 'refs/tags/v') }}
shell: pwsh
run: |
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'

git fetch origin main --depth 1 | Out-Null

$tagSha = (git rev-parse '${{ github.ref }}^{}').Trim()
$mainSha = (git rev-parse 'origin/main').Trim()

if ($tagSha -ne $mainSha) {
throw "Tag does not point to origin/main HEAD. Tag SHA=$tagSha, main SHA=$mainSha"
}

Write-Host "Tag points to origin/main HEAD ($mainSha)."

- name: Require green CI on the target commit
if: ${{ inputs.publish_release == true || inputs.publish_psgallery == true || startsWith(github.ref, 'refs/tags/v') }}
shell: pwsh
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'

$repo = '${{ github.repository }}'
$sha = '${{ github.sha }}'

# NOTE: This must match the workflow filename for CI.
# In this repository it is: .github/workflows/ci.yml
$uri = "https://api.github.com/repos/$repo/actions/workflows/ci.yml/runs?per_page=100&head_sha=$sha"

$headers = @{
Authorization = "Bearer $env:GH_TOKEN"
Accept = "application/vnd.github+json"
"X-GitHub-Api-Version" = "2022-11-28"
}

$resp = Invoke-RestMethod -Method Get -Uri $uri -Headers $headers
$run = $resp.workflow_runs | Sort-Object run_number -Descending | Select-Object -First 1

if (-not $run) {
throw "No completed CI workflow run found for SHA $sha. If CI is currently running for this commit, wait for it to complete successfully on main before releasing."
}

if ($run.status -ne 'completed' -or $run.conclusion -ne 'success') {
throw "CI is not green for SHA $sha. Status=$($run.status), Conclusion=$($run.conclusion)."
}

Write-Host "CI is green for SHA $sha (run #$($run.run_number))."

- name: Validate tag version matches module manifest version(s)
shell: pwsh
run: |
$tag = '${{ steps.tag.outputs.value }}'

# Accept stable tags and prerelease tags, but always extract the base version (MAJOR.MINOR.PATCH).
# Examples:
# - v0.7.0 -> 0.7.0
# - v0.7.0-preview.1 -> 0.7.0
if ($tag -notmatch '^v(\d+\.\d+\.\d+)(?:-[0-9A-Za-z][0-9A-Za-z\.\-]*)?$') {
Write-Host "Tag '$tag' is not SemVer-like. Skipping module version validation."
exit 0
Expand All @@ -96,7 +142,6 @@ jobs:
$expectedVersion = $Matches[1]
Write-Host "Expected module version from tag: $expectedVersion"

# Strict mode (default): all shipped module manifests must match the tag version.
$manifestPaths = @(
'src/IdLE/IdLE.psd1',
'src/IdLE.Core/IdLE.Core.psd1',
Expand Down Expand Up @@ -164,15 +209,15 @@ jobs:

- name: Upload workflow artifact (expanded folder)
if: ${{ inputs.publish_release != true && !startsWith(github.ref, 'refs/tags/v') }}
uses: actions/upload-artifact@v6
uses: actions/upload-artifact@v4
with:
name: IdLE-${{ steps.tag.outputs.value }}-expanded
path: artifacts/staging-${{ steps.tag.outputs.value }}
if-no-files-found: error

- name: Upload workflow artifact (zip)
if: ${{ inputs.publish_release == true || startsWith(github.ref, 'refs/tags/v') }}
uses: actions/upload-artifact@v6
uses: actions/upload-artifact@v4
Comment thread
blindzero marked this conversation as resolved.
with:
name: IdLE-${{ steps.tag.outputs.value }}-zip
path: artifacts/IdLE-${{ steps.tag.outputs.value }}.zip
Expand All @@ -196,7 +241,7 @@ jobs:

steps:
- name: Checkout repository
uses: actions/checkout@v6
uses: actions/checkout@v4
with:
fetch-depth: 0
ref: ${{ github.ref }}
Expand Down Expand Up @@ -264,7 +309,7 @@ jobs:

steps:
- name: Checkout repository
uses: actions/checkout@v6
uses: actions/checkout@v4
with:
fetch-depth: 0
ref: ${{ github.ref }}
Expand Down
40 changes: 25 additions & 15 deletions docs/advanced/releases.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,23 @@ Pre-release tags are **GitHub-only** (no PowerShell Gallery publish).
- Publishing to PowerShell Gallery is protected via a GitHub **Environment** (`psgallery-prod`) and requires approval.
- A local end-to-end publish test runs in CI (publishes to a local repository, then installs/imports the module).

## Release workflow safety gates

The Release workflow enforces additional guardrails for tagged releases:

- **Tag must point to `origin/main` HEAD** (fail-fast).
- **CI must be green for the tag commit** (`ci.yml` must have a successful run for the same SHA).
- **Tag base version must match all shipped module manifests**.

These checks prevent "broken" releases (e.g., tagging the wrong commit or forgetting the version bump).

## Versioning policy

### Stable tags

Stable releases use tags in the form:

- `vMAJOR.MINOR.PATCH` (example: `v0.7.0`)
- `vMAJOR.MINOR.PATCH` (example: `v1.2.0`)

For stable releases, all shipped module manifests must have:

Expand All @@ -33,24 +43,24 @@ For stable releases, all shipped module manifests must have:

Pre-releases use tags in the form:

- `vMAJOR.MINOR.PATCH-<label>` (example: `v0.7.0-preview.1`, `v0.7.0-rc.1`)
- `vMAJOR.MINOR.PATCH-<label>` (example: `v1.2.0-preview.1`, `v1.2.0-rc.1`)

**Important:** the module manifests still use the *base* version:

- `ModuleVersion = MAJOR.MINOR.PATCH`

That means: to cut `v0.7.4-preview.1`, the manifests must already be bumped to `0.7.4`.
That means: to cut `v1.2.4-preview.1`, the manifests must already be bumped to `1.2.4`.
(The Release workflow extracts the base version and validates it against all module manifests.)

Pre-release tags do **not** publish to PowerShell Gallery.

## Release preparation (always via PR)

1. Create a branch for the release preparation (for example: `release/v0.7.0`).
1. Create a branch for the release preparation (for example: `release/v1.2.0`).
2. Bump all shipped module versions using the repository tool:

```powershell
pwsh -NoProfile -File ./tools/Set-IdleModuleVersion.ps1 -TargetVersion 0.7.0
pwsh -NoProfile -File ./tools/Set-IdleModuleVersion.ps1 -TargetVersion 1.2.0
```

3. Run tests:
Expand Down Expand Up @@ -83,15 +93,15 @@ The workflow uploads an expanded artifact as a workflow run artifact.

Use this when you want a GitHub-only preview build.

1. Ensure the manifests are bumped to the target base version (PR merged), e.g. `0.7.4`.
1. Ensure the manifests are bumped to the target base version (PR merged), e.g. `1.2.4`.
2. Create an annotated tag on the `main` merge commit:

```bash
git checkout main
git pull --ff-only

git tag -a v0.7.4-preview.1 -m "IdLE v0.7.4-preview.1"
git push origin v0.7.4-preview.1
git tag -a v1.2.4-preview.1 -m "IdLE v1.2.4-preview.1"
git push origin v1.2.4-preview.1
```

3. The Release workflow will:
Expand All @@ -104,18 +114,18 @@ If you need another preview, repeat with `preview.2`, etc. (no version bump requ

## Cut a stable release (GitHub Release + optional PSGallery publish)

1. Ensure the manifests are bumped to the target version (PR merged), e.g. `0.7.0`.
1. Ensure the manifests are bumped to the target version (PR merged), e.g. `1.2.0`.
2. Create an annotated tag:

```bash
git checkout main
git pull --ff-only

# Create an annotated tag (recommended)
git tag -a v0.7.0 -m "IdLE v0.7.0"
git tag -a v1.2.0 -m "IdLE v1.2.0"

# Push the tag to trigger the Release workflow
git push origin v0.7.0
git push origin v1.2.0
```

3. The Release workflow will:
Expand Down Expand Up @@ -150,8 +160,8 @@ This script copies the `IdLE` meta-module and required nested modules into a loc

## Versioning and naming

- Use `vMAJOR.MINOR.PATCH` tags (for example `v0.7.0`).
- Pre-releases are allowed (for example `v0.7.0-rc.1`). They should be tested via the dry-run path first.
- Use `vMAJOR.MINOR.PATCH` tags (for example `v1.2.0`).
- Pre-releases are allowed (for example `v1.2.0-rc.1`). They should be tested via the dry-run path first.
- Avoid deleting and reusing tags.

## Troubleshooting
Expand All @@ -162,7 +172,7 @@ This script copies the `IdLE` meta-module and required nested modules into a loc
- Run the packaging script locally in list-only mode to inspect the file list:

```powershell
pwsh -NoProfile -File ./tools/New-IdleReleaseArtifact.ps1 -Tag v0.7.0-test -ListOnly
pwsh -NoProfile -File ./tools/New-IdleReleaseArtifact.ps1 -Tag v1.2.0-test -ListOnly
```

### Tag was pushed but the workflow fails with a version mismatch
Expand All @@ -182,7 +192,7 @@ With immutable releases enabled, treat published releases as immutable.
Preferred approach:

1. Fix the issue on `main`.
2. Cut a new version tag (for example `v0.7.1`).
2. Cut a new version tag (for example `v1.2.1`).

### The PSGallery publish job is blocked

Expand Down
1 change: 0 additions & 1 deletion docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,4 +50,3 @@ used between IdLE and its hosts.
- [Contributing](../CONTRIBUTING.md)
- [Style guide](../STYLEGUIDE.md)
- [Examples](../examples/README.md)