Skip to content

Commit bd7c85f

Browse files
📖 [Docs]: Workflow lifecycle behavior ready for review (#517)
## Summary Defines the Process-PSModule workflow lifecycle specification and architecture. The change documents behavior and implementation boundaries; it does not change functional workflow code. ## Scope - Adds behavior-driven requirements for dispatch recovery, scheduled published-artifact validation, pull-request routing, stable aggregation, concurrency recovery, Gallery disposition, and auditable outcomes. - Defines Plan as the sole lifecycle-policy authority, immutable Settings, and one release executor for stable, prerelease, recovery/resume, cleanup-only, and no-op actions. - Defines the caller boundary: explicit credentials, structured optional `TestData`, narrow built-in-token use, step-scoped App tokens, restricted fork validation, and capability-gated downstream jobs. - Adds both lifecycle pages to the Reference navigation. ## Validation - `npx --yes markdownlint-cli2 --config .github/linters/.markdown-lint.yml docs/content/reference/process-workflow-lifecycle-specification.md docs/content/reference/process-workflow-lifecycle-design.md` - `zensical build --clean` from `docs` --------- Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
1 parent 0d604a3 commit bd7c85f

3 files changed

Lines changed: 647 additions & 0 deletions

File tree

Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
---
2+
title: Process-PSModule workflow lifecycle design
3+
description: Architecture for Process-PSModule event routing, stamped artifacts, recovery release notes, and concurrency isolation.
4+
---
5+
6+
# Process-PSModule workflow lifecycle design
7+
8+
This design defines the Process-PSModule lifecycle architecture. It implements the
9+
[workflow lifecycle specification](process-workflow-lifecycle-specification.md) through a single policy authority,
10+
immutable release records, and a general release executor.
11+
12+
## Architecture
13+
14+
Plan is the sole lifecycle-policy authority. It resolves each event before build and release work, emits enriched
15+
Settings, and gates downstream execution. Build, validation, release, cleanup, and reporting consume Settings without
16+
reinterpreting events, labels, or repository settings.
17+
18+
One general module release action or reusable workflow consumes Settings and performs stable release, prerelease,
19+
recovery or resume, cleanup-only, or no-op actions. It verifies artifacts when required and reconciles only the
20+
requested state.
21+
22+
## Event routing
23+
24+
| Event | Plan classification | Release action | Concurrency | Result |
25+
| --- | --- | --- | --- | --- |
26+
| `workflow_dispatch` on the default branch | Recovery or resume | Stable release or no-op | Full-ref serialization | Rebuild and validate the selected commit; reconstruct unreleased release notes. |
27+
| `schedule` | Published-artifact validation | No-op after validation | Full-ref serialization | Validate the latest published stable artifact and documentation. |
28+
| Fork `pull_request` | Restricted read-only validation | No-op after validation | Pull-request cancellation | Perform repository-local checkout, build, lint, and test only. |
29+
| Pull request `opened`, `reopened`, `synchronize` | Pull-request classification | Prerelease or no-op | Pull-request cancellation | Run validation and execute the planned release action. |
30+
| Pull request `labeled`, `unlabeled` | Pull-request classification refresh | Prerelease, cleanup-only, or no-op | Pull-request cancellation | Resolve the complete current classification and execute its action. |
31+
| Merged pull request `closed` | Post-merge close | No-op | Pull-request cancellation | Leave promotion cleanup to the stable release. |
32+
| Abandoned pull request `closed` | Abandoned-close classification | Cleanup-only | Pull-request cancellation | Reconcile only prereleases owned by the abandoned pull request. |
33+
| Push to the default branch | Stable release | Stable release | Full-ref serialization | Aggregate merged-pull-request intent, publish, and perform promotion cleanup. |
34+
35+
## Caller boundary
36+
37+
The [Process-PSModule caller contract](process-workflow-fleet-standard.md) contains exactly one reusable-workflow call
38+
job and the shared top-level triggers, concurrency, permissions, Plan authorization, and credential boundary that govern
39+
it. Repository-owned jobs MAY coexist in the same workflow file or in separate workflows. They are visible to
40+
conformance reporting and MUST NOT weaken or bypass the Process-PSModule call boundary.
41+
42+
## Event authorization
43+
44+
The caller invokes the reusable workflow without a caller-level fork or event condition. The controlled upstream Plan
45+
implementation derives its security and capability envelope from immutable GitHub event metadata before it interprets
46+
repository settings or executes checked-out code.
47+
48+
For a normal fork `pull_request`, Plan emits a restricted Settings record:
49+
50+
```text
51+
IsFork=true
52+
AllowAppToken=false
53+
AllowPublication=false
54+
AllowMutation=false
55+
```
56+
57+
The restricted route permits only repository-local checkout, build, lint, and test with the least-privilege built-in
58+
token. It provides a green or red validation outcome without contributor secrets. Fork settings and checked-out files
59+
are untrusted validation and build inputs and cannot alter the capability envelope.
60+
61+
Restricted routes do not create App tokens; access PowerShell Gallery; mutate pull requests, statuses, releases, tags,
62+
or assets; perform cleanup; deploy Pages; or run other privileged or user-facing operations. `pull_request_target` is
63+
rejected before credentials or repository-defined code run.
64+
65+
Every downstream job first requires successful Plan execution and valid Settings. Jobs using `always()` apply this gate
66+
before their own failure-handling logic. Privileged jobs also require their relevant Settings capability and never parse
67+
missing or invalid Settings.
68+
69+
## Settings contract
70+
71+
Settings contains one immutable release record:
72+
73+
| Field | Purpose |
74+
| --- | --- |
75+
| Event and run type | Identifies the GitHub event and lifecycle classification. |
76+
| Event action | Preserves the pull-request activity or non-pull-request action. |
77+
| Pull-request identity, state, and merge status | Distinguishes active, merged, and abandoned outcomes. |
78+
| Authorization capabilities and evidence | Records `IsFork`, immutable event metadata, and App-token, publication, and mutation capabilities. |
79+
| Labels and repository settings result | Records the inputs resolved by Plan. |
80+
| Version bump and base version | Defines the version transition. |
81+
| Manifest version, prerelease identifier, and full version or tag | Defines the only version and tag permitted in an artifact and release. |
82+
| Target commit | Binds validation, artifact, and release to one source revision. |
83+
| Resolved release action and create or publish flags | Selects stable, prerelease, recovery or resume, cleanup-only, or no-op execution. |
84+
| Cleanup intent and artifact identity | Defines the exact artifacts release execution may reconcile. |
85+
| Release-note source and boundary | Identifies merged pull requests eligible for release notes. |
86+
87+
## Artifact and version boundary
88+
89+
Build stamps exactly the manifest version and prerelease identifier in Settings into the module artifact. Before any
90+
package, tag, or release becomes visible, release execution verifies that the artifact equals the Settings record. A
91+
mismatch stops execution; release execution does not recalculate versions or retag artifacts.
92+
93+
## Repository authorization
94+
95+
The caller uses the following job boundary:
96+
97+
```yaml
98+
permissions: {}
99+
jobs:
100+
Process-PSModule:
101+
permissions:
102+
contents: read
103+
pages: write
104+
id-token: write
105+
secrets:
106+
PSGALLERY_API_KEY: ${{ secrets.PSGALLERY_API_KEY }}
107+
GitHubAppClientId: ${{ secrets.SHELLY_CLIENT_ID }}
108+
GitHubAppPrivateKey: ${{ secrets.SHELLY_PRIVATE_KEY }}
109+
```
110+
111+
These explicit secret mappings are required. `secrets: inherit` is prohibited. The only optional secret mapping is
112+
`TestData` for module-local tests:
113+
114+
```yaml
115+
TestData: ${{ secrets.TestData }}
116+
```
117+
118+
When present, `TestData` contains a JSON object with separate `secrets` and `variables` maps. It is omitted when
119+
unused. Callers do not set `with.Debug: true`; the reusable workflow default is `false`.
120+
121+
Built-in `GITHUB_TOKEN` authorizes checkout, repository-local reads, and standard Pages/OIDC deployment within the job
122+
boundary. Step-scoped GitHub App installation tokens authorize pull-request comments and labels, commit statuses and
123+
check-facing reporting, releases, tags, assets, and cleanup. An App-required operation fails before its API request or
124+
mutation when its App token is unavailable; it never falls back to the built-in token.
125+
126+
Restricted fork Settings override the caller job boundary: no App token is created, no Pages deployment runs, and no
127+
repository mutation or user-facing action runs.
128+
129+
## Stable aggregation and recovery
130+
131+
Every stable push and recovery target finds the last successfully published stable version and associated target commit,
132+
then aggregates merged pull requests through the requested target commit. The aggregated release intent determines the
133+
stable version and release-note range.
134+
135+
Manual recovery accepts a selected default-branch commit and applies the same aggregation and validation path as a
136+
default-branch push. It returns no-op when a stable publication already covers that commit. Release notes use the
137+
ordered, de-duplicated merged-pull-request range rather than manual-dispatch payload data.
138+
139+
## Scheduled validation and close behavior
140+
141+
Scheduled validation resolves the latest published stable version as input and sets a validation-only mutation class.
142+
Publication and cleanup execution do not run.
143+
144+
A merged pull-request close performs no prerelease cleanup. The successful default-branch stable release owns promotion
145+
cleanup. An abandoned pull-request close receives a pull-request-scoped artifact set and performs only prerelease
146+
cleanup. Broad prerelease deletion requires its own exclusive scope and does not share the abandoned-close route.
147+
148+
## Concurrency and recovery
149+
150+
The caller uses:
151+
152+
```yaml
153+
concurrency:
154+
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
155+
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
156+
```
157+
158+
All pull-request events for one pull request share a group and cancel superseded runs. Push, manual-dispatch, and
159+
scheduled events use full `github.ref`, do not cancel a running run, and serialize by ref. GitHub retains one running
160+
and one pending run per group; a later same-group event can replace an earlier pending run. Full `github.ref` prevents
161+
branch and tag name collisions that `github.ref_name` cannot distinguish.
162+
163+
Cancellation leaves only transient partial state. A subsequent `synchronize`, `labeled`, `unlabeled`, or `closed`
164+
event resumes and reconciles the latest pull-request state:
165+
166+
| External operation | Reconciliation |
167+
| --- | --- |
168+
| PowerShell Gallery publication | Resolve a deterministic pull-request-scoped prerelease identity, detect the existing version, and continue without duplicate publication. |
169+
| GitHub Release creation | Resume or upsert the release and replace its asset set. |
170+
| Prerelease cleanup | Repeat safely after partial deletion and converge to the latest pull-request state. |
171+
| Production boundary | Do not create a stable or signable production artifact from a pull-request event. |
172+
173+
## Gallery prerelease disposition
174+
175+
PowerShell Gallery packages are immutable and cannot be overwritten. Each prerelease uses a deterministic
176+
pull-request-scoped version. When a prerelease becomes obsolete, release execution unlists it through a supported
177+
Gallery API when feasible. When unlisting is infeasible, release execution retains and records the immutable version.
178+
GitHub Release and tag cleanup execute independently from Gallery disposition.
179+
180+
## Verification
181+
182+
The lifecycle is verified with event payload fixtures and publication fakes before credentials are used:
183+
184+
| Behavior | Verification |
185+
| --- | --- |
186+
| Event routing | Fixtures for each supported event and pull-request activity, including merged and abandoned close outcomes. |
187+
| Version boundary | A mismatched artifact fixture proves publication stops. |
188+
| Recovery release notes | Merged-pull-request query fixtures cover empty, single, and multiple pull-request ranges. |
189+
| Scheduled validation | A published-version fixture proves no release mutation is requested. |
190+
| Pull-request convergence | Canceled prerelease publication and cleanup fixtures followed by synchronize, label, unlabel, and close events prove reconciliation. |
191+
| Gallery disposition | Fixtures cover deterministic identity, existing-version detection, supported unlisting, and retained-version recording. |
192+
| Stable aggregation | Push, manual-dispatch, and scheduled bursts replace a pending run and prove all unreleased merged pull requests are aggregated. |
193+
| Caller authorization | Fixtures verify the explicit permissions and credential mappings, App-token failure, and no built-in-token fallback. |
194+
| Fork authorization | Fixtures verify immutable-metadata-first restricted Settings, no privileged operations, and `pull_request_target` rejection. |
195+
| Caller boundary | Fixtures verify repository-owned jobs remain visible without weakening or bypassing the reusable-workflow call boundary. |
196+
197+
## Related
198+
199+
- [Process-PSModule workflow lifecycle specification](process-workflow-lifecycle-specification.md)
200+
- [Process-PSModule caller contract](process-workflow-fleet-standard.md)

0 commit comments

Comments
 (0)