|
| 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