Skip to content

Commit 92611a8

Browse files
Merge branch 'main' into fix/workflow-run-pipeline-deletion
2 parents bae5ded + 4da2113 commit 92611a8

12 files changed

Lines changed: 303 additions & 104 deletions

.github/instructions/tests.instructions.md

Lines changed: 58 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
---
2-
description: "Use when writing, editing, or reviewing Pester test files under the tests/ folder. Covers shared repository setup, auth case iteration, naming conventions, and skip patterns for the GitHub module integration tests."
2+
description: "Use when writing, editing, or reviewing Pester test files under the tests/ folder. Covers per-test-file repository setup, self-contained test lifecycle, auth case iteration, naming conventions, and skip patterns for the GitHub module integration tests."
33
applyTo: "tests/**"
44
---
55
# Integration Test Conventions
@@ -22,9 +22,17 @@ Secrets:
2222

2323
### APP_ENT — PSModule Enterprise App
2424

25-
Homed in `MSX`. ClientID: `Iv23lieHcDQDwVV3alK1`.
25+
Homed in `MSX` (enterprise slug: `msx`). ClientID: `Iv23lieHcDQDwVV3alK1`.
2626
Installed on [psmodule-test-org3](https://github.com/orgs/psmodule-test-org3) (enterprise org) with all permissions and push events.
2727

28+
Required enterprise-scoped permissions (configured on the app):
29+
30+
- `enterprise_organization_installations: write` — required by `Install-GitHubApp` on enterprise-owned organizations
31+
([docs](https://docs.github.com/rest/enterprise-admin/organization-installations#install-a-github-app-on-an-enterprise-owned-organization)).
32+
The Organizations test creates an enterprise organization and then installs the app on it; the
33+
endpoint returns 404 (not 403) when this permission is missing, which makes a missing
34+
permission look like a missing resource. See issue #596.
35+
2836
Secrets: `TEST_APP_ENT_CLIENT_ID`, `TEST_APP_ENT_PRIVATE_KEY`
2937

3038
### APP_ORG — PSModule Organization App
@@ -54,43 +62,52 @@ Cases 4 (`repository`) and 7 (`enterprise`) skip repo creation. Cases 1 and 3 sh
5462

5563
## Setup and teardown
5664

57-
Shared test infrastructure is provisioned once per workflow run using `BeforeAll.ps1` and torn down using `AfterAll.ps1`.
65+
Test infrastructure is provisioned once per workflow run using `BeforeAll.ps1` and torn down using `AfterAll.ps1`.
5866
For generic guidance on setup/teardown scripts, see the
5967
[Process-PSModule documentation](https://github.com/PSModule/Process-PSModule#setup-and-teardown-scripts).
6068

69+
Each test file gets its own repository, scoped by test name: `{TestName}-{OS}-{TokenType}-{RunID}`. This
70+
eliminates cross-file resource collisions when test files run in parallel across OSes and in sequence
71+
across auth contexts.
72+
6173
### `BeforeAll.ps1` — global setup
6274

6375
Runs once before all parallel test files. For each auth case (except `GITHUB_TOKEN`):
6476

6577
1. Connects using the auth case credentials
6678
2. Removes any existing repositories for the deterministic names used by the run
67-
3. Provisions a primary shared repository per OS using `Set-GitHubRepository`: `Test-{OS}-{TokenType}-{GITHUB_RUN_ID}`
68-
- Includes `-AddReadme`, `-License 'mit'`, and `-Gitignore 'VisualStudio'` so release tests have a default branch with content
79+
3. Provisions a per-test-file repository per OS using `Set-GitHubRepository`: `{TestName}-{OS}-{TokenType}-{GITHUB_RUN_ID}`
80+
- Includes `-AddReadme`, `-License 'mit'`, and `-Gitignore 'VisualStudio'` so tests have a default branch with content
6981
- For `user` owners: `Set-GitHubRepository -Name $repoName ...`
7082
- For `organization` owners: `Set-GitHubRepository -Organization $Owner -Name $repoName ...`
71-
4. For `organization` owners only, provisions two extra repositories per OS (`-2`, `-3` suffix) for
72-
Secrets/Variables `SelectedRepository` tests
83+
4. For `organization` owners only, provisions extra repositories (`-2`, `-3` suffix) for
84+
test files that need companion repos (e.g., Secrets/Variables `SelectedRepository` tests)
7385

74-
`Set-GitHubRepository` is idempotent — if the repository already exists it updates it in place (issuing a
75-
PATCH), and if it does not exist it creates it. Because the same parameters are passed each time, the
76-
end-state is identical regardless of how many times the setup runs. The extra PATCH on the happy path is
77-
a deliberate trade-off for simplicity: one call handles both first-run and partial-rerun scenarios without
78-
branching logic.
86+
Global setup deliberately removes any matching deterministic repositories before provisioning them again.
87+
That gives workflow reruns a clean repository state for resources such as releases, tags, environments,
88+
secrets, and variables. Each individual test file still calls `Set-GitHubRepository` in its per-context
89+
`BeforeAll` as an idempotent safety net, so a single test file or auth context can be rerun independently
90+
even when the global setup step did not run first.
7991

8092
### `AfterAll.ps1` — global teardown
8193

8294
Runs once after all parallel test files complete. For each auth case (except `GITHUB_TOKEN`):
8395

8496
1. Connects using the auth case credentials
85-
2. Removes the run-scoped repositories by their known names
97+
2. Removes the per-test-file repositories by their deterministic names
8698

87-
## Shared test repositories
99+
## Per-test-file repositories
88100

89-
Each test file that depends on a GitHub repository must ensure it exists using `Set-GitHubRepository`
90-
in its per-context `BeforeAll`. `Set-GitHubRepository` is idempotent — if the repository already exists
91-
it updates it in place (PATCH), and if it does not exist it creates it. When the same parameters are
92-
passed each time the end-state is identical. This makes every test file self-sufficient regardless of
93-
whether the global `BeforeAll.ps1` already provisioned the repository.
101+
Each test file that depends on a GitHub repository uses its own repository, scoped by test name:
102+
`{TestName}-{OS}-{TokenType}-{RunID}`. This prevents cross-file resource collisions — test files
103+
run in parallel across OSes and in sequence across auth contexts, so one test file must never
104+
create resources on another test file's repository.
105+
106+
Each test file must ensure its repository exists using `Set-GitHubRepository` in its per-context
107+
`BeforeAll`. `Set-GitHubRepository` is idempotent — if the repository already exists it updates it
108+
in place (PATCH), and if it does not exist it creates it. When the same parameters are passed each
109+
time the end-state is identical. This makes every test file self-sufficient regardless of whether
110+
the global `BeforeAll.ps1` already provisioned the repository.
94111

95112
**Do not** use `Get-GitHubRepository` with a throw guard — that breaks partial reruns.
96113
**Do not** use `New-GitHubRepository` — that fails if the repository already exists.
@@ -103,7 +120,7 @@ Skip provisioning for those owner types and set `$repo = $null` so that repo-dep
103120
be skipped cleanly:
104121

105122
```powershell
106-
$repoPrefix = "Test-$os-$TokenType"
123+
$repoPrefix = "$testName-$os-$TokenType"
107124
$repoName = "$repoPrefix-$id"
108125
if ($OwnerType -in ('repository', 'enterprise')) {
109126
$repo = $null
@@ -148,7 +165,7 @@ Describe 'TestName' {
148165
$context = Connect-GitHubApp @connectAppParams -PassThru -Default -Silent
149166
}
150167
151-
$repoPrefix = "Test-$os-$TokenType"
168+
$repoPrefix = "$testName-$os-$TokenType"
152169
$repoName = "$repoPrefix-$id"
153170
if ($OwnerType -in ('repository', 'enterprise')) {
154171
$repo = $null
@@ -164,9 +181,14 @@ Describe 'TestName' {
164181
'organization' { Set-GitHubRepository @repoParams -Organization $Owner }
165182
}
166183
}
184+
185+
# Clean up stale resources from prior runs (re-runs with same GITHUB_RUN_ID)
186+
# Example: remove leftover releases, environments, etc.
167187
}
168188
169189
AfterAll {
190+
# Remove all test-specific resources created during this context
191+
# (environments, releases, secrets, etc.)
170192
Get-GitHubContext -ListAvailable | Disconnect-GitHubAccount -Silent
171193
}
172194
@@ -179,20 +201,24 @@ Describe 'TestName' {
179201

180202
## Naming conventions
181203

182-
| Resource | Pattern | Example |
183-
|------------|----------------------------------------------|----------------------------------|
184-
| Repo | `Test-{OS}-{TokenType}-{RunID}` | `Test-Linux-USER_FG_PAT-1234` |
185-
| Extra repo | `Test-{OS}-{TokenType}-{RunID}-{N}` | `Test-Linux-USER_FG_PAT-1234-2` |
186-
| Secret | `{TestName}_{OS}_{TokenType}_{RunID}` | `Secrets_Linux_PAT_1234` |
187-
| Variable | `{TestName}_{OS}_{TokenType}_{RunID}` | `Variables_Linux_PAT_1234` |
188-
| Team | `{TestName}_{OS}_{TokenType}_{RunID}_{Name}` | `Teams_Linux_APP_ORG_1234_Pull` |
189-
| Env | `{TestName}-{OS}-{TokenType}-{RunID}` | `Secrets-Linux-PAT-1234` |
204+
| Resource | Pattern | Example |
205+
|------------|----------------------------------------------|---------------------------------------|
206+
| Repo | `{TestName}-{OS}-{TokenType}-{RunID}` | `Releases-Linux-USER_FG_PAT-1234` |
207+
| Extra repo | `{TestName}-{OS}-{TokenType}-{RunID}-{N}` | `Secrets-Linux-ORG_FG_PAT-1234-2` |
208+
| Secret | `{TestName}_{OS}_{TokenType}_{RunID}` | `Secrets_Linux_PAT_1234` |
209+
| Variable | `{TestName}_{OS}_{TokenType}_{RunID}` | `Variables_Linux_PAT_1234` |
210+
| Team | `{TestName}_{OS}_{TokenType}_{RunID}_{Name}` | `Teams_Linux_APP_ORG_1234_Pull` |
211+
| Env | `{TestName}-{OS}-{TokenType}-{RunID}` | `Secrets-Linux-PAT-1234` |
190212

191213
## Key rules
192214

193215
- `$id` must always be `$env:GITHUB_RUN_ID` — never `[guid]::NewGuid()` or `Get-Random`.
194216
- Skip repo-dependent tests with `-Skip:($OwnerType -in ('repository', 'enterprise'))`.
195217
- Disconnect all sessions in `AfterAll`: `Get-GitHubContext -ListAvailable | Disconnect-GitHubAccount -Silent`.
196-
- Test-specific ephemeral resources (releases, secrets, variables, environments, teams) are created and
197-
cleaned up within each test file. Only repositories are shared.
198-
- `Repositories.Tests.ps1` is the exception — it creates and deletes its own repos because it tests CRUD.
218+
- Each test file uses its own repository: `{TestName}-{OS}-{TokenType}-{RunID}`. No two test files share a repository.
219+
- Each test file is self-contained and responsible for its own setup and teardown:
220+
- **BeforeAll (per-context):** Ensure the repository exists via `Set-GitHubRepository`. Clean up stale test-specific resources from prior runs (re-runs with the same `GITHUB_RUN_ID`).
221+
- **AfterAll (per-context):** Remove all test-specific resources created during the run (environments, releases, secrets, variables, etc.).
222+
- Any individual test file or auth context can be re-run independently. Tests must not assume clean initial state — they must be idempotent.
223+
- Tests run in parallel across OSes (Linux, macOS, Windows) and in sequence across auth contexts (7 cases). Resource names must include enough dimensions to prevent collisions across all parallel and sequential axes.
224+
- `Repositories.Tests.ps1` is independent — it creates and deletes its own repos because it tests CRUD.

.github/workflows/Process-PSModule.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,5 +27,5 @@ permissions:
2727

2828
jobs:
2929
Process-PSModule:
30-
uses: PSModule/Process-PSModule/.github/workflows/workflow.yml@205d193f34cbbaf9992955c21d842bcf98a1859f # v5.4.6
30+
uses: PSModule/Process-PSModule/.github/workflows/workflow.yml@11117919e65242d3388727819a751f74ad24ea9e # v5.5.0
3131
secrets: inherit

tests/Actions.Tests.ps1

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,7 @@ Describe 'Actions' {
136136
Write-Host ($context | Format-List | Out-String)
137137
}
138138
}
139-
$repoPrefix = "Test-$os-$TokenType"
139+
$repoPrefix = "$testName-$os-$TokenType"
140140
$repoName = "$repoPrefix-$id"
141141

142142
LogGroup "Using Repository - [$repoName]" {

tests/AfterAll.ps1

Lines changed: 24 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ LogGroup 'AfterAll - Global Test Teardown' {
1111
if (-not $env:Settings) {
1212
throw 'Settings environment variable is not set. Process-PSModule must populate it with the test suite configuration.'
1313
}
14-
$prefix = 'Test'
1514

1615
# Derive the list of OS names from the Settings JSON provided by Process-PSModule.
1716
try {
@@ -30,6 +29,11 @@ LogGroup 'AfterAll - Global Test Teardown' {
3029
}
3130
Write-Host "Cleaning up test repositories for OSes: $($osNames -join ', ')"
3231

32+
# Source the single authoritative list of test-file repositories so setup and teardown
33+
# always operate on the same set. See tests/Data/TestRepos.ps1.
34+
$testRepos = . "$PSScriptRoot/Data/TestRepos.ps1"
35+
$testNames = $testRepos.TestNames
36+
$testNamesWithExtraRepos = $testRepos.TestNamesWithExtraRepos
3337
foreach ($authCase in $authCases) {
3438
$authCase.GetEnumerator() | ForEach-Object { Set-Variable -Name $_.Key -Value $_.Value }
3539

@@ -46,25 +50,27 @@ LogGroup 'AfterAll - Global Test Teardown' {
4650
Write-Host ($context | Format-List | Out-String)
4751

4852
foreach ($os in $osNames) {
49-
$repoPrefix = "$prefix-$os-$TokenType"
50-
$repoName = "$repoPrefix-$id"
53+
foreach ($testName in $testNames) {
54+
$repoPrefix = "$testName-$os-$TokenType"
55+
$repoName = "$repoPrefix-$id"
5156

52-
LogGroup "Repository cleanup - $AuthType-$TokenType - $os" {
53-
# Use deterministic name lookups instead of listing all repos to reduce API calls.
54-
$cleanupRepoNames = @($repoName)
55-
if ($OwnerType -eq 'organization') {
56-
$cleanupRepoNames += "$repoName-2", "$repoName-3"
57-
}
57+
LogGroup "Repository cleanup - $AuthType-$TokenType - $os - $testName" {
58+
# Use deterministic name lookups instead of listing all repos to reduce API calls.
59+
$cleanupRepoNames = @($repoName)
60+
if ($OwnerType -eq 'organization' -and $testName -in $testNamesWithExtraRepos) {
61+
$cleanupRepoNames += "$repoName-2", "$repoName-3"
62+
}
5863

59-
foreach ($cleanupRepoName in $cleanupRepoNames) {
60-
switch ($OwnerType) {
61-
'user' {
62-
Get-GitHubRepository -Name $cleanupRepoName -ErrorAction SilentlyContinue |
63-
Remove-GitHubRepository -Confirm:$false
64-
}
65-
'organization' {
66-
Get-GitHubRepository -Owner $Owner -Name $cleanupRepoName -ErrorAction SilentlyContinue |
67-
Remove-GitHubRepository -Confirm:$false
64+
foreach ($cleanupRepoName in $cleanupRepoNames) {
65+
switch ($OwnerType) {
66+
'user' {
67+
Get-GitHubRepository -Name $cleanupRepoName -ErrorAction SilentlyContinue |
68+
Remove-GitHubRepository -Confirm:$false
69+
}
70+
'organization' {
71+
Get-GitHubRepository -Owner $Owner -Name $cleanupRepoName -ErrorAction SilentlyContinue |
72+
Remove-GitHubRepository -Confirm:$false
73+
}
6874
}
6975
}
7076
}

0 commit comments

Comments
 (0)