From 6e303672748161bfcaecff5694c745b562e883d6 Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Sat, 15 Aug 2026 15:00:30 +0200 Subject: [PATCH 01/13] docs: specify workflow lifecycle candidate Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../process-workflow-lifecycle-design.md | 98 ++++++++ ...rocess-workflow-lifecycle-specification.md | 228 ++++++++++++++++++ docs/zensical.toml | 2 + 3 files changed, 328 insertions(+) create mode 100644 docs/content/reference/process-workflow-lifecycle-design.md create mode 100644 docs/content/reference/process-workflow-lifecycle-specification.md diff --git a/docs/content/reference/process-workflow-lifecycle-design.md b/docs/content/reference/process-workflow-lifecycle-design.md new file mode 100644 index 00000000..3af627e9 --- /dev/null +++ b/docs/content/reference/process-workflow-lifecycle-design.md @@ -0,0 +1,98 @@ +--- +title: Process-PSModule workflow lifecycle candidate design +description: Candidate design for Process-PSModule event routing, stamped artifacts, recovery release notes, and concurrency isolation. +--- + +# Process-PSModule workflow lifecycle candidate design + +**Status:** This is a candidate design for discussion. It is not approved and does not change the reusable workflow or its caller contract. + +This design describes one way to meet the [workflow lifecycle candidate specification](process-workflow-lifecycle-specification.md). It follows the [spec/design separation](https://msx.no/docs/Ways-of-Working/Spec-Driven-Development/#what-a-design-is); detailed implementation choices remain free until the candidate is approved. + +## Confirmed implementation baseline + +The confirmed reusable workflow runs a `Plan` job, enriches one settings object with the resolved version, and passes that object to downstream jobs. The module build action accepts the resolved version and prerelease identity; the publish workflow consumes the built artifact and uses the resolved full version for the release. + +The version resolver treats non-pull-request events, including `workflow_dispatch` and `schedule`, as events without a release decision. The workflow's concurrency identity contains either the pull request number or the Git ref, and its runs are not canceled. The documented scenario matrix also identifies closed-pull-request cleanup as non-stable behavior. + +These facts establish the starting point. They do not implement the scheduled validation or manual recovery behavior proposed below. + +## Candidate event routing + +The planner classifies the caller event before build and publication work begins. Each route produces one of three mutation classes: validation only, prerelease mutation, or stable-release mutation. + +| Event | Candidate route | Mutation class | Required result | +| --- | --- | --- | --- | +| `workflow_dispatch` on the default branch | Recovery release | Stable release or explicit no-op | Rebuild and validate the selected commit; reconstruct the unreleased release notes. | +| `schedule` | Published-artifact validation | Validation only | Validate the latest published stable artifact and its documentation. | +| Pull request `opened`, `reopened`, `synchronize` | Pull-request CI | Validation only | Report configured validation on the pull request. | +| Pull request `labeled`, `unlabeled` | Prerelease evaluation | Prerelease or validation only | Re-evaluate the full label set and publish only an eligible prerelease. | +| Pull request `closed` | Pull-request cleanup | Prerelease cleanup only | Remove only prereleases owned by the closed pull request. | +| Push to the default branch | Stable release | Stable release | Resolve merged-pull-request intent when applicable, then publish after required gates. | + +The classifier records the route, mutable resource scope, commit identity, and release decision in the plan result. Downstream jobs consume that record rather than infer the event again. + +## Candidate artifact and version boundary + +Version resolution is the boundary between planning and release-capable work. The candidate carries one immutable release record through build, test, and publication: + +| Record field | Purpose | +| --- | --- | +| Commit identity | Binds validation, artifact, and release to one source revision. | +| Resolved stable version and prerelease identity | Defines the only version permitted in the built artifact. | +| Event route and mutation class | Restricts each downstream stage to its authorized behavior. | +| Release-note range | Identifies the merged pull requests eligible for a recovery release note. | + +The build stage stamps the resolved version into the module artifact. Before any package, tag, or release becomes visible, the publication stage verifies that the artifact version and prerelease identity equal the immutable release record. A mismatch stops publication; it is not corrected by retagging or by recalculating a version after the artifact is built. + +## Candidate manual recovery + +A manual recovery route accepts only a selected default-branch commit. It first determines whether a stable publication already covers that commit and returns a no-op when one exists. + +For a missing publication, the route identifies the last published stable version and its associated default-branch commit. It then queries merged pull requests targeting the default branch between that publication boundary and the selected commit. The release-note reconstruction uses that ordered, de-duplicated result rather than the manual-dispatch event payload, which has no pull-request context. + +The recovery route validates the selected commit using the same release gates as a default-branch push. It produces a stable release only after the artifact/version boundary succeeds. This keeps recovery notes traceable even when the normal main-push run was missed or interrupted. + +## Candidate scheduled validation + +The scheduled route resolves the latest published stable version as an input, not as a version to create. It validates the downloaded package and its published documentation with the checks appropriate to a published consumer artifact. Its plan record sets the mutation class to validation only, so publication and cleanup stages cannot run. + +## Candidate concurrency isolation + +Closed-pull-request cleanup and default-branch publication use independent concurrency identities: + +| Lifecycle route | Concurrency scope | Mutable resources | +| --- | --- | --- | +| Closed pull request | Pull request number | Prereleases associated with that pull request. | +| Default-branch push or manual recovery | Default-branch ref | The stable release and its versioned artifacts. | +| Scheduled validation | Published-version read scope | No release-related mutable resources. | + +The routes do not cancel one another. Cleanup receives a pull-request-scoped artifact set and MUST NOT perform a broad prerelease deletion while a stable release can be active. Any future broad cleanup needs a separately approved exclusive scope; it cannot share the closed-pull-request route. + +## Candidate verification strategy + +The lifecycle contract is exercised with event payload fixtures and publication fakes before credentials are used: + +| Candidate behavior | Verification | +| --- | --- | +| Event routing | One fixture for each supported event and pull-request activity. | +| Version boundary | A mismatched artifact fixture that proves publication stops. | +| Recovery release notes | Merged-pull-request query fixtures covering an empty range, one pull request, and multiple pull requests. | +| Scheduled validation | A published-version fixture that proves no release mutation is requested. | +| Concurrency isolation | Overlapping closed-pull-request and default-branch-push runs that prove neither run cancels or broadens the other's scope. | + +## Decisions requiring approval + +The candidate does not decide the following: + +- Whether a recovery release always uses the normal next patch version or permits an explicit version input. +- Which source is authoritative when a PowerShell Gallery publication and GitHub release disagree about the last published stable version. +- Which consumer-facing checks comprise scheduled published-artifact validation. +- Whether removing prerelease eligibility cleans up existing prereleases immediately or leaves cleanup to pull-request closure. +- The exact caller triggers and concurrency expression, which remain subject to [PSModule/Process-PSModule#514](https://github.com/PSModule/Process-PSModule/issues/514). + +## Related + +- [Candidate specification](process-workflow-lifecycle-specification.md) — behavior and acceptance criteria. +- [Scenario matrix](scenario-matrix.md) — established job-level routing reference. +- [Process-PSModule caller workflow candidate](process-workflow-fleet-standard.md) — candidate event and concurrency contract. diff --git a/docs/content/reference/process-workflow-lifecycle-specification.md b/docs/content/reference/process-workflow-lifecycle-specification.md new file mode 100644 index 00000000..d7b7165e --- /dev/null +++ b/docs/content/reference/process-workflow-lifecycle-specification.md @@ -0,0 +1,228 @@ +--- +title: Process-PSModule workflow lifecycle candidate specification +description: Candidate behavior-driven requirements for Process-PSModule caller event routing, recovery releases, validation, and cleanup. +--- + +# Process-PSModule workflow lifecycle candidate specification + +**Status:** This is a candidate for discussion. It is not an approved workflow standard and does not change the candidate caller contract in [Process-PSModule caller workflow candidate](process-workflow-fleet-standard.md). + +## Problem and outcome + +Module repositories need each workflow event to have one safe, observable lifecycle outcome. A failed or missed publication needs a recoverable path; published artifacts need continuing validation; and pull-request activity must not create an accidental stable release. + +This candidate defines the behavior required from a Process-PSModule workflow lifecycle. Its requirements follow [spec-driven development](https://msx.no/docs/Ways-of-Working/Spec-Driven-Development/) and use [Given / When / Then scenarios](https://msx.no/docs/Ways-of-Working/Spec-Driven-Development/#behavioral-scenarios) as the acceptance contract. + +## Scope + +The candidate covers dispatch recovery, scheduled validation, pull-request validation and prerelease evaluation, closed-pull-request cleanup, and stable publication after a default-branch push. + +It does not approve a caller layout, change module build or publication implementation, define label names, or prescribe release-note presentation. Those choices remain in the caller candidate, the existing versioning guidance, and the companion [candidate design](process-workflow-lifecycle-design.md). + +## Confirmed implementation baseline + +The reusable workflow has a single planning decision that enriches downstream settings with a resolved version and release decision. It also serializes runs by pull request number or ref without canceling a running workflow. + +The version resolver treats non-pull-request events, including `workflow_dispatch` and `schedule`, as events without a pull request and does not create a release decision. The existing workflow supports pull-request validation, prerelease publication, default-branch publication, and closed-pull-request prerelease cleanup. Scheduled published-artifact validation and manual recovery publication are not confirmed behavior. + +The following requirements describe desired behavior, not a claim that the confirmed implementation already satisfies it. + +## Functional requirements + +### FR1 — Manual dispatch MUST provide a safe recovery release {#fr1} + +A default-branch manual dispatch MUST either publish one recoverable stable release after all required validation succeeds or report that the selected commit is already covered by a stable publication. It MUST NOT create a duplicate stable publication. + +#### Behavioral scenarios {#fr1-scenarios} + +```gherkin +Scenario: Recover a missing stable publication + Given the default branch contains a validated commit without a stable publication + When a maintainer dispatches the workflow for that commit + Then the workflow publishes one stable artifact and release for the commit + And the release notes identify merged pull requests since the previous published version + +Scenario: Repeat a completed recovery dispatch + Given a stable publication already covers the selected default-branch commit + When a maintainer dispatches the workflow again + Then the workflow reports that no recovery release is required + And it does not create another artifact, tag, or release +``` + +### FR2 — Scheduled runs MUST validate published artifacts without publishing {#fr2} + +A scheduled run MUST validate the latest published stable artifact and its published documentation against the repository's configured checks. It MUST NOT create, replace, or delete a package, tag, release, or prerelease. + +#### Behavioral scenarios {#fr2-scenarios} + +```gherkin +Scenario: Validate the latest published artifact + Given a stable module version and its documentation are published + When the scheduled workflow runs + Then the workflow validates that published version + And it reports the validated version and result + And it creates no release-related artifact +``` + +### FR3 — Pull-request delivery events MUST run validation only {#fr3} + +An `opened`, `reopened`, or `synchronize` pull-request event targeting the default branch MUST run the configured validation for the pull request. It MUST NOT create a stable publication. + +#### Behavioral scenarios {#fr3-scenarios} + +```gherkin +Scenario: Validate a synchronized pull request + Given a pull request targets the default branch + When a new commit synchronizes the pull request + Then the workflow reports the configured validation result on that pull request + And it does not publish a stable version +``` + +### FR4 — Label changes MUST re-evaluate prerelease eligibility {#fr4} + +A `labeled` or `unlabeled` pull-request event targeting the default branch MUST re-evaluate prerelease eligibility from the complete current label set. A prerelease publication MUST occur only when the pull request is eligible and every required validation succeeds. + +#### Behavioral scenarios {#fr4-scenarios} + +```gherkin +Scenario: Add prerelease eligibility + Given a validated pull request has no prerelease eligibility + When a prerelease label is added + Then the workflow re-evaluates the pull request + And it publishes at most one eligible prerelease version + +Scenario: Remove prerelease eligibility + Given a pull request has prerelease eligibility + When its prerelease label is removed + Then the workflow re-evaluates the pull request as ineligible + And it does not create a new prerelease version +``` + +### FR5 — Closed pull requests MUST clean up only their prereleases {#fr5} + +A closed pull request MUST clean up only prerelease artifacts associated with that pull request when cleanup is enabled. It MUST NOT authorize or create a stable publication. + +#### Behavioral scenarios {#fr5-scenarios} + +```gherkin +Scenario: Close a pull request with prereleases + Given a closed pull request owns prerelease artifacts + When the cleanup workflow completes + Then the pull request's prerelease artifacts are removed according to configuration + And no stable artifact, tag, or release is created +``` + +### FR6 — Default-branch pushes MUST authorize stable publication after validation {#fr6} + +A push to the default branch MUST publish a stable version only after all required build, test, quality, and publication gates succeed. When the pushed commit is the merge commit of a pull request, the stable-release decision MUST use that pull request's release intent. + +#### Behavioral scenarios {#fr6-scenarios} + +```gherkin +Scenario: Publish a merged pull request + Given a merged pull request has an unambiguous release intent + And its merge commit is pushed to the default branch + When all required validation gates succeed + Then the workflow publishes the resulting stable version + And the publication is associated with the pushed commit +``` + +### FR7 — Published artifacts MUST match the resolved version {#fr7} + +Every prerelease or stable publication MUST contain the version and prerelease identity resolved for its workflow run. A version mismatch MUST fail publication before the release is made visible. + +#### Behavioral scenarios {#fr7-scenarios} + +```gherkin +Scenario: Reject an incorrectly stamped artifact + Given a workflow resolves a release version + And the built artifact reports a different version + When publication is attempted + Then publication fails + And no release is made visible for that artifact +``` + +## Non-functional requirements + +### NFR1 — Lifecycle mutations MUST be idempotent {#nfr1} + +Retrying the same event for the same commit and resolved version MUST produce no more than one package, tag, and release for that version. + +#### Behavioral scenarios {#nfr1-scenarios} + +```gherkin +Scenario: Retry a publication after an interrupted run + Given a publication for a resolved version was interrupted + When the workflow retries the same event + Then it completes the missing work or reports the completed work + And it does not duplicate the package, tag, or release +``` + +### NFR2 — Closed-pull-request cleanup and default-branch publication MUST be isolated {#nfr2} + +A closed-pull-request cleanup and a default-branch push MUST use distinct concurrency identities and MUST NOT cancel each other. Cleanup MUST remain limited to its pull request's prerelease artifacts while a default-branch push publishes a stable version. + +#### Behavioral scenarios {#nfr2-scenarios} + +```gherkin +Scenario: Cleanup and stable publication overlap + Given a pull request closes while another pull request is pushed to the default branch + When both workflow runs start + Then neither run cancels the other + And cleanup does not remove artifacts outside the closed pull request + And stable publication completes independently +``` + +### NFR3 — Each lifecycle outcome MUST be auditable {#nfr3} + +Every run MUST report its event category, resolved version or validated published version, release decision, and terminal outcome before the run completes. + +#### Behavioral scenarios {#nfr3-scenarios} + +```gherkin +Scenario: Inspect a scheduled validation result + Given a scheduled validation has completed + When a maintainer inspects the workflow result + Then the result identifies the validated published version + And it identifies whether validation passed or failed + And it identifies that no release mutation occurred +``` + +## Cross-cutting acceptance criteria + +### AC1 — Verifies: [FR1](#fr1), [FR6](#fr6), [FR7](#fr7), [NFR1](#nfr1) + +```gherkin +Scenario: Recover release notes after a missed main-push publication + Given merged pull requests exist after the last published stable version + And the selected default-branch commit has no stable publication + When a maintainer dispatches a recovery release + Then the published artifact matches the resolved version + And the release notes identify the merged pull requests in that unreleased range + And a retry creates no duplicate publication +``` + +### AC2 — Verifies: [FR2](#fr2), [FR5](#fr5), [NFR2](#nfr2), [NFR3](#nfr3) + +```gherkin +Scenario: Non-stable lifecycle events remain non-mutating + Given a scheduled validation and a closed-pull-request cleanup overlap a main-push release + When all three runs complete + Then the scheduled run reports validation without a release mutation + And the cleanup affects only the closed pull request's prereleases + And the main-push run is the only run that can publish the stable release +``` + +## Impact + +This candidate aims to reduce time to restore a missed publication and reduce change failure risk by separating validation, cleanup, prerelease, and stable-release authority. Its domain signal is the count of duplicate, missing, or incorrectly stamped published versions per release cycle; the target is zero. + +## Dependencies and constraints + +Approval of the caller event and concurrency contract in [PSModule/Process-PSModule#514](https://github.com/PSModule/Process-PSModule/issues/514) is required before this candidate becomes an implementation commitment. The candidate depends on repository credentials that can query pull requests and publish module artifacts. It retains the caller candidate's default-branch and fork boundaries. + +## Related + +- [Candidate design](process-workflow-lifecycle-design.md) — proposed routing and recovery approach. +- [Scenario matrix](scenario-matrix.md) — established job-level routing reference. +- [Process-PSModule caller workflow candidate](process-workflow-fleet-standard.md) — candidate caller event and concurrency contract. diff --git a/docs/zensical.toml b/docs/zensical.toml index cdd222bf..83627c27 100644 --- a/docs/zensical.toml +++ b/docs/zensical.toml @@ -47,6 +47,8 @@ nav = [ {"Scenario matrix" = "reference/scenario-matrix.md"}, {"Framework test IDs" = "reference/framework-test-ids.md"}, {"Dependencies" = "reference/dependencies.md"}, + {"Workflow lifecycle candidate specification" = "reference/process-workflow-lifecycle-specification.md"}, + {"Workflow lifecycle candidate design" = "reference/process-workflow-lifecycle-design.md"}, ]}, {"Specification" = [ "specification/index.md", From 722083330bcf88d3f900b5594a96826937785bb7 Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Sat, 15 Aug 2026 15:03:36 +0200 Subject: [PATCH 02/13] docs: refine lifecycle concurrency candidate Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../process-workflow-lifecycle-design.md | 61 +++++++++++++------ ...rocess-workflow-lifecycle-specification.md | 58 ++++++++++++------ 2 files changed, 79 insertions(+), 40 deletions(-) diff --git a/docs/content/reference/process-workflow-lifecycle-design.md b/docs/content/reference/process-workflow-lifecycle-design.md index 3af627e9..334f223e 100644 --- a/docs/content/reference/process-workflow-lifecycle-design.md +++ b/docs/content/reference/process-workflow-lifecycle-design.md @@ -19,18 +19,19 @@ These facts establish the starting point. They do not implement the scheduled va ## Candidate event routing -The planner classifies the caller event before build and publication work begins. Each route produces one of three mutation classes: validation only, prerelease mutation, or stable-release mutation. +The planner classifies the caller event before build and publication work begins. Each route produces one of three mutation classes: validation only, prerelease mutation, or stable-release mutation. The candidate also assigns an explicit mutability mode, because callers cannot safely infer it from labels alone. -| Event | Candidate route | Mutation class | Required result | -| --- | --- | --- | --- | -| `workflow_dispatch` on the default branch | Recovery release | Stable release or explicit no-op | Rebuild and validate the selected commit; reconstruct the unreleased release notes. | -| `schedule` | Published-artifact validation | Validation only | Validate the latest published stable artifact and its documentation. | -| Pull request `opened`, `reopened`, `synchronize` | Pull-request CI | Validation only | Report configured validation on the pull request. | -| Pull request `labeled`, `unlabeled` | Prerelease evaluation | Prerelease or validation only | Re-evaluate the full label set and publish only an eligible prerelease. | -| Pull request `closed` | Pull-request cleanup | Prerelease cleanup only | Remove only prereleases owned by the closed pull request. | -| Push to the default branch | Stable release | Stable release | Resolve merged-pull-request intent when applicable, then publish after required gates. | +| Event | Candidate route | Mutability mode | Cancellation | Required result | +| --- | --- | --- | --- | --- | +| `workflow_dispatch` on the default branch | Recovery release | Stable release or explicit no-op | Never cancel | Rebuild and validate the selected commit; reconstruct the unreleased release notes. | +| `schedule` | Published-artifact validation | Validation only | May cancel only another explicitly read-only validation; never cancel a default-branch release | Validate the latest published stable artifact and its documentation. | +| Pull request `opened`, `reopened`, `synchronize` | Pull-request CI | Explicitly read-only | May cancel a superseded run in the same read-only CI domain | Report configured validation on the pull request. | +| Pull request `labeled`, `unlabeled` | Prerelease evaluation | Potential prerelease mutation | Never cancel | Re-evaluate the full label set and publish only an eligible prerelease. | +| Merged pull request `closed` | Post-merge close | Validation only | May cancel only when explicitly routed as read-only | Do not clean up; the successful main-push release owns promotion cleanup. | +| Abandoned pull request `closed` | Pull-request cleanup | Prerelease cleanup only | Never cancel | Remove only prereleases owned by the abandoned pull request. | +| Push to the default branch | Stable release | Stable release | Never cancel | Resolve merged-pull-request intent when applicable, then publish and perform promotion cleanup after required gates. | -The classifier records the route, mutable resource scope, commit identity, and release decision in the plan result. Downstream jobs consume that record rather than infer the event again. +The classifier records the route, mutability mode, mutable resource scope, commit identity, and release decision in the plan result. Downstream jobs consume that record rather than infer the event again. ## Candidate artifact and version boundary @@ -57,17 +58,37 @@ The recovery route validates the selected commit using the same release gates as The scheduled route resolves the latest published stable version as an input, not as a version to create. It validates the downloaded package and its published documentation with the checks appropriate to a published consumer artifact. Its plan record sets the mutation class to validation only, so publication and cleanup stages cannot run. +## Candidate close behavior + +A merged pull-request close performs no prerelease cleanup because the default-branch push is the release authority. After its stable release succeeds, that main-push route owns promotion cleanup. An abandoned pull-request close runs only pull-request-scoped prerelease cleanup because no main push will occur. + +The alternative of never cleaning up on a close requires scheduled garbage collection and leaves abandoned prereleases available until that collection runs. This candidate selects abandoned-close cleanup instead; it does not change the existing unapproved caller contract. + ## Candidate concurrency isolation -Closed-pull-request cleanup and default-branch publication use independent concurrency identities: +Cancellation is conditional, not globally disabled. Only an explicit read-only mode may cancel a superseded run. A route that can mutate an external release resource never cancels and is never canceled. + +| Lifecycle route | Concurrency domain | Cancellation policy | Mutable resources | +| --- | --- | --- | --- | +| Explicitly read-only pull-request CI | Pull request number and read-only mode | Superseded runs may cancel | None. | +| Prerelease evaluation and abandoned-close cleanup | Pull request number and mutation mode | Never cancel | Prerelease artifacts associated with that pull request. | +| Default-branch push and manual recovery | Default-branch ref and mutation mode | Never cancel | Stable package, GitHub Release, tag, uploads, Pages, and promotion cleanup. | +| Scheduled validation | Published-version read domain | May cancel only another explicit scheduled validation | None. | + +Concurrency selection occurs before the reusable workflow can produce a planner output. The candidate therefore requires either an explicit event mode at the caller boundary or separate read-only and mutation workflow domains. Caller-level label inspection is insufficient: repository settings can make a label event release-capable. Manual and scheduled runs must not share a cancellation domain that can cancel a default-branch release. + +Cleanup receives a pull-request-scoped artifact set and MUST NOT perform a broad prerelease deletion while a stable release can be active. Any future broad cleanup needs a separately approved exclusive scope; it cannot share the abandoned-close route. -| Lifecycle route | Concurrency scope | Mutable resources | -| --- | --- | --- | -| Closed pull request | Pull request number | Prereleases associated with that pull request. | -| Default-branch push or manual recovery | Default-branch ref | The stable release and its versioned artifacts. | -| Scheduled validation | Published-version read scope | No release-related mutable resources. | +## Why mutation-capable runs never cancel -The routes do not cancel one another. Cleanup receives a pull-request-scoped artifact set and MUST NOT perform a broad prerelease deletion while a stable release can be active. Any future broad cleanup needs a separately approved exclusive scope; it cannot share the closed-pull-request route. +Cancellation after an external mutation begins can leave the release lifecycle partially complete: + +| Mutation path | Cancellation hazard | +| --- | --- | +| PowerShell Gallery publication | The package can publish before the GitHub Release, release-asset upload, or pull-request comment completes. | +| Prerelease cleanup | Cleanup can delete only part of a prerelease tag and release set. | +| Default-branch release | A main release can stop between package publication, tag or GitHub Release creation, uploads, comments, Pages deployment, and promotion cleanup. | +| Manual or scheduled run sharing a ref | A cancellation domain keyed only by ref can interrupt a main release with a manual dispatch or scheduled validation. | ## Candidate verification strategy @@ -75,11 +96,11 @@ The lifecycle contract is exercised with event payload fixtures and publication | Candidate behavior | Verification | | --- | --- | -| Event routing | One fixture for each supported event and pull-request activity. | +| Event routing | One fixture for each supported event and pull-request activity, including merged and abandoned close outcomes. | | Version boundary | A mismatched artifact fixture that proves publication stops. | | Recovery release notes | Merged-pull-request query fixtures covering an empty range, one pull request, and multiple pull requests. | | Scheduled validation | A published-version fixture that proves no release mutation is requested. | -| Concurrency isolation | Overlapping closed-pull-request and default-branch-push runs that prove neither run cancels or broadens the other's scope. | +| Conditional cancellation | A superseded read-only CI fixture plus release-capable label, cleanup, main-push, manual, and scheduled fixtures that prove mutation-capable runs do not cancel. | ## Decisions requiring approval @@ -88,7 +109,7 @@ The candidate does not decide the following: - Whether a recovery release always uses the normal next patch version or permits an explicit version input. - Which source is authoritative when a PowerShell Gallery publication and GitHub release disagree about the last published stable version. - Which consumer-facing checks comprise scheduled published-artifact validation. -- Whether removing prerelease eligibility cleans up existing prereleases immediately or leaves cleanup to pull-request closure. +- Whether removing prerelease eligibility cleans up existing prereleases immediately or leaves them until the abandoned-close cleanup route. - The exact caller triggers and concurrency expression, which remain subject to [PSModule/Process-PSModule#514](https://github.com/PSModule/Process-PSModule/issues/514). ## Related diff --git a/docs/content/reference/process-workflow-lifecycle-specification.md b/docs/content/reference/process-workflow-lifecycle-specification.md index d7b7165e..734ebbf5 100644 --- a/docs/content/reference/process-workflow-lifecycle-specification.md +++ b/docs/content/reference/process-workflow-lifecycle-specification.md @@ -98,23 +98,29 @@ Scenario: Remove prerelease eligibility And it does not create a new prerelease version ``` -### FR5 — Closed pull requests MUST clean up only their prereleases {#fr5} +### FR5 — Closed pull requests MUST route cleanup by close outcome {#fr5} -A closed pull request MUST clean up only prerelease artifacts associated with that pull request when cleanup is enabled. It MUST NOT authorize or create a stable publication. +A merged pull request close MUST NOT perform prerelease cleanup. A successful default-branch release for the merge MUST own promotion cleanup. An abandoned pull request close MUST run cleanup-only behavior for prerelease artifacts associated with that pull request when cleanup is enabled. Neither close outcome MUST authorize or create a stable publication. #### Behavioral scenarios {#fr5-scenarios} ```gherkin -Scenario: Close a pull request with prereleases - Given a closed pull request owns prerelease artifacts - When the cleanup workflow completes - Then the pull request's prerelease artifacts are removed according to configuration +Scenario: Merge a pull request with prereleases + Given a merged pull request owns prerelease artifacts + When its close event is processed + Then the close event does not clean up prerelease artifacts + And the successful default-branch release owns promotion cleanup + +Scenario: Abandon a pull request with prereleases + Given an unmerged closed pull request owns prerelease artifacts + When its close event is processed + Then the workflow runs cleanup only for that pull request's prerelease artifacts And no stable artifact, tag, or release is created ``` ### FR6 — Default-branch pushes MUST authorize stable publication after validation {#fr6} -A push to the default branch MUST publish a stable version only after all required build, test, quality, and publication gates succeed. When the pushed commit is the merge commit of a pull request, the stable-release decision MUST use that pull request's release intent. +A push to the default branch MUST publish a stable version only after all required build, test, quality, and publication gates succeed. When the pushed commit is the merge commit of a pull request, the stable-release decision MUST use that pull request's release intent. A successful stable release for a merged pull request MUST own promotion cleanup. #### Behavioral scenarios {#fr6-scenarios} @@ -125,6 +131,7 @@ Scenario: Publish a merged pull request When all required validation gates succeed Then the workflow publishes the resulting stable version And the publication is associated with the pushed commit + And the successful release performs promotion cleanup ``` ### FR7 — Published artifacts MUST match the resolved version {#fr7} @@ -158,19 +165,29 @@ Scenario: Retry a publication after an interrupted run And it does not duplicate the package, tag, or release ``` -### NFR2 — Closed-pull-request cleanup and default-branch publication MUST be isolated {#nfr2} +### NFR2 — Cancellation MUST be conditional on an explicit mutability mode {#nfr2} -A closed-pull-request cleanup and a default-branch push MUST use distinct concurrency identities and MUST NOT cancel each other. Cleanup MUST remain limited to its pull request's prerelease artifacts while a default-branch push publishes a stable version. +A run explicitly classified as read-only pull-request CI SHOULD cancel a superseded `opened`, `reopened`, or `synchronize` run for the same pull request. A run that may publish to the PowerShell Gallery, create or upload a GitHub Release or tag, deploy Pages, or clean up prereleases MUST NOT be canceled. Caller-level label inspection MUST NOT determine mutability because repository settings can change the release decision. Manual and scheduled runs sharing a ref MUST NOT cancel a default-branch release. #### Behavioral scenarios {#nfr2-scenarios} ```gherkin -Scenario: Cleanup and stable publication overlap - Given a pull request closes while another pull request is pushed to the default branch - When both workflow runs start - Then neither run cancels the other - And cleanup does not remove artifacts outside the closed pull request - And stable publication completes independently +Scenario: Supersede read-only pull-request CI + Given an explicitly read-only pull-request CI run is in progress + When a newer synchronize event starts the same read-only route + Then the older run may be canceled + And no release-related resource is mutated by either run + +Scenario: Preserve a release-capable run + Given a run may publish, deploy, or clean up prereleases + When a newer event starts + Then the release-capable run is not canceled + And the newer event does not infer safety from pull-request labels alone + +Scenario: Keep main release independent from manual and scheduled runs + Given a default-branch release is in progress + When a manual dispatch or scheduled validation starts for the same ref + Then neither event cancels the default-branch release ``` ### NFR3 — Each lifecycle outcome MUST be auditable {#nfr3} @@ -202,15 +219,16 @@ Scenario: Recover release notes after a missed main-push publication And a retry creates no duplicate publication ``` -### AC2 — Verifies: [FR2](#fr2), [FR5](#fr5), [NFR2](#nfr2), [NFR3](#nfr3) +### AC2 — Verifies: [FR2](#fr2), [FR5](#fr5), [FR6](#fr6), [NFR2](#nfr2), [NFR3](#nfr3) ```gherkin -Scenario: Non-stable lifecycle events remain non-mutating - Given a scheduled validation and a closed-pull-request cleanup overlap a main-push release +Scenario: Lifecycle runs preserve release ownership + Given a scheduled validation and an abandoned pull-request cleanup overlap a main-push release When all three runs complete Then the scheduled run reports validation without a release mutation - And the cleanup affects only the closed pull request's prereleases - And the main-push run is the only run that can publish the stable release + And the cleanup affects only the abandoned pull request's prereleases + And none of the runs cancel the main-push release + And the main-push run is the only run that publishes the stable release and performs promotion cleanup ``` ## Impact From ed22f0bef73f6393cd022e04a3b923531e4faf64 Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Sat, 15 Aug 2026 15:09:00 +0200 Subject: [PATCH 03/13] docs: select resumable PR concurrency Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../process-workflow-lifecycle-design.md | 57 ++++++++-------- ...rocess-workflow-lifecycle-specification.md | 67 +++++++++++++------ 2 files changed, 76 insertions(+), 48 deletions(-) diff --git a/docs/content/reference/process-workflow-lifecycle-design.md b/docs/content/reference/process-workflow-lifecycle-design.md index 334f223e..58d73e42 100644 --- a/docs/content/reference/process-workflow-lifecycle-design.md +++ b/docs/content/reference/process-workflow-lifecycle-design.md @@ -19,19 +19,19 @@ These facts establish the starting point. They do not implement the scheduled va ## Candidate event routing -The planner classifies the caller event before build and publication work begins. Each route produces one of three mutation classes: validation only, prerelease mutation, or stable-release mutation. The candidate also assigns an explicit mutability mode, because callers cannot safely infer it from labels alone. +The planner classifies the caller event before build and publication work begins. Each route produces one of three mutation classes: validation only, prerelease mutation, or stable-release mutation. Pull-request events may cancel their predecessors, so every pull-request route must converge to the latest pull-request state. -| Event | Candidate route | Mutability mode | Cancellation | Required result | +| Event | Candidate route | Mutation class | Cancellation | Required result | | --- | --- | --- | --- | --- | | `workflow_dispatch` on the default branch | Recovery release | Stable release or explicit no-op | Never cancel | Rebuild and validate the selected commit; reconstruct the unreleased release notes. | -| `schedule` | Published-artifact validation | Validation only | May cancel only another explicitly read-only validation; never cancel a default-branch release | Validate the latest published stable artifact and its documentation. | -| Pull request `opened`, `reopened`, `synchronize` | Pull-request CI | Explicitly read-only | May cancel a superseded run in the same read-only CI domain | Report configured validation on the pull request. | -| Pull request `labeled`, `unlabeled` | Prerelease evaluation | Potential prerelease mutation | Never cancel | Re-evaluate the full label set and publish only an eligible prerelease. | -| Merged pull request `closed` | Post-merge close | Validation only | May cancel only when explicitly routed as read-only | Do not clean up; the successful main-push release owns promotion cleanup. | -| Abandoned pull request `closed` | Pull-request cleanup | Prerelease cleanup only | Never cancel | Remove only prereleases owned by the abandoned pull request. | +| `schedule` | Published-artifact validation | Validation only | Never cancel | Validate the latest published stable artifact and its documentation. | +| Pull request `opened`, `reopened`, `synchronize` | Pull-request CI | Validation only | Cancels a superseded pull-request run; the later run converges state | Report configured validation on the pull request. | +| Pull request `labeled`, `unlabeled` | Prerelease evaluation | Prerelease or validation only | Cancels a superseded pull-request run; the later run converges state | Re-evaluate the full label set and publish only an eligible prerelease. | +| Merged pull request `closed` | Post-merge close | Validation only | Cancels a superseded pull-request run; the later run converges state | Do not clean up; the successful main-push release owns promotion cleanup. | +| Abandoned pull request `closed` | Pull-request cleanup | Prerelease cleanup only | Cancels a superseded pull-request run; the later run converges state | Remove only prereleases owned by the abandoned pull request. | | Push to the default branch | Stable release | Stable release | Never cancel | Resolve merged-pull-request intent when applicable, then publish and perform promotion cleanup after required gates. | -The classifier records the route, mutability mode, mutable resource scope, commit identity, and release decision in the plan result. Downstream jobs consume that record rather than infer the event again. +The classifier records the route, mutable resource scope, commit identity, and release decision in the plan result. Downstream jobs consume that record rather than infer the event again. ## Candidate artifact and version boundary @@ -64,31 +64,29 @@ A merged pull-request close performs no prerelease cleanup because the default-b The alternative of never cleaning up on a close requires scheduled garbage collection and leaves abandoned prereleases available until that collection runs. This candidate selects abandoned-close cleanup instead; it does not change the existing unapproved caller contract. -## Candidate concurrency isolation +## Candidate concurrency and recovery -Cancellation is conditional, not globally disabled. Only an explicit read-only mode may cancel a superseded run. A route that can mutate an external release resource never cancels and is never canceled. +The candidate caller recommendation is: -| Lifecycle route | Concurrency domain | Cancellation policy | Mutable resources | -| --- | --- | --- | --- | -| Explicitly read-only pull-request CI | Pull request number and read-only mode | Superseded runs may cancel | None. | -| Prerelease evaluation and abandoned-close cleanup | Pull request number and mutation mode | Never cancel | Prerelease artifacts associated with that pull request. | -| Default-branch push and manual recovery | Default-branch ref and mutation mode | Never cancel | Stable package, GitHub Release, tag, uploads, Pages, and promotion cleanup. | -| Scheduled validation | Published-version read domain | May cancel only another explicit scheduled validation | None. | +```yaml +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} +``` -Concurrency selection occurs before the reusable workflow can produce a planner output. The candidate therefore requires either an explicit event mode at the caller boundary or separate read-only and mutation workflow domains. Caller-level label inspection is insufficient: repository settings can make a label event release-capable. Manual and scheduled runs must not share a cancellation domain that can cancel a default-branch release. +This is a candidate, not an approved caller contract. All pull-request events for one pull request share a group and may cancel an earlier run. Push, manual-dispatch, and scheduled events serialize by ref and never cancel; a manual dispatch or schedule therefore cannot interrupt a main release. -Cleanup receives a pull-request-scoped artifact set and MUST NOT perform a broad prerelease deletion while a stable release can be active. Any future broad cleanup needs a separately approved exclusive scope; it cannot share the abandoned-close route. - -## Why mutation-capable runs never cancel - -Cancellation after an external mutation begins can leave the release lifecycle partially complete: +Cancellation can leave only transient partial state. Every pull-request route, including prerelease publication and abandoned-close cleanup, resumes and reconciles on the next `synchronize`, `labeled`, `unlabeled`, or `closed` event: -| Mutation path | Cancellation hazard | +| External operation | Required recovery behavior | | --- | --- | -| PowerShell Gallery publication | The package can publish before the GitHub Release, release-asset upload, or pull-request comment completes. | -| Prerelease cleanup | Cleanup can delete only part of a prerelease tag and release set. | -| Default-branch release | A main release can stop between package publication, tag or GitHub Release creation, uploads, comments, Pages deployment, and promotion cleanup. | -| Manual or scheduled run sharing a ref | A cancellation domain keyed only by ref can interrupt a main release with a manual dispatch or scheduled validation. | +| PowerShell Gallery publication | Detect the existing resolved version and continue without duplicate publication. | +| GitHub Release creation | Resume or upsert the release and replace its asset set. | +| Prerelease cleanup | Repeat safely after partial deletion and converge to the latest pull-request state. | +| Prerelease lifecycle | A subsequent synchronize, label, unlabel, or close reconciles obsolete prereleases. | +| Production boundary | No pull-request event creates a stable or signable production artifact. | + +Cleanup receives a pull-request-scoped artifact set and MUST NOT perform a broad prerelease deletion while a stable release can be active. Any future broad cleanup needs a separately approved exclusive scope; it cannot share the abandoned-close route. ## Candidate verification strategy @@ -100,7 +98,8 @@ The lifecycle contract is exercised with event payload fixtures and publication | Version boundary | A mismatched artifact fixture that proves publication stops. | | Recovery release notes | Merged-pull-request query fixtures covering an empty range, one pull request, and multiple pull requests. | | Scheduled validation | A published-version fixture that proves no release mutation is requested. | -| Conditional cancellation | A superseded read-only CI fixture plus release-capable label, cleanup, main-push, manual, and scheduled fixtures that prove mutation-capable runs do not cancel. | +| Pull-request convergence | Canceled prerelease-publication and cleanup fixtures followed by synchronize, label, unlabel, and close events that prove the latest pull-request state is reconciled. | +| Non-pull-request serialization | Overlapping main-push, manual-dispatch, and scheduled fixtures that prove runs queue by ref and do not cancel. | ## Decisions requiring approval @@ -110,7 +109,7 @@ The candidate does not decide the following: - Which source is authoritative when a PowerShell Gallery publication and GitHub release disagree about the last published stable version. - Which consumer-facing checks comprise scheduled published-artifact validation. - Whether removing prerelease eligibility cleans up existing prereleases immediately or leaves them until the abandoned-close cleanup route. -- The exact caller triggers and concurrency expression, which remain subject to [PSModule/Process-PSModule#514](https://github.com/PSModule/Process-PSModule/issues/514). +- Approval of the selected caller concurrency expression in [PSModule/Process-PSModule#514](https://github.com/PSModule/Process-PSModule/issues/514). ## Related diff --git a/docs/content/reference/process-workflow-lifecycle-specification.md b/docs/content/reference/process-workflow-lifecycle-specification.md index 734ebbf5..4a5b23e6 100644 --- a/docs/content/reference/process-workflow-lifecycle-specification.md +++ b/docs/content/reference/process-workflow-lifecycle-specification.md @@ -165,29 +165,23 @@ Scenario: Retry a publication after an interrupted run And it does not duplicate the package, tag, or release ``` -### NFR2 — Cancellation MUST be conditional on an explicit mutability mode {#nfr2} +### NFR2 — Pull-request cancellation MUST preserve non-pull-request serialization {#nfr2} -A run explicitly classified as read-only pull-request CI SHOULD cancel a superseded `opened`, `reopened`, or `synchronize` run for the same pull request. A run that may publish to the PowerShell Gallery, create or upload a GitHub Release or tag, deploy Pages, or clean up prereleases MUST NOT be canceled. Caller-level label inspection MUST NOT determine mutability because repository settings can change the release decision. Manual and scheduled runs sharing a ref MUST NOT cancel a default-branch release. +All pull-request events for one pull request MUST share a cancellation scope, so a newer pull-request event cancels a superseded run. Push, manual dispatch, and scheduled runs MUST share ref-based serialization and MUST NOT cancel an in-progress run. #### Behavioral scenarios {#nfr2-scenarios} ```gherkin -Scenario: Supersede read-only pull-request CI - Given an explicitly read-only pull-request CI run is in progress - When a newer synchronize event starts the same read-only route - Then the older run may be canceled - And no release-related resource is mutated by either run - -Scenario: Preserve a release-capable run - Given a run may publish, deploy, or clean up prereleases - When a newer event starts - Then the release-capable run is not canceled - And the newer event does not infer safety from pull-request labels alone - -Scenario: Keep main release independent from manual and scheduled runs +Scenario: Supersede a pull-request run + Given a pull-request run is in progress + When a newer synchronize, label, unlabel, or close event starts + Then the older pull-request run is canceled + +Scenario: Serialize non-pull-request runs Given a default-branch release is in progress When a manual dispatch or scheduled validation starts for the same ref - Then neither event cancels the default-branch release + Then the later run waits for the default-branch release + And neither run cancels the other ``` ### NFR3 — Each lifecycle outcome MUST be auditable {#nfr3} @@ -205,6 +199,40 @@ Scenario: Inspect a scheduled validation result And it identifies that no release mutation occurred ``` +### NFR4 — Pull-request mutation paths MUST resume and converge {#nfr4} + +Every pull-request path, including prerelease publication and cleanup, MUST be idempotent and resumable after cancellation. The next `synchronize`, `labeled`, `unlabeled`, or `closed` event MUST converge release-related state to the latest pull-request state. Cancellation MAY leave transient partial state, but it MUST NOT leave a permanent duplicate or obsolete artifact. + +#### Behavioral scenarios {#nfr4-scenarios} + +```gherkin +Scenario: Resume a canceled prerelease publication + Given a canceled pull-request run published a prerelease package but did not finish its release metadata + When a later pull-request event is processed + Then the workflow detects the existing version + And it completes or reconciles the prerelease release state without duplication + +Scenario: Reconcile obsolete prereleases + Given a canceled pull-request run left prerelease artifacts for an earlier pull-request state + When a synchronize, label, unlabel, or close event is processed + Then the workflow reconciles prerelease artifacts to the latest pull-request state + And no obsolete prerelease artifact remains after reconciliation +``` + +### NFR5 — Pull-request events MUST NOT produce production artifacts {#nfr5} + +A pull-request event MUST NOT create a stable or signable production artifact. Pull-request events MAY create only eligible prerelease artifacts and their associated metadata. + +#### Behavioral scenarios {#nfr5-scenarios} + +```gherkin +Scenario: Evaluate an eligible prerelease pull request + Given a pull request is eligible for prerelease publication + When its release-capable path completes + Then it creates only prerelease artifacts and metadata + And it does not create a stable or signable production artifact +``` + ## Cross-cutting acceptance criteria ### AC1 — Verifies: [FR1](#fr1), [FR6](#fr6), [FR7](#fr7), [NFR1](#nfr1) @@ -219,14 +247,15 @@ Scenario: Recover release notes after a missed main-push publication And a retry creates no duplicate publication ``` -### AC2 — Verifies: [FR2](#fr2), [FR5](#fr5), [FR6](#fr6), [NFR2](#nfr2), [NFR3](#nfr3) +### AC2 — Verifies: [FR2](#fr2), [FR4](#fr4), [FR5](#fr5), [FR6](#fr6), [NFR2](#nfr2), [NFR3](#nfr3), [NFR4](#nfr4), [NFR5](#nfr5) ```gherkin -Scenario: Lifecycle runs preserve release ownership +Scenario: Lifecycle runs preserve release ownership after cancellation Given a scheduled validation and an abandoned pull-request cleanup overlap a main-push release + And the cleanup supersedes a canceled prerelease publication When all three runs complete Then the scheduled run reports validation without a release mutation - And the cleanup affects only the abandoned pull request's prereleases + And the cleanup reconciles only the abandoned pull request's prereleases And none of the runs cancel the main-push release And the main-push run is the only run that publishes the stable release and performs promotion cleanup ``` From 87ba2aadafcfbf82b08c6e8333ab65a86eeaaf08 Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Sat, 15 Aug 2026 15:10:01 +0200 Subject: [PATCH 04/13] docs: define Gallery prerelease recovery Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../process-workflow-lifecycle-design.md | 17 ++++++- ...rocess-workflow-lifecycle-specification.md | 47 +++++++++++++++++-- 2 files changed, 59 insertions(+), 5 deletions(-) diff --git a/docs/content/reference/process-workflow-lifecycle-design.md b/docs/content/reference/process-workflow-lifecycle-design.md index 58d73e42..1a5a472a 100644 --- a/docs/content/reference/process-workflow-lifecycle-design.md +++ b/docs/content/reference/process-workflow-lifecycle-design.md @@ -15,7 +15,7 @@ The confirmed reusable workflow runs a `Plan` job, enriches one settings object The version resolver treats non-pull-request events, including `workflow_dispatch` and `schedule`, as events without a release decision. The workflow's concurrency identity contains either the pull request number or the Git ref, and its runs are not canceled. The documented scenario matrix also identifies closed-pull-request cleanup as non-stable behavior. -These facts establish the starting point. They do not implement the scheduled validation or manual recovery behavior proposed below. +The current code detects an existing published version, resumes GitHub Release creation, and repeats GitHub Release and tag cleanup. Full reconciliation of immutable PowerShell Gallery prereleases is a design gap: it requires implementation and cancellation-boundary tests. These facts establish the starting point. They do not implement the scheduled validation or manual recovery behavior proposed below. ## Candidate event routing @@ -80,7 +80,7 @@ Cancellation can leave only transient partial state. Every pull-request route, i | External operation | Required recovery behavior | | --- | --- | -| PowerShell Gallery publication | Detect the existing resolved version and continue without duplicate publication. | +| PowerShell Gallery publication | Use a deterministic pull-request-scoped prerelease identity, detect the existing resolved version, and continue without duplicate publication. | | GitHub Release creation | Resume or upsert the release and replace its asset set. | | Prerelease cleanup | Repeat safely after partial deletion and converge to the latest pull-request state. | | Prerelease lifecycle | A subsequent synchronize, label, unlabel, or close reconciles obsolete prereleases. | @@ -88,6 +88,17 @@ Cancellation can leave only transient partial state. Every pull-request route, i Cleanup receives a pull-request-scoped artifact set and MUST NOT perform a broad prerelease deletion while a stable release can be active. Any future broad cleanup needs a separately approved exclusive scope; it cannot share the abandoned-close route. +## Candidate Gallery prerelease disposition + +PowerShell Gallery packages are immutable and cannot be overwritten. Gallery reconciliation is therefore separate from GitHub Release and tag cleanup: + +| Obsolete Gallery prerelease condition | Candidate disposition | +| --- | --- | +| A supported Gallery API can unlist the version | Unlist the immutable package through that API. | +| Unlisting is not feasible | Retain the immutable version and record it as retained in the lifecycle result. | + +The selected disposition policy remains unapproved. A cancellation can leave a published immutable package even when its GitHub Release and tag cleanup has not completed; the next pull-request event must detect that version and apply the approved Gallery disposition rather than attempting to overwrite it. + ## Candidate verification strategy The lifecycle contract is exercised with event payload fixtures and publication fakes before credentials are used: @@ -99,6 +110,7 @@ The lifecycle contract is exercised with event payload fixtures and publication | Recovery release notes | Merged-pull-request query fixtures covering an empty range, one pull request, and multiple pull requests. | | Scheduled validation | A published-version fixture that proves no release mutation is requested. | | Pull-request convergence | Canceled prerelease-publication and cleanup fixtures followed by synchronize, label, unlabel, and close events that prove the latest pull-request state is reconciled. | +| Gallery immutability | Deterministic pull-request identity, existing-version detection, supported-unlist, and retained-version fixtures across the cancellation boundary. | | Non-pull-request serialization | Overlapping main-push, manual-dispatch, and scheduled fixtures that prove runs queue by ref and do not cancel. | ## Decisions requiring approval @@ -109,6 +121,7 @@ The candidate does not decide the following: - Which source is authoritative when a PowerShell Gallery publication and GitHub release disagree about the last published stable version. - Which consumer-facing checks comprise scheduled published-artifact validation. - Whether removing prerelease eligibility cleans up existing prereleases immediately or leaves them until the abandoned-close cleanup route. +- Whether the supported Gallery API can unlist obsolete prereleases; otherwise, how retained immutable versions are recorded. - Approval of the selected caller concurrency expression in [PSModule/Process-PSModule#514](https://github.com/PSModule/Process-PSModule/issues/514). ## Related diff --git a/docs/content/reference/process-workflow-lifecycle-specification.md b/docs/content/reference/process-workflow-lifecycle-specification.md index 4a5b23e6..40a84487 100644 --- a/docs/content/reference/process-workflow-lifecycle-specification.md +++ b/docs/content/reference/process-workflow-lifecycle-specification.md @@ -149,6 +149,25 @@ Scenario: Reject an incorrectly stamped artifact And no release is made visible for that artifact ``` +### FR8 — Pull-request prereleases MUST have deterministic scoped identities {#fr8} + +A pull-request prerelease MUST use a deterministic identity scoped to its pull request. Reprocessing the same pull-request state MUST resolve the same prerelease identity, and different pull requests MUST NOT resolve the same identity. + +#### Behavioral scenarios {#fr8-scenarios} + +```gherkin +Scenario: Reprocess the same pull-request state + Given a pull request has resolved a prerelease identity + When the same pull-request state is processed again + Then the workflow resolves the same prerelease identity + And it detects an existing publication instead of attempting an overwrite + +Scenario: Publish prereleases for distinct pull requests + Given two pull requests are eligible for prerelease publication + When both pull requests are processed + Then each pull request resolves a distinct prerelease identity +``` + ## Non-functional requirements ### NFR1 — Lifecycle mutations MUST be idempotent {#nfr1} @@ -201,7 +220,7 @@ Scenario: Inspect a scheduled validation result ### NFR4 — Pull-request mutation paths MUST resume and converge {#nfr4} -Every pull-request path, including prerelease publication and cleanup, MUST be idempotent and resumable after cancellation. The next `synchronize`, `labeled`, `unlabeled`, or `closed` event MUST converge release-related state to the latest pull-request state. Cancellation MAY leave transient partial state, but it MUST NOT leave a permanent duplicate or obsolete artifact. +Every pull-request path, including prerelease publication and cleanup, MUST be idempotent and resumable after cancellation. The next `synchronize`, `labeled`, `unlabeled`, or `closed` event MUST converge release-related state to the latest pull-request state. Cancellation MAY leave transient partial state, but it MUST NOT leave a permanent duplicate or an obsolete artifact without the required disposition. #### Behavioral scenarios {#nfr4-scenarios} @@ -216,7 +235,7 @@ Scenario: Reconcile obsolete prereleases Given a canceled pull-request run left prerelease artifacts for an earlier pull-request state When a synchronize, label, unlabel, or close event is processed Then the workflow reconciles prerelease artifacts to the latest pull-request state - And no obsolete prerelease artifact remains after reconciliation + And every obsolete prerelease has the required disposition ``` ### NFR5 — Pull-request events MUST NOT produce production artifacts {#nfr5} @@ -233,6 +252,28 @@ Scenario: Evaluate an eligible prerelease pull request And it does not create a stable or signable production artifact ``` +### NFR6 — Immutable Gallery prereleases MUST have a disposition policy {#nfr6} + +PowerShell Gallery packages MUST be treated as immutable and MUST NOT be overwritten. The candidate MUST define whether obsolete pull-request prereleases are unlisted through a supported Gallery API or retained as documented immutable versions when unlisting is not feasible. This policy is distinct from GitHub Release and tag cleanup. + +#### Behavioral scenarios {#nfr6-scenarios} + +```gherkin +Scenario: Dispose of an obsolete Gallery prerelease + Given a pull-request prerelease is obsolete + And a supported Gallery API can unlist that version + When the prerelease is reconciled + Then the workflow unlists the immutable Gallery package + And it performs GitHub Release and tag cleanup independently + +Scenario: Retain an immutable Gallery prerelease + Given a pull-request prerelease is obsolete + And unlisting that Gallery version is not feasible + When the prerelease is reconciled + Then the workflow records the retained immutable Gallery version + And it performs GitHub Release and tag cleanup independently +``` + ## Cross-cutting acceptance criteria ### AC1 — Verifies: [FR1](#fr1), [FR6](#fr6), [FR7](#fr7), [NFR1](#nfr1) @@ -247,7 +288,7 @@ Scenario: Recover release notes after a missed main-push publication And a retry creates no duplicate publication ``` -### AC2 — Verifies: [FR2](#fr2), [FR4](#fr4), [FR5](#fr5), [FR6](#fr6), [NFR2](#nfr2), [NFR3](#nfr3), [NFR4](#nfr4), [NFR5](#nfr5) +### AC2 — Verifies: [FR2](#fr2), [FR4](#fr4), [FR5](#fr5), [FR6](#fr6), [FR8](#fr8), [NFR2](#nfr2), [NFR3](#nfr3), [NFR4](#nfr4), [NFR5](#nfr5), [NFR6](#nfr6) ```gherkin Scenario: Lifecycle runs preserve release ownership after cancellation From 7a0a1d32c4b2d49f45874827df33284163c9f425 Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Sat, 15 Aug 2026 15:14:37 +0200 Subject: [PATCH 05/13] docs: centralize lifecycle decisions in plan Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../process-workflow-lifecycle-design.md | 42 ++++++++++++------- ...rocess-workflow-lifecycle-specification.md | 36 ++++++++++++---- 2 files changed, 54 insertions(+), 24 deletions(-) diff --git a/docs/content/reference/process-workflow-lifecycle-design.md b/docs/content/reference/process-workflow-lifecycle-design.md index 1a5a472a..bedac6a8 100644 --- a/docs/content/reference/process-workflow-lifecycle-design.md +++ b/docs/content/reference/process-workflow-lifecycle-design.md @@ -19,32 +19,42 @@ The current code detects an existing published version, resumes GitHub Release c ## Candidate event routing -The planner classifies the caller event before build and publication work begins. Each route produces one of three mutation classes: validation only, prerelease mutation, or stable-release mutation. Pull-request events may cancel their predecessors, so every pull-request route must converge to the latest pull-request state. +Plan resolves the caller event into one release classification before build and publication work begins. The classification is stable release, prerelease, recovery or resume, cleanup-only, or no-op. Pull-request events may cancel their predecessors, so every pull-request route must converge to the latest pull-request state. -| Event | Candidate route | Mutation class | Cancellation | Required result | +| Event | Candidate Plan classification | Desired release action | Cancellation | Required result | | --- | --- | --- | --- | --- | -| `workflow_dispatch` on the default branch | Recovery release | Stable release or explicit no-op | Never cancel | Rebuild and validate the selected commit; reconstruct the unreleased release notes. | -| `schedule` | Published-artifact validation | Validation only | Never cancel | Validate the latest published stable artifact and its documentation. | -| Pull request `opened`, `reopened`, `synchronize` | Pull-request CI | Validation only | Cancels a superseded pull-request run; the later run converges state | Report configured validation on the pull request. | -| Pull request `labeled`, `unlabeled` | Prerelease evaluation | Prerelease or validation only | Cancels a superseded pull-request run; the later run converges state | Re-evaluate the full label set and publish only an eligible prerelease. | -| Merged pull request `closed` | Post-merge close | Validation only | Cancels a superseded pull-request run; the later run converges state | Do not clean up; the successful main-push release owns promotion cleanup. | -| Abandoned pull request `closed` | Pull-request cleanup | Prerelease cleanup only | Cancels a superseded pull-request run; the later run converges state | Remove only prereleases owned by the abandoned pull request. | +| `workflow_dispatch` on the default branch | Recovery or resume | Stable release or no-op | Never cancel | Rebuild and validate the selected commit; reconstruct the unreleased release notes. | +| `schedule` | Published-artifact validation | No-op after validation | Never cancel | Validate the latest published stable artifact and its documentation. | +| Pull request `opened`, `reopened`, `synchronize` | Pull-request classification | Prerelease or no-op | Cancels a superseded pull-request run; the later run converges state | Report configured validation and execute the planned release action. | +| Pull request `labeled`, `unlabeled` | Pull-request classification refresh | Prerelease, cleanup-only, or no-op | Cancels a superseded pull-request run; the later run converges state | Resolve the complete current classification and execute its action. | +| Merged pull request `closed` | Post-merge close | No-op | Cancels a superseded pull-request run; the later run converges state | Do not clean up; the successful main-push release owns promotion cleanup. | +| Abandoned pull request `closed` | Abandoned-close classification | Cleanup-only | Cancels a superseded pull-request run; the later run converges state | Reconcile only prereleases owned by the abandoned pull request. | | Push to the default branch | Stable release | Stable release | Never cancel | Resolve merged-pull-request intent when applicable, then publish and perform promotion cleanup after required gates. | -The classifier records the route, mutable resource scope, commit identity, and release decision in the plan result. Downstream jobs consume that record rather than infer the event again. +Plan records its classification and release decision in enriched Settings. Downstream jobs consume that Settings object and do not infer policy from events, labels, or repository settings again. ## Candidate artifact and version boundary -Version resolution is the boundary between planning and release-capable work. The candidate carries one immutable release record through build, test, and publication: +Version resolution is the boundary between planning and release-capable work. The candidate carries one immutable release record in enriched Settings through build, test, and release execution: | Record field | Purpose | | --- | --- | -| Commit identity | Binds validation, artifact, and release to one source revision. | -| Resolved stable version and prerelease identity | Defines the only version permitted in the built artifact. | -| Event route and mutation class | Restricts each downstream stage to its authorized behavior. | -| Release-note range | Identifies the merged pull requests eligible for a recovery release note. | - -The build stage stamps the resolved version into the module artifact. Before any package, tag, or release becomes visible, the publication stage verifies that the artifact version and prerelease identity equal the immutable release record. A mismatch stops publication; it is not corrected by retagging or by recalculating a version after the artifact is built. +| Event and run type | Identifies the GitHub event and its candidate lifecycle classification. | +| Event action | Preserves the relevant pull-request activity or non-pull-request action. | +| Pull-request identity, state, and merged status | Distinguishes active, merged, and abandoned pull-request outcomes. | +| Labels and repository settings result | Captures the input policy state used only by Plan. | +| Version bump and base version | Explains the selected version transition. | +| Manifest version, prerelease identifier, and full version or tag | Defines the only version and tag permitted in the built artifact and release. | +| Target commit | Binds validation, artifact, and release to one source revision. | +| Desired release action and create or publish flags | Selects stable release, prerelease, recovery or resume, cleanup-only, or no-op. | +| Cleanup intent and artifact identity | Defines the exact artifacts that release execution may reconcile. | +| Release-note source and boundary | Identifies the merged pull requests eligible for a recovery release note. | + +The build stage stamps exactly the planned manifest version and prerelease identifier into the module artifact. Before any package, tag, or release becomes visible, release execution verifies that the artifact equals the immutable Settings record. A mismatch stops execution; it is not corrected by retagging or by recalculating a version after the artifact is built. + +## Candidate general release execution + +One general module release action or reusable workflow consumes enriched Settings after validation. It handles stable release, prerelease, recovery or resume, cleanup-only, and no-op actions according to the planned desired release action and flags. It verifies the artifact when an artifact is required and reconciles only the requested state. It does not recompute versioning, labels, event routing, or cleanup policy. ## Candidate manual recovery diff --git a/docs/content/reference/process-workflow-lifecycle-specification.md b/docs/content/reference/process-workflow-lifecycle-specification.md index 40a84487..70b125bc 100644 --- a/docs/content/reference/process-workflow-lifecycle-specification.md +++ b/docs/content/reference/process-workflow-lifecycle-specification.md @@ -78,9 +78,9 @@ Scenario: Validate a synchronized pull request And it does not publish a stable version ``` -### FR4 — Label changes MUST re-evaluate prerelease eligibility {#fr4} +### FR4 — Label changes MUST refresh the planned release classification {#fr4} -A `labeled` or `unlabeled` pull-request event targeting the default branch MUST re-evaluate prerelease eligibility from the complete current label set. A prerelease publication MUST occur only when the pull request is eligible and every required validation succeeds. +A `labeled` or `unlabeled` pull-request event targeting the default branch MUST refresh the complete planned release classification from the current label set and repository settings. A prerelease publication MUST occur only when the planned classification is prerelease and every required validation succeeds. #### Behavioral scenarios {#fr4-scenarios} @@ -88,13 +88,13 @@ A `labeled` or `unlabeled` pull-request event targeting the default branch MUST Scenario: Add prerelease eligibility Given a validated pull request has no prerelease eligibility When a prerelease label is added - Then the workflow re-evaluates the pull request + Then the plan resolves the pull request as prerelease eligible And it publishes at most one eligible prerelease version Scenario: Remove prerelease eligibility Given a pull request has prerelease eligibility When its prerelease label is removed - Then the workflow re-evaluates the pull request as ineligible + Then the plan resolves the pull request as prerelease ineligible And it does not create a new prerelease version ``` @@ -134,15 +134,15 @@ Scenario: Publish a merged pull request And the successful release performs promotion cleanup ``` -### FR7 — Published artifacts MUST match the resolved version {#fr7} +### FR7 — Published artifacts MUST match the planned version {#fr7} -Every prerelease or stable publication MUST contain the version and prerelease identity resolved for its workflow run. A version mismatch MUST fail publication before the release is made visible. +Every prerelease or stable publication MUST contain the version and prerelease identity in the planned release decision. A version mismatch MUST fail publication before the release is made visible. #### Behavioral scenarios {#fr7-scenarios} ```gherkin Scenario: Reject an incorrectly stamped artifact - Given a workflow resolves a release version + Given the plan resolves a release version And the built artifact reports a different version When publication is attempted Then publication fails @@ -168,6 +168,26 @@ Scenario: Publish prereleases for distinct pull requests Then each pull request resolves a distinct prerelease identity ``` +### FR9 — Lifecycle policy MUST be resolved once before downstream work {#fr9} + +The plan MUST resolve the lifecycle policy before build, test, or release execution begins. Downstream work MUST consume that planned policy and MUST NOT reinterpret event data, labels, or repository settings. + +#### Behavioral scenarios {#fr9-scenarios} + +```gherkin +Scenario: Execute a planned prerelease action + Given the plan resolves a pull request as an eligible prerelease publication + When downstream work executes + Then it consumes the planned release action and version + And it does not re-evaluate pull-request labels or event data + +Scenario: Execute a planned cleanup-only action + Given the plan resolves an abandoned pull-request close as cleanup only + When release execution runs + Then it reconciles only the planned cleanup state + And it does not create or publish an artifact +``` + ## Non-functional requirements ### NFR1 — Lifecycle mutations MUST be idempotent {#nfr1} @@ -288,7 +308,7 @@ Scenario: Recover release notes after a missed main-push publication And a retry creates no duplicate publication ``` -### AC2 — Verifies: [FR2](#fr2), [FR4](#fr4), [FR5](#fr5), [FR6](#fr6), [FR8](#fr8), [NFR2](#nfr2), [NFR3](#nfr3), [NFR4](#nfr4), [NFR5](#nfr5), [NFR6](#nfr6) +### AC2 — Verifies: [FR2](#fr2), [FR4](#fr4), [FR5](#fr5), [FR6](#fr6), [FR8](#fr8), [FR9](#fr9), [NFR2](#nfr2), [NFR3](#nfr3), [NFR4](#nfr4), [NFR5](#nfr5), [NFR6](#nfr6) ```gherkin Scenario: Lifecycle runs preserve release ownership after cancellation From fcb5856f6c8582c79c8b7f9f7f12820388171538 Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Sat, 15 Aug 2026 15:19:27 +0200 Subject: [PATCH 06/13] docs: aggregate skipped stable run intent Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../process-workflow-lifecycle-design.md | 16 ++++++--- ...rocess-workflow-lifecycle-specification.md | 35 +++++++++++++++---- 2 files changed, 41 insertions(+), 10 deletions(-) diff --git a/docs/content/reference/process-workflow-lifecycle-design.md b/docs/content/reference/process-workflow-lifecycle-design.md index bedac6a8..6b2a9e84 100644 --- a/docs/content/reference/process-workflow-lifecycle-design.md +++ b/docs/content/reference/process-workflow-lifecycle-design.md @@ -56,11 +56,17 @@ The build stage stamps exactly the planned manifest version and prerelease ident One general module release action or reusable workflow consumes enriched Settings after validation. It handles stable release, prerelease, recovery or resume, cleanup-only, and no-op actions according to the planned desired release action and flags. It verifies the artifact when an artifact is required and reconciles only the requested state. It does not recompute versioning, labels, event routing, or cleanup policy. +## Candidate stable Plan aggregation + +Every stable push and recovery target uses the same aggregation rule. Plan finds the last successfully published stable version and its associated target commit, then queries merged pull requests through the requested target commit. It aggregates their release intent and uses that range for the stable version decision and release notes. + +This aggregation is not limited to manual recovery. GitHub can replace an intermediate pending run when a newer run enters the same concurrency group, so the later stable target must carry forward every merged pull request since the last successful publication. + ## Candidate manual recovery -A manual recovery route accepts only a selected default-branch commit. It first determines whether a stable publication already covers that commit and returns a no-op when one exists. +A manual recovery route accepts only a selected default-branch commit. It uses the stable Plan aggregation rule and returns a no-op when a stable publication already covers that commit. -For a missing publication, the route identifies the last published stable version and its associated default-branch commit. It then queries merged pull requests targeting the default branch between that publication boundary and the selected commit. The release-note reconstruction uses that ordered, de-duplicated result rather than the manual-dispatch event payload, which has no pull-request context. +For a missing publication, the release-note reconstruction uses the ordered, de-duplicated aggregated pull-request range rather than the manual-dispatch event payload, which has no pull-request context. The recovery route validates the selected commit using the same release gates as a default-branch push. It produces a stable release only after the artifact/version boundary succeeds. This keeps recovery notes traceable even when the normal main-push run was missed or interrupted. @@ -84,7 +90,9 @@ concurrency: cancel-in-progress: ${{ github.event_name == 'pull_request' }} ``` -This is a candidate, not an approved caller contract. All pull-request events for one pull request share a group and may cancel an earlier run. Push, manual-dispatch, and scheduled events serialize by ref and never cancel; a manual dispatch or schedule therefore cannot interrupt a main release. +This is a candidate, not an approved caller contract. All pull-request events for one pull request share a group and may cancel an earlier run. Push, manual-dispatch, and scheduled events serialize by the full `github.ref` and do not cancel a running run. GitHub permits at most one running and one pending run per group, so a newer same-group event can replace an older pending run even when cancellation is disabled. A manual dispatch on `main` shares `refs/heads/main` with a push: it queues behind a running push but can replace an older pending main run. + +The candidate retains full `github.ref`. `github.ref_name` neither prevents pending-run replacement nor distinguishes branches and tags with the same name. Cancellation can leave only transient partial state. Every pull-request route, including prerelease publication and abandoned-close cleanup, resumes and reconciles on the next `synchronize`, `labeled`, `unlabeled`, or `closed` event: @@ -121,7 +129,7 @@ The lifecycle contract is exercised with event payload fixtures and publication | Scheduled validation | A published-version fixture that proves no release mutation is requested. | | Pull-request convergence | Canceled prerelease-publication and cleanup fixtures followed by synchronize, label, unlabel, and close events that prove the latest pull-request state is reconciled. | | Gallery immutability | Deterministic pull-request identity, existing-version detection, supported-unlist, and retained-version fixtures across the cancellation boundary. | -| Non-pull-request serialization | Overlapping main-push, manual-dispatch, and scheduled fixtures that prove runs queue by ref and do not cancel. | +| Stable aggregation | Bursts of main-push, manual-dispatch, and scheduled fixtures that replace an intermediate pending run and prove the later stable target aggregates all unreleased merged pull requests. | ## Decisions requiring approval diff --git a/docs/content/reference/process-workflow-lifecycle-specification.md b/docs/content/reference/process-workflow-lifecycle-specification.md index 70b125bc..4a73489e 100644 --- a/docs/content/reference/process-workflow-lifecycle-specification.md +++ b/docs/content/reference/process-workflow-lifecycle-specification.md @@ -120,7 +120,7 @@ Scenario: Abandon a pull request with prereleases ### FR6 — Default-branch pushes MUST authorize stable publication after validation {#fr6} -A push to the default branch MUST publish a stable version only after all required build, test, quality, and publication gates succeed. When the pushed commit is the merge commit of a pull request, the stable-release decision MUST use that pull request's release intent. A successful stable release for a merged pull request MUST own promotion cleanup. +A push to the default branch MUST publish a stable version only after all required build, test, quality, and publication gates succeed. A successful stable release MUST own promotion cleanup. #### Behavioral scenarios {#fr6-scenarios} @@ -188,6 +188,27 @@ Scenario: Execute a planned cleanup-only action And it does not create or publish an artifact ``` +### FR10 — Stable targets MUST aggregate unreleased merged pull requests {#fr10} + +For every stable push or recovery target, the plan MUST aggregate merged pull requests and their release intent from the last successfully published version through the target commit. The resulting stable action MUST converge correctly when GitHub replaces an intermediate pending run. + +#### Behavioral scenarios {#fr10-scenarios} + +```gherkin +Scenario: Publish after an intermediate pending push is replaced + Given merged pull requests exist after the last successfully published version + And an intermediate default-branch push is replaced while pending + When a later default-branch push is planned + Then the plan aggregates every merged pull request through the later target commit + And the stable release uses the aggregated release intent + +Scenario: Recover a range of unreleased merged pull requests + Given merged pull requests exist after the last successfully published version + When a maintainer dispatches recovery for a later default-branch commit + Then the plan aggregates every merged pull request through that target commit + And the release notes use that aggregated range +``` + ## Non-functional requirements ### NFR1 — Lifecycle mutations MUST be idempotent {#nfr1} @@ -206,7 +227,7 @@ Scenario: Retry a publication after an interrupted run ### NFR2 — Pull-request cancellation MUST preserve non-pull-request serialization {#nfr2} -All pull-request events for one pull request MUST share a cancellation scope, so a newer pull-request event cancels a superseded run. Push, manual dispatch, and scheduled runs MUST share ref-based serialization and MUST NOT cancel an in-progress run. +All pull-request events for one pull request MUST share a cancellation scope, so a newer pull-request event cancels a superseded run. Push, manual dispatch, and scheduled runs MUST share a full-ref serialization scope and MUST NOT cancel an in-progress run. Because GitHub permits at most one running and one pending run per group, a newer same-group non-pull-request run MAY replace an older pending run; the stable plan MUST therefore converge from the last successfully published version. #### Behavioral scenarios {#nfr2-scenarios} @@ -218,9 +239,11 @@ Scenario: Supersede a pull-request run Scenario: Serialize non-pull-request runs Given a default-branch release is in progress - When a manual dispatch or scheduled validation starts for the same ref - Then the later run waits for the default-branch release - And neither run cancels the other + And an earlier default-branch run is pending + When a manual dispatch or scheduled validation starts for the same full ref + Then the later run does not cancel the running release + And it may replace the older pending run + And the next stable plan aggregates the unreleased merged pull requests ``` ### NFR3 — Each lifecycle outcome MUST be auditable {#nfr3} @@ -308,7 +331,7 @@ Scenario: Recover release notes after a missed main-push publication And a retry creates no duplicate publication ``` -### AC2 — Verifies: [FR2](#fr2), [FR4](#fr4), [FR5](#fr5), [FR6](#fr6), [FR8](#fr8), [FR9](#fr9), [NFR2](#nfr2), [NFR3](#nfr3), [NFR4](#nfr4), [NFR5](#nfr5), [NFR6](#nfr6) +### AC2 — Verifies: [FR2](#fr2), [FR4](#fr4), [FR5](#fr5), [FR6](#fr6), [FR8](#fr8), [FR9](#fr9), [FR10](#fr10), [NFR2](#nfr2), [NFR3](#nfr3), [NFR4](#nfr4), [NFR5](#nfr5), [NFR6](#nfr6) ```gherkin Scenario: Lifecycle runs preserve release ownership after cancellation From 8825edcca917de42017ab980d391e30d003e0ee3 Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Sat, 15 Aug 2026 15:26:24 +0200 Subject: [PATCH 07/13] docs: require App-authorized workflow access Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../process-workflow-lifecycle-design.md | 22 ++++++++++- ...rocess-workflow-lifecycle-specification.md | 38 ++++++++++++++++++- 2 files changed, 58 insertions(+), 2 deletions(-) diff --git a/docs/content/reference/process-workflow-lifecycle-design.md b/docs/content/reference/process-workflow-lifecycle-design.md index 6b2a9e84..bb35366f 100644 --- a/docs/content/reference/process-workflow-lifecycle-design.md +++ b/docs/content/reference/process-workflow-lifecycle-design.md @@ -15,7 +15,7 @@ The confirmed reusable workflow runs a `Plan` job, enriches one settings object The version resolver treats non-pull-request events, including `workflow_dispatch` and `schedule`, as events without a release decision. The workflow's concurrency identity contains either the pull request number or the Git ref, and its runs are not canceled. The documented scenario matrix also identifies closed-pull-request cleanup as non-stable behavior. -The current code detects an existing published version, resumes GitHub Release creation, and repeats GitHub Release and tag cleanup. Full reconciliation of immutable PowerShell Gallery prereleases is a design gap: it requires implementation and cancellation-boundary tests. These facts establish the starting point. They do not implement the scheduled validation or manual recovery behavior proposed below. +The current code detects an existing published version, resumes GitHub Release creation, and repeats GitHub Release and tag cleanup. Full reconciliation of immutable PowerShell Gallery prereleases is a design gap: it requires implementation and cancellation-boundary tests. Current reusable jobs create App tokens after checkout, and the Pages workflow declares `pages: write` and `id-token: write`; the permissionless-caller candidate is therefore not confirmed behavior. These facts establish the starting point. They do not implement the scheduled validation or manual recovery behavior proposed below. ## Candidate event routing @@ -56,6 +56,22 @@ The build stage stamps exactly the planned manifest version and prerelease ident One general module release action or reusable workflow consumes enriched Settings after validation. It handles stable release, prerelease, recovery or resume, cleanup-only, and no-op actions according to the planned desired release action and flags. It verifies the artifact when an artifact is required and reconciles only the requested state. It does not recompute versioning, labels, event routing, or cleanup policy. +## Candidate repository authorization + +The caller remains permissionless: + +```yaml +permissions: {} +``` + +It grants no job permissions. Every reusable-workflow job creates a narrowly scoped GitHub App installation token before checkout and passes that token explicitly to checkout. The same explicit App token authorizes `gh` and API requests, linter statuses and comments, releases, and cleanup. Downstream jobs do not rely on built-in `GITHUB_TOKEN` authority. + +Each job requests only the App installation permissions required for its operation. A missing App token is an authorization failure: the job stops before checkout or its repository operation, without falling back to the built-in workflow token. + +## Candidate Pages deployment boundary + +The current Pages deployment uses `actions/deploy-pages`, which requires `pages: write` and `id-token: write`. This is a design gap under the permissionless-caller candidate. The implementation must either replace it with an App-authenticated supported deployment path or document an explicit, unavoidable Pages/OIDC exception. It MUST NOT silently grant caller or job `GITHUB_TOKEN` permissions. + ## Candidate stable Plan aggregation Every stable push and recovery target uses the same aggregation rule. Plan finds the last successfully published stable version and its associated target commit, then queries merged pull requests through the requested target commit. It aggregates their release intent and uses that range for the stable version decision and release notes. @@ -130,6 +146,9 @@ The lifecycle contract is exercised with event payload fixtures and publication | Pull-request convergence | Canceled prerelease-publication and cleanup fixtures followed by synchronize, label, unlabel, and close events that prove the latest pull-request state is reconciled. | | Gallery immutability | Deterministic pull-request identity, existing-version detection, supported-unlist, and retained-version fixtures across the cancellation boundary. | | Stable aggregation | Bursts of main-push, manual-dispatch, and scheduled fixtures that replace an intermediate pending run and prove the later stable target aggregates all unreleased merged pull requests. | +| Permissionless caller | Empty caller permissions and no job grants with explicit App-token checkout, API, status, comment, release, and cleanup verification. | +| Authorization failure | Missing-App-token fixtures that prove each repository job fails closed without built-in token fallback. | +| Pages boundary | A supported App-authenticated deployment-path test or an explicit Pages/OIDC exception test. | ## Decisions requiring approval @@ -140,6 +159,7 @@ The candidate does not decide the following: - Which consumer-facing checks comprise scheduled published-artifact validation. - Whether removing prerelease eligibility cleans up existing prereleases immediately or leaves them until the abandoned-close cleanup route. - Whether the supported Gallery API can unlist obsolete prereleases; otherwise, how retained immutable versions are recorded. +- Whether Pages can use a supported App-authenticated deployment path or requires an explicit Pages/OIDC exception. - Approval of the selected caller concurrency expression in [PSModule/Process-PSModule#514](https://github.com/PSModule/Process-PSModule/issues/514). ## Related diff --git a/docs/content/reference/process-workflow-lifecycle-specification.md b/docs/content/reference/process-workflow-lifecycle-specification.md index 4a73489e..d5ccf09b 100644 --- a/docs/content/reference/process-workflow-lifecycle-specification.md +++ b/docs/content/reference/process-workflow-lifecycle-specification.md @@ -209,6 +209,27 @@ Scenario: Recover a range of unreleased merged pull requests And the release notes use that aggregated range ``` +### FR11 — Repository operations MUST use explicit GitHub App authorization {#fr11} + +The caller MUST declare top-level `permissions: {}` and MUST NOT grant job permissions. The reusable workflow MUST create narrowly scoped GitHub App installation tokens before repository access and pass them explicitly to every repository operation. Built-in `GITHUB_TOKEN` authority MUST NOT authorize checkout, API access, status reporting, comments, releases, or cleanup. + +#### Behavioral scenarios {#fr11-scenarios} + +```gherkin +Scenario: Run from a permissionless caller + Given the caller declares top-level permissions as an empty object + And the caller grants no job permissions + When the reusable workflow runs with authorized GitHub App installation tokens + Then checkout and repository operations use the explicit App tokens + And the built-in workflow token does not authorize those operations + +Scenario: Start a repository operation + Given the reusable workflow requires repository access + When the workflow begins + Then it creates the required GitHub App token before checkout + And it passes that token explicitly to checkout and subsequent repository operations +``` + ## Non-functional requirements ### NFR1 — Lifecycle mutations MUST be idempotent {#nfr1} @@ -317,6 +338,21 @@ Scenario: Retain an immutable Gallery prerelease And it performs GitHub Release and tag cleanup independently ``` +### NFR7 — Missing App authorization MUST fail closed {#nfr7} + +When the required GitHub App authorization is unavailable, every repository-mutating or reporting job MUST fail before performing an unauthorized checkout, API request, status update, comment, release, or cleanup. It MUST NOT fall back to built-in `GITHUB_TOKEN` authority. + +#### Behavioral scenarios {#nfr7-scenarios} + +```gherkin +Scenario: Reject a job without GitHub App authorization + Given a caller with no granted token permissions + And a reusable-workflow job cannot create its required GitHub App token + When the job attempts repository access + Then the job fails before the repository operation + And it does not use the built-in workflow token as a fallback +``` + ## Cross-cutting acceptance criteria ### AC1 — Verifies: [FR1](#fr1), [FR6](#fr6), [FR7](#fr7), [NFR1](#nfr1) @@ -331,7 +367,7 @@ Scenario: Recover release notes after a missed main-push publication And a retry creates no duplicate publication ``` -### AC2 — Verifies: [FR2](#fr2), [FR4](#fr4), [FR5](#fr5), [FR6](#fr6), [FR8](#fr8), [FR9](#fr9), [FR10](#fr10), [NFR2](#nfr2), [NFR3](#nfr3), [NFR4](#nfr4), [NFR5](#nfr5), [NFR6](#nfr6) +### AC2 — Verifies: [FR2](#fr2), [FR4](#fr4), [FR5](#fr5), [FR6](#fr6), [FR8](#fr8), [FR9](#fr9), [FR10](#fr10), [FR11](#fr11), [NFR2](#nfr2), [NFR3](#nfr3), [NFR4](#nfr4), [NFR5](#nfr5), [NFR6](#nfr6), [NFR7](#nfr7) ```gherkin Scenario: Lifecycle runs preserve release ownership after cancellation From 89d3d9bf6123a1bdcdb06f394331854bb9bf6c2c Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Sat, 15 Aug 2026 15:28:44 +0200 Subject: [PATCH 08/13] docs: scope workflow token authority Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../process-workflow-lifecycle-design.md | 25 +++++++----- ...rocess-workflow-lifecycle-specification.md | 38 +++++++++---------- 2 files changed, 33 insertions(+), 30 deletions(-) diff --git a/docs/content/reference/process-workflow-lifecycle-design.md b/docs/content/reference/process-workflow-lifecycle-design.md index bb35366f..bbc3d9ce 100644 --- a/docs/content/reference/process-workflow-lifecycle-design.md +++ b/docs/content/reference/process-workflow-lifecycle-design.md @@ -15,7 +15,7 @@ The confirmed reusable workflow runs a `Plan` job, enriches one settings object The version resolver treats non-pull-request events, including `workflow_dispatch` and `schedule`, as events without a release decision. The workflow's concurrency identity contains either the pull request number or the Git ref, and its runs are not canceled. The documented scenario matrix also identifies closed-pull-request cleanup as non-stable behavior. -The current code detects an existing published version, resumes GitHub Release creation, and repeats GitHub Release and tag cleanup. Full reconciliation of immutable PowerShell Gallery prereleases is a design gap: it requires implementation and cancellation-boundary tests. Current reusable jobs create App tokens after checkout, and the Pages workflow declares `pages: write` and `id-token: write`; the permissionless-caller candidate is therefore not confirmed behavior. These facts establish the starting point. They do not implement the scheduled validation or manual recovery behavior proposed below. +The current code detects an existing published version, resumes GitHub Release creation, and repeats GitHub Release and tag cleanup. Full reconciliation of immutable PowerShell Gallery prereleases is a design gap: it requires implementation and cancellation-boundary tests. Current reusable jobs create App tokens after checkout; that sequencing does not satisfy the candidate App-token boundary for user-facing operations. The Pages workflow's `pages: write` and `id-token: write` permissions align with the candidate's standard Pages/OIDC path. These facts establish the starting point. They do not implement the scheduled validation or manual recovery behavior proposed below. ## Candidate event routing @@ -58,19 +58,25 @@ One general module release action or reusable workflow consumes enriched Setting ## Candidate repository authorization -The caller remains permissionless: +The caller sets no top-level default and grants its Process-PSModule job only the built-in permissions needed for repository-local reads and standard Pages/OIDC deployment: ```yaml permissions: {} +jobs: + Process-PSModule: + permissions: + contents: read + pages: write + id-token: write ``` -It grants no job permissions. Every reusable-workflow job creates a narrowly scoped GitHub App installation token before checkout and passes that token explicitly to checkout. The same explicit App token authorizes `gh` and API requests, linter statuses and comments, releases, and cleanup. Downstream jobs do not rely on built-in `GITHUB_TOKEN` authority. +Built-in `GITHUB_TOKEN` authorization is permitted for checkout, repository-local reads, and standard Pages/OIDC deployment within that job boundary. GitHub App installation tokens are step-scoped and authorize every user-facing interaction and every operation that needs broader reach or permissions: pull-request comments and labels, commit statuses and check-facing reporting, releases, tags, assets, and cleanup. -Each job requests only the App installation permissions required for its operation. A missing App token is an authorization failure: the job stops before checkout or its repository operation, without falling back to the built-in workflow token. +Each App-token step requests only the installation permissions required for its operation. A missing App token is an authorization failure for App-required work: that operation stops before its API request or mutation, without silently falling back to the built-in workflow token. Built-in-token reads and Pages deployment remain available only within the explicit caller job permissions. -## Candidate Pages deployment boundary +## Candidate Pages authorization -The current Pages deployment uses `actions/deploy-pages`, which requires `pages: write` and `id-token: write`. This is a design gap under the permissionless-caller candidate. The implementation must either replace it with an App-authenticated supported deployment path or document an explicit, unavoidable Pages/OIDC exception. It MUST NOT silently grant caller or job `GITHUB_TOKEN` permissions. +The standard `actions/deploy-pages` path uses the built-in token's `pages: write` and `id-token: write` permissions granted only to the caller's Process-PSModule job. It remains inside the candidate's repository-local, non-user-facing boundary and does not require an App token. ## Candidate stable Plan aggregation @@ -146,9 +152,9 @@ The lifecycle contract is exercised with event payload fixtures and publication | Pull-request convergence | Canceled prerelease-publication and cleanup fixtures followed by synchronize, label, unlabel, and close events that prove the latest pull-request state is reconciled. | | Gallery immutability | Deterministic pull-request identity, existing-version detection, supported-unlist, and retained-version fixtures across the cancellation boundary. | | Stable aggregation | Bursts of main-push, manual-dispatch, and scheduled fixtures that replace an intermediate pending run and prove the later stable target aggregates all unreleased merged pull requests. | -| Permissionless caller | Empty caller permissions and no job grants with explicit App-token checkout, API, status, comment, release, and cleanup verification. | -| Authorization failure | Missing-App-token fixtures that prove each repository job fails closed without built-in token fallback. | -| Pages boundary | A supported App-authenticated deployment-path test or an explicit Pages/OIDC exception test. | +| Scoped caller permissions | Empty caller top-level permissions, the three job grants, built-in-token checkout/read, and standard Pages/OIDC verification. | +| App authorization failure | Missing-App-token fixtures that prove user-facing operations fail closed without built-in token fallback. | +| Token boundary | Fixtures that prove App tokens are step-scoped and built-in-token operations remain within the caller job's boundary. | ## Decisions requiring approval @@ -159,7 +165,6 @@ The candidate does not decide the following: - Which consumer-facing checks comprise scheduled published-artifact validation. - Whether removing prerelease eligibility cleans up existing prereleases immediately or leaves them until the abandoned-close cleanup route. - Whether the supported Gallery API can unlist obsolete prereleases; otherwise, how retained immutable versions are recorded. -- Whether Pages can use a supported App-authenticated deployment path or requires an explicit Pages/OIDC exception. - Approval of the selected caller concurrency expression in [PSModule/Process-PSModule#514](https://github.com/PSModule/Process-PSModule/issues/514). ## Related diff --git a/docs/content/reference/process-workflow-lifecycle-specification.md b/docs/content/reference/process-workflow-lifecycle-specification.md index d5ccf09b..c8936b8c 100644 --- a/docs/content/reference/process-workflow-lifecycle-specification.md +++ b/docs/content/reference/process-workflow-lifecycle-specification.md @@ -209,25 +209,24 @@ Scenario: Recover a range of unreleased merged pull requests And the release notes use that aggregated range ``` -### FR11 — Repository operations MUST use explicit GitHub App authorization {#fr11} +### FR11 — Repository operations MUST use scoped authorization {#fr11} -The caller MUST declare top-level `permissions: {}` and MUST NOT grant job permissions. The reusable workflow MUST create narrowly scoped GitHub App installation tokens before repository access and pass them explicitly to every repository operation. Built-in `GITHUB_TOKEN` authority MUST NOT authorize checkout, API access, status reporting, comments, releases, or cleanup. +The caller MUST declare top-level `permissions: {}`. Its Process-PSModule job MUST grant only `contents: read`, `pages: write`, and `id-token: write`. Built-in `GITHUB_TOKEN` MAY authorize repository-local, non-user-facing work when those permissions are sufficient, including checkout, reads, and standard Pages/OIDC deployment. GitHub App installation tokens MUST authorize all user-facing interactions and any operation that exceeds the built-in token's reach or permissions, including pull-request comments and labels, commit statuses and check-facing reporting, releases, tags, assets, and cleanup. Tokens MUST remain scoped to the steps that require them. #### Behavioral scenarios {#fr11-scenarios} ```gherkin -Scenario: Run from a permissionless caller +Scenario: Run with the caller's minimum permissions Given the caller declares top-level permissions as an empty object - And the caller grants no job permissions - When the reusable workflow runs with authorized GitHub App installation tokens - Then checkout and repository operations use the explicit App tokens - And the built-in workflow token does not authorize those operations - -Scenario: Start a repository operation - Given the reusable workflow requires repository access - When the workflow begins - Then it creates the required GitHub App token before checkout - And it passes that token explicitly to checkout and subsequent repository operations + And its Process-PSModule job grants only contents read, Pages write, and ID-token write + When the reusable workflow performs checkout or standard Pages deployment + Then it may use the built-in workflow token within that granted boundary + +Scenario: Perform a user-facing repository operation + Given the reusable workflow must create a pull-request comment or release + When the operation requires authority beyond the built-in token boundary + Then it creates a narrowly scoped GitHub App installation token + And it uses the App token only for the steps that require that authority ``` ## Non-functional requirements @@ -338,18 +337,17 @@ Scenario: Retain an immutable Gallery prerelease And it performs GitHub Release and tag cleanup independently ``` -### NFR7 — Missing App authorization MUST fail closed {#nfr7} +### NFR7 — App-required operations MUST fail closed {#nfr7} -When the required GitHub App authorization is unavailable, every repository-mutating or reporting job MUST fail before performing an unauthorized checkout, API request, status update, comment, release, or cleanup. It MUST NOT fall back to built-in `GITHUB_TOKEN` authority. +When an operation requires GitHub App authorization and the required App token is unavailable, the operation MUST fail before an unauthorized API request, status update, comment, release, tag or asset mutation, or cleanup. It MUST NOT silently fall back to built-in `GITHUB_TOKEN` authority. Repository-local reads and standard Pages/OIDC deployment MAY continue only within the caller job's explicit built-in-token permissions. #### Behavioral scenarios {#nfr7-scenarios} ```gherkin -Scenario: Reject a job without GitHub App authorization - Given a caller with no granted token permissions - And a reusable-workflow job cannot create its required GitHub App token - When the job attempts repository access - Then the job fails before the repository operation +Scenario: Reject a user-facing operation without GitHub App authorization + Given a reusable-workflow job cannot create its required GitHub App token + When the job attempts to create a pull-request comment + Then the operation fails before the API request And it does not use the built-in workflow token as a fallback ``` From 7a1975e678c4b49f5a705dcc83b6b488f607a3c2 Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Sat, 15 Aug 2026 15:32:03 +0200 Subject: [PATCH 09/13] docs: authorize workflow events in plan Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../process-workflow-fleet-standard.md | 34 +++++++++---------- .../process-workflow-lifecycle-design.md | 9 +++++ ...rocess-workflow-lifecycle-specification.md | 22 +++++++++++- 3 files changed, 46 insertions(+), 19 deletions(-) diff --git a/docs/content/reference/process-workflow-fleet-standard.md b/docs/content/reference/process-workflow-fleet-standard.md index 4c356e7f..38719200 100644 --- a/docs/content/reference/process-workflow-fleet-standard.md +++ b/docs/content/reference/process-workflow-fleet-standard.md @@ -108,18 +108,16 @@ on: concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} - cancel-in-progress: false + cancel-in-progress: ${{ github.event_name == 'pull_request' }} -permissions: - contents: write - pull-requests: write - statuses: write - pages: write - id-token: write +permissions: {} jobs: Process-PSModule: - if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository }} + permissions: + contents: read + pages: write + id-token: write uses: PSModule/Process-PSModule/.github/workflows/workflow.yml@v8 secrets: PSGALLERY_API_KEY: ${{ secrets.PSGALLERY_API_KEY }} @@ -136,9 +134,9 @@ decisions before canonical guides, templates, or consumer workflows adopt it: | Wrapper scope | Exactly one reusable-workflow job. | Permit repository-specific jobs in the same file, or define pre/post extension jobs. | | Trigger ownership | The caller owns manual, schedule, default-branch push, and pull-request triggers. | Move some trigger policy into separate workflows or omit selected event classes. | | Pull-request activities | Keep all six listed activity types. | Reduce the activity list if a v8 behavior is intentionally unsupported. | -| Concurrency | Use the PR-number-or-ref key and never cancel a release-capable run. | Use separate groups per event class or permit cancellation for non-mutating paths. | -| Permissions | Declare the five current scopes at workflow level. | Introduce settings-based least-privilege profiles or split read-only validation from release work. | -| Fork behavior | Skip fork-originated pull requests in this credentialed wrapper. | Add a separate secret-free workflow or define another supported fork-validation design. | +| Concurrency | Use the PR-number-or-ref key and cancel only superseded pull-request runs. | Use separate groups per event class or disable cancellation for all runs. | +| Permissions | Set top-level permissions to empty and grant only `contents: read`, `pages: write`, and `id-token: write` to the caller job. | Define a narrower profile for repositories that do not publish Pages. | +| Fork behavior | Invoke the reusable workflow unconditionally; Plan rejects unsupported fork events before credentialed or repository-defined code. | Add an independent secret-free fork-CI workflow. | | Credentials | Explicitly map the three v8 credentials. | Define a narrower credential profile for repositories that cannot publish. | | Optional surface | Permit only documented `TestData`, workflow inputs, schedule timing, and presentation metadata. | Allow additional extension points after naming and compatibility rules are agreed. | @@ -171,9 +169,9 @@ fleet campaign. Branch names, `latest`, floating minor tags, and unqualified tar | Default-branch push | Keep `push.branches: [main]`. | `v8` authorizes stable releases from the tested default-branch push. | | Manual dispatch | Keep `workflow_dispatch`. | Provides the documented default-branch manual release and recovery path. | | Schedule | Keep a scheduled health run. | Exercises current dependencies even when repository code is unchanged. | -| Concurrency | Use the PR-number-or-ref key with `cancel-in-progress: false`. | Cleanup and stable release runs stay distinct; release mutations queue instead of being interrupted. | -| Permissions | Declare the five documented permissions explicitly. | The called workflow cannot elevate caller permissions. | -| Fork guard | Skip pull requests whose head repository differs from `github.repository`. | GitHub withholds the required repository secrets from fork pull requests. | +| Concurrency | Use the PR-number-or-ref key and cancel only pull-request runs. | Pull-request changes converge promptly while non-pull-request runs serialize by ref. | +| Permissions | Use empty top-level permissions and the three caller-job permissions shown above. | Repository-local reads and Pages/OIDC stay narrow; App tokens provide broader authority. | +| Fork authorization | Leave the caller job unconditional. | Plan rejects unsupported forks before credentials or repository-defined code run. | | Reference | Use the intended internal floating major tag (`v8`) after tag governance is enforced. | Compatible owned releases roll out centrally; breaking releases require a new major and campaign. | | Credentials | Explicitly map the three required secrets. | Satisfies the `v7+` contract and prevents unrelated secret inheritance. | | Scope | Keep the caller as a single delegation job. | Repository-specific automation remains independently understandable and maintainable. | @@ -202,14 +200,14 @@ an approved structure: - any Process-PSModule reference other than the intended major tag (`v8`), including a branch, `latest`, minor tag, exact patch tag, or full commit SHA; - missing `push` or `unlabeled` triggers; -- `cancel-in-progress: true` or the old ref-only concurrency key; +- a `cancel-in-progress` expression other than `github.event_name == 'pull_request'` or the old ref-only concurrency key; - trigger-level path filters that bypass Process-PSModule important-file evaluation; - unrelated additional jobs in the caller wrapper; - omitted documented permissions without a verified settings-based least-privilege profile. -Fork-originated pull requests are skipped by the candidate caller because reusable-workflow caller jobs cannot select a -GitHub Environment and repository secrets are unavailable to forks. Supporting fork CI requires a separate, secret-free, -read-only validation workflow under this candidate; #514 must approve that boundary. +The candidate caller invokes the reusable workflow for fork-originated pull requests. Plan rejects unsupported fork events +before credentials or repository-defined code run. Supporting fork CI requires a separate, secret-free, read-only workflow; +issue [#514](https://github.com/PSModule/Process-PSModule/issues/514) must approve that boundary. The candidate keeps repository-specific automation in a separate workflow file. That keeps the Process-PSModule wrapper identical enough for automated comparison while allowing modules to own unrelated schedules, generation, or integration diff --git a/docs/content/reference/process-workflow-lifecycle-design.md b/docs/content/reference/process-workflow-lifecycle-design.md index bbc3d9ce..98d68d89 100644 --- a/docs/content/reference/process-workflow-lifecycle-design.md +++ b/docs/content/reference/process-workflow-lifecycle-design.md @@ -33,6 +33,14 @@ Plan resolves the caller event into one release classification before build and Plan records its classification and release decision in enriched Settings. Downstream jobs consume that Settings object and do not infer policy from events, labels, or repository settings again. +## Candidate event authorization + +The caller invokes the reusable workflow without a caller-level fork or event condition. Plan is the event-authorization boundary: it rejects unsupported fork pull requests before credentialed or repository-defined code runs. An authorization rejection produces no usable Settings and no credentialed follow-on work. + +Every downstream job depends on a successful authorized Plan and valid Settings. This requirement applies equally to jobs that use `always()`: their conditions first require the Plan result and Settings validity, then apply their own failure-handling logic. A downstream job never parses missing or invalid Settings and cannot bypass the Plan gate. + +Secret-free fork CI, if needed, is a separate workflow with its own trigger, authorization, and read-only contract. It is not a mode of the credentialed Process-PSModule reusable workflow. + ## Candidate artifact and version boundary Version resolution is the boundary between planning and release-capable work. The candidate carries one immutable release record in enriched Settings through build, test, and release execution: @@ -155,6 +163,7 @@ The lifecycle contract is exercised with event payload fixtures and publication | Scoped caller permissions | Empty caller top-level permissions, the three job grants, built-in-token checkout/read, and standard Pages/OIDC verification. | | App authorization failure | Missing-App-token fixtures that prove user-facing operations fail closed without built-in token fallback. | | Token boundary | Fixtures that prove App tokens are step-scoped and built-in-token operations remain within the caller job's boundary. | +| Event authorization | Unsupported-fork fixtures that prove Plan rejects before credentialed or repository-defined code, including for downstream `always()` jobs. | ## Decisions requiring approval diff --git a/docs/content/reference/process-workflow-lifecycle-specification.md b/docs/content/reference/process-workflow-lifecycle-specification.md index c8936b8c..fe4e2d5b 100644 --- a/docs/content/reference/process-workflow-lifecycle-specification.md +++ b/docs/content/reference/process-workflow-lifecycle-specification.md @@ -229,6 +229,26 @@ Scenario: Perform a user-facing repository operation And it uses the App token only for the steps that require that authority ``` +### FR12 — Plan MUST authorize events before downstream execution {#fr12} + +The caller MUST invoke the reusable workflow without a caller-level fork or event condition. Plan MUST reject unsupported fork pull-request execution before any credentialed or repository-defined code runs. Every downstream job, including a job with `always()`, MUST require a successful authorized Plan and valid Settings. No downstream job MAY evaluate missing or invalid Settings or bypass the Plan gate. + +#### Behavioral scenarios {#fr12-scenarios} + +```gherkin +Scenario: Reject an unsupported fork pull request + Given a pull request originates from an unsupported fork + When Plan evaluates the event + Then Plan rejects the event before credentialed or repository-defined code runs + And no downstream job receives authorized Settings + +Scenario: Gate an always-running downstream job + Given Plan rejects an event or produces invalid Settings + When a downstream job with an always condition is evaluated + Then the job does not run + And it does not evaluate the missing or invalid Settings +``` + ## Non-functional requirements ### NFR1 — Lifecycle mutations MUST be idempotent {#nfr1} @@ -365,7 +385,7 @@ Scenario: Recover release notes after a missed main-push publication And a retry creates no duplicate publication ``` -### AC2 — Verifies: [FR2](#fr2), [FR4](#fr4), [FR5](#fr5), [FR6](#fr6), [FR8](#fr8), [FR9](#fr9), [FR10](#fr10), [FR11](#fr11), [NFR2](#nfr2), [NFR3](#nfr3), [NFR4](#nfr4), [NFR5](#nfr5), [NFR6](#nfr6), [NFR7](#nfr7) +### AC2 — Verifies: [FR2](#fr2), [FR4](#fr4), [FR5](#fr5), [FR6](#fr6), [FR8](#fr8), [FR9](#fr9), [FR10](#fr10), [FR11](#fr11), [FR12](#fr12), [NFR2](#nfr2), [NFR3](#nfr3), [NFR4](#nfr4), [NFR5](#nfr5), [NFR6](#nfr6), [NFR7](#nfr7) ```gherkin Scenario: Lifecycle runs preserve release ownership after cancellation From e65b2d486795334ec9c66c04f7710b54118a9947 Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Sat, 15 Aug 2026 15:33:49 +0200 Subject: [PATCH 10/13] docs: restrict fork workflow capabilities Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../process-workflow-fleet-standard.md | 19 ++++++-- .../process-workflow-lifecycle-design.md | 38 ++++++++++++--- ...rocess-workflow-lifecycle-specification.md | 46 +++++++++++++++++-- 3 files changed, 89 insertions(+), 14 deletions(-) diff --git a/docs/content/reference/process-workflow-fleet-standard.md b/docs/content/reference/process-workflow-fleet-standard.md index 38719200..7dd4dabb 100644 --- a/docs/content/reference/process-workflow-fleet-standard.md +++ b/docs/content/reference/process-workflow-fleet-standard.md @@ -136,7 +136,7 @@ decisions before canonical guides, templates, or consumer workflows adopt it: | Pull-request activities | Keep all six listed activity types. | Reduce the activity list if a v8 behavior is intentionally unsupported. | | Concurrency | Use the PR-number-or-ref key and cancel only superseded pull-request runs. | Use separate groups per event class or disable cancellation for all runs. | | Permissions | Set top-level permissions to empty and grant only `contents: read`, `pages: write`, and `id-token: write` to the caller job. | Define a narrower profile for repositories that do not publish Pages. | -| Fork behavior | Invoke the reusable workflow unconditionally; Plan rejects unsupported fork events before credentialed or repository-defined code. | Add an independent secret-free fork-CI workflow. | +| Fork behavior | Invoke the reusable workflow unconditionally; Plan classifies normal fork `pull_request` events into restricted read-only validation and rejects `pull_request_target` until separately designed. | Omit fork validation or design a separate `pull_request_target` trust boundary. | | Credentials | Explicitly map the three v8 credentials. | Define a narrower credential profile for repositories that cannot publish. | | Optional surface | Permit only documented `TestData`, workflow inputs, schedule timing, and presentation metadata. | Allow additional extension points after naming and compatibility rules are agreed. | @@ -171,7 +171,7 @@ fleet campaign. Branch names, `latest`, floating minor tags, and unqualified tar | Schedule | Keep a scheduled health run. | Exercises current dependencies even when repository code is unchanged. | | Concurrency | Use the PR-number-or-ref key and cancel only pull-request runs. | Pull-request changes converge promptly while non-pull-request runs serialize by ref. | | Permissions | Use empty top-level permissions and the three caller-job permissions shown above. | Repository-local reads and Pages/OIDC stay narrow; App tokens provide broader authority. | -| Fork authorization | Leave the caller job unconditional. | Plan rejects unsupported forks before credentials or repository-defined code run. | +| Fork authorization | Leave the caller job unconditional. | Plan grants normal fork `pull_request` events only restricted read-only validation capabilities and rejects `pull_request_target` before credentials or repository-defined code run. | | Reference | Use the intended internal floating major tag (`v8`) after tag governance is enforced. | Compatible owned releases roll out centrally; breaking releases require a new major and campaign. | | Credentials | Explicitly map the three required secrets. | Satisfies the `v7+` contract and prevents unrelated secret inheritance. | | Scope | Keep the caller as a single delegation job. | Repository-specific automation remains independently understandable and maintainable. | @@ -205,9 +205,18 @@ an approved structure: - unrelated additional jobs in the caller wrapper; - omitted documented permissions without a verified settings-based least-privilege profile. -The candidate caller invokes the reusable workflow for fork-originated pull requests. Plan rejects unsupported fork events -before credentials or repository-defined code run. Supporting fork CI requires a separate, secret-free, read-only workflow; -issue [#514](https://github.com/PSModule/Process-PSModule/issues/514) must approve that boundary. +The candidate caller invokes the reusable workflow for fork-originated `pull_request` events. Plan classifies them into a +restricted, read-only validation mode that permits only repository-local checkout, build, lint, and test with the +least-privilege built-in token. It emits explicit capabilities that prohibit App-token creation, publication, mutation, +Pages deployment, cleanup, and user-facing reporting. `pull_request_target` remains unsupported and Plan must reject it +before credentials or repository-defined code run; issue [#514](https://github.com/PSModule/Process-PSModule/issues/514) +must approve any separate trust boundary for that event. + +The controlled upstream `v8` Plan implementation derives that restricted envelope first from immutable GitHub event +metadata, including fork, base, and head identities. It may then use fork settings and checked-out files only as +untrusted validation and build inputs; they cannot enable credentials, broaden permissions, or change the capability +envelope. This permits ordinary fork contributors to receive the standard workflow green/red validation without +configuring secrets. The candidate keeps repository-specific automation in a separate workflow file. That keeps the Process-PSModule wrapper identical enough for automated comparison while allowing modules to own unrelated schedules, generation, or integration diff --git a/docs/content/reference/process-workflow-lifecycle-design.md b/docs/content/reference/process-workflow-lifecycle-design.md index 98d68d89..8304c0f4 100644 --- a/docs/content/reference/process-workflow-lifecycle-design.md +++ b/docs/content/reference/process-workflow-lifecycle-design.md @@ -25,6 +25,7 @@ Plan resolves the caller event into one release classification before build and | --- | --- | --- | --- | --- | | `workflow_dispatch` on the default branch | Recovery or resume | Stable release or no-op | Never cancel | Rebuild and validate the selected commit; reconstruct the unreleased release notes. | | `schedule` | Published-artifact validation | No-op after validation | Never cancel | Validate the latest published stable artifact and its documentation. | +| Fork `pull_request` | Restricted read-only validation | No-op after validation | Cancels a superseded pull-request run; the later run converges state | Perform only safe repository-local checkout, build, lint, and test with no privileged or user-facing operation. | | Pull request `opened`, `reopened`, `synchronize` | Pull-request classification | Prerelease or no-op | Cancels a superseded pull-request run; the later run converges state | Report configured validation and execute the planned release action. | | Pull request `labeled`, `unlabeled` | Pull-request classification refresh | Prerelease, cleanup-only, or no-op | Cancels a superseded pull-request run; the later run converges state | Resolve the complete current classification and execute its action. | | Merged pull request `closed` | Post-merge close | No-op | Cancels a superseded pull-request run; the later run converges state | Do not clean up; the successful main-push release owns promotion cleanup. | @@ -35,11 +36,29 @@ Plan records its classification and release decision in enriched Settings. Downs ## Candidate event authorization -The caller invokes the reusable workflow without a caller-level fork or event condition. Plan is the event-authorization boundary: it rejects unsupported fork pull requests before credentialed or repository-defined code runs. An authorization rejection produces no usable Settings and no credentialed follow-on work. - -Every downstream job depends on a successful authorized Plan and valid Settings. This requirement applies equally to jobs that use `always()`: their conditions first require the Plan result and Settings validity, then apply their own failure-handling logic. A downstream job never parses missing or invalid Settings and cannot bypass the Plan gate. - -Secret-free fork CI, if needed, is a separate workflow with its own trigger, authorization, and read-only contract. It is not a mode of the credentialed Process-PSModule reusable workflow. +The caller invokes the reusable workflow without a caller-level fork or event condition. Plan is the event-authorization +boundary. For a normal fork `pull_request`, the controlled upstream Process-PSModule `v8` Plan implementation first +derives the security and capability envelope from immutable GitHub event metadata: the fork condition and the base and +head identities. It may query trusted upstream or base version and state as needed. GitHub withholds fork secrets, so +Plan then emits an authorized restricted read-only validation record rather than rejecting the event. That record sets +`IsFork=true`, `AllowAppToken=false`, `AllowPublication=false`, and `AllowMutation=false`. It permits only +repository-local checkout, build, lint, and test with the least-privilege built-in token, giving the contributor the +standard green/red validation outcome without configured secrets. + +Repository settings and checked-out fork files are consumed only after that envelope is fixed, and only as untrusted +validation or build inputs. They cannot enable an App token; publication, mutation, deployment, or cleanup; or broader +permissions. + +No restricted fork path may create an App token; access PowerShell Gallery; comment on, label, or mutate a pull request; +write a status or other check-facing report; create releases, tags, or assets; perform cleanup; deploy Pages; or perform +another privileged or user-facing operation. `pull_request_target` remains unsupported because it could combine +privileged context with untrusted code. Plan rejects that event before credentials or repository-defined code run. An +authorization rejection produces no usable Settings and no credentialed follow-on work. + +Every downstream job depends on a successful authorized Plan and valid Settings. This requirement applies equally to jobs +that use `always()`: their conditions first require the Plan result and Settings validity, then apply their own +failure-handling logic. Privileged jobs additionally require their relevant explicit capability. A downstream job never +parses missing or invalid Settings and cannot bypass the Plan gate. ## Candidate artifact and version boundary @@ -50,6 +69,7 @@ Version resolution is the boundary between planning and release-capable work. Th | Event and run type | Identifies the GitHub event and its candidate lifecycle classification. | | Event action | Preserves the relevant pull-request activity or non-pull-request action. | | Pull-request identity, state, and merged status | Distinguishes active, merged, and abandoned pull-request outcomes. | +| Authorization capabilities and evidence | Records `IsFork`, the immutable event metadata used to derive it, and the explicit App-token, publication, and mutation capabilities that each downstream job must enforce. | | Labels and repository settings result | Captures the input policy state used only by Plan. | | Version bump and base version | Explains the selected version transition. | | Manifest version, prerelease identifier, and full version or tag | Defines the only version and tag permitted in the built artifact and release. | @@ -82,6 +102,11 @@ Built-in `GITHUB_TOKEN` authorization is permitted for checkout, repository-loca Each App-token step requests only the installation permissions required for its operation. A missing App token is an authorization failure for App-required work: that operation stops before its API request or mutation, without silently falling back to the built-in workflow token. Built-in-token reads and Pages deployment remain available only within the explicit caller job permissions. +For a Plan-classified restricted fork run, the Settings capabilities override the caller job's otherwise available +permissions: no App token is created, no Pages deployment runs, and no repository mutation or user-facing action runs. +The fork path has only the least-privilege built-in-token access required for repository-local checkout, build, lint, +and test. + ## Candidate Pages authorization The standard `actions/deploy-pages` path uses the built-in token's `pages: write` and `id-token: write` permissions granted only to the caller's Process-PSModule job. It remains inside the candidate's repository-local, non-user-facing boundary and does not require an App token. @@ -163,7 +188,8 @@ The lifecycle contract is exercised with event payload fixtures and publication | Scoped caller permissions | Empty caller top-level permissions, the three job grants, built-in-token checkout/read, and standard Pages/OIDC verification. | | App authorization failure | Missing-App-token fixtures that prove user-facing operations fail closed without built-in token fallback. | | Token boundary | Fixtures that prove App tokens are step-scoped and built-in-token operations remain within the caller job's boundary. | -| Event authorization | Unsupported-fork fixtures that prove Plan rejects before credentialed or repository-defined code, including for downstream `always()` jobs. | +| Event authorization | Normal-fork `pull_request` fixtures that prove the controlled upstream Plan derives restricted capabilities from immutable fork/base/head metadata before it consumes settings or checked-out code, then permits only checkout/build/lint/test with a green/red outcome and no configured secrets; `pull_request_target` fixtures prove Plan rejects before credentialed or repository-defined code, including for downstream `always()` jobs. | +| Capability enforcement | Restricted-fork fixtures that prove App-token creation, Gallery access, comments, labels, statuses, releases, tags, assets, cleanup, Pages deployment, and other privileged paths cannot run. | ## Decisions requiring approval diff --git a/docs/content/reference/process-workflow-lifecycle-specification.md b/docs/content/reference/process-workflow-lifecycle-specification.md index fe4e2d5b..f5d53b12 100644 --- a/docs/content/reference/process-workflow-lifecycle-specification.md +++ b/docs/content/reference/process-workflow-lifecycle-specification.md @@ -231,13 +231,46 @@ Scenario: Perform a user-facing repository operation ### FR12 — Plan MUST authorize events before downstream execution {#fr12} -The caller MUST invoke the reusable workflow without a caller-level fork or event condition. Plan MUST reject unsupported fork pull-request execution before any credentialed or repository-defined code runs. Every downstream job, including a job with `always()`, MUST require a successful authorized Plan and valid Settings. No downstream job MAY evaluate missing or invalid Settings or bypass the Plan gate. +The caller MUST invoke the reusable workflow without a caller-level fork or event condition. Plan MUST classify a normal +fork `pull_request` event into an authorized restricted read-only validation mode before downstream execution. Its valid +Settings MUST explicitly set `IsFork=true`, `AllowAppToken=false`, `AllowPublication=false`, and +`AllowMutation=false`. That mode MAY perform only safe repository-local checkout, build, lint, and test with the +least-privilege built-in token. It MUST NOT create an App token; access PowerShell Gallery; create pull-request +comments, labels, or status mutations; create releases, tags, or assets; perform cleanup; deploy Pages; or run another +privileged or user-facing operation. + +The controlled upstream reusable-workflow Plan implementation MUST derive the restricted capability envelope first from +immutable GitHub event metadata, including fork, base, and head identities, before interpreting repository settings or +executing checked-out repository code. It MAY query the trusted upstream or base version and state as needed. Fork +settings and files MAY be consumed only as untrusted validation and build inputs; they MUST NOT enable App tokens, +mutation, publication, deployment, cleanup, or broaden permissions. The restricted mode MUST provide the ordinary +green/red validation outcome without requiring contributor-configured secrets. + +`pull_request_target` MUST remain unsupported until a separate trust boundary is designed and approved. Plan MUST reject +that event before credentials or repository-defined code run. Every downstream job, including a job with `always()`, +MUST require a successful authorized Plan and valid Settings, and every privileged downstream job MUST also require its +relevant planned capability. No downstream job MAY evaluate missing or invalid Settings or bypass the Plan gate. #### Behavioral scenarios {#fr12-scenarios} ```gherkin -Scenario: Reject an unsupported fork pull request - Given a pull request originates from an unsupported fork +Scenario: Validate a normal fork pull request in restricted mode + Given a pull request originates from a fork through the pull_request event + When Plan evaluates the event + Then Plan emits valid Settings with IsFork true and App-token, publication, and mutation capabilities false + And downstream work may perform only repository-local checkout, build, lint, and test with the least-privilege built-in token + And no App token, Gallery access, user-facing operation, Pages deployment, publication, or cleanup runs + And the contributor receives the standard green or red validation outcome without configured secrets + +Scenario: Derive fork capabilities before interpreting untrusted repository inputs + Given a pull request originates from a fork through the pull_request event + And its repository settings attempt to enable publication + When the controlled upstream Plan evaluates immutable fork, base, and head metadata + Then it fixes the restricted capability envelope before reading the settings or checked-out files + And the settings cannot enable App tokens, mutation, publication, deployment, cleanup, or broader permissions + +Scenario: Reject an unsupported pull_request_target event + Given a pull_request_target event is received When Plan evaluates the event Then Plan rejects the event before credentialed or repository-defined code runs And no downstream job receives authorized Settings @@ -247,6 +280,13 @@ Scenario: Gate an always-running downstream job When a downstream job with an always condition is evaluated Then the job does not run And it does not evaluate the missing or invalid Settings + +Scenario: Gate a privileged job for a restricted fork run + Given Plan emits valid restricted Settings for a fork pull request + And the planned capability for publication is false + When a publication job is evaluated + Then the job does not run + And it does not create an App token or parse an absent publication configuration ``` ## Non-functional requirements From 497420bc4622a966e6c8d44c360f97de6bb0ec12 Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Sat, 15 Aug 2026 15:48:48 +0200 Subject: [PATCH 11/13] docs: define caller credential baseline Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../process-workflow-fleet-standard.md | 30 ++++++++++++++----- .../process-workflow-lifecycle-design.md | 15 ++++++++++ ...rocess-workflow-lifecycle-specification.md | 22 +++++++++++++- 3 files changed, 59 insertions(+), 8 deletions(-) diff --git a/docs/content/reference/process-workflow-fleet-standard.md b/docs/content/reference/process-workflow-fleet-standard.md index 7dd4dabb..a771a642 100644 --- a/docs/content/reference/process-workflow-fleet-standard.md +++ b/docs/content/reference/process-workflow-fleet-standard.md @@ -137,7 +137,7 @@ decisions before canonical guides, templates, or consumer workflows adopt it: | Concurrency | Use the PR-number-or-ref key and cancel only superseded pull-request runs. | Use separate groups per event class or disable cancellation for all runs. | | Permissions | Set top-level permissions to empty and grant only `contents: read`, `pages: write`, and `id-token: write` to the caller job. | Define a narrower profile for repositories that do not publish Pages. | | Fork behavior | Invoke the reusable workflow unconditionally; Plan classifies normal fork `pull_request` events into restricted read-only validation and rejects `pull_request_target` until separately designed. | Omit fork validation or design a separate `pull_request_target` trust boundary. | -| Credentials | Explicitly map the three v8 credentials. | Define a narrower credential profile for repositories that cannot publish. | +| Credentials | Explicitly map `PSGALLERY_API_KEY`, `GitHubAppClientId`, and `GitHubAppPrivateKey`; `secrets: inherit` is nonconforming. | Define a narrower profile only for repositories with an approved non-publication contract. | | Optional surface | Permit only documented `TestData`, workflow inputs, schedule timing, and presentation metadata. | Allow additional extension points after naming and compatibility rules are agreed. | The `v8` reference is the controlled moving major tag for this PSModule-owned workflow. On 2026-08-15, `v8`, `v8.0`, @@ -173,7 +173,7 @@ fleet campaign. Branch names, `latest`, floating minor tags, and unqualified tar | Permissions | Use empty top-level permissions and the three caller-job permissions shown above. | Repository-local reads and Pages/OIDC stay narrow; App tokens provide broader authority. | | Fork authorization | Leave the caller job unconditional. | Plan grants normal fork `pull_request` events only restricted read-only validation capabilities and rejects `pull_request_target` before credentials or repository-defined code run. | | Reference | Use the intended internal floating major tag (`v8`) after tag governance is enforced. | Compatible owned releases roll out centrally; breaking releases require a new major and campaign. | -| Credentials | Explicitly map the three required secrets. | Satisfies the `v7+` contract and prevents unrelated secret inheritance. | +| Credentials | Explicitly map `PSGALLERY_API_KEY`, `GitHubAppClientId`, and `GitHubAppPrivateKey`; do not use `secrets: inherit`. | Satisfies the `v7+` contract and prevents unrelated secret inheritance. | | Scope | Keep the caller as a single delegation job. | Repository-specific automation remains independently understandable and maintainable. | ## Candidate optional elements @@ -182,20 +182,20 @@ These are evidence-based candidate variations, not approved policy. | Option | When it is appropriate | Constraint | | --- | --- | --- | -| `TestData` secret | Module-local tests need caller-defined secrets or variables. | Use the documented compact single-line JSON object and expose only required values. | +| `TestData` secret mapping | Module-local tests need caller-defined secrets or variables. | Map `TestData: ${{ secrets.TestData }}` only when used. Its value is a JSON object with separate `secrets` and `variables` maps; omit the mapping when unused. | | `with.SettingsPath` | The settings file is not `.github/PSModule.yml`. | Prefer the standard path for normal module repositories. | | `with.WorkingDirectory` | The module is intentionally rooted below the repository root. | Keep the default `.` for the standard layout. | | `with.ImportantFilePatterns` | A caller must override change detection at the workflow boundary. | Prefer stable configuration in `.github/PSModule.yml`; the supplied list replaces all defaults. | -| `with.Debug`, `Verbose`, `Version`, or `Prerelease` | A deliberate diagnostic or dependency-selection scenario needs it. | Do not hard-code temporary diagnostics into the fleet baseline. | +| `with.Verbose`, `Version`, or `Prerelease` | A deliberate diagnostic or dependency-selection scenario needs it. | Do not hard-code temporary diagnostics into the fleet baseline. | | Schedule time | Health runs need staggering or a repository-specific maintenance window. | Keep at least one documented schedule unless the repository records why health runs are unnecessary. | | `run-name` | A repository needs clearer run presentation. | Presentation must not change job names or routing behavior. | ## Variations requiring a decision -The following differ from the candidate. They are inventory classifications, not policy violations, until #514 records -an approved structure: +The following are nonconforming with locked candidate decisions. They remain inventory classifications rather than +approved organization policy until #514 records an approved structure: -- `secrets: inherit`; +- `secrets: inherit`, which is nonconforming because the candidate requires the three explicit baseline mappings; - `APIKey` or `APIKEY` mappings from the pre-`v7` contract; - any Process-PSModule reference other than the intended major tag (`v8`), including a branch, `latest`, minor tag, exact patch tag, or full commit SHA; @@ -204,6 +204,22 @@ an approved structure: - trigger-level path filters that bypass Process-PSModule important-file evaluation; - unrelated additional jobs in the caller wrapper; - omitted documented permissions without a verified settings-based least-privilege profile. +- `with.Debug: true`, which is nonconforming because the reusable workflow default remains `false`. + +When required, the optional test-data mapping extends the candidate baseline without broadening it: + +```yaml + secrets: + PSGALLERY_API_KEY: ${{ secrets.PSGALLERY_API_KEY }} + GitHubAppClientId: ${{ secrets.SHELLY_CLIENT_ID }} + GitHubAppPrivateKey: ${{ secrets.SHELLY_PRIVATE_KEY }} + TestData: ${{ secrets.TestData }} +``` + +`TestData` is a JSON object with separate `secrets` and `variables` maps, for example +`{"secrets":{"Example":"value"},"variables":{"Feature":"enabled"}}`. Callers omit this mapping entirely when +module-local tests do not need it. No conforming caller sets `with.Debug: true`; the reusable workflow's default remains +`false`. The candidate caller invokes the reusable workflow for fork-originated `pull_request` events. Plan classifies them into a restricted, read-only validation mode that permits only repository-local checkout, build, lint, and test with the diff --git a/docs/content/reference/process-workflow-lifecycle-design.md b/docs/content/reference/process-workflow-lifecycle-design.md index 8304c0f4..3580af3b 100644 --- a/docs/content/reference/process-workflow-lifecycle-design.md +++ b/docs/content/reference/process-workflow-lifecycle-design.md @@ -96,8 +96,22 @@ jobs: contents: read pages: write id-token: write + secrets: + PSGALLERY_API_KEY: ${{ secrets.PSGALLERY_API_KEY }} + GitHubAppClientId: ${{ secrets.SHELLY_CLIENT_ID }} + GitHubAppPrivateKey: ${{ secrets.SHELLY_PRIVATE_KEY }} ``` +These three explicit mappings are the required caller baseline; `secrets: inherit` is nonconforming. The only optional +secret mapping is `TestData` for module-local tests: + +```yaml + TestData: ${{ secrets.TestData }} +``` + +When present, `TestData` contains a JSON object with separate `secrets` and `variables` maps. It is omitted when +unused. A conforming caller does not set `with.Debug: true`; the reusable workflow default remains `false`. + Built-in `GITHUB_TOKEN` authorization is permitted for checkout, repository-local reads, and standard Pages/OIDC deployment within that job boundary. GitHub App installation tokens are step-scoped and authorize every user-facing interaction and every operation that needs broader reach or permissions: pull-request comments and labels, commit statuses and check-facing reporting, releases, tags, assets, and cleanup. Each App-token step requests only the installation permissions required for its operation. A missing App token is an authorization failure for App-required work: that operation stops before its API request or mutation, without silently falling back to the built-in workflow token. Built-in-token reads and Pages deployment remain available only within the explicit caller job permissions. @@ -186,6 +200,7 @@ The lifecycle contract is exercised with event payload fixtures and publication | Gallery immutability | Deterministic pull-request identity, existing-version detection, supported-unlist, and retained-version fixtures across the cancellation boundary. | | Stable aggregation | Bursts of main-push, manual-dispatch, and scheduled fixtures that replace an intermediate pending run and prove the later stable target aggregates all unreleased merged pull requests. | | Scoped caller permissions | Empty caller top-level permissions, the three job grants, built-in-token checkout/read, and standard Pages/OIDC verification. | +| Caller credential contract | Explicit baseline mappings for `PSGALLERY_API_KEY`, `GitHubAppClientId`, and `GitHubAppPrivateKey`; rejected `secrets: inherit`; optional `TestData` JSON with separate `secrets` and `variables` maps; and no `with.Debug: true`. | | App authorization failure | Missing-App-token fixtures that prove user-facing operations fail closed without built-in token fallback. | | Token boundary | Fixtures that prove App tokens are step-scoped and built-in-token operations remain within the caller job's boundary. | | Event authorization | Normal-fork `pull_request` fixtures that prove the controlled upstream Plan derives restricted capabilities from immutable fork/base/head metadata before it consumes settings or checked-out code, then permits only checkout/build/lint/test with a green/red outcome and no configured secrets; `pull_request_target` fixtures prove Plan rejects before credentialed or repository-defined code, including for downstream `always()` jobs. | diff --git a/docs/content/reference/process-workflow-lifecycle-specification.md b/docs/content/reference/process-workflow-lifecycle-specification.md index f5d53b12..5b6e1ac7 100644 --- a/docs/content/reference/process-workflow-lifecycle-specification.md +++ b/docs/content/reference/process-workflow-lifecycle-specification.md @@ -211,7 +211,9 @@ Scenario: Recover a range of unreleased merged pull requests ### FR11 — Repository operations MUST use scoped authorization {#fr11} -The caller MUST declare top-level `permissions: {}`. Its Process-PSModule job MUST grant only `contents: read`, `pages: write`, and `id-token: write`. Built-in `GITHUB_TOKEN` MAY authorize repository-local, non-user-facing work when those permissions are sufficient, including checkout, reads, and standard Pages/OIDC deployment. GitHub App installation tokens MUST authorize all user-facing interactions and any operation that exceeds the built-in token's reach or permissions, including pull-request comments and labels, commit statuses and check-facing reporting, releases, tags, assets, and cleanup. Tokens MUST remain scoped to the steps that require them. +The caller MUST declare top-level `permissions: {}`. Its Process-PSModule job MUST grant only `contents: read`, `pages: write`, and `id-token: write`. It MUST explicitly map `PSGALLERY_API_KEY`, `GitHubAppClientId`, and `GitHubAppPrivateKey`; `secrets: inherit` is nonconforming. It MAY additionally map `TestData` only for module-local tests. When present, `TestData` MUST contain a JSON object with separate `secrets` and `variables` maps; callers MUST omit it when unused. No conforming caller MAY set `with.Debug: true`; the reusable workflow default remains `false`. + +Built-in `GITHUB_TOKEN` MAY authorize repository-local, non-user-facing work when those permissions are sufficient, including checkout, reads, and standard Pages/OIDC deployment. GitHub App installation tokens MUST authorize all user-facing interactions and any operation that exceeds the built-in token's reach or permissions, including pull-request comments and labels, commit statuses and check-facing reporting, releases, tags, assets, and cleanup. Tokens MUST remain scoped to the steps that require them. #### Behavioral scenarios {#fr11-scenarios} @@ -222,6 +224,24 @@ Scenario: Run with the caller's minimum permissions When the reusable workflow performs checkout or standard Pages deployment Then it may use the built-in workflow token within that granted boundary +Scenario: Provide the required caller credentials explicitly + Given a conforming caller invokes the reusable workflow + When it maps credentials to the Process-PSModule job + Then it maps PSGALLERY_API_KEY, GitHubAppClientId, and GitHubAppPrivateKey explicitly + And it does not use secrets inherit + +Scenario: Provide optional module-local test data + Given module-local tests require caller-provided data + When the caller maps TestData + Then its secret value is a JSON object with separate secrets and variables maps + And the caller omits TestData when tests do not require it + +Scenario: Keep caller debug disabled + Given a conforming caller invokes the reusable workflow + When it sets workflow inputs + Then it does not set Debug to true + And the reusable workflow uses its false default + Scenario: Perform a user-facing repository operation Given the reusable workflow must create a pull-request comment or release When the operation requires authority beyond the built-in token boundary From 391a556f16d9e2befd8e0709655147f37e7306b0 Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Sat, 15 Aug 2026 15:51:00 +0200 Subject: [PATCH 12/13] docs: scope caller conformance Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../process-workflow-fleet-standard.md | 19 +++++++---- .../process-workflow-lifecycle-design.md | 9 +++++ ...rocess-workflow-lifecycle-specification.md | 34 +++++++++++++++++-- 3 files changed, 52 insertions(+), 10 deletions(-) diff --git a/docs/content/reference/process-workflow-fleet-standard.md b/docs/content/reference/process-workflow-fleet-standard.md index a771a642..e48abb1e 100644 --- a/docs/content/reference/process-workflow-fleet-standard.md +++ b/docs/content/reference/process-workflow-fleet-standard.md @@ -57,6 +57,10 @@ The uniform wrapper is a strong starting point, but it predates the two latest b - `v8.0.0` moves stable publication to a default-branch `push`, adds `unlabeled` routing, and requires non-cancelling pull-request-or-ref concurrency. +The observed absence of additional jobs is inventory context, not a conformance requirement. The candidate standardizes +the Process-PSModule reusable-workflow call and the shared top-level controls that govern that call, rather than the +repository owner's entire workflow. + No current caller has the `v8.0.0` trigger and concurrency contract. The fleet spans nine older versions: | Version | Repositories | @@ -131,7 +135,7 @@ decisions before canonical guides, templates, or consumer workflows adopt it: | Decision | Candidate | Alternatives still open | | --- | --- | --- | -| Wrapper scope | Exactly one reusable-workflow job. | Permit repository-specific jobs in the same file, or define pre/post extension jobs. | +| Wrapper scope | Exactly one conforming Process-PSModule reusable-workflow call job plus the shared top-level controls that govern it. Repository-owned jobs may coexist in the same file or separate workflows when they do not weaken or bypass that boundary. | Decide only the placement of repository-owned jobs. | | Trigger ownership | The caller owns manual, schedule, default-branch push, and pull-request triggers. | Move some trigger policy into separate workflows or omit selected event classes. | | Pull-request activities | Keep all six listed activity types. | Reduce the activity list if a v8 behavior is intentionally unsupported. | | Concurrency | Use the PR-number-or-ref key and cancel only superseded pull-request runs. | Use separate groups per event class or disable cancellation for all runs. | @@ -174,7 +178,7 @@ fleet campaign. Branch names, `latest`, floating minor tags, and unqualified tar | Fork authorization | Leave the caller job unconditional. | Plan grants normal fork `pull_request` events only restricted read-only validation capabilities and rejects `pull_request_target` before credentials or repository-defined code run. | | Reference | Use the intended internal floating major tag (`v8`) after tag governance is enforced. | Compatible owned releases roll out centrally; breaking releases require a new major and campaign. | | Credentials | Explicitly map `PSGALLERY_API_KEY`, `GitHubAppClientId`, and `GitHubAppPrivateKey`; do not use `secrets: inherit`. | Satisfies the `v7+` contract and prevents unrelated secret inheritance. | -| Scope | Keep the caller as a single delegation job. | Repository-specific automation remains independently understandable and maintainable. | +| Scope | Require exactly one conforming Process-PSModule delegation job and its governing triggers, concurrency, permissions, Plan authorization, and credential boundary. | Repository-owned jobs may be reported for visibility but are not nonconforming merely by existing. | ## Candidate optional elements @@ -202,8 +206,7 @@ approved organization policy until #514 records an approved structure: - missing `push` or `unlabeled` triggers; - a `cancel-in-progress` expression other than `github.event_name == 'pull_request'` or the old ref-only concurrency key; - trigger-level path filters that bypass Process-PSModule important-file evaluation; -- unrelated additional jobs in the caller wrapper; -- omitted documented permissions without a verified settings-based least-privilege profile. +- omitted documented permissions without a verified settings-based least-privilege profile; - `with.Debug: true`, which is nonconforming because the reusable workflow default remains `false`. When required, the optional test-data mapping extends the candidate baseline without broadening it: @@ -234,9 +237,11 @@ untrusted validation and build inputs; they cannot enable credentials, broaden p envelope. This permits ordinary fork contributors to receive the standard workflow green/red validation without configuring secrets. -The candidate keeps repository-specific automation in a separate workflow file. That keeps the Process-PSModule wrapper -identical enough for automated comparison while allowing modules to own unrelated schedules, generation, or integration -tasks. +The candidate requires exactly one conforming Process-PSModule reusable-workflow call job and the shared top-level triggers, +concurrency, permissions, Plan authorization, and credential boundary that govern it. Repository-owned jobs may exist in +the same file or in separate workflows and are reported only for visibility. Their existence is not nonconforming, but +they MUST NOT weaken or bypass the Process-PSModule call's trigger, concurrency, permissions, Plan authorization, or +credential boundary. ## Rollout boundary diff --git a/docs/content/reference/process-workflow-lifecycle-design.md b/docs/content/reference/process-workflow-lifecycle-design.md index 3580af3b..03431040 100644 --- a/docs/content/reference/process-workflow-lifecycle-design.md +++ b/docs/content/reference/process-workflow-lifecycle-design.md @@ -34,6 +34,14 @@ Plan resolves the caller event into one release classification before build and Plan records its classification and release decision in enriched Settings. Downstream jobs consume that Settings object and do not infer policy from events, labels, or repository settings again. +## Candidate caller boundary + +The candidate standardizes exactly one Process-PSModule reusable-workflow call job and the shared top-level triggers, +concurrency, permissions, Plan authorization, and credential boundary that govern it. It does not standardize the +repository owner's entire workflow. Repository-owned jobs may coexist in the same workflow file or in separate workflows +and are reported only for visibility. They must not weaken or bypass the Process-PSModule call's trigger, concurrency, +permissions, Plan authorization, or credential boundary. + ## Candidate event authorization The caller invokes the reusable workflow without a caller-level fork or event condition. Plan is the event-authorization @@ -201,6 +209,7 @@ The lifecycle contract is exercised with event payload fixtures and publication | Stable aggregation | Bursts of main-push, manual-dispatch, and scheduled fixtures that replace an intermediate pending run and prove the later stable target aggregates all unreleased merged pull requests. | | Scoped caller permissions | Empty caller top-level permissions, the three job grants, built-in-token checkout/read, and standard Pages/OIDC verification. | | Caller credential contract | Explicit baseline mappings for `PSGALLERY_API_KEY`, `GitHubAppClientId`, and `GitHubAppPrivateKey`; rejected `secrets: inherit`; optional `TestData` JSON with separate `secrets` and `variables` maps; and no `with.Debug: true`. | +| Caller boundary | Exactly one conforming Process-PSModule call job with governing shared top-level controls, plus repository-owned jobs in the same file and separate workflows that prove their presence is visible but does not weaken or bypass the call boundary. | | App authorization failure | Missing-App-token fixtures that prove user-facing operations fail closed without built-in token fallback. | | Token boundary | Fixtures that prove App tokens are step-scoped and built-in-token operations remain within the caller job's boundary. | | Event authorization | Normal-fork `pull_request` fixtures that prove the controlled upstream Plan derives restricted capabilities from immutable fork/base/head metadata before it consumes settings or checked-out code, then permits only checkout/build/lint/test with a green/red outcome and no configured secrets; `pull_request_target` fixtures prove Plan rejects before credentialed or repository-defined code, including for downstream `always()` jobs. | diff --git a/docs/content/reference/process-workflow-lifecycle-specification.md b/docs/content/reference/process-workflow-lifecycle-specification.md index 5b6e1ac7..a90f3a3f 100644 --- a/docs/content/reference/process-workflow-lifecycle-specification.md +++ b/docs/content/reference/process-workflow-lifecycle-specification.md @@ -15,9 +15,14 @@ This candidate defines the behavior required from a Process-PSModule workflow li ## Scope -The candidate covers dispatch recovery, scheduled validation, pull-request validation and prerelease evaluation, closed-pull-request cleanup, and stable publication after a default-branch push. +The candidate covers dispatch recovery, scheduled validation, pull-request validation and prerelease evaluation, closed-pull-request cleanup, stable publication after a default-branch push, and the Process-PSModule reusable-workflow caller boundary. -It does not approve a caller layout, change module build or publication implementation, define label names, or prescribe release-note presentation. Those choices remain in the caller candidate, the existing versioning guidance, and the companion [candidate design](process-workflow-lifecycle-design.md). +It standardizes exactly one conforming Process-PSModule reusable-workflow call job and the shared top-level controls that govern +it; it does not standardize the repository owner's whole workflow. Repository-owned jobs may exist in the same workflow +file or separate workflows, provided they do not weaken or bypass the call's trigger, concurrency, permissions, Plan +authorization, or credential boundary. It does not change module build or publication implementation, define label +names, or prescribe release-note presentation. Those choices remain in the caller candidate, the existing versioning +guidance, and the companion [candidate design](process-workflow-lifecycle-design.md). ## Confirmed implementation baseline @@ -309,6 +314,29 @@ Scenario: Gate a privileged job for a restricted fork run And it does not create an App token or parse an absent publication configuration ``` +### FR13 — Caller conformance MUST be limited to the reusable-workflow boundary {#fr13} + +A conforming caller MUST contain exactly one Process-PSModule reusable-workflow call job and the shared top-level triggers, +concurrency, permissions, Plan authorization, and credential boundary that govern it. Repository-owned jobs MAY coexist +in the same workflow file or in separate workflows. They are not nonconforming merely by existing, but they MUST NOT +weaken or bypass any of those controls for the Process-PSModule call. + +#### Behavioral scenarios {#fr13-scenarios} + +```gherkin +Scenario: Retain a repository-owned job beside the reusable-workflow call + Given a workflow contains one conforming Process-PSModule reusable-workflow call job + And a repository-owned documentation job exists in the same workflow file + When the workflow is evaluated for caller conformance + Then the documentation job is reported for visibility + And its existence does not make the Process-PSModule call nonconforming + +Scenario: Prevent a repository-owned job from bypassing the caller boundary + Given a repository-owned job exists beside or outside the caller workflow + When it could weaken or bypass the Process-PSModule call's trigger, concurrency, permissions, Plan authorization, or credential boundary + Then the caller arrangement is nonconforming +``` + ## Non-functional requirements ### NFR1 — Lifecycle mutations MUST be idempotent {#nfr1} @@ -445,7 +473,7 @@ Scenario: Recover release notes after a missed main-push publication And a retry creates no duplicate publication ``` -### AC2 — Verifies: [FR2](#fr2), [FR4](#fr4), [FR5](#fr5), [FR6](#fr6), [FR8](#fr8), [FR9](#fr9), [FR10](#fr10), [FR11](#fr11), [FR12](#fr12), [NFR2](#nfr2), [NFR3](#nfr3), [NFR4](#nfr4), [NFR5](#nfr5), [NFR6](#nfr6), [NFR7](#nfr7) +### AC2 — Verifies: [FR2](#fr2), [FR4](#fr4), [FR5](#fr5), [FR6](#fr6), [FR8](#fr8), [FR9](#fr9), [FR10](#fr10), [FR11](#fr11), [FR12](#fr12), [FR13](#fr13), [NFR2](#nfr2), [NFR3](#nfr3), [NFR4](#nfr4), [NFR5](#nfr5), [NFR6](#nfr6), [NFR7](#nfr7) ```gherkin Scenario: Lifecycle runs preserve release ownership after cancellation From a18d325aa4f61ed7a198df5feded720a99cb01c0 Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Sat, 15 Aug 2026 15:54:00 +0200 Subject: [PATCH 13/13] docs: define workflow lifecycle standard Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../process-workflow-lifecycle-design.md | 293 ++++++++--------- ...rocess-workflow-lifecycle-specification.md | 306 +++++++----------- docs/zensical.toml | 4 +- 3 files changed, 257 insertions(+), 346 deletions(-) diff --git a/docs/content/reference/process-workflow-lifecycle-design.md b/docs/content/reference/process-workflow-lifecycle-design.md index 03431040..72642640 100644 --- a/docs/content/reference/process-workflow-lifecycle-design.md +++ b/docs/content/reference/process-workflow-lifecycle-design.md @@ -1,100 +1,98 @@ --- -title: Process-PSModule workflow lifecycle candidate design -description: Candidate design for Process-PSModule event routing, stamped artifacts, recovery release notes, and concurrency isolation. +title: Process-PSModule workflow lifecycle design +description: Architecture for Process-PSModule event routing, stamped artifacts, recovery release notes, and concurrency isolation. --- -# Process-PSModule workflow lifecycle candidate design +# Process-PSModule workflow lifecycle design -**Status:** This is a candidate design for discussion. It is not approved and does not change the reusable workflow or its caller contract. +This design defines the Process-PSModule lifecycle architecture. It implements the +[workflow lifecycle specification](process-workflow-lifecycle-specification.md) through a single policy authority, +immutable release records, and a general release executor. -This design describes one way to meet the [workflow lifecycle candidate specification](process-workflow-lifecycle-specification.md). It follows the [spec/design separation](https://msx.no/docs/Ways-of-Working/Spec-Driven-Development/#what-a-design-is); detailed implementation choices remain free until the candidate is approved. +## Architecture -## Confirmed implementation baseline +Plan is the sole lifecycle-policy authority. It resolves each event before build and release work, emits enriched +Settings, and gates downstream execution. Build, validation, release, cleanup, and reporting consume Settings without +reinterpreting events, labels, or repository settings. -The confirmed reusable workflow runs a `Plan` job, enriches one settings object with the resolved version, and passes that object to downstream jobs. The module build action accepts the resolved version and prerelease identity; the publish workflow consumes the built artifact and uses the resolved full version for the release. +One general module release action or reusable workflow consumes Settings and performs stable release, prerelease, +recovery or resume, cleanup-only, or no-op actions. It verifies artifacts when required and reconciles only the +requested state. -The version resolver treats non-pull-request events, including `workflow_dispatch` and `schedule`, as events without a release decision. The workflow's concurrency identity contains either the pull request number or the Git ref, and its runs are not canceled. The documented scenario matrix also identifies closed-pull-request cleanup as non-stable behavior. +## Event routing -The current code detects an existing published version, resumes GitHub Release creation, and repeats GitHub Release and tag cleanup. Full reconciliation of immutable PowerShell Gallery prereleases is a design gap: it requires implementation and cancellation-boundary tests. Current reusable jobs create App tokens after checkout; that sequencing does not satisfy the candidate App-token boundary for user-facing operations. The Pages workflow's `pages: write` and `id-token: write` permissions align with the candidate's standard Pages/OIDC path. These facts establish the starting point. They do not implement the scheduled validation or manual recovery behavior proposed below. +| Event | Plan classification | Release action | Concurrency | Result | +| --- | --- | --- | --- | --- | +| `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. | +| `schedule` | Published-artifact validation | No-op after validation | Full-ref serialization | Validate the latest published stable artifact and documentation. | +| Fork `pull_request` | Restricted read-only validation | No-op after validation | Pull-request cancellation | Perform repository-local checkout, build, lint, and test only. | +| Pull request `opened`, `reopened`, `synchronize` | Pull-request classification | Prerelease or no-op | Pull-request cancellation | Run validation and execute the planned release action. | +| 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. | +| Merged pull request `closed` | Post-merge close | No-op | Pull-request cancellation | Leave promotion cleanup to the stable release. | +| Abandoned pull request `closed` | Abandoned-close classification | Cleanup-only | Pull-request cancellation | Reconcile only prereleases owned by the abandoned pull request. | +| Push to the default branch | Stable release | Stable release | Full-ref serialization | Aggregate merged-pull-request intent, publish, and perform promotion cleanup. | + +## Caller boundary + +The [Process-PSModule caller contract](process-workflow-fleet-standard.md) contains exactly one reusable-workflow call +job and the shared top-level triggers, concurrency, permissions, Plan authorization, and credential boundary that govern +it. Repository-owned jobs MAY coexist in the same workflow file or in separate workflows. They are visible to +conformance reporting and MUST NOT weaken or bypass the Process-PSModule call boundary. + +## Event authorization + +The caller invokes the reusable workflow without a caller-level fork or event condition. The controlled upstream Plan +implementation derives its security and capability envelope from immutable GitHub event metadata before it interprets +repository settings or executes checked-out code. + +For a normal fork `pull_request`, Plan emits a restricted Settings record: + +```text +IsFork=true +AllowAppToken=false +AllowPublication=false +AllowMutation=false +``` -## Candidate event routing +The restricted route permits only repository-local checkout, build, lint, and test with the least-privilege built-in +token. It provides a green or red validation outcome without contributor secrets. Fork settings and checked-out files +are untrusted validation and build inputs and cannot alter the capability envelope. -Plan resolves the caller event into one release classification before build and publication work begins. The classification is stable release, prerelease, recovery or resume, cleanup-only, or no-op. Pull-request events may cancel their predecessors, so every pull-request route must converge to the latest pull-request state. +Restricted routes do not create App tokens; access PowerShell Gallery; mutate pull requests, statuses, releases, tags, +or assets; perform cleanup; deploy Pages; or run other privileged or user-facing operations. `pull_request_target` is +rejected before credentials or repository-defined code run. -| Event | Candidate Plan classification | Desired release action | Cancellation | Required result | -| --- | --- | --- | --- | --- | -| `workflow_dispatch` on the default branch | Recovery or resume | Stable release or no-op | Never cancel | Rebuild and validate the selected commit; reconstruct the unreleased release notes. | -| `schedule` | Published-artifact validation | No-op after validation | Never cancel | Validate the latest published stable artifact and its documentation. | -| Fork `pull_request` | Restricted read-only validation | No-op after validation | Cancels a superseded pull-request run; the later run converges state | Perform only safe repository-local checkout, build, lint, and test with no privileged or user-facing operation. | -| Pull request `opened`, `reopened`, `synchronize` | Pull-request classification | Prerelease or no-op | Cancels a superseded pull-request run; the later run converges state | Report configured validation and execute the planned release action. | -| Pull request `labeled`, `unlabeled` | Pull-request classification refresh | Prerelease, cleanup-only, or no-op | Cancels a superseded pull-request run; the later run converges state | Resolve the complete current classification and execute its action. | -| Merged pull request `closed` | Post-merge close | No-op | Cancels a superseded pull-request run; the later run converges state | Do not clean up; the successful main-push release owns promotion cleanup. | -| Abandoned pull request `closed` | Abandoned-close classification | Cleanup-only | Cancels a superseded pull-request run; the later run converges state | Reconcile only prereleases owned by the abandoned pull request. | -| Push to the default branch | Stable release | Stable release | Never cancel | Resolve merged-pull-request intent when applicable, then publish and perform promotion cleanup after required gates. | - -Plan records its classification and release decision in enriched Settings. Downstream jobs consume that Settings object and do not infer policy from events, labels, or repository settings again. - -## Candidate caller boundary - -The candidate standardizes exactly one Process-PSModule reusable-workflow call job and the shared top-level triggers, -concurrency, permissions, Plan authorization, and credential boundary that govern it. It does not standardize the -repository owner's entire workflow. Repository-owned jobs may coexist in the same workflow file or in separate workflows -and are reported only for visibility. They must not weaken or bypass the Process-PSModule call's trigger, concurrency, -permissions, Plan authorization, or credential boundary. - -## Candidate event authorization - -The caller invokes the reusable workflow without a caller-level fork or event condition. Plan is the event-authorization -boundary. For a normal fork `pull_request`, the controlled upstream Process-PSModule `v8` Plan implementation first -derives the security and capability envelope from immutable GitHub event metadata: the fork condition and the base and -head identities. It may query trusted upstream or base version and state as needed. GitHub withholds fork secrets, so -Plan then emits an authorized restricted read-only validation record rather than rejecting the event. That record sets -`IsFork=true`, `AllowAppToken=false`, `AllowPublication=false`, and `AllowMutation=false`. It permits only -repository-local checkout, build, lint, and test with the least-privilege built-in token, giving the contributor the -standard green/red validation outcome without configured secrets. - -Repository settings and checked-out fork files are consumed only after that envelope is fixed, and only as untrusted -validation or build inputs. They cannot enable an App token; publication, mutation, deployment, or cleanup; or broader -permissions. - -No restricted fork path may create an App token; access PowerShell Gallery; comment on, label, or mutate a pull request; -write a status or other check-facing report; create releases, tags, or assets; perform cleanup; deploy Pages; or perform -another privileged or user-facing operation. `pull_request_target` remains unsupported because it could combine -privileged context with untrusted code. Plan rejects that event before credentials or repository-defined code run. An -authorization rejection produces no usable Settings and no credentialed follow-on work. - -Every downstream job depends on a successful authorized Plan and valid Settings. This requirement applies equally to jobs -that use `always()`: their conditions first require the Plan result and Settings validity, then apply their own -failure-handling logic. Privileged jobs additionally require their relevant explicit capability. A downstream job never -parses missing or invalid Settings and cannot bypass the Plan gate. - -## Candidate artifact and version boundary - -Version resolution is the boundary between planning and release-capable work. The candidate carries one immutable release record in enriched Settings through build, test, and release execution: - -| Record field | Purpose | +Every downstream job first requires successful Plan execution and valid Settings. Jobs using `always()` apply this gate +before their own failure-handling logic. Privileged jobs also require their relevant Settings capability and never parse +missing or invalid Settings. + +## Settings contract + +Settings contains one immutable release record: + +| Field | Purpose | | --- | --- | -| Event and run type | Identifies the GitHub event and its candidate lifecycle classification. | -| Event action | Preserves the relevant pull-request activity or non-pull-request action. | -| Pull-request identity, state, and merged status | Distinguishes active, merged, and abandoned pull-request outcomes. | -| Authorization capabilities and evidence | Records `IsFork`, the immutable event metadata used to derive it, and the explicit App-token, publication, and mutation capabilities that each downstream job must enforce. | -| Labels and repository settings result | Captures the input policy state used only by Plan. | -| Version bump and base version | Explains the selected version transition. | -| Manifest version, prerelease identifier, and full version or tag | Defines the only version and tag permitted in the built artifact and release. | +| Event and run type | Identifies the GitHub event and lifecycle classification. | +| Event action | Preserves the pull-request activity or non-pull-request action. | +| Pull-request identity, state, and merge status | Distinguishes active, merged, and abandoned outcomes. | +| Authorization capabilities and evidence | Records `IsFork`, immutable event metadata, and App-token, publication, and mutation capabilities. | +| Labels and repository settings result | Records the inputs resolved by Plan. | +| Version bump and base version | Defines the version transition. | +| Manifest version, prerelease identifier, and full version or tag | Defines the only version and tag permitted in an artifact and release. | | Target commit | Binds validation, artifact, and release to one source revision. | -| Desired release action and create or publish flags | Selects stable release, prerelease, recovery or resume, cleanup-only, or no-op. | -| Cleanup intent and artifact identity | Defines the exact artifacts that release execution may reconcile. | -| Release-note source and boundary | Identifies the merged pull requests eligible for a recovery release note. | +| Resolved release action and create or publish flags | Selects stable, prerelease, recovery or resume, cleanup-only, or no-op execution. | +| Cleanup intent and artifact identity | Defines the exact artifacts release execution may reconcile. | +| Release-note source and boundary | Identifies merged pull requests eligible for release notes. | -The build stage stamps exactly the planned manifest version and prerelease identifier into the module artifact. Before any package, tag, or release becomes visible, release execution verifies that the artifact equals the immutable Settings record. A mismatch stops execution; it is not corrected by retagging or by recalculating a version after the artifact is built. +## Artifact and version boundary -## Candidate general release execution +Build stamps exactly the manifest version and prerelease identifier in Settings into the module artifact. Before any +package, tag, or release becomes visible, release execution verifies that the artifact equals the Settings record. A +mismatch stops execution; release execution does not recalculate versions or retag artifacts. -One general module release action or reusable workflow consumes enriched Settings after validation. It handles stable release, prerelease, recovery or resume, cleanup-only, and no-op actions according to the planned desired release action and flags. It verifies the artifact when an artifact is required and reconciles only the requested state. It does not recompute versioning, labels, event routing, or cleanup policy. +## Repository authorization -## Candidate repository authorization - -The caller sets no top-level default and grants its Process-PSModule job only the built-in permissions needed for repository-local reads and standard Pages/OIDC deployment: +The caller uses the following job boundary: ```yaml permissions: {} @@ -110,56 +108,46 @@ jobs: GitHubAppPrivateKey: ${{ secrets.SHELLY_PRIVATE_KEY }} ``` -These three explicit mappings are the required caller baseline; `secrets: inherit` is nonconforming. The only optional -secret mapping is `TestData` for module-local tests: +These explicit secret mappings are required. `secrets: inherit` is prohibited. The only optional secret mapping is +`TestData` for module-local tests: ```yaml TestData: ${{ secrets.TestData }} ``` When present, `TestData` contains a JSON object with separate `secrets` and `variables` maps. It is omitted when -unused. A conforming caller does not set `with.Debug: true`; the reusable workflow default remains `false`. - -Built-in `GITHUB_TOKEN` authorization is permitted for checkout, repository-local reads, and standard Pages/OIDC deployment within that job boundary. GitHub App installation tokens are step-scoped and authorize every user-facing interaction and every operation that needs broader reach or permissions: pull-request comments and labels, commit statuses and check-facing reporting, releases, tags, assets, and cleanup. - -Each App-token step requests only the installation permissions required for its operation. A missing App token is an authorization failure for App-required work: that operation stops before its API request or mutation, without silently falling back to the built-in workflow token. Built-in-token reads and Pages deployment remain available only within the explicit caller job permissions. - -For a Plan-classified restricted fork run, the Settings capabilities override the caller job's otherwise available -permissions: no App token is created, no Pages deployment runs, and no repository mutation or user-facing action runs. -The fork path has only the least-privilege built-in-token access required for repository-local checkout, build, lint, -and test. - -## Candidate Pages authorization +unused. Callers do not set `with.Debug: true`; the reusable workflow default is `false`. -The standard `actions/deploy-pages` path uses the built-in token's `pages: write` and `id-token: write` permissions granted only to the caller's Process-PSModule job. It remains inside the candidate's repository-local, non-user-facing boundary and does not require an App token. +Built-in `GITHUB_TOKEN` authorizes checkout, repository-local reads, and standard Pages/OIDC deployment within the job +boundary. Step-scoped GitHub App installation tokens authorize pull-request comments and labels, commit statuses and +check-facing reporting, releases, tags, assets, and cleanup. An App-required operation fails before its API request or +mutation when its App token is unavailable; it never falls back to the built-in token. -## Candidate stable Plan aggregation +Restricted fork Settings override the caller job boundary: no App token is created, no Pages deployment runs, and no +repository mutation or user-facing action runs. -Every stable push and recovery target uses the same aggregation rule. Plan finds the last successfully published stable version and its associated target commit, then queries merged pull requests through the requested target commit. It aggregates their release intent and uses that range for the stable version decision and release notes. +## Stable aggregation and recovery -This aggregation is not limited to manual recovery. GitHub can replace an intermediate pending run when a newer run enters the same concurrency group, so the later stable target must carry forward every merged pull request since the last successful publication. +Every stable push and recovery target finds the last successfully published stable version and associated target commit, +then aggregates merged pull requests through the requested target commit. The aggregated release intent determines the +stable version and release-note range. -## Candidate manual recovery +Manual recovery accepts a selected default-branch commit and applies the same aggregation and validation path as a +default-branch push. It returns no-op when a stable publication already covers that commit. Release notes use the +ordered, de-duplicated merged-pull-request range rather than manual-dispatch payload data. -A manual recovery route accepts only a selected default-branch commit. It uses the stable Plan aggregation rule and returns a no-op when a stable publication already covers that commit. +## Scheduled validation and close behavior -For a missing publication, the release-note reconstruction uses the ordered, de-duplicated aggregated pull-request range rather than the manual-dispatch event payload, which has no pull-request context. +Scheduled validation resolves the latest published stable version as input and sets a validation-only mutation class. +Publication and cleanup execution do not run. -The recovery route validates the selected commit using the same release gates as a default-branch push. It produces a stable release only after the artifact/version boundary succeeds. This keeps recovery notes traceable even when the normal main-push run was missed or interrupted. +A merged pull-request close performs no prerelease cleanup. The successful default-branch stable release owns promotion +cleanup. An abandoned pull-request close receives a pull-request-scoped artifact set and performs only prerelease +cleanup. Broad prerelease deletion requires its own exclusive scope and does not share the abandoned-close route. -## Candidate scheduled validation +## Concurrency and recovery -The scheduled route resolves the latest published stable version as an input, not as a version to create. It validates the downloaded package and its published documentation with the checks appropriate to a published consumer artifact. Its plan record sets the mutation class to validation only, so publication and cleanup stages cannot run. - -## Candidate close behavior - -A merged pull-request close performs no prerelease cleanup because the default-branch push is the release authority. After its stable release succeeds, that main-push route owns promotion cleanup. An abandoned pull-request close runs only pull-request-scoped prerelease cleanup because no main push will occur. - -The alternative of never cleaning up on a close requires scheduled garbage collection and leaves abandoned prereleases available until that collection runs. This candidate selects abandoned-close cleanup instead; it does not change the existing unapproved caller contract. - -## Candidate concurrency and recovery - -The candidate caller recommendation is: +The caller uses: ```yaml concurrency: @@ -167,67 +155,46 @@ concurrency: cancel-in-progress: ${{ github.event_name == 'pull_request' }} ``` -This is a candidate, not an approved caller contract. All pull-request events for one pull request share a group and may cancel an earlier run. Push, manual-dispatch, and scheduled events serialize by the full `github.ref` and do not cancel a running run. GitHub permits at most one running and one pending run per group, so a newer same-group event can replace an older pending run even when cancellation is disabled. A manual dispatch on `main` shares `refs/heads/main` with a push: it queues behind a running push but can replace an older pending main run. +All pull-request events for one pull request share a group and cancel superseded runs. Push, manual-dispatch, and +scheduled events use full `github.ref`, do not cancel a running run, and serialize by ref. GitHub retains one running +and one pending run per group; a later same-group event can replace an earlier pending run. Full `github.ref` prevents +branch and tag name collisions that `github.ref_name` cannot distinguish. -The candidate retains full `github.ref`. `github.ref_name` neither prevents pending-run replacement nor distinguishes branches and tags with the same name. +Cancellation leaves only transient partial state. A subsequent `synchronize`, `labeled`, `unlabeled`, or `closed` +event resumes and reconciles the latest pull-request state: -Cancellation can leave only transient partial state. Every pull-request route, including prerelease publication and abandoned-close cleanup, resumes and reconciles on the next `synchronize`, `labeled`, `unlabeled`, or `closed` event: - -| External operation | Required recovery behavior | +| External operation | Reconciliation | | --- | --- | -| PowerShell Gallery publication | Use a deterministic pull-request-scoped prerelease identity, detect the existing resolved version, and continue without duplicate publication. | +| PowerShell Gallery publication | Resolve a deterministic pull-request-scoped prerelease identity, detect the existing version, and continue without duplicate publication. | | GitHub Release creation | Resume or upsert the release and replace its asset set. | | Prerelease cleanup | Repeat safely after partial deletion and converge to the latest pull-request state. | -| Prerelease lifecycle | A subsequent synchronize, label, unlabel, or close reconciles obsolete prereleases. | -| Production boundary | No pull-request event creates a stable or signable production artifact. | - -Cleanup receives a pull-request-scoped artifact set and MUST NOT perform a broad prerelease deletion while a stable release can be active. Any future broad cleanup needs a separately approved exclusive scope; it cannot share the abandoned-close route. - -## Candidate Gallery prerelease disposition +| Production boundary | Do not create a stable or signable production artifact from a pull-request event. | -PowerShell Gallery packages are immutable and cannot be overwritten. Gallery reconciliation is therefore separate from GitHub Release and tag cleanup: - -| Obsolete Gallery prerelease condition | Candidate disposition | -| --- | --- | -| A supported Gallery API can unlist the version | Unlist the immutable package through that API. | -| Unlisting is not feasible | Retain the immutable version and record it as retained in the lifecycle result. | +## Gallery prerelease disposition -The selected disposition policy remains unapproved. A cancellation can leave a published immutable package even when its GitHub Release and tag cleanup has not completed; the next pull-request event must detect that version and apply the approved Gallery disposition rather than attempting to overwrite it. +PowerShell Gallery packages are immutable and cannot be overwritten. Each prerelease uses a deterministic +pull-request-scoped version. When a prerelease becomes obsolete, release execution unlists it through a supported +Gallery API when feasible. When unlisting is infeasible, release execution retains and records the immutable version. +GitHub Release and tag cleanup execute independently from Gallery disposition. -## Candidate verification strategy +## Verification -The lifecycle contract is exercised with event payload fixtures and publication fakes before credentials are used: +The lifecycle is verified with event payload fixtures and publication fakes before credentials are used: -| Candidate behavior | Verification | +| Behavior | Verification | | --- | --- | -| Event routing | One fixture for each supported event and pull-request activity, including merged and abandoned close outcomes. | -| Version boundary | A mismatched artifact fixture that proves publication stops. | -| Recovery release notes | Merged-pull-request query fixtures covering an empty range, one pull request, and multiple pull requests. | -| Scheduled validation | A published-version fixture that proves no release mutation is requested. | -| Pull-request convergence | Canceled prerelease-publication and cleanup fixtures followed by synchronize, label, unlabel, and close events that prove the latest pull-request state is reconciled. | -| Gallery immutability | Deterministic pull-request identity, existing-version detection, supported-unlist, and retained-version fixtures across the cancellation boundary. | -| Stable aggregation | Bursts of main-push, manual-dispatch, and scheduled fixtures that replace an intermediate pending run and prove the later stable target aggregates all unreleased merged pull requests. | -| Scoped caller permissions | Empty caller top-level permissions, the three job grants, built-in-token checkout/read, and standard Pages/OIDC verification. | -| Caller credential contract | Explicit baseline mappings for `PSGALLERY_API_KEY`, `GitHubAppClientId`, and `GitHubAppPrivateKey`; rejected `secrets: inherit`; optional `TestData` JSON with separate `secrets` and `variables` maps; and no `with.Debug: true`. | -| Caller boundary | Exactly one conforming Process-PSModule call job with governing shared top-level controls, plus repository-owned jobs in the same file and separate workflows that prove their presence is visible but does not weaken or bypass the call boundary. | -| App authorization failure | Missing-App-token fixtures that prove user-facing operations fail closed without built-in token fallback. | -| Token boundary | Fixtures that prove App tokens are step-scoped and built-in-token operations remain within the caller job's boundary. | -| Event authorization | Normal-fork `pull_request` fixtures that prove the controlled upstream Plan derives restricted capabilities from immutable fork/base/head metadata before it consumes settings or checked-out code, then permits only checkout/build/lint/test with a green/red outcome and no configured secrets; `pull_request_target` fixtures prove Plan rejects before credentialed or repository-defined code, including for downstream `always()` jobs. | -| Capability enforcement | Restricted-fork fixtures that prove App-token creation, Gallery access, comments, labels, statuses, releases, tags, assets, cleanup, Pages deployment, and other privileged paths cannot run. | - -## Decisions requiring approval - -The candidate does not decide the following: - -- Whether a recovery release always uses the normal next patch version or permits an explicit version input. -- Which source is authoritative when a PowerShell Gallery publication and GitHub release disagree about the last published stable version. -- Which consumer-facing checks comprise scheduled published-artifact validation. -- Whether removing prerelease eligibility cleans up existing prereleases immediately or leaves them until the abandoned-close cleanup route. -- Whether the supported Gallery API can unlist obsolete prereleases; otherwise, how retained immutable versions are recorded. -- Approval of the selected caller concurrency expression in [PSModule/Process-PSModule#514](https://github.com/PSModule/Process-PSModule/issues/514). +| Event routing | Fixtures for each supported event and pull-request activity, including merged and abandoned close outcomes. | +| Version boundary | A mismatched artifact fixture proves publication stops. | +| Recovery release notes | Merged-pull-request query fixtures cover empty, single, and multiple pull-request ranges. | +| Scheduled validation | A published-version fixture proves no release mutation is requested. | +| Pull-request convergence | Canceled prerelease publication and cleanup fixtures followed by synchronize, label, unlabel, and close events prove reconciliation. | +| Gallery disposition | Fixtures cover deterministic identity, existing-version detection, supported unlisting, and retained-version recording. | +| Stable aggregation | Push, manual-dispatch, and scheduled bursts replace a pending run and prove all unreleased merged pull requests are aggregated. | +| Caller authorization | Fixtures verify the explicit permissions and credential mappings, App-token failure, and no built-in-token fallback. | +| Fork authorization | Fixtures verify immutable-metadata-first restricted Settings, no privileged operations, and `pull_request_target` rejection. | +| Caller boundary | Fixtures verify repository-owned jobs remain visible without weakening or bypassing the reusable-workflow call boundary. | ## Related -- [Candidate specification](process-workflow-lifecycle-specification.md) — behavior and acceptance criteria. -- [Scenario matrix](scenario-matrix.md) — established job-level routing reference. -- [Process-PSModule caller workflow candidate](process-workflow-fleet-standard.md) — candidate event and concurrency contract. +- [Process-PSModule workflow lifecycle specification](process-workflow-lifecycle-specification.md) +- [Process-PSModule caller contract](process-workflow-fleet-standard.md) diff --git a/docs/content/reference/process-workflow-lifecycle-specification.md b/docs/content/reference/process-workflow-lifecycle-specification.md index a90f3a3f..3024eef8 100644 --- a/docs/content/reference/process-workflow-lifecycle-specification.md +++ b/docs/content/reference/process-workflow-lifecycle-specification.md @@ -1,42 +1,33 @@ --- -title: Process-PSModule workflow lifecycle candidate specification -description: Candidate behavior-driven requirements for Process-PSModule caller event routing, recovery releases, validation, and cleanup. +title: Process-PSModule workflow lifecycle specification +description: Behavior-driven requirements for Process-PSModule event routing, recovery releases, validation, and cleanup. --- -# Process-PSModule workflow lifecycle candidate specification +# Process-PSModule workflow lifecycle specification -**Status:** This is a candidate for discussion. It is not an approved workflow standard and does not change the candidate caller contract in [Process-PSModule caller workflow candidate](process-workflow-fleet-standard.md). - -## Problem and outcome - -Module repositories need each workflow event to have one safe, observable lifecycle outcome. A failed or missed publication needs a recoverable path; published artifacts need continuing validation; and pull-request activity must not create an accidental stable release. - -This candidate defines the behavior required from a Process-PSModule workflow lifecycle. Its requirements follow [spec-driven development](https://msx.no/docs/Ways-of-Working/Spec-Driven-Development/) and use [Given / When / Then scenarios](https://msx.no/docs/Ways-of-Working/Spec-Driven-Development/#behavioral-scenarios) as the acceptance contract. +This specification defines the lifecycle requirements for the Process-PSModule reusable workflow. Requirements follow +[spec-driven development](https://msx.no/docs/Ways-of-Working/Spec-Driven-Development/) and use +[Given / When / Then scenarios](https://msx.no/docs/Ways-of-Working/Spec-Driven-Development/#behavioral-scenarios) as +the acceptance contract. ## Scope -The candidate covers dispatch recovery, scheduled validation, pull-request validation and prerelease evaluation, closed-pull-request cleanup, stable publication after a default-branch push, and the Process-PSModule reusable-workflow caller boundary. - -It standardizes exactly one conforming Process-PSModule reusable-workflow call job and the shared top-level controls that govern -it; it does not standardize the repository owner's whole workflow. Repository-owned jobs may exist in the same workflow -file or separate workflows, provided they do not weaken or bypass the call's trigger, concurrency, permissions, Plan -authorization, or credential boundary. It does not change module build or publication implementation, define label -names, or prescribe release-note presentation. Those choices remain in the caller candidate, the existing versioning -guidance, and the companion [candidate design](process-workflow-lifecycle-design.md). - -## Confirmed implementation baseline - -The reusable workflow has a single planning decision that enriches downstream settings with a resolved version and release decision. It also serializes runs by pull request number or ref without canceling a running workflow. +The lifecycle covers dispatch recovery, scheduled published-artifact validation, pull-request validation and prerelease +evaluation, closed-pull-request cleanup, and stable publication after a default-branch push. -The version resolver treats non-pull-request events, including `workflow_dispatch` and `schedule`, as events without a pull request and does not create a release decision. The existing workflow supports pull-request validation, prerelease publication, default-branch publication, and closed-pull-request prerelease cleanup. Scheduled published-artifact validation and manual recovery publication are not confirmed behavior. - -The following requirements describe desired behavior, not a claim that the confirmed implementation already satisfies it. +The [Process-PSModule caller contract](process-workflow-fleet-standard.md) requires exactly one reusable-workflow call +job and the shared top-level controls that govern it. Repository-owned jobs MAY exist in the same workflow file or in +separate workflows, provided they do not weaken or bypass the call's trigger, concurrency, permissions, Plan +authorization, or credential boundary. ## Functional requirements ### FR1 — Manual dispatch MUST provide a safe recovery release {#fr1} -A default-branch manual dispatch MUST either publish one recoverable stable release after all required validation succeeds or report that the selected commit is already covered by a stable publication. It MUST NOT create a duplicate stable publication. +A default-branch manual dispatch MUST either publish one stable release after all required validation succeeds or report +that the selected commit is already covered by a stable publication. It MUST NOT create a duplicate stable publication. +Release notes MUST identify the merged pull requests from the last successfully published stable version through the +selected commit. #### Behavioral scenarios {#fr1-scenarios} @@ -56,7 +47,8 @@ Scenario: Repeat a completed recovery dispatch ### FR2 — Scheduled runs MUST validate published artifacts without publishing {#fr2} -A scheduled run MUST validate the latest published stable artifact and its published documentation against the repository's configured checks. It MUST NOT create, replace, or delete a package, tag, release, or prerelease. +A scheduled run MUST validate the latest published stable artifact and its published documentation against configured +checks. It MUST NOT create, replace, or delete a package, tag, release, or prerelease. #### Behavioral scenarios {#fr2-scenarios} @@ -71,7 +63,8 @@ Scenario: Validate the latest published artifact ### FR3 — Pull-request delivery events MUST run validation only {#fr3} -An `opened`, `reopened`, or `synchronize` pull-request event targeting the default branch MUST run the configured validation for the pull request. It MUST NOT create a stable publication. +An `opened`, `reopened`, or `synchronize` pull-request event targeting the default branch MUST run configured +validation. It MUST NOT create a stable publication. #### Behavioral scenarios {#fr3-scenarios} @@ -79,13 +72,15 @@ An `opened`, `reopened`, or `synchronize` pull-request event targeting the defau Scenario: Validate a synchronized pull request Given a pull request targets the default branch When a new commit synchronizes the pull request - Then the workflow reports the configured validation result on that pull request + Then the workflow reports the configured validation result And it does not publish a stable version ``` -### FR4 — Label changes MUST refresh the planned release classification {#fr4} +### FR4 — Label changes MUST refresh the release classification {#fr4} -A `labeled` or `unlabeled` pull-request event targeting the default branch MUST refresh the complete planned release classification from the current label set and repository settings. A prerelease publication MUST occur only when the planned classification is prerelease and every required validation succeeds. +A `labeled` or `unlabeled` pull-request event targeting the default branch MUST resolve the complete classification +from the current label set and repository settings. A prerelease publication MUST occur only when the classification is +prerelease and every required validation succeeds. #### Behavioral scenarios {#fr4-scenarios} @@ -93,19 +88,21 @@ A `labeled` or `unlabeled` pull-request event targeting the default branch MUST Scenario: Add prerelease eligibility Given a validated pull request has no prerelease eligibility When a prerelease label is added - Then the plan resolves the pull request as prerelease eligible + Then Plan resolves the pull request as prerelease eligible And it publishes at most one eligible prerelease version Scenario: Remove prerelease eligibility Given a pull request has prerelease eligibility When its prerelease label is removed - Then the plan resolves the pull request as prerelease ineligible + Then Plan resolves the pull request as prerelease ineligible And it does not create a new prerelease version ``` ### FR5 — Closed pull requests MUST route cleanup by close outcome {#fr5} -A merged pull request close MUST NOT perform prerelease cleanup. A successful default-branch release for the merge MUST own promotion cleanup. An abandoned pull request close MUST run cleanup-only behavior for prerelease artifacts associated with that pull request when cleanup is enabled. Neither close outcome MUST authorize or create a stable publication. +A merged pull-request close MUST NOT perform prerelease cleanup. A successful default-branch stable release MUST own +promotion cleanup. An abandoned pull-request close MUST run cleanup-only behavior for prerelease artifacts associated +with that pull request. Neither close outcome MUST authorize or create a stable publication. #### Behavioral scenarios {#fr5-scenarios} @@ -125,7 +122,8 @@ Scenario: Abandon a pull request with prereleases ### FR6 — Default-branch pushes MUST authorize stable publication after validation {#fr6} -A push to the default branch MUST publish a stable version only after all required build, test, quality, and publication gates succeed. A successful stable release MUST own promotion cleanup. +A push to the default branch MUST publish a stable version only after all required build, test, quality, and publication +gates succeed. A successful stable release MUST own promotion cleanup. #### Behavioral scenarios {#fr6-scenarios} @@ -141,22 +139,25 @@ Scenario: Publish a merged pull request ### FR7 — Published artifacts MUST match the planned version {#fr7} -Every prerelease or stable publication MUST contain the version and prerelease identity in the planned release decision. A version mismatch MUST fail publication before the release is made visible. +Every prerelease or stable publication MUST contain the planned manifest version and prerelease identity. A mismatch +MUST fail publication before the release becomes visible. #### Behavioral scenarios {#fr7-scenarios} ```gherkin Scenario: Reject an incorrectly stamped artifact - Given the plan resolves a release version + Given Plan resolves a release version And the built artifact reports a different version When publication is attempted Then publication fails And no release is made visible for that artifact ``` -### FR8 — Pull-request prereleases MUST have deterministic scoped identities {#fr8} +### FR8 — Pull-request prereleases MUST have deterministic immutable identities {#fr8} -A pull-request prerelease MUST use a deterministic identity scoped to its pull request. Reprocessing the same pull-request state MUST resolve the same prerelease identity, and different pull requests MUST NOT resolve the same identity. +A pull-request prerelease MUST use a deterministic identity scoped to its pull request. Reprocessing the same +pull-request state MUST resolve the same identity, and different pull requests MUST NOT resolve the same identity. +PowerShell Gallery versions MUST be treated as immutable and MUST NOT be overwritten. #### Behavioral scenarios {#fr8-scenarios} @@ -173,21 +174,25 @@ Scenario: Publish prereleases for distinct pull requests Then each pull request resolves a distinct prerelease identity ``` -### FR9 — Lifecycle policy MUST be resolved once before downstream work {#fr9} +### FR9 — Plan MUST resolve lifecycle policy before downstream work {#fr9} -The plan MUST resolve the lifecycle policy before build, test, or release execution begins. Downstream work MUST consume that planned policy and MUST NOT reinterpret event data, labels, or repository settings. +Plan MUST resolve lifecycle policy before build, test, or release execution. Its enriched Settings MUST contain the +event and run type, event action, pull-request identity, state and merge status, labels, version bump, base version, +manifest version, prerelease identifier, full version or tag, target commit, resolved release action, create and publish +flags, cleanup intent, artifact identity, release-note source and boundary, and authorization capabilities. Downstream +work MUST consume Settings and MUST NOT reinterpret event data, labels, or repository settings. #### Behavioral scenarios {#fr9-scenarios} ```gherkin Scenario: Execute a planned prerelease action - Given the plan resolves a pull request as an eligible prerelease publication + Given Plan resolves a pull request as an eligible prerelease publication When downstream work executes Then it consumes the planned release action and version And it does not re-evaluate pull-request labels or event data Scenario: Execute a planned cleanup-only action - Given the plan resolves an abandoned pull-request close as cleanup only + Given Plan resolves an abandoned pull-request close as cleanup only When release execution runs Then it reconciles only the planned cleanup state And it does not create or publish an artifact @@ -195,7 +200,9 @@ Scenario: Execute a planned cleanup-only action ### FR10 — Stable targets MUST aggregate unreleased merged pull requests {#fr10} -For every stable push or recovery target, the plan MUST aggregate merged pull requests and their release intent from the last successfully published version through the target commit. The resulting stable action MUST converge correctly when GitHub replaces an intermediate pending run. +For every stable push or recovery target, Plan MUST aggregate merged pull requests and their release intent from the +last successfully published version through the target commit. The resulting stable action MUST converge when GitHub +replaces an intermediate pending run. #### Behavioral scenarios {#fr10-scenarios} @@ -204,21 +211,29 @@ Scenario: Publish after an intermediate pending push is replaced Given merged pull requests exist after the last successfully published version And an intermediate default-branch push is replaced while pending When a later default-branch push is planned - Then the plan aggregates every merged pull request through the later target commit + Then Plan aggregates every merged pull request through the later target commit And the stable release uses the aggregated release intent Scenario: Recover a range of unreleased merged pull requests Given merged pull requests exist after the last successfully published version When a maintainer dispatches recovery for a later default-branch commit - Then the plan aggregates every merged pull request through that target commit + Then Plan aggregates every merged pull request through that target commit And the release notes use that aggregated range ``` ### FR11 — Repository operations MUST use scoped authorization {#fr11} -The caller MUST declare top-level `permissions: {}`. Its Process-PSModule job MUST grant only `contents: read`, `pages: write`, and `id-token: write`. It MUST explicitly map `PSGALLERY_API_KEY`, `GitHubAppClientId`, and `GitHubAppPrivateKey`; `secrets: inherit` is nonconforming. It MAY additionally map `TestData` only for module-local tests. When present, `TestData` MUST contain a JSON object with separate `secrets` and `variables` maps; callers MUST omit it when unused. No conforming caller MAY set `with.Debug: true`; the reusable workflow default remains `false`. +The caller MUST declare top-level `permissions: {}`. Its Process-PSModule job MUST grant only `contents: read`, +`pages: write`, and `id-token: write`. It MUST explicitly map `PSGALLERY_API_KEY`, `GitHubAppClientId`, and +`GitHubAppPrivateKey`; `secrets: inherit` MUST NOT be used. It MAY map `TestData` only for module-local tests. +When present, `TestData` MUST contain a JSON object with separate `secrets` and `variables` maps; callers MUST omit it +when unused. A caller MUST NOT set `with.Debug: true`; the reusable workflow default is `false`. -Built-in `GITHUB_TOKEN` MAY authorize repository-local, non-user-facing work when those permissions are sufficient, including checkout, reads, and standard Pages/OIDC deployment. GitHub App installation tokens MUST authorize all user-facing interactions and any operation that exceeds the built-in token's reach or permissions, including pull-request comments and labels, commit statuses and check-facing reporting, releases, tags, assets, and cleanup. Tokens MUST remain scoped to the steps that require them. +Built-in `GITHUB_TOKEN` MAY authorize repository-local, non-user-facing work when those permissions are sufficient, +including checkout, reads, and standard Pages/OIDC deployment. GitHub App installation tokens MUST authorize all +user-facing interactions and operations beyond the built-in token boundary, including pull-request comments and labels, +commit statuses and check-facing reporting, releases, tags, assets, and cleanup. Tokens MUST remain scoped to the steps +that require them. #### Behavioral scenarios {#fr11-scenarios} @@ -256,56 +271,45 @@ Scenario: Perform a user-facing repository operation ### FR12 — Plan MUST authorize events before downstream execution {#fr12} -The caller MUST invoke the reusable workflow without a caller-level fork or event condition. Plan MUST classify a normal -fork `pull_request` event into an authorized restricted read-only validation mode before downstream execution. Its valid -Settings MUST explicitly set `IsFork=true`, `AllowAppToken=false`, `AllowPublication=false`, and -`AllowMutation=false`. That mode MAY perform only safe repository-local checkout, build, lint, and test with the -least-privilege built-in token. It MUST NOT create an App token; access PowerShell Gallery; create pull-request -comments, labels, or status mutations; create releases, tags, or assets; perform cleanup; deploy Pages; or run another -privileged or user-facing operation. - -The controlled upstream reusable-workflow Plan implementation MUST derive the restricted capability envelope first from -immutable GitHub event metadata, including fork, base, and head identities, before interpreting repository settings or -executing checked-out repository code. It MAY query the trusted upstream or base version and state as needed. Fork -settings and files MAY be consumed only as untrusted validation and build inputs; they MUST NOT enable App tokens, -mutation, publication, deployment, cleanup, or broaden permissions. The restricted mode MUST provide the ordinary -green/red validation outcome without requiring contributor-configured secrets. - -`pull_request_target` MUST remain unsupported until a separate trust boundary is designed and approved. Plan MUST reject -that event before credentials or repository-defined code run. Every downstream job, including a job with `always()`, -MUST require a successful authorized Plan and valid Settings, and every privileged downstream job MUST also require its -relevant planned capability. No downstream job MAY evaluate missing or invalid Settings or bypass the Plan gate. +The caller MUST invoke the reusable workflow without a caller-level fork or event condition. For a normal fork +`pull_request`, the controlled upstream Plan implementation MUST derive a restricted capability envelope from immutable +GitHub event metadata, including fork, base, and head identities, before interpreting repository settings or executing +checked-out code. The Settings record MUST set `IsFork=true`, `AllowAppToken=false`, `AllowPublication=false`, and +`AllowMutation=false`. + +The restricted mode MAY perform repository-local checkout, build, lint, and test with the least-privilege built-in +token. It MUST NOT create an App token; access PowerShell Gallery; create pull-request comments, labels, or status +mutations; create releases, tags, or assets; perform cleanup; deploy Pages; or run another privileged or user-facing +operation. Fork settings and files MAY be consumed only as untrusted validation and build inputs and MUST NOT broaden +the capability envelope. The restricted mode MUST provide a green or red validation outcome without contributor secrets. + +`pull_request_target` MUST be rejected before credentials or repository-defined code run. Every downstream job, +including a job with `always()`, MUST require successful Plan execution and valid Settings. Every privileged downstream +job MUST also require its relevant planned capability. No downstream job MAY evaluate missing or invalid Settings or +bypass the Plan gate. #### Behavioral scenarios {#fr12-scenarios} ```gherkin Scenario: Validate a normal fork pull request in restricted mode Given a pull request originates from a fork through the pull_request event - When Plan evaluates the event - Then Plan emits valid Settings with IsFork true and App-token, publication, and mutation capabilities false - And downstream work may perform only repository-local checkout, build, lint, and test with the least-privilege built-in token - And no App token, Gallery access, user-facing operation, Pages deployment, publication, or cleanup runs - And the contributor receives the standard green or red validation outcome without configured secrets + When Plan evaluates immutable fork, base, and head metadata + Then Plan emits valid restricted Settings + And downstream work may perform only repository-local checkout, build, lint, and test + And the contributor receives a green or red validation outcome without configured secrets -Scenario: Derive fork capabilities before interpreting untrusted repository inputs +Scenario: Prevent untrusted inputs from expanding fork capabilities Given a pull request originates from a fork through the pull_request event And its repository settings attempt to enable publication - When the controlled upstream Plan evaluates immutable fork, base, and head metadata - Then it fixes the restricted capability envelope before reading the settings or checked-out files - And the settings cannot enable App tokens, mutation, publication, deployment, cleanup, or broader permissions + When Plan reads the settings or checked-out files + Then App-token, publication, mutation, deployment, and cleanup capabilities remain false -Scenario: Reject an unsupported pull_request_target event +Scenario: Reject a pull_request_target event Given a pull_request_target event is received When Plan evaluates the event - Then Plan rejects the event before credentialed or repository-defined code runs + Then Plan rejects the event before credentials or repository-defined code run And no downstream job receives authorized Settings -Scenario: Gate an always-running downstream job - Given Plan rejects an event or produces invalid Settings - When a downstream job with an always condition is evaluated - Then the job does not run - And it does not evaluate the missing or invalid Settings - Scenario: Gate a privileged job for a restricted fork run Given Plan emits valid restricted Settings for a fork pull request And the planned capability for publication is false @@ -316,10 +320,10 @@ Scenario: Gate a privileged job for a restricted fork run ### FR13 — Caller conformance MUST be limited to the reusable-workflow boundary {#fr13} -A conforming caller MUST contain exactly one Process-PSModule reusable-workflow call job and the shared top-level triggers, -concurrency, permissions, Plan authorization, and credential boundary that govern it. Repository-owned jobs MAY coexist -in the same workflow file or in separate workflows. They are not nonconforming merely by existing, but they MUST NOT -weaken or bypass any of those controls for the Process-PSModule call. +A conforming caller MUST contain exactly one Process-PSModule reusable-workflow call job and the shared top-level +triggers, concurrency, permissions, Plan authorization, and credential boundary that govern it. Repository-owned jobs +MAY coexist in the same workflow file or in separate workflows. They MUST NOT weaken or bypass any of those controls for +the Process-PSModule call. #### Behavioral scenarios {#fr13-scenarios} @@ -333,7 +337,7 @@ Scenario: Retain a repository-owned job beside the reusable-workflow call Scenario: Prevent a repository-owned job from bypassing the caller boundary Given a repository-owned job exists beside or outside the caller workflow - When it could weaken or bypass the Process-PSModule call's trigger, concurrency, permissions, Plan authorization, or credential boundary + When it weakens or bypasses the Process-PSModule call's trigger, concurrency, permissions, Plan authorization, or credential boundary Then the caller arrangement is nonconforming ``` @@ -341,9 +345,8 @@ Scenario: Prevent a repository-owned job from bypassing the caller boundary ### NFR1 — Lifecycle mutations MUST be idempotent {#nfr1} -Retrying the same event for the same commit and resolved version MUST produce no more than one package, tag, and release for that version. - -#### Behavioral scenarios {#nfr1-scenarios} +Retrying the same event for the same commit and resolved version MUST produce no more than one package, tag, and +release for that version. ```gherkin Scenario: Retry a publication after an interrupted run @@ -355,79 +358,51 @@ Scenario: Retry a publication after an interrupted run ### NFR2 — Pull-request cancellation MUST preserve non-pull-request serialization {#nfr2} -All pull-request events for one pull request MUST share a cancellation scope, so a newer pull-request event cancels a superseded run. Push, manual dispatch, and scheduled runs MUST share a full-ref serialization scope and MUST NOT cancel an in-progress run. Because GitHub permits at most one running and one pending run per group, a newer same-group non-pull-request run MAY replace an older pending run; the stable plan MUST therefore converge from the last successfully published version. +The caller MUST use: -#### Behavioral scenarios {#nfr2-scenarios} +```yaml +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} +``` -```gherkin -Scenario: Supersede a pull-request run - Given a pull-request run is in progress - When a newer synchronize, label, unlabel, or close event starts - Then the older pull-request run is canceled +All pull-request events for one pull request share a cancellation scope. Push, manual dispatch, and scheduled runs +share a full-ref serialization scope and MUST NOT cancel an in-progress run. GitHub permits one running and one pending +run per group; a later non-pull-request run MAY replace an earlier pending run. Stable planning MUST therefore converge +from the last successfully published version. The group MUST use full `github.ref`; `github.ref_name` does not +distinguish colliding branch and tag names. +```gherkin Scenario: Serialize non-pull-request runs Given a default-branch release is in progress And an earlier default-branch run is pending When a manual dispatch or scheduled validation starts for the same full ref Then the later run does not cancel the running release - And it may replace the older pending run + And it may replace the earlier pending run And the next stable plan aggregates the unreleased merged pull requests ``` ### NFR3 — Each lifecycle outcome MUST be auditable {#nfr3} -Every run MUST report its event category, resolved version or validated published version, release decision, and terminal outcome before the run completes. - -#### Behavioral scenarios {#nfr3-scenarios} - -```gherkin -Scenario: Inspect a scheduled validation result - Given a scheduled validation has completed - When a maintainer inspects the workflow result - Then the result identifies the validated published version - And it identifies whether validation passed or failed - And it identifies that no release mutation occurred -``` +Every run MUST report its event category, resolved or validated version, release decision, and terminal outcome. ### NFR4 — Pull-request mutation paths MUST resume and converge {#nfr4} -Every pull-request path, including prerelease publication and cleanup, MUST be idempotent and resumable after cancellation. The next `synchronize`, `labeled`, `unlabeled`, or `closed` event MUST converge release-related state to the latest pull-request state. Cancellation MAY leave transient partial state, but it MUST NOT leave a permanent duplicate or an obsolete artifact without the required disposition. - -#### Behavioral scenarios {#nfr4-scenarios} - -```gherkin -Scenario: Resume a canceled prerelease publication - Given a canceled pull-request run published a prerelease package but did not finish its release metadata - When a later pull-request event is processed - Then the workflow detects the existing version - And it completes or reconciles the prerelease release state without duplication - -Scenario: Reconcile obsolete prereleases - Given a canceled pull-request run left prerelease artifacts for an earlier pull-request state - When a synchronize, label, unlabel, or close event is processed - Then the workflow reconciles prerelease artifacts to the latest pull-request state - And every obsolete prerelease has the required disposition -``` +Every pull-request path, including prerelease publication and cleanup, MUST be idempotent and resumable after +cancellation. The next `synchronize`, `labeled`, `unlabeled`, or `closed` event MUST reconcile release-related state +to the latest pull-request state. Cancellation MAY leave transient partial state but MUST NOT leave a permanent +duplicate or obsolete artifact without its required disposition. ### NFR5 — Pull-request events MUST NOT produce production artifacts {#nfr5} -A pull-request event MUST NOT create a stable or signable production artifact. Pull-request events MAY create only eligible prerelease artifacts and their associated metadata. +A pull-request event MUST NOT create a stable or signable production artifact. Pull-request events MAY create only +eligible prerelease artifacts and associated metadata. -#### Behavioral scenarios {#nfr5-scenarios} - -```gherkin -Scenario: Evaluate an eligible prerelease pull request - Given a pull request is eligible for prerelease publication - When its release-capable path completes - Then it creates only prerelease artifacts and metadata - And it does not create a stable or signable production artifact -``` +### NFR6 — Immutable Gallery prereleases MUST have a durable disposition {#nfr6} -### NFR6 — Immutable Gallery prereleases MUST have a disposition policy {#nfr6} - -PowerShell Gallery packages MUST be treated as immutable and MUST NOT be overwritten. The candidate MUST define whether obsolete pull-request prereleases are unlisted through a supported Gallery API or retained as documented immutable versions when unlisting is not feasible. This policy is distinct from GitHub Release and tag cleanup. - -#### Behavioral scenarios {#nfr6-scenarios} +An obsolete PowerShell Gallery prerelease MUST be unlisted through a supported Gallery API when feasible. When unlisting +is infeasible, the workflow MUST retain and record the immutable version. GitHub Release and tag cleanup MUST execute +independently from Gallery disposition. ```gherkin Scenario: Dispose of an obsolete Gallery prerelease @@ -439,7 +414,7 @@ Scenario: Dispose of an obsolete Gallery prerelease Scenario: Retain an immutable Gallery prerelease Given a pull-request prerelease is obsolete - And unlisting that Gallery version is not feasible + And unlisting that Gallery version is infeasible When the prerelease is reconciled Then the workflow records the retained immutable Gallery version And it performs GitHub Release and tag cleanup independently @@ -447,34 +422,12 @@ Scenario: Retain an immutable Gallery prerelease ### NFR7 — App-required operations MUST fail closed {#nfr7} -When an operation requires GitHub App authorization and the required App token is unavailable, the operation MUST fail before an unauthorized API request, status update, comment, release, tag or asset mutation, or cleanup. It MUST NOT silently fall back to built-in `GITHUB_TOKEN` authority. Repository-local reads and standard Pages/OIDC deployment MAY continue only within the caller job's explicit built-in-token permissions. - -#### Behavioral scenarios {#nfr7-scenarios} - -```gherkin -Scenario: Reject a user-facing operation without GitHub App authorization - Given a reusable-workflow job cannot create its required GitHub App token - When the job attempts to create a pull-request comment - Then the operation fails before the API request - And it does not use the built-in workflow token as a fallback -``` +When an operation requires GitHub App authorization and the required App token is unavailable, the operation MUST fail +before an unauthorized API request, status update, comment, release, tag or asset mutation, or cleanup. It MUST NOT +fall back to built-in `GITHUB_TOKEN` authority. ## Cross-cutting acceptance criteria -### AC1 — Verifies: [FR1](#fr1), [FR6](#fr6), [FR7](#fr7), [NFR1](#nfr1) - -```gherkin -Scenario: Recover release notes after a missed main-push publication - Given merged pull requests exist after the last published stable version - And the selected default-branch commit has no stable publication - When a maintainer dispatches a recovery release - Then the published artifact matches the resolved version - And the release notes identify the merged pull requests in that unreleased range - And a retry creates no duplicate publication -``` - -### AC2 — Verifies: [FR2](#fr2), [FR4](#fr4), [FR5](#fr5), [FR6](#fr6), [FR8](#fr8), [FR9](#fr9), [FR10](#fr10), [FR11](#fr11), [FR12](#fr12), [FR13](#fr13), [NFR2](#nfr2), [NFR3](#nfr3), [NFR4](#nfr4), [NFR5](#nfr5), [NFR6](#nfr6), [NFR7](#nfr7) - ```gherkin Scenario: Lifecycle runs preserve release ownership after cancellation Given a scheduled validation and an abandoned pull-request cleanup overlap a main-push release @@ -486,16 +439,7 @@ Scenario: Lifecycle runs preserve release ownership after cancellation And the main-push run is the only run that publishes the stable release and performs promotion cleanup ``` -## Impact - -This candidate aims to reduce time to restore a missed publication and reduce change failure risk by separating validation, cleanup, prerelease, and stable-release authority. Its domain signal is the count of duplicate, missing, or incorrectly stamped published versions per release cycle; the target is zero. - -## Dependencies and constraints - -Approval of the caller event and concurrency contract in [PSModule/Process-PSModule#514](https://github.com/PSModule/Process-PSModule/issues/514) is required before this candidate becomes an implementation commitment. The candidate depends on repository credentials that can query pull requests and publish module artifacts. It retains the caller candidate's default-branch and fork boundaries. - ## Related -- [Candidate design](process-workflow-lifecycle-design.md) — proposed routing and recovery approach. -- [Scenario matrix](scenario-matrix.md) — established job-level routing reference. -- [Process-PSModule caller workflow candidate](process-workflow-fleet-standard.md) — candidate caller event and concurrency contract. +- [Process-PSModule workflow lifecycle design](process-workflow-lifecycle-design.md) +- [Process-PSModule caller contract](process-workflow-fleet-standard.md) diff --git a/docs/zensical.toml b/docs/zensical.toml index 83627c27..aacf6057 100644 --- a/docs/zensical.toml +++ b/docs/zensical.toml @@ -47,8 +47,8 @@ nav = [ {"Scenario matrix" = "reference/scenario-matrix.md"}, {"Framework test IDs" = "reference/framework-test-ids.md"}, {"Dependencies" = "reference/dependencies.md"}, - {"Workflow lifecycle candidate specification" = "reference/process-workflow-lifecycle-specification.md"}, - {"Workflow lifecycle candidate design" = "reference/process-workflow-lifecycle-design.md"}, + {"Workflow lifecycle specification" = "reference/process-workflow-lifecycle-specification.md"}, + {"Workflow lifecycle design" = "reference/process-workflow-lifecycle-design.md"}, ]}, {"Specification" = [ "specification/index.md",