From afa3408b5b83d38cde68eaecc2cbf29e7b950878 Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Sun, 9 Aug 2026 00:56:57 +0200 Subject: [PATCH 01/14] docs: migrate documentation site Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- README.md | 4 +- docs/.gitignore | 1 + docs/content/Dictionary/api.md | 11 + docs/content/Dictionary/azure-devops.md | 8 + docs/content/Dictionary/azure-functions.md | 8 + docs/content/Dictionary/azure.md | 10 + docs/content/Dictionary/branch.md | 10 + docs/content/Dictionary/build-pipeline.md | 8 + docs/content/Dictionary/ci-cd.md | 11 + docs/content/Dictionary/cmdlet.md | 10 + docs/content/Dictionary/container.md | 8 + docs/content/Dictionary/devops.md | 8 + docs/content/Dictionary/docker.md | 8 + docs/content/Dictionary/git.md | 9 + docs/content/Dictionary/github-actions.md | 11 + docs/content/Dictionary/github.md | 10 + docs/content/Dictionary/index.md | 64 +++ docs/content/Dictionary/lts.md | 10 + docs/content/Dictionary/markdown.md | 8 + docs/content/Dictionary/mkdocs.md | 9 + docs/content/Dictionary/module.md | 8 + docs/content/Dictionary/pipeline.md | 8 + docs/content/Dictionary/powershell.md | 8 + docs/content/Dictionary/pull-request.md | 8 + docs/content/Dictionary/repository.md | 8 + docs/content/Dictionary/rest-api.md | 11 + .../content/Dictionary/semantic-versioning.md | 10 + .../Dictionary/static-site-generator.md | 8 + docs/content/Dictionary/template.md | 8 + docs/content/Dictionary/testing.md | 8 + docs/content/Dictionary/version-control.md | 8 + .../content/Dictionary/virtual-environment.md | 8 + docs/content/Dictionary/workflow.md | 10 + docs/content/Dictionary/yaml.md | 11 + docs/content/Modules/Catalog/index.md | 21 + docs/content/Modules/Module-Types.md | 106 +++++ .../build-test-pack-publish.md | 21 + .../content/Modules/Process-PSModule/index.md | 16 + .../Process-PSModule/module-anatomy.md | 29 ++ .../Process-PSModule/module-bootstrap.md | 60 +++ .../module-build-validation.md | 94 +++++ .../Process-PSModule/repository-structure.md | 26 ++ .../Process-PSModule/template-quickstart.md | 23 + docs/content/Modules/Repository-Standard.md | 394 ++++++++++++++++++ docs/content/Modules/Standards.md | 368 ++++++++++++++++ docs/content/Modules/Test-Specification.md | 65 +++ docs/content/Modules/Versioning.md | 167 ++++++++ docs/content/Modules/index.md | 19 + docs/content/PowerShell/DSC/index.md | 10 + docs/content/PowerShell/FunctionApps/index.md | 10 + docs/content/PowerShell/Scripts/index.md | 9 + docs/content/PowerShell/Standard/index.md | 12 + docs/content/Solutions/index.md | 10 + docs/content/Style-Guides/index.md | 14 + docs/content/assets/images/icon.png | Bin 0 -> 5882 bytes .../images/module-catalog/githubissues.svg | 1 + .../module-catalog/githubpullrequests.svg | 1 + .../images/module-catalog/githubstars.svg | 1 + .../images/module-catalog/githubtags.svg | 1 + .../package-variant-closed-check.svg | 1 + docs/content/index.md | 50 +++ docs/includes/abbreviations.md | 50 +++ .../overrides/assets/javascripts/tablesort.js | 16 + docs/zensical.toml | 157 +++++++ 64 files changed, 2098 insertions(+), 2 deletions(-) create mode 100644 docs/.gitignore create mode 100644 docs/content/Dictionary/api.md create mode 100644 docs/content/Dictionary/azure-devops.md create mode 100644 docs/content/Dictionary/azure-functions.md create mode 100644 docs/content/Dictionary/azure.md create mode 100644 docs/content/Dictionary/branch.md create mode 100644 docs/content/Dictionary/build-pipeline.md create mode 100644 docs/content/Dictionary/ci-cd.md create mode 100644 docs/content/Dictionary/cmdlet.md create mode 100644 docs/content/Dictionary/container.md create mode 100644 docs/content/Dictionary/devops.md create mode 100644 docs/content/Dictionary/docker.md create mode 100644 docs/content/Dictionary/git.md create mode 100644 docs/content/Dictionary/github-actions.md create mode 100644 docs/content/Dictionary/github.md create mode 100644 docs/content/Dictionary/index.md create mode 100644 docs/content/Dictionary/lts.md create mode 100644 docs/content/Dictionary/markdown.md create mode 100644 docs/content/Dictionary/mkdocs.md create mode 100644 docs/content/Dictionary/module.md create mode 100644 docs/content/Dictionary/pipeline.md create mode 100644 docs/content/Dictionary/powershell.md create mode 100644 docs/content/Dictionary/pull-request.md create mode 100644 docs/content/Dictionary/repository.md create mode 100644 docs/content/Dictionary/rest-api.md create mode 100644 docs/content/Dictionary/semantic-versioning.md create mode 100644 docs/content/Dictionary/static-site-generator.md create mode 100644 docs/content/Dictionary/template.md create mode 100644 docs/content/Dictionary/testing.md create mode 100644 docs/content/Dictionary/version-control.md create mode 100644 docs/content/Dictionary/virtual-environment.md create mode 100644 docs/content/Dictionary/workflow.md create mode 100644 docs/content/Dictionary/yaml.md create mode 100644 docs/content/Modules/Catalog/index.md create mode 100644 docs/content/Modules/Module-Types.md create mode 100644 docs/content/Modules/Process-PSModule/build-test-pack-publish.md create mode 100644 docs/content/Modules/Process-PSModule/index.md create mode 100644 docs/content/Modules/Process-PSModule/module-anatomy.md create mode 100644 docs/content/Modules/Process-PSModule/module-bootstrap.md create mode 100644 docs/content/Modules/Process-PSModule/module-build-validation.md create mode 100644 docs/content/Modules/Process-PSModule/repository-structure.md create mode 100644 docs/content/Modules/Process-PSModule/template-quickstart.md create mode 100644 docs/content/Modules/Repository-Standard.md create mode 100644 docs/content/Modules/Standards.md create mode 100644 docs/content/Modules/Test-Specification.md create mode 100644 docs/content/Modules/Versioning.md create mode 100644 docs/content/Modules/index.md create mode 100644 docs/content/PowerShell/DSC/index.md create mode 100644 docs/content/PowerShell/FunctionApps/index.md create mode 100644 docs/content/PowerShell/Scripts/index.md create mode 100644 docs/content/PowerShell/Standard/index.md create mode 100644 docs/content/Solutions/index.md create mode 100644 docs/content/Style-Guides/index.md create mode 100644 docs/content/assets/images/icon.png create mode 100644 docs/content/assets/images/module-catalog/githubissues.svg create mode 100644 docs/content/assets/images/module-catalog/githubpullrequests.svg create mode 100644 docs/content/assets/images/module-catalog/githubstars.svg create mode 100644 docs/content/assets/images/module-catalog/githubtags.svg create mode 100644 docs/content/assets/images/module-catalog/package-variant-closed-check.svg create mode 100644 docs/content/index.md create mode 100644 docs/includes/abbreviations.md create mode 100644 docs/overrides/assets/javascripts/tablesort.js create mode 100644 docs/zensical.toml diff --git a/README.md b/README.md index db35db76..b960411a 100644 --- a/README.md +++ b/README.md @@ -6,8 +6,8 @@ Documentation site generation is powered by Zensical. Repositories define site c ## Documentation -The full documentation lives on the MSX / Docs site: +The full documentation is published at: -📖 **[Process-PSModule documentation](https://msxorg.github.io/docs/Frameworks/Process-PSModule/)** +📖 **[Process-PSModule documentation](https://psmodule.io/docs/)** It covers getting started, the pipeline stages, usage, configuration, repository structure, and the principles behind the framework. diff --git a/docs/.gitignore b/docs/.gitignore new file mode 100644 index 00000000..46ff2463 --- /dev/null +++ b/docs/.gitignore @@ -0,0 +1 @@ +/site/ diff --git a/docs/content/Dictionary/api.md b/docs/content/Dictionary/api.md new file mode 100644 index 00000000..59d994eb --- /dev/null +++ b/docs/content/Dictionary/api.md @@ -0,0 +1,11 @@ +--- +title: 'Application Programming Interface' +abbreviations: + - 'API' +--- + +# Application Programming Interface + +Application Programming Interface (API) - A set of protocols, routines, and tools for building software applications. APIs specify how software components should interact and are used when programming graphical user interface (GUI) components. + +**Example**: The [GitHub](github.md) API allows developers to interact with GitHub repositories, issues, and pull requests programmatically. diff --git a/docs/content/Dictionary/azure-devops.md b/docs/content/Dictionary/azure-devops.md new file mode 100644 index 00000000..b90e3a83 --- /dev/null +++ b/docs/content/Dictionary/azure-devops.md @@ -0,0 +1,8 @@ +--- +title: 'Azure DevOps' +abbreviations: [] +--- + +# Azure DevOps + +A set of development tools and services from Microsoft for software development teams, including version control, build automation, and project management. diff --git a/docs/content/Dictionary/azure-functions.md b/docs/content/Dictionary/azure-functions.md new file mode 100644 index 00000000..3a014d80 --- /dev/null +++ b/docs/content/Dictionary/azure-functions.md @@ -0,0 +1,8 @@ +--- +title: 'Azure Functions' +abbreviations: [] +--- + +# Azure Functions + +A serverless compute service that lets you run event-triggered code without having to explicitly provision or manage infrastructure. diff --git a/docs/content/Dictionary/azure.md b/docs/content/Dictionary/azure.md new file mode 100644 index 00000000..12ffc212 --- /dev/null +++ b/docs/content/Dictionary/azure.md @@ -0,0 +1,10 @@ +--- +title: 'Azure' +abbreviations: [] +--- + +# Azure + +Microsoft's cloud computing platform that provides a wide range of cloud services, including compute, analytics, storage, and networking. + +**Related**: [Azure Functions](azure-functions.md), [Azure DevOps](azure-devops.md) diff --git a/docs/content/Dictionary/branch.md b/docs/content/Dictionary/branch.md new file mode 100644 index 00000000..357ca156 --- /dev/null +++ b/docs/content/Dictionary/branch.md @@ -0,0 +1,10 @@ +--- +title: 'Branch' +abbreviations: [] +--- + +# Branch + +In version control systems like [Git](git.md), a branch is a parallel version of a [repository](repository.md) that diverges from the main working project. + +**Example**: Feature branches are used to develop new features in isolation before merging back to the main branch. diff --git a/docs/content/Dictionary/build-pipeline.md b/docs/content/Dictionary/build-pipeline.md new file mode 100644 index 00000000..c81d305d --- /dev/null +++ b/docs/content/Dictionary/build-pipeline.md @@ -0,0 +1,8 @@ +--- +title: 'Build Pipeline' +abbreviations: [] +--- + +# Build Pipeline + +An automated process that compiles, tests, and packages source code into deployable artifacts. diff --git a/docs/content/Dictionary/ci-cd.md b/docs/content/Dictionary/ci-cd.md new file mode 100644 index 00000000..17a1b244 --- /dev/null +++ b/docs/content/Dictionary/ci-cd.md @@ -0,0 +1,11 @@ +--- +title: 'Continuous Integration and Continuous Deployment' +abbreviations: + - 'CI/CD' + - 'CI' + - 'CD' +--- + +# Continuous Integration and Continuous Deployment + +Continuous Integration/Continuous Deployment (CI/CD) - A software development practice where developers regularly merge their code changes into a central repository, after which automated builds and tests are run. diff --git a/docs/content/Dictionary/cmdlet.md b/docs/content/Dictionary/cmdlet.md new file mode 100644 index 00000000..364bf230 --- /dev/null +++ b/docs/content/Dictionary/cmdlet.md @@ -0,0 +1,10 @@ +--- +title: 'Cmdlet' +abbreviations: [] +--- + +# Cmdlet + +A lightweight [PowerShell](powershell.md) command that follows the verb-noun naming convention and is designed to perform a specific function. + +**Example**: `Get-Process`, `Set-Location`, `New-Item` diff --git a/docs/content/Dictionary/container.md b/docs/content/Dictionary/container.md new file mode 100644 index 00000000..e49f1b56 --- /dev/null +++ b/docs/content/Dictionary/container.md @@ -0,0 +1,8 @@ +--- +title: 'Container' +abbreviations: [] +--- + +# Container + +A lightweight, standalone, executable package that includes everything needed to run an application: code, runtime, system tools, libraries, and settings. diff --git a/docs/content/Dictionary/devops.md b/docs/content/Dictionary/devops.md new file mode 100644 index 00000000..8537c2a6 --- /dev/null +++ b/docs/content/Dictionary/devops.md @@ -0,0 +1,8 @@ +--- +title: 'DevOps' +abbreviations: [] +--- + +# DevOps + +A set of practices that combines software development (Dev) and IT operations (Ops) to shorten the systems development lifecycle and provide continuous delivery. diff --git a/docs/content/Dictionary/docker.md b/docs/content/Dictionary/docker.md new file mode 100644 index 00000000..018df514 --- /dev/null +++ b/docs/content/Dictionary/docker.md @@ -0,0 +1,8 @@ +--- +title: 'Docker' +abbreviations: [] +--- + +# Docker + +A platform for developing, shipping, and running applications using containerization technology. diff --git a/docs/content/Dictionary/git.md b/docs/content/Dictionary/git.md new file mode 100644 index 00000000..f37fde39 --- /dev/null +++ b/docs/content/Dictionary/git.md @@ -0,0 +1,9 @@ +--- +title: 'Git' +abbreviations: + - 'Git' +--- + +# Git + +A distributed version control system for tracking changes in source code during software development. diff --git a/docs/content/Dictionary/github-actions.md b/docs/content/Dictionary/github-actions.md new file mode 100644 index 00000000..8fe876d9 --- /dev/null +++ b/docs/content/Dictionary/github-actions.md @@ -0,0 +1,11 @@ +--- +title: 'GitHub Actions' +abbreviations: + - 'GitHub Actions' +--- + +# GitHub Actions + +[GitHub's](github.md) built-in [CI/CD](ci-cd.md) platform that allows you to automate your build, test, and deployment [pipeline](pipeline.md). + +**Example**: Automatically running tests when a [pull request](pull-request.md) is created. diff --git a/docs/content/Dictionary/github.md b/docs/content/Dictionary/github.md new file mode 100644 index 00000000..d1de21b4 --- /dev/null +++ b/docs/content/Dictionary/github.md @@ -0,0 +1,10 @@ +--- +title: 'GitHub' +abbreviations: + - 'GitHub' + - 'GH' +--- + +# GitHub + +A web-based platform for version control and collaboration that lets you and others work together on projects from anywhere. diff --git a/docs/content/Dictionary/index.md b/docs/content/Dictionary/index.md new file mode 100644 index 00000000..d78d688f --- /dev/null +++ b/docs/content/Dictionary/index.md @@ -0,0 +1,64 @@ +# Glossary + +Welcome to the PSModule Glossary, a reference of terms, concepts, and technologies used throughout our projects and documentation. + +## Table of Contents + +- [API](api.md) +- [Azure](azure.md) +- [Azure DevOps](azure-devops.md) +- [Azure Functions](azure-functions.md) +- [Branch](branch.md) +- [Build Pipeline](build-pipeline.md) +- [CI/CD](ci-cd.md) +- [Cmdlet](cmdlet.md) +- [Container](container.md) +- [DevOps](devops.md) +- [Docker](docker.md) +- [Git](git.md) +- [GitHub](github.md) +- [GitHub Actions](github-actions.md) +- [LTS](lts.md) +- [Markdown](markdown.md) +- [MkDocs](mkdocs.md) +- [Module](module.md) +- [Pipeline](pipeline.md) +- [PowerShell](powershell.md) +- [Pull Request](pull-request.md) +- [Repository](repository.md) +- [REST API](rest-api.md) +- [Semantic Versioning](semantic-versioning.md) +- [Static Site Generator](static-site-generator.md) +- [Template](template.md) +- [Testing](testing.md) +- [Version Control](version-control.md) +- [Virtual Environment](virtual-environment.md) +- [Workflow](workflow.md) +- [YAML](yaml.md) + +## Quick Reference + +Common terms you might encounter: +- [API](api.md) - Application Programming Interface +- [Azure](azure.md) - Microsoft's cloud platform +- [CI/CD](ci-cd.md) - Continuous Integration/Continuous Deployment +- [Cmdlet](cmdlet.md) - PowerShell command +- [Git](git.md) - Version control system +- [GitHub](github.md) - Code hosting platform +- [LTS](lts.md) - Long-Term Servicing +- [MkDocs](mkdocs.md) - Documentation generator +- [Module](module.md) - PowerShell package +- [Pipeline](pipeline.md) - Automated process chain +- [PowerShell](powershell.md) - Task automation framework +- [Repository](repository.md) - Code storage location +- [Workflow](workflow.md) - Automated process series + +## Overview + +This glossary serves as a reference for developers, contributors, and users to understand the terminology and concepts used in PSModule projects. Each entry includes clear definitions, usage examples, and related links where applicable. + +!!! info "Contributing to the Glossary" + Found a term that should be added or need to update an existing definition? Please [create an issue](https://github.com/PSModule/Process-PSModule/issues) or submit a pull request with your suggestions. + +!!! tip "Search Functionality" + Use the search feature in the top navigation to quickly find specific terms and their definitions across the entire documentation site. diff --git a/docs/content/Dictionary/lts.md b/docs/content/Dictionary/lts.md new file mode 100644 index 00000000..cb25293f --- /dev/null +++ b/docs/content/Dictionary/lts.md @@ -0,0 +1,10 @@ +--- +title: 'Long-Term Servicing' +abbreviations: + - 'LTS' +--- + +# Long-Term Servicing + +Long-Term Servicing (LTS). +For more info visit: [PowerShell Support Lifecycle](https://learn.microsoft.com/en-us/powershell/scripting/install/powershell-support-lifecycle?view=powershell-7.5) diff --git a/docs/content/Dictionary/markdown.md b/docs/content/Dictionary/markdown.md new file mode 100644 index 00000000..cceefddf --- /dev/null +++ b/docs/content/Dictionary/markdown.md @@ -0,0 +1,8 @@ +--- +title: 'Markdown' +abbreviations: [] +--- + +# Markdown + +A lightweight markup language with plain text formatting syntax, designed to be converted to HTML and many other formats. diff --git a/docs/content/Dictionary/mkdocs.md b/docs/content/Dictionary/mkdocs.md new file mode 100644 index 00000000..9439e55f --- /dev/null +++ b/docs/content/Dictionary/mkdocs.md @@ -0,0 +1,9 @@ +--- +title: 'MkDocs' +abbreviations: + - 'MkDocs' +--- + +# MkDocs + +A fast, simple static site generator that's geared towards building project documentation using [Markdown](markdown.md) files. diff --git a/docs/content/Dictionary/module.md b/docs/content/Dictionary/module.md new file mode 100644 index 00000000..5fa1debc --- /dev/null +++ b/docs/content/Dictionary/module.md @@ -0,0 +1,8 @@ +--- +title: 'Module' +abbreviations: [] +--- + +# Module + +In [PowerShell](powershell.md), a package that contains PowerShell members, such as [cmdlets](cmdlet.md), providers, functions, workflows, variables, and aliases. diff --git a/docs/content/Dictionary/pipeline.md b/docs/content/Dictionary/pipeline.md new file mode 100644 index 00000000..b42407b5 --- /dev/null +++ b/docs/content/Dictionary/pipeline.md @@ -0,0 +1,8 @@ +--- +title: 'Pipeline' +abbreviations: [] +--- + +# Pipeline + +A series of automated processes that allow developers and [DevOps](devops.md) professionals to reliably and efficiently compile, build, and deploy their code. diff --git a/docs/content/Dictionary/powershell.md b/docs/content/Dictionary/powershell.md new file mode 100644 index 00000000..6c078e6f --- /dev/null +++ b/docs/content/Dictionary/powershell.md @@ -0,0 +1,8 @@ +--- +title: 'PowerShell' +abbreviations: [] +--- + +# PowerShell + +A task automation and configuration management framework from Microsoft, consisting of a command-line shell and the associated scripting language. diff --git a/docs/content/Dictionary/pull-request.md b/docs/content/Dictionary/pull-request.md new file mode 100644 index 00000000..abefc5b6 --- /dev/null +++ b/docs/content/Dictionary/pull-request.md @@ -0,0 +1,8 @@ +--- +title: 'Pull Request' +abbreviations: [] +--- + +# Pull Request + +Pull Request (PR) - A method of submitting contributions to a software project where changes are proposed and reviewed before being merged into the main codebase. diff --git a/docs/content/Dictionary/repository.md b/docs/content/Dictionary/repository.md new file mode 100644 index 00000000..448631c6 --- /dev/null +++ b/docs/content/Dictionary/repository.md @@ -0,0 +1,8 @@ +--- +title: 'Repository' +abbreviations: [] +--- + +# Repository + +Repository (Repo) - A storage location for software packages, often used in version control systems to store project files and their revision history. diff --git a/docs/content/Dictionary/rest-api.md b/docs/content/Dictionary/rest-api.md new file mode 100644 index 00000000..c56ad28a --- /dev/null +++ b/docs/content/Dictionary/rest-api.md @@ -0,0 +1,11 @@ +--- +title: 'Representational State Transfer API' +abbreviations: + - 'REST API' + - 'REST' + - 'API' +--- + +# Representational State Transfer API + +An architectural style for designing web services that uses HTTP requests to GET, PUT, POST, and DELETE data. diff --git a/docs/content/Dictionary/semantic-versioning.md b/docs/content/Dictionary/semantic-versioning.md new file mode 100644 index 00000000..07406d65 --- /dev/null +++ b/docs/content/Dictionary/semantic-versioning.md @@ -0,0 +1,10 @@ +--- +title: 'Semantic Versioning' +abbreviations: [] +--- + +# Semantic Versioning + +Semantic Versioning (SemVer) - A versioning scheme that uses a three-part version number: MAJOR.MINOR.PATCH, where each part is incremented based on the type of changes made. + +**Example**: Version 1.2.3 where 1 is major, 2 is minor, and 3 is patch. diff --git a/docs/content/Dictionary/static-site-generator.md b/docs/content/Dictionary/static-site-generator.md new file mode 100644 index 00000000..64e24e3e --- /dev/null +++ b/docs/content/Dictionary/static-site-generator.md @@ -0,0 +1,8 @@ +--- +title: 'Static Site Generator' +abbreviations: [] +--- + +# Static Site Generator + +A tool that generates a full static HTML site based on raw data and a set of templates. diff --git a/docs/content/Dictionary/template.md b/docs/content/Dictionary/template.md new file mode 100644 index 00000000..6bf53e46 --- /dev/null +++ b/docs/content/Dictionary/template.md @@ -0,0 +1,8 @@ +--- +title: 'Template' +abbreviations: [] +--- + +# Template + +A pre-designed format or structure that can be used as a starting point for creating new files, projects, or configurations. diff --git a/docs/content/Dictionary/testing.md b/docs/content/Dictionary/testing.md new file mode 100644 index 00000000..344f508d --- /dev/null +++ b/docs/content/Dictionary/testing.md @@ -0,0 +1,8 @@ +--- +title: 'Testing' +abbreviations: [] +--- + +# Testing + +The process of evaluating and verifying that a software application or system meets specified requirements and functions correctly. diff --git a/docs/content/Dictionary/version-control.md b/docs/content/Dictionary/version-control.md new file mode 100644 index 00000000..8a53b768 --- /dev/null +++ b/docs/content/Dictionary/version-control.md @@ -0,0 +1,8 @@ +--- +title: 'Version Control' +abbreviations: [] +--- + +# Version Control + +A system that records changes to a file or set of files over time so that you can recall specific versions later. diff --git a/docs/content/Dictionary/virtual-environment.md b/docs/content/Dictionary/virtual-environment.md new file mode 100644 index 00000000..0ebf2726 --- /dev/null +++ b/docs/content/Dictionary/virtual-environment.md @@ -0,0 +1,8 @@ +--- +title: 'Virtual Environment' +abbreviations: [] +--- + +# Virtual Environment + +An isolated environment that allows you to install packages and dependencies without affecting the global system installation. diff --git a/docs/content/Dictionary/workflow.md b/docs/content/Dictionary/workflow.md new file mode 100644 index 00000000..86c600c0 --- /dev/null +++ b/docs/content/Dictionary/workflow.md @@ -0,0 +1,10 @@ +--- +title: 'Workflow' +abbreviations: [] +--- + +# Workflow + +A series of automated steps or processes that execute in response to specific events or triggers. + +**Example**: A [GitHub Actions](github-actions.md) workflow that runs tests every time code is pushed to a [repository](repository.md). diff --git a/docs/content/Dictionary/yaml.md b/docs/content/Dictionary/yaml.md new file mode 100644 index 00000000..b5cd9eff --- /dev/null +++ b/docs/content/Dictionary/yaml.md @@ -0,0 +1,11 @@ +--- +title: 'YAML Ain''t Markup Language' +abbreviations: + - 'YAML' +--- + +# YAML Ain't Markup Language + +YAML Ain't Markup Language (YAML) - A human-readable data serialization standard commonly used for configuration files and data exchange. + +**Example**: [GitHub Actions](github-actions.md) workflows are defined using YAML files. diff --git a/docs/content/Modules/Catalog/index.md b/docs/content/Modules/Catalog/index.md new file mode 100644 index 00000000..5db88f7b --- /dev/null +++ b/docs/content/Modules/Catalog/index.md @@ -0,0 +1,21 @@ +# Module Catalog + +This catalog tracks modules maintained in the PSModule organization and what each module does. + +## Scope + +Each module page should capture: + +- purpose and problem statement +- install and import baseline +- key exported capabilities +- owner and maintenance notes +- lifecycle status + +## Catalog generation + +The module list and linked module pages are generated from PSModule repository metadata and README content, then refreshed automatically. + + + + diff --git a/docs/content/Modules/Module-Types.md b/docs/content/Modules/Module-Types.md new file mode 100644 index 00000000..9a93e423 --- /dev/null +++ b/docs/content/Modules/Module-Types.md @@ -0,0 +1,106 @@ +# Module types + +Most PSModule modules fall into one of a few archetypes. The general rules in +[PowerShell module standard](Standards.md) and [PowerShell Standards](../PowerShell/Standard/index.md) always apply; this +page adds the conventions that are specific to a module's type so that modules of the same kind feel +the same to use. + +Two archetypes have enough shared shape to standardize: + +- **Integration (API) modules** wrap an external service's REST or GraphQL API. +- **Data modules** convert or manage a data format or in-memory structure. + +A module can be both (for example, an integration module that also exposes conversion helpers). +Apply each relevant section. + +## Integration (API) modules + +Integration modules are the PowerShell face of an external service. `GitHub`, and the +service-client modules such as `Anthropic`, `OpenAI`, `Bluesky`, and `Domeneshop`, are integration +modules. + +### Command naming maps to the resource, not the HTTP method + +Name commands after the resource and the intent, using approved verbs. Never name a command after +the HTTP method or the endpoint path. Map REST methods to verbs: + +| REST method | PowerShell verb | Example | +| ----------- | --------------- | ------- | +| `GET` | `Get-` | `Get-GitHubRepository` | +| `POST` (create) | `New-` / `Add-` | `New-GitHubRepository` | +| `PUT` / `PATCH` (update) | `Set-` / `Update-` | `Set-GitHubRepository` | +| `DELETE` | `Remove-` | `Remove-GitHubRepository` | +| Non-CRUD action | Approved verb for the intent | `Invoke-`, `Start-`, `Stop-`, `Enable-`, ... | + +Prefix the noun with the service's term of art (`GitHubRepository`, not `Repository`). + +### Transport abstraction + +Lower-level helpers own the concrete `Invoke-RestMethod` / GraphQL / HTTP calls. How you expose +or hide this abstraction is a design choice: + +- **Private transport** (common): Keep REST, GraphQL, and HTTP helpers private. Public functions + accept resolved inputs and typed objects. This follows the Dependency Inversion rule from + [Standards](Standards.md#solid-applied) applied to the network boundary. +- **Public transport**: Expose REST or GraphQL functions publicly for power users or module + composition. +- **Public Context**: Expose the `Context` module as public so users can configure and manage + module state, secrets, and settings directly. + +Choose the strategy that best serves your module's audience. + +### Use Context for user and module settings + +Integration modules persist state with the [`Context`](https://github.com/PSModule/Context) module +rather than inventing bespoke storage. Context provides on-disk storage for user data and secrets, +organized by context and environment. Two kinds of state are both standard: + +- **User settings and secrets**: accounts, tokens, sessions, and per-user configuration. Store these + in a per-user context. `Context` encrypts secrets at rest (via `Sodium`), so a user can resume work + without reconfiguring or logging in again when the service supports session refresh. +- **Module settings**: module-wide defaults, endpoints, and feature flags that are not tied to a + single user. Store these in a module-scoped context. + +Your module must expose functions and object types so users can target specific contexts and +environments. Users need to be able to read from, write to, and manage contexts programmatically, +selecting which environment or context their functions operate against. Persisting both through +`Context` gives every integration module the same, discoverable settings model and keeps secrets +out of source, logs, and plain files. + +## Data modules + +Data modules convert between representations or manage an in-memory structure. `Hashtable` is the +reference shape; `Base64`, `Json`, `Lua`, `Hcl`, `Sodium`, and `Uri` follow the same pattern. + +### The neutral object is the pivot + +Every conversion goes through the neutral PowerShell object model +(`[PSCustomObject]` / `[hashtable]` / `[PSObject]`). `ConvertFrom-` parses a +format-specific representation into an object; `ConvertTo-` renders an object into the +format. Converting through the object as a common pivot means any format interoperates with any +other, instead of writing a direct converter for every pair. + +Always ship both directions so data can round-trip between the format and the object model. + +### Verb vocabulary + +| Verb pattern | Purpose | +| ------------ | ------- | +| `ConvertFrom-` | Format-specific text/representation → `[PSCustomObject]` / `[hashtable]` | +| `ConvertTo-` | `[PSCustomObject]` / `[hashtable]` → format-specific text/representation | +| `Import-` | Read from a file or store into objects | +| `Export-` | Write objects to a file or store | +| `Format-` | Produce a normalized or pretty rendering | +| `Merge-` | Combine two structures | +| `Compare-` | Diff two structures | +| `Test-` | Validate a value or structure | +| `Remove-Entry` | Remove elements by criteria | + +The `Hashtable` module demonstrates the full set: `ConvertFrom-Hashtable`, `ConvertTo-Hashtable`, +`Import-Hashtable`, `Export-Hashtable`, `Format-Hashtable`, `Merge-Hashtable`, and +`Remove-HashtableEntry`. + +## Where this connects + +- [PowerShell module standard](Standards.md): layout, private functions, and the mandatory context parameter. +- [Repository Standard](Repository-Standard.md): repository files, README shape, and agent onboarding. diff --git a/docs/content/Modules/Process-PSModule/build-test-pack-publish.md b/docs/content/Modules/Process-PSModule/build-test-pack-publish.md new file mode 100644 index 00000000..9b499cd8 --- /dev/null +++ b/docs/content/Modules/Process-PSModule/build-test-pack-publish.md @@ -0,0 +1,21 @@ +# Build, Test, Pack, Publish + +Process-PSModule orchestrates the module lifecycle through GitHub Actions. + +## Flow + +1. resolve settings and release intent +2. build module artifact from `src/` +3. run tests and quality checks +4. package docs/site artifacts when enabled +5. publish module and release metadata when release conditions are met + +## Release behavior + +Version progression is label-driven in pull requests and resolved in the plan stage. + +## Validation behavior + +Test and lint stages run before publish gates, and publish is blocked when required checks fail. + +For complete cross-org release capability docs, use [MSX Capabilities](https://msxorg.github.io/docs/Capabilities/). diff --git a/docs/content/Modules/Process-PSModule/index.md b/docs/content/Modules/Process-PSModule/index.md new file mode 100644 index 00000000..1b507c4b --- /dev/null +++ b/docs/content/Modules/Process-PSModule/index.md @@ -0,0 +1,16 @@ +# Process-PSModule + +Process-PSModule is the module delivery framework used in PSModule repositories. + +This section documents how module repositories are formed and how they move from source to published artifact. + +## In this section + +- [Repository Structure](repository-structure.md) +- [Module Anatomy](module-anatomy.md) +- [Module Build Validation](module-build-validation.md) +- [Build, Test, Pack, Publish](build-test-pack-publish.md) +- [Template Quickstart](template-quickstart.md) +- [Module Bootstrap](module-bootstrap.md) + +For broader framework context, see [MSX Frameworks / Process-PSModule](https://msxorg.github.io/docs/Frameworks/Process-PSModule/). diff --git a/docs/content/Modules/Process-PSModule/module-anatomy.md b/docs/content/Modules/Process-PSModule/module-anatomy.md new file mode 100644 index 00000000..2d77edaa --- /dev/null +++ b/docs/content/Modules/Process-PSModule/module-anatomy.md @@ -0,0 +1,29 @@ +# Module Anatomy + +This page explains what goes where inside PSModule module repositories. + +## Public command surface + +Public command files belong in `src/functions/public//` and define the module API. + +## Private implementation + +Private helpers belong in `src/functions/private//` and are not exported. + +## Types and format + +- public and private classes under `src/classes/` +- formatting definitions under `src/formats/` +- type extensions under `src/types/` + +## Initialization and shared state + +- import-time setup in `src/init/` +- scoped variables in `src/variables/private/` and `src/variables/public/` + +## Tests and examples + +- behavior tests in `tests/` +- representative usage in `examples/` + +The goal is stable repository anatomy so both humans and automation know exactly where to place and find module concerns. diff --git a/docs/content/Modules/Process-PSModule/module-bootstrap.md b/docs/content/Modules/Process-PSModule/module-bootstrap.md new file mode 100644 index 00000000..143f7935 --- /dev/null +++ b/docs/content/Modules/Process-PSModule/module-bootstrap.md @@ -0,0 +1,60 @@ +# Module Bootstrap + +A brand-new module usually has a small **load-bearing core**: the piece(s) every other function will depend on, without which nothing else in the module can work at all. A single feature PR cannot carry that much scope and still be small and focused, so bootstrap uses one integration branch instead. + +## Identify the load-bearing core first + +What counts as "load-bearing" follows the module's archetype from [Module types](../Module-Types.md): + +- **Data modules** — the conversion pivot: `ConvertFrom-` / `ConvertTo-` (and whatever parser/serializer they wrap). Every other function (`Import-`, `Export-`, `Format-`, `Merge-`, ...) is built on top of this pivot and is meaningless without it. +- **Integration (API) modules** — a [`Context`](https://github.com/PSModule/Context)-backed credential/config store, the client setup that uses it, and at least one API function that consumes the context end-to-end. Every other API function needs the same context and client to do anything. + +Scope the integration branch to exactly that core, not to everything planned for v1. Keeping it to the minimum that "everyone needs" gets a usable release out faster, and lets independent follow-up functions be developed in parallel — by different people or agents — as soon as the core is stable enough to build against, even before it merges. + +## Pattern + +1. Cut one long-lived branch from the default branch for the initial release, named for the outcome, e.g. `build-thing-module`. +2. Open one pull request per function (or small group of related functions) targeting that branch instead of `main`. These PRs can land in parallel — there is no strict order between them, unlike a [stacked pull request](https://msxorg.github.io/docs/Ways-of-Working/Branching-and-Merging/#stacked-pull-requests). +3. Once the load-bearing core is coherent and complete, open the pull request that merges the integration branch into `main`. This becomes the module's first real release (`v1.0.0`). +4. Smaller follow-up features (one more function, a formatter, an alias) can keep targeting the integration branch before it lands, the same way they targeted it during bootstrap. + +## After the core lands + +Once the core has merged as `v1.0.0`, ordinary [SemVer](../Versioning.md) applies: a new function built on the stable core is a **minor** bump, a fix is a **patch** bump, and only a change to the core's own contract (signature, exported class shape, behavior) is a **major** bump. No special versioning exception is needed once the core is in place — the bootstrap phase exists only to get that core to a first release quickly. + +```mermaid +gitGraph + commit id: "main" + branch build-thing-module + checkout build-thing-module + commit id: "load-bearing core" + branch function-a + branch function-b + checkout function-a + commit id: "PR: function A" + checkout function-b + commit id: "PR: function B" + checkout build-thing-module + merge function-a + merge function-b + checkout main + merge build-thing-module id: "v1.0.0" +``` + +`function-a` and `function-b` are independent — both cut from `build-thing-module` and merged back in any order, unlike a stacked pull request where each layer depends on the one before it. + +## Example + +A module bootstrapped this way: + +| PR | Targets | Contents | +| --- | --- | --- | +| #1 | `main` | `build-thing-module` branch, the load-bearing core (e.g. conversion pivot, or context + client + first API function) | +| #2 | `build-thing-module` | a follow-up function built on the core, added before the integration branch merged | + +## When to use this + +- The module has no usable release yet, and the load-bearing core hasn't landed. +- Use this only for the initial bootstrap. Once `main` has a first release, ongoing feature work targets `main` directly with ordinary topic branches, or a [stacked pull request](https://msxorg.github.io/docs/Ways-of-Working/Branching-and-Merging/#stacked-pull-requests) when changes genuinely depend on each other. + +For the general branching and merge model, see [MSX Branching and Merging](https://msxorg.github.io/docs/Ways-of-Working/Branching-and-Merging/). diff --git a/docs/content/Modules/Process-PSModule/module-build-validation.md b/docs/content/Modules/Process-PSModule/module-build-validation.md new file mode 100644 index 00000000..f3db03e5 --- /dev/null +++ b/docs/content/Modules/Process-PSModule/module-build-validation.md @@ -0,0 +1,94 @@ +--- +title: Module Build Validation +description: PSModule-specific validation checks that extend the shared MSXOrg Build/Implement workflow step before a draft pull request is marked ready. +--- + +# Module Build Validation + +Use this page after scaffolding a module change or implementing a function and before you finish self-review or mark a draft pull request ready. It extends the shared MSXOrg [Workflow Build step](https://msxorg.github.io/docs/Ways-of-Working/Workflow/#build) and [Implement guidance](https://msxorg.github.io/docs/Agents/implement/) with the PSModule-specific validation checks that module repositories must pass. + +Do not repeat the shared workflow here. Follow the shared branch → draft PR → implement → test → self-review loop first, then run this PSModule pass to confirm the change still fits the module's design, documentation, and PowerShell standards. + +## Validation sequence + +1. **Module design alignment** + + Confirm that the change fits the module's archetype and the module-specific design rules before reviewing individual files. + + Check: + + - the module still follows the right archetype from [Module types](../Module-Types.md) + - the change respects the layout, private-helper boundaries, context rules, and SOLID guidance in [PowerShell module standard](../Standards.md) + - the function belongs in this module instead of a different module, a shared helper, or a follow-up issue + + A human contributor or agent should be able to explain why this change belongs in this module, in this shape, without inventing new local rules. + +2. **Module documentation** + + Confirm that the repository-level and group-level documentation still matches the delivered behavior. + + Check: + + - `README.md` still answers the start-page questions and reflects any user-visible behavior, prerequisites, or setup changes from [Repository Standard](../Repository-Standard.md#readme-default) + - the relevant public command-group overview page (`src/functions/public//.md`) exists or is updated when the change affects that group's purpose or usage, per [PowerShell module standard](../Standards.md#repository-layout) + - any module-level documentation under `docs/` or other published surfaces is updated when the change adds or changes guidance that should not live only in comment-based help + + If the change teaches the user something new, confirm that the user can discover it from the published documentation surfaces, not only from the diff. + +3. **Function structure** + + Confirm that each changed function still follows the expected repository anatomy. + + Check: + + - public functions live under `src/functions/public//` and private helpers live under `src/functions/private//` + - file placement, grouping, and exported surface match [Module Anatomy](module-anatomy.md) and the layout rules in [PowerShell module standard](../Standards.md#repository-layout) + - there are no nested helper functions, multi-function files, or naming shortcuts that break the "one declaration per file" rule + + This step is about shape, not behavior: the goal is that a reader or tool can find the module surface and its helpers exactly where PSModule expects them. + +4. **Function documentation** + + Confirm that every changed function carries complete comment-based help and that the help matches the implementation contract. + + Check: + + - comment-based help is present for every changed function, including private helpers + - help sections, examples, `.INPUTS`, `.OUTPUTS`, and parameter documentation match the function contract from [MSX PowerShell Functions](https://msxorg.github.io/docs/Coding-Standards/PowerShell/Functions/) + - public-function links and usage examples are current enough that generated documentation will stay accurate + + Do not treat help as optional cleanup. In PSModule repositories, the function help is part of the delivered behavior. + +5. **PowerShell best practices** + + Confirm that the implementation still reads like idiomatic PowerShell after the mechanical checks pass. + + Check: + + - advanced-function structure, parameter typing and validation, `ShouldProcess`, output behavior, and error handling align with [MSX PowerShell](https://msxorg.github.io/docs/Coding-Standards/PowerShell/) and [MSX PowerShell Functions](https://msxorg.github.io/docs/Coding-Standards/PowerShell/Functions/) + - the code also satisfies the PSModule-specific conventions in [PowerShell module standard](../Standards.md), especially around private helpers, context handling, and repository layout + - PSScriptAnalyzer warnings are addressed or intentionally justified, but review does not stop there; also look for awkward parameter design, leaky transport details, non-idiomatic output, or code that technically passes lint but is not good PowerShell + + PSScriptAnalyzer is part of the validation loop, not the whole loop. + +6. **Coding standards alignment** + + Run one final cross-check against the shared MSX coding standards before leaving self-review. + + Check: + + - naming, documentation, error handling, testing expectations, and security posture align with the relevant pages under [MSX Coding Standards](https://msxorg.github.io/docs/Coding-Standards/) + - the change follows the shared "written once, referenced everywhere" rule by linking canonical guidance instead of copying it into local docs or code comments, as described in [Agentic Development](https://msxorg.github.io/docs/Ways-of-Working/Agentic-Development/) + - the draft PR description, issue progress, and any follow-up issues reflect what actually shipped and what still belongs out of scope + + This is the last author-side gate before a PSModule draft PR is ready for independent review. + +## Where this connects + +- [MSX Workflow Build step](https://msxorg.github.io/docs/Ways-of-Working/Workflow/#build) +- [MSX Implement guidance](https://msxorg.github.io/docs/Agents/implement/) +- [PowerShell module standard](../Standards.md) +- [Module types](../Module-Types.md) +- [Module Anatomy](module-anatomy.md) +- [Repository Standard](../Repository-Standard.md) +- [MSX Coding Standards](https://msxorg.github.io/docs/Coding-Standards/) diff --git a/docs/content/Modules/Process-PSModule/repository-structure.md b/docs/content/Modules/Process-PSModule/repository-structure.md new file mode 100644 index 00000000..a69acf2d --- /dev/null +++ b/docs/content/Modules/Process-PSModule/repository-structure.md @@ -0,0 +1,26 @@ +# Process-PSModule Repository Structure + +A module repository in PSModule follows a predictable structure so the framework can build and validate it consistently. + +## Top-level contract + +- `src/`: module source input for build +- `tests/`: Pester tests and test helpers +- `examples/`: usage examples for consumers +- `icon/`: module icon assets +- `.github/workflows/Process-PSModule.yml`: caller workflow that invokes the reusable `PSModule/Process-PSModule/.github/workflows/workflow.yml` +- `.github/PSModule.yml`: repository-level framework settings + +## Source layout overview + +- `src/functions/public/`: exported commands +- `src/functions/private/`: internal helpers +- `src/classes/public/`: user-facing classes +- `src/classes/private/`: internal classes +- `src/data/`: static data files +- `src/init/`: import-time initialization code +- `src/formats/`: formatting views +- `src/types/`: type metadata +- `src/variables/`: variables split by visibility + +Detailed coding standards are canonical in [MSX Coding Standards](https://msxorg.github.io/docs/Coding-Standards/). diff --git a/docs/content/Modules/Process-PSModule/template-quickstart.md b/docs/content/Modules/Process-PSModule/template-quickstart.md new file mode 100644 index 00000000..0a0fad58 --- /dev/null +++ b/docs/content/Modules/Process-PSModule/template-quickstart.md @@ -0,0 +1,23 @@ +# Template Quickstart + +Start new modules from the PSModule template repository: + +- [Template-PSModule](https://github.com/PSModule/Template-PSModule) + +## Quickstart + +1. Create a new repository from the template. +2. Replace placeholder metadata and remove scaffold sample files. +3. Add your first public command and tests. +4. Validate `.github/PSModule.yml` defaults for your module. +5. Open a draft pull request and run the full pipeline. + +If the module needs several interdependent commands before it is usable at all, see [Module Bootstrap](module-bootstrap.md) instead of shipping them as one command per step. + +## Expected outcomes + +- repository follows Process-PSModule structure +- module can be built and tested in CI +- release strategy is ready when functionality is implemented + +For framework-level practices, refer to [MSX Ways of Working](https://msxorg.github.io/docs/Ways-of-Working/). diff --git a/docs/content/Modules/Repository-Standard.md b/docs/content/Modules/Repository-Standard.md new file mode 100644 index 00000000..851d9f63 --- /dev/null +++ b/docs/content/Modules/Repository-Standard.md @@ -0,0 +1,394 @@ +# Repository Standard + +This is the PSModule organization's Repository Standard. It applies to the PSModule organization and is the standard for PowerShell module repositories. It describes what a newly created or maintained module repository should look like before module-specific code, tests, documentation, and managed repository files are considered. + +This standard operates at the same altitude as the [MSX Enterprise Repository Standard](https://msxorg.github.io/docs/Ways-of-Working/Repository-Standard/): MSX sets the enterprise-wide default, and this standard adds to and adjusts that default for PowerShell module repositories. Rules this standard does not change are inherited from the MSX default; where this standard adds or overrides a rule, it governs PowerShell module repositories. + +The implementation standard still lives in [PowerShell module standard](Standards.md). Type-specific conventions for integration (API) and data modules live in [Module types](Module-Types.md). This page covers the repository standard for module repositories: files, metadata, README shape, release integration, placeholder handling, shared community files, and managed-file distribution. + +## Scope + +This standard applies to repositories whose primary artifact is a PowerShell module published through the PSModule framework. + +It does not apply directly to: + +- GitHub Action repositories such as `Build-PSModule`, `Invoke-Pester`, or `Publish-PSModule`. +- Documentation sites published from repositories such as `PSModule/Process-PSModule`. +- Template repositories other than `Template-PSModule`. +- Test, archive, service, or infrastructure repositories that are not published as module artifacts. + +Two baseline expectations still apply to every PSModule repository, including the types listed above. Each repository stands on its own: it carries its own governance and community files instead of relying on the organization `.github` fallback, and each repository ships the [agent onboarding files](#agent-onboarding-files) so an agent can work in it without prior context. What differs by type is the concrete file set and layout: the required files, README shape, and framework wiring on the rest of this page are the module standard, and non-module repositories keep only the equivalent baseline appropriate to their own type. This documentation project, maintained in `PSModule/Process-PSModule`, follows those two baseline expectations itself. + +Each initiative should keep its own repository standards in its central documentation repository. For the PSModule organization, this repository is the source of truth. + +## Repository creation + +Create new module repositories from [`PSModule/Template-PSModule`](https://github.com/PSModule/Template-PSModule). The template provides the framework wiring, starter layout, and CI/CD expectations. + +After creating the repository: + +1. Replace template tokens such as `{{ NAME }}` and `{{ DESCRIPTION }}`. +2. Remove scaffold functions, tests, and examples that do not represent the module. +3. Set repository metadata and custom properties. +4. Confirm the README answers the start-page questions and uses `Install-PSResource` for installation. +5. Confirm required common files are present. +6. Confirm `.github/PSModule.yml` only overrides defaults when the module needs different behavior. + +## Required repository metadata + +Each module repository should have: + +- A concise GitHub repository description that starts with or clearly says `A PowerShell module ...`. +- `Type: Module` as the repository custom property. +- Topics that help users find the module, when relevant. +- Branch protection and workflow requirements inherited from organization defaults. +- `main` as the default branch unless there is a documented legacy reason. + +The repository description is used as a short landing-page summary in documentation and automation. Keep it user-facing and avoid implementation details. + +### Organization custom properties + +Custom properties are defined once for the whole organization and set per repository. The organization schema is the source of truth; read it with `gh api /orgs/PSModule/properties/schema` before automating against a property, and update this page when the schema changes. + +| Property | Value type | Required | Module repository expectation | +| --- | --- | --- | --- | +| `Type` | Single select: `Action`, `Archive`, `Docs`, `Framework`, `FunctionApp`, `Memory`, `Module`, `Other`, `Template`, `Workflow` | Yes, organization default `Other` | `Module`. Set it explicitly after repository creation; a new repository otherwise inherits `Other`. `Template-PSModule` itself is `Template`. | +| `SubscribeTo` | Multi select: `Custom Instructions`, `Prompts`, `Hooks`, `CODEOWNERS`, `dependabot.yml`, `PSModule Settings`, `Linter Settings`, `gitattributes`, `gitignore`, `License` | No | Opt-in for [managed file distribution](#managed-file-distribution). Select the file types the distribution runtime should own in this repository; leave a type unselected to keep a repository-local version. | +| `Description` | String | No | Optional machine-readable description for automation that needs it independently of the GitHub repository description. | +| `Archive` | True/false | No | Set to `true` only when the repository is no longer maintained. | +| `Upstream` | URL | No | Set when the module wraps, mirrors, or is generated from an upstream project. | + +Automation and repository search select module repositories with the `props.Type:Module` qualifier, for example `gh search repos --owner PSModule 'props.Type:Module'`. + +## Default branch and worktrees + +Use `main` for active module repositories. Legacy repositories that still use `master` should not be used as examples for new work. + +Local work should use the organization worktree convention: + +- The bare repository stays at the repository root. +- `main/` tracks the default branch. +- Feature worktrees use `-` directories and `/` branches. + +For branch and worktree details, see [Git Worktrees](https://msxorg.github.io/docs/Ways-of-Working/Git-Worktrees/). + +## Default repository layout + +Module repositories use the PSModule framework layout: + +| Path | Default purpose | +| ---- | --------------- | +| `README.md` | Concise start page for the module. | +| `LICENSE` | Repository license. PSModule module repositories default to MIT unless a different license is explicitly decided. | +| `CONTRIBUTING.md` | Self-contained contribution workflow for this repository. Does not rely on an organization-level fallback. | +| `SECURITY.md` | Security support policy and private vulnerability reporting instructions. | +| `SUPPORT.md` | Support expectations and where users ask for help. | +| `CODE_OF_CONDUCT.md` | Community conduct expectations. | +| `AGENTS.md` | Agent onboarding entry point. Points agents to the canonical guidance at `https://psmodule.io/docs/`. | +| `CLAUDE.md` | Claude Code entry point. Imports `AGENTS.md` so Claude reads the same instructions. | +| `.github/PSModule.yml` | Module workflow configuration overrides. | +| `.github/workflows/Process-PSModule.yml` | Caller workflow that runs the module's CI/CD by calling the shared Process-PSModule workflow. | +| `.github/release.yml` | Release-note and changelog categorization for GitHub releases. | +| `.github/linters/` | Linter configuration used by the framework's linting stage, including `.markdown-lint.yml` and `.powershell-psscriptanalyzer.psd1`. | +| `.github/dependabot.yml` | Configures ecosystem-appropriate dependency-update pull requests. For PowerShell module repositories the `github-actions` ecosystem is expected; add any other ecosystems the module actually develops in. | +| `.github/CODEOWNERS` | Ownership routing for reviews and protected areas. | +| `.github/pull_request_template.md` | Scaffolds pull requests in the MSX PR Format (PR Manager) style — an icon + change-type + user-facing-outcome title, user-facing description sections, an optional technical-details block, and a related-issues block. | +| `.gitattributes` | Normalizes line endings and declares text/binary handling so the module can be developed and built consistently on Linux, macOS, and Windows. | +| `.gitignore` | Ignores files that must never be committed, tailored to the PowerShell-module ecosystem: operating-system files, editor and developer-tooling files, PowerShell and Pester test-harness artifacts, and all local build outputs and files created during build and test. | +| `src/` | Module source compiled into the shipped artifact. | +| `src/functions/public/` | Exported commands, grouped by domain. | +| `src/functions/private/` | Internal helper commands, grouped by domain. | +| `src/classes/public/` | Public classes that are part of the user-facing model. | +| `src/classes/private/` | Internal implementation classes. | +| `src/data/` | Static module data that ships with the module. | +| `examples/` | Realistic user scenarios, not copies of command help. | +| `docs/` | Product documentation source when the module needs documentation beyond generated command help. | +| `tests/` | Pester tests and test data. | +| `icon/` | Module icon assets. | + +Detailed source layout rules live in [PowerShell module standard](Standards.md#repository-layout). + +### Caller workflow and reusable workflow + +The module repository owns a caller workflow; the framework owns the reusable workflow it calls. These are two separate files in two separate repositories: + +| Role | Repository | File | +| --- | --- | --- | +| Caller workflow | The module repository | `.github/workflows/Process-PSModule.yml` | +| Reusable workflow | [`PSModule/Process-PSModule`](https://github.com/PSModule/Process-PSModule) | `.github/workflows/workflow.yml` | + +The caller workflow declares the triggers, concurrency, and permissions for the module repository, and delegates the work: + +```yaml +jobs: + Process-PSModule: + uses: PSModule/Process-PSModule/.github/workflows/workflow.yml@ # + secrets: + APIKey: ${{ secrets.APIKEY }} +``` + +Name the caller file `Process-PSModule.yml`, matching [`PSModule/Template-PSModule`](https://github.com/PSModule/Template-PSModule) and every existing module repository. `workflow.yml` is the reusable workflow's own filename inside `PSModule/Process-PSModule` and belongs only in the `uses:` reference. Pin the reference to a commit SHA with the version tag in a trailing comment so Dependabot can update it. + +## Required common files + +Every module repository must carry the same baseline community, governance, and automation files. GitHub's organization-level `.github` community-file fallback is useful for display defaults, but it is not enough as the long-term PSModule standard because: + +- agents and humans need the files in the repository they are changing, not only inherited through GitHub UI behavior; +- tools such as Dependabot and CODEOWNERS read repository-local files — as do linters and release automation when the module uses those linters or generates releases; +- reviews need diffs against the actual managed file in the target repository; +- repository-local files make the standard portable to other initiatives such as MSXOrg, where each initiative should define its own standards and managed files; +- central fallback files in `PSModule/.github` do not provide a reliable enforcement or update workflow across all repositories. + +Required baseline files for module repositories: + +| File | Why it is required | +| ---- | ------------------ | +| `README.md` | Repository landing page and evergreen context for humans and agents. | +| `LICENSE` | Clear legal terms for reuse, packaging, and redistribution. | +| `CONTRIBUTING.md` | Self-contained contribution workflow and expectations for this repository. | +| `SECURITY.md` | Private vulnerability reporting and latest-version support policy. | +| `SUPPORT.md` | Support channel and issue-routing expectations. | +| `CODE_OF_CONDUCT.md` | Community participation rules. | +| `AGENTS.md` | Cross-tool agent instructions pointing to the canonical guidance at `https://psmodule.io/docs/`. | +| `CLAUDE.md` | Claude Code entry point that imports `AGENTS.md`. | +| `.github/dependabot.yml` | Configures ecosystem-appropriate dependency-update pull requests. For PowerShell module repositories the `github-actions` ecosystem is expected; add any other ecosystems the module actually develops in. | +| `.github/CODEOWNERS` | Review routing for source, docs, and GitHub workflow files. | +| `.github/pull_request_template.md` | Scaffolds pull requests in the MSX PR Format (PR Manager) style — an icon + change-type + user-facing-outcome title, user-facing description sections, an optional technical-details block, and a related-issues block. | +| `.github/PSModule.yml` | Module workflow defaults and overrides. | +| `.gitattributes` | Normalizes line endings and declares text/binary handling so the module can be developed and built consistently on Linux, macOS, and Windows. | +| `.gitignore` | Ignores files that must never be committed, tailored to the PowerShell-module ecosystem: operating-system files, editor and developer-tooling files, PowerShell and Pester test-harness artifacts, and all local build outputs and files created during build and test. | + +Repositories can add local files, but they should not remove these baseline files unless the repository is explicitly outside the module standard. + +Each repository must stand on its own. It carries its own copy of every file above and does not depend on the organization `.github` fallback: that fallback is only surfaced in GitHub's web UI, and agents, linters, and local tooling do not read it. + +## Agent onboarding files + +Every repository must be usable by an agent that has never seen it before, without special configuration. Each repository carries its own agent entry points that point to the authoritative documentation instead of restating it: + +- `AGENTS.md`: the cross-tool entry point, read by the GitHub Copilot coding agent, VS Code, and other AGENTS.md-aware tools. It names what the repository is in a line or two and points to the canonical agent guidance at [psmodule.io/docs](https://psmodule.io/docs/). +- `CLAUDE.md`: a thin file that imports `AGENTS.md` with `@AGENTS.md` so Claude Code reads the same instructions. Claude-specific notes, if any, go below the import. + +See [PSModule/Template-PSModule](https://github.com/PSModule/Template-PSModule) for a concrete implementation example of `AGENTS.md` and `CLAUDE.md`. + +`AGENTS.md` and `CLAUDE.md` are the required set. `AGENTS.md` is the entry point that AGENTS.md-aware runtimes read directly, so a repository is usable by an agent without a per-runtime copy of the same pointer. + +Runtime-specific adapter files such as `.github/copilot-instructions.md` and `.github/instructions/*.instructions.md` are optional. MSX treats them as client adapters that *may* add runtime-specific loading or path rules, described in [Agentic Development](https://msxorg.github.io/docs/Ways-of-Working/Agentic-Development/) and its [capability specification](https://msxorg.github.io/docs/Capabilities/agentic-development/spec/). Add one when a runtime needs loading or path rules that `AGENTS.md` cannot express, and keep it pointing at `AGENTS.md` rather than restating it. `Template-PSModule` ships without one. + +These files are the agent equivalent of the README: pointers, not copies. Keep them short so the linked documentation stays the single source of truth. Like the other governance files, they live in the repository itself so it can stand on its own. + +## Managed file distribution + +**Policy ownership and distribution runtime are separate concerns.** This page — and this documentation project in [`PSModule/Process-PSModule`](https://github.com/PSModule/Process-PSModule) — defines *what* files must exist in module repositories and *what standards they must meet*. The distribution runtime is handled by [`MSXOrg/Custo`](https://github.com/MSXOrg/Custo). + +For PSModule module repositories, the requirements are: + +- Repositories must contain the required baseline files defined on this page. +- Managed copies of those files are treated as generated distribution artifacts, not repository-specific source. +- Standard changes to managed-file content are made in the distribution engine, not by patching generated copies in receiving repositories. + +### Migration for existing repositories + +Repositories still aligned to `PSModule/Distributor` (the previous distribution engine) should migrate to Custo as the runtime implementation. + +This page defines the required target state (the file requirements). Runtime migration mechanics and rollout design (repository targeting, update flow, overwrite behavior, and deletion behavior) are owned by Custo and should be documented and executed from the Custo repository. + +## Supply-chain defaults + +Every module repository must include `.github/dependabot.yml`. Dependabot is part of the repository supply-chain control, not an optional convenience. + +Configure the `github-actions` ecosystem. It keeps the pinned actions current, including the pinned `PSModule/Process-PSModule` reference in the [caller workflow](#caller-workflow-and-reusable-workflow). This is what [`PSModule/Template-PSModule`](https://github.com/PSModule/Template-PSModule) ships, and it is the default for new repositories: + +```yaml +version: 2 +updates: + - package-ecosystem: github-actions + directory: / + labels: + - dependencies + - github-actions + schedule: + interval: daily + cooldown: + default-days: 7 +``` + +Add `nuget` when the module ships or builds against .NET dependencies, as [`PSModule/Sodium`](https://github.com/PSModule/Sodium) does: + +```yaml + - package-ecosystem: nuget + directory: / + labels: + - dependencies + - .NET + schedule: + interval: weekly +``` + +Repositories with other package ecosystems add them explicitly rather than replacing the `github-actions` entry. Older repositories still use a weekly interval without a cooldown; align them with the template default when the file is touched anyway. + +Dependabot PRs still go through normal review. Automated dependency updates are not a substitute for reviewing release notes, changed permissions, pinned SHAs, or generated lockfiles. + +### PowerShell dependencies + +Dependabot's valid `package-ecosystem` values are enumerated in its configuration parser ([`common/lib/dependabot/config/file.rb`](https://github.com/dependabot/dependabot-core/blob/main/common/lib/dependabot/config/file.rb)) and listed in the [Dependabot options reference](https://docs.github.com/en/code-security/reference/supply-chain-security/dependabot-options-reference#package-ecosystem). Configure only values from that list: `powershell` is not among them, and an unsupported value makes `.github/dependabot.yml` invalid, which puts the repository's whole Dependabot configuration at risk, including the `github-actions` entry that does work. + +PowerShell module dependencies are therefore declared with `#Requires -Modules` in the function files that use them, as described in [PowerShell module standard](Standards.md), and the build collects them into the compiled manifest. Keeping those declarations current is a review responsibility. + +A PowerShell ecosystem is proposed in [dependabot/dependabot-core#15501](https://github.com/dependabot/dependabot-core/issues/15501) and implemented in [dependabot/dependabot-core#15666](https://github.com/dependabot/dependabot-core/pull/15666), covering PowerShell's native declarations — `#Requires -Modules` in `.ps1` and `.psm1` files, and `RequiredModules` in a `.psd1` manifest — resolved against the PowerShell Gallery. Adopt it once it ships and `powershell` appears in the options reference, updating this section and the `dependabot.yml` that `Template-PSModule` distributes together. + +## README default + +A module README is a start page, not the command reference or full manual. It brings a reader in, answers the first questions, and sends them to the right documentation surface. + +Making the README shorter must not delete unique information. The README is published as the module's landing page on the documentation site (for example `psmodule.io/`); the per-command reference is generated separately from comment-based help. So the README is often the only published home for prerequisites, platform and dependency notes, authentication and setup guidance, operational behavior such as caching, state, or update and versioning semantics, and upstream attribution. Trimming the README must preserve that content: keep it on the landing page, or move it only to another surface that also publishes (a command group's overview page under `src/functions/public//.md`, comment-based help, or published documentation in `docs/`). Only remove content that is genuinely duplicated by the generated command reference. + +The README answers these questions, in this order: + +| Question | Module README responsibility | +| --- | --- | +| What is it? | Name the module and define its scope in one short paragraph. | +| Why should I care? | State the value or kind of task the module makes easier. | +| How do I get it? | Show the PowerShell Gallery install and import commands. | +| How does it work? | Show one to three representative capabilities or usage examples. | +| How do I get more info? | Link to generated module documentation and PowerShell help. | + +Module installation examples must use PSResourceGet: + +```powershell +Install-PSResource -Name +``` + +Do not use `Install-Module` in new module repository documentation. `Install-Module` belongs only in legacy/historical context where changing it would misrepresent the referenced system. + +For implemented modules, use this shape: + +````markdown +# + + + +## Installation + +Install the module from the PowerShell Gallery: + +```powershell +Install-PSResource -Name +Import-Module -Name +``` + +## Capabilities + +Use this section as a short showcase and introduction to how the module works. Show the most important things the module makes possible with one to three realistic examples. + +The goal is discovery and marketing, not exhaustive command documentation. A reader should understand why the module exists and what kind of tasks it helps with. + +```powershell +# Replace this with a real example that demonstrates the module's value. +Get-Command -Module +``` + +## Documentation + +Documentation is published at [psmodule.io/](https://psmodule.io//). + +Use PowerShell help and command discovery for module details: + +```powershell +Get-Command -Module +Get-Help -Name -Examples +``` +```` + +In the documentation examples, replace `` with a real command exported by the module, for example `Get-Help -Name Get-GitHubRepository -Examples`, so the snippet runs as written. Do not ship placeholder tokens such as `'CommandName'` or `` as if they were runnable commands. + +Implemented modules must include the capabilities or usage showcase before the documentation link. Keep it focused on discovery: show one to three representative outcomes, not every command, parameter, or edge case. A landing page with only an installation snippet and a documentation link is not enough for a module that has working commands. + +Keep, trim, or relocate content — do not delete it: + +- **Keep on the landing page:** the overview, prerequisites and requirements (PowerShell version, supported platforms, module or native dependencies), installation, the capabilities showcase, and the short operational notes a reader needs before first use. +- **Trim:** exhaustive command inventories, parameter tables, and repetitive examples that differ only by a parameter. These come from comment-based help — point to `Get-Help` and the documentation site instead of restating them. +- **Relocate only to a published home — never drop:** long-form guides and unique conceptual content (authentication and setup walkthroughs, deep operational detail, end-to-end scenarios) may move out of the README only into a surface that is actually published: a command group's overview page under `src/functions/public//.md`, comment-based help, or published documentation in `docs/` or `examples/`. Only relocate to unpublished areas if there is no published home for it yet; keep the full content in the README to ensure it reaches users. A longer landing page is acceptable and expected for feature-rich modules; do not shorten by deleting. + +Retain upstream attribution and licensing context. Credit, acknowledgements, donation notes, and third-party license notices for wrapped or bundled work must stay in the README, or move to a clearly linked place. The rule below about community and policy sections does not apply to attribution the project is expected to carry. + +README pages should not duplicate generated command documentation. Do not add full command inventories, parameter tables, or long reference sections when those details are already produced from comment-based help. + +Do not add a community-file or policy link section by default. Readers can find standard repository files such as `LICENSE`, `CONTRIBUTING.md`, `SECURITY.md`, and `CODE_OF_CONDUCT.md` through GitHub conventions and the repository file tree. Link them only when the module has an unusual rule the user must know before using it, or when it carries required upstream attribution. + +## Placeholder and in-progress repositories + +If a repository is reserved for a future module or still contains scaffold code, say that directly. Do not leave `{{ NAME }}`, `{{ DESCRIPTION }}`, `PSModuleTemplate`, `Greet-Entity`, or similar template examples in the README. + +Use this shape for placeholder repositories: + +````markdown +# + + + +## Status + +This repository is currently a placeholder. The module source still contains scaffold code, so there are no supported commands or usage examples to document yet. + +## Documentation + +When this module is implemented, command details should live in PowerShell help and generated documentation rather than being duplicated in this README. +```` + +Use the same pattern for in-progress modules with stub commands, but name the stub honestly: + +```markdown +This repository is currently in progress. The current `` command is a stub and throws `NotImplementedException`, so there are no supported conversion commands or usage examples to document yet. +``` + +## README validation + +Before opening a README-only PR, check that the README follows the default and does not contain leftover scaffolding: + +```powershell +Select-String -Path README.md -SimpleMatch -Pattern 'Greet-Entity', 'PSModuleTemplate', 'YourModuleName' +Select-String -Path README.md -SimpleMatch -Pattern '{{ NAME }}', '{{ DESCRIPTION }}' +Select-String -Path README.md -SimpleMatch -Pattern '', '', "-Name 'CommandName'" +Select-String -Path README.md -Pattern '^## (Commands|Capabilities)$' +Select-String -Path README.md -Pattern '^Install-Module\b' +git diff --check -- README.md +``` + +`Template-PSModule` is the exception: it intentionally keeps `{{ NAME }}` and `{{ DESCRIPTION }}` tokens because those are template inputs. + +For an implemented module, also confirm the README keeps a capabilities or usage showcase and that any unique content removed from the previous version — prerequisites, setup or authentication guidance, operational notes, or upstream attribution — was relocated to `docs/`, `examples/`, or comment-based help rather than deleted. + +## Documentation ownership + +Command details belong in comment-based help and generated documentation. The README can showcase capability, then points to those sources for reference detail. + +Use these defaults: + +- Command synopsis, parameters, examples, links, and outputs live in comment-based help. +- Group overview pages live next to public command groups in `src/functions/public//.md`. +- Realistic end-to-end scenarios live in `examples/`. +- Product docs beyond generated command help live under `docs/` and publish through GitHub Pages or the initiative's module documentation site. +- README capability examples are short, representative, and user-facing. +- README pages stay focused and stable, and keep the narrative content that has no other published home. + +This keeps the repository landing page readable and prevents drift between README content, PowerShell help, and generated documentation. + +## Release and PR defaults + +Module repositories use the Process-PSModule workflow. Version and release behavior is driven by PR labels and workflow settings. + +Default expectations: + +- `Major`, `Minor`, `Patch`, and `Prerelease` labels determine release behavior. +- Documentation-only README standardization PRs use the `Docs`/`NoRelease` behavior when available. +- Source changes under `src/` are module-impacting and should trigger the full module workflow. +- README and documentation changes should update the site without pretending to be module API changes. + +See [Versioning](Versioning.md) for semantic version rules and [PowerShell module standard](Standards.md#cicd-pipeline) for the Process-PSModule pipeline. + +## Template maintenance + +`Template-PSModule` defines the default README shape and starter repository contract. When this page changes a default, update `Template-PSModule` in the same work item when practical. + +The template README may contain tokens, but generated module repositories should not keep them after the initial setup commit. diff --git a/docs/content/Modules/Standards.md b/docs/content/Modules/Standards.md new file mode 100644 index 00000000..0124b4e7 --- /dev/null +++ b/docs/content/Modules/Standards.md @@ -0,0 +1,368 @@ +# PowerShell module standard + +Standards for implementing and reviewing PowerShell modules in the PSModule organization. These rules apply to modules built with the [PSModule framework](https://github.com/PSModule/Process-PSModule). + +For general PowerShell coding standards (naming, style, function structure, documentation, readability, error handling), see [PowerShell Standards](../PowerShell/Standard/index.md). This page covers only module-specific conventions. + +## Supported PowerShell version + +PSModule modules support only the latest PowerShell Long Term Support (LTS) release. Do not preserve compatibility with Windows PowerShell 5.1; use language and platform capabilities available in the latest LTS when they improve the implementation. + +> **Repo-local config wins.** Repo-level `.github/linters/.powershell-psscriptanalyzer.psd1` and `.github/PSModule.yml` override anything below. This standard fills the gap. + +## Repository layout + +The framework treats `src/` as the source for the compiled module. Place code in the folder that matches its responsibility. + +| Folder or file | Purpose | Do not put here | +| --------------------------------------------- | ------------------------------------------------------------------------- | -------------------------------------------------------- | +| `src/header.ps1` | Single comment block at the top of the compiled module | Runtime code | +| `src/manifest.psd1` | Intentional manifest overrides (e.g., `Author`) | Generated values (functions, types, version, GUID, etc.) | +| `src/data/*.psd1` | Static read-only configuration | Mutable state, secrets, computed values | +| `src/init/*.ps1` | Code that runs once at import (module-scope init, completer registration) | Per-call logic, network calls, slow work | +| `src/classes/private/*.ps1` | Internal classes | Public pipeline output types | +| `src/classes/public/*.ps1` | Classes returned to users or accepted as parameter types | Transport wrappers with no user-facing model | +| `src/enums/*.ps1` | Enums for class properties, parameters, validation | Configuration constants | +| `src/types/.Types.ps1xml` | Type metadata — aliases, script properties, member sets | Display views or duplicated class properties | +| `src/formats/.Format.ps1xml` | Default table / list / wide display views | Behavior, business logic, type aliases | +| `src/functions/private//Verb-Noun.ps1` | One private helper per file, grouped by domain | Public aliases, pipeline input, context defaulting | +| `src/functions/public//Verb-Noun.ps1` | One exported command per file, grouped by resource or domain | Raw transport code, multiple commands per file | +| `src/assemblies/` | Bundled binaries copied into the build artifact | Source-compiled code | +| `src/modules/` | Nested modules packaged with the compiled output | Functions that belong in `src/functions/` | +| `src/variables/private/*.ps1` | Module-scoped variables not exported | Public API values | +| `src/variables/public/*.ps1` | Variables exported and documented | Computed or mutable state | +| `src/scripts/*.ps1` | Scripts loaded via the manifest into caller scope | Normal module-internal code | +| `src/finally.ps1` | End-of-module wiring (removal handlers, etc.) | Per-call logic | +| `examples/*.ps1` | Realistic user scenarios | Substitutes for comment-based help examples | +| `icon/icon.png` | Module icon linked from manifest and documentation | Source code or configuration | +| `tests/` | Pester tests and test data | Generated test results or build output | +| `tests/BeforeAll.ps1` | Shared setup script executed once before the test matrix | Per-test setup | +| `tests/AfterAll.ps1` | Shared teardown script executed once after the test matrix | Per-test teardown | +| `.github/PSModule.yml` | Workflow configuration (build, test, publish, linter settings) | Source code or runtime config | +| `tools/*.ps1` | Build-time helper scripts (numbered for execution order) | Runtime module code | + +Layout rules: + +- **One declaration per file.** Filename matches the declared symbol exactly, including casing. +- **Group by domain.** Use resource or behaviour groups — not verb folders, not endpoint paths. +- **Mirror public and private domains.** Public command under `src/functions/public/Projects/` has related private helpers under `src/functions/private/Projects/`. +- **No nested functions.** Do not define helper functions inside other functions. Extract helper logic to a separate function file and decide whether it belongs in `src/functions/private//` (internal-only) or `src/functions/public//` (part of the module API). +- **Declare dependencies where they are used.** Use `#Requires -Modules ` at the top of each function file that needs an external module. Do not add `RequiredModules` to `src/manifest.psd1` — the build collects all `#Requires` declarations automatically and writes them into the compiled manifest. Entries in `src/manifest.psd1` are silently ignored for this purpose. +- **Group documentation pages with source.** Place a `.md` file alongside the function files in each `src/functions/public//` folder to provide a category overview in generated documentation. + +## Module naming + +- Prefix public nouns with the module's term of art: `ContosoProject`, not `Project`. +- Public aliases only when they preserve compatibility or bridge terminology. Private functions and parameters have no aliases. + +## Private functions + +- Mandatory context parameter (e.g., `[GitHubContext] $Context`) or equivalent for the module. +- No aliases. +- No pipeline input. +- No defaulting from context — public callers resolve before calling. +- Required inputs declared as mandatory. + +## SOLID applied + +- **Single Responsibility.** One file, one function. One function, one user-visible behaviour. If the synopsis needs "and", split it. +- **Open/Closed.** Extend via parameter sets, private helpers, or new files — not boolean switches that fork large blocks. +- **Liskov.** A derived class must work wherever the base is expected. +- **Interface Segregation.** Narrow, intent-named parameter sets. +- **Dependency Inversion.** Public functions depend on resolved inputs and local abstractions. Private helpers own the concrete REST / GraphQL / filesystem / process calls. + +## Parameter design + +### Smart defaults + +Every parameter that has a natural default should declare one. The most common use case must work with the fewest parameters possible. Reserve `Mandatory` for values the command genuinely cannot infer or derive. + +- Prefer `= 'main'` over making `-Branch` mandatory in commands that act on branches. +- Prefer sensible page sizes, counts, or ranges over requiring callers to specify them. +- Prefer `= $false` for switches rather than leaving the behaviour implicit. + +This is a usability expression of the **Open/Closed** principle: the common path is open by default; callers that need different behaviour extend it by supplying alternatives — they are never forced to know the default just to get started. + +### Object-first parameters + +Parameters represent attributes of a real-world entity. Design them to reflect that structure. + +- **Prefer typed objects.** If a concept has a class — `[GitHubRepo]`, `[GitHubContext]`, `[ContosoProject]` — accept the object rather than its constituent fields. The caller can pass the whole thing; the implementation unpacks what it needs. +- **Group flat parameters by their conceptual object.** When a typed object is not available, prefix each parameter with the name of the entity it belongs to: `$ServerHost`, `$ServerPort`, `$ServerTls` rather than `$Host`, `$Port`, `$Tls`. The prefix makes the grouping explicit, prevents collisions with unrelated parameters, and avoids shadowing PowerShell built-in variables (`$Host`). +- **Offer both modes when callers need both.** A parameter set that accepts an object and a parallel parameter set that accepts its individual attributes are equally valid; use both when that genuinely serves users. + +This mirrors **Interface Segregation**: each parameter set represents one coherent input contract — not a flat list of loosely related knobs — so callers only touch what they need. + +> Note: the rule "avoid repeating the noun" (e.g. `-ID` not `-ProjectID` in `Get-ContosoProject`) applies to a command's *own* noun. The object-prefix convention above applies when a single command spans *multiple* conceptual objects (`$ServerHost` vs `$ClientID`) — the prefix is the object name, not the command noun, so there is no conflict. + +## DRY with judgment + +Extract a helper after the same non-trivial logic appears in three or more places, or when duplicated logic is load-bearing. Don't create a helper for a single caller. + +## Principles + +### Colocation of concerns + +Keep related things together so the connection between code and its context is visible at a glance: + +- `#Requires -Modules` belongs in the function files that use the dependency, not in a central manifest. +- Parameter descriptions are `#` comments directly above each parameter in `param()`, not `.PARAMETER` blocks. +- Tests follow the same domain boundaries as public source: cover every public command under `tests/`, and align grouped suites with `src/functions/public//`. + +### Linear versioning + +The release process treats each merged PR as a release on a single linear ancestry. There is no patching of older versions — security fixes go on the current tip of `main` only. + +### Release and feature branches + +For large work, open a release branch and target it from feature branches. Apply the `Prerelease` label on the release branch PR to publish preview versions before the final merge to `main`. + +## CI/CD pipeline + +The [Process-PSModule](https://github.com/PSModule/Process-PSModule) workflow orchestrates the full lifecycle. Every PR triggers a **Plan** job that resolves configuration and version, then conditionally runs build, test, lint, and publish stages. + +### Pipeline stages + +```mermaid +graph LR + Plan --> Lint-Repository + Plan --> Build-Module + Plan --> Test-SourceCode + Plan --> Lint-SourceCode + Build-Module --> Test-Module + Build-Module --> BeforeAll-ModuleLocal + BeforeAll-ModuleLocal --> Test-ModuleLocal + Test-ModuleLocal --> AfterAll-ModuleLocal + Test-SourceCode --> Get-TestResults + Test-Module --> Get-TestResults + Test-ModuleLocal --> Get-TestResults + Test-Module --> Get-CodeCoverage + Test-ModuleLocal --> Get-CodeCoverage + Get-TestResults --> Publish-Module + Get-CodeCoverage --> Publish-Module + Build-Module --> Build-Docs + Build-Docs --> Build-Site + Build-Site --> Publish-Site +``` + +| Stage | Runs on | Purpose | +| ----- | ------- | ------- | +| **Plan** | All events | Loads `.github/PSModule.yml`, resolves version from PR labels, produces the Settings JSON | +| **Lint-Repository** | Open/Updated PR | Runs super-linter on the full repo (Markdown, YAML, etc.) | +| **Lint-SourceCode** | Open/Updated PR, Merged PR, Manual | Runs PSScriptAnalyzer against `src/` | +| **Build-Module** | Open/Updated PR, Merged PR, Manual | Compiles source into a versioned module artifact | +| **Test-SourceCode** | Open/Updated PR, Merged PR, Manual | Framework tests on raw source files | +| **Test-Module** | Open/Updated PR, Merged PR, Manual | Pester tests against the built module artifact | +| **BeforeAll-ModuleLocal** | Open/Updated PR, Merged PR, Manual | Runs `tests/BeforeAll.ps1` once before the local test matrix | +| **Test-ModuleLocal** | Open/Updated PR, Merged PR, Manual | Pester tests with the module installed locally (cross-OS matrix) | +| **AfterAll-ModuleLocal** | Always (if tests started) | Runs `tests/AfterAll.ps1` for cleanup | +| **Get-TestResults** | Always (if Plan succeeded) | Aggregates and reports test results | +| **Get-CodeCoverage** | Always (if Plan succeeded) | Calculates and reports code coverage | +| **Publish-Module** | Merged PR (or Prerelease label) | Publishes to PowerShell Gallery and creates a GitHub Release | +| **Build-Docs / Build-Site** | Open/Updated PR, Merged PR, Manual | Generates documentation site from source | +| **Publish-Site** | Merged PR | Deploys documentation site to GitHub Pages | + +### Important file patterns + +The workflow only triggers build, test, and publish stages when changed files match the `ImportantFilePatterns` setting. The default patterns are: + +```text +^src/ +^README\.md$ +``` + +Changes that do not match any pattern result in `ReleaseType: None` — the pipeline skips build, test, and publish entirely. Override in `.github/PSModule.yml`: + +```yaml +ImportantFilePatterns: + - '^src/' + - '^README\.md$' + - '^\.github/workflows/' +``` + +### Version resolution + +The **Plan** job resolves the next version before any build occurs. This means the tested artifact carries the exact version that will be published — no re-stamping happens at publish time. + +**Flow:** + +1. `Get-PSModuleSettings` loads `.github/PSModule.yml` and determines `ReleaseType` from PR labels +2. `Resolve-PSModuleVersion` calculates the next semantic version from the latest Git tag +3. `Build-PSModule` stamps the resolved version into the compiled manifest +4. `Publish-PSModule` reads the version from the manifest (read-only) and publishes + +**PR label to version bump mapping:** + +| Labels (configurable) | Bump type | Default label values | +| --------------------- | --------- | -------------------- | +| Major | Major (`X.0.0`) | `major`, `breaking` | +| Minor | Minor (`x.Y.0`) | `minor`, `feature` | +| Patch | Patch (`x.y.Z`) | `patch`, `fix` | +| Ignore | No release | `NoRelease` | +| None of the above | Patch (when `AutoPatching: true`) | — | + +**Prerelease versions:** Adding a `Prerelease` label to the PR produces a prerelease tag (e.g., `1.2.3-preview0001`). The format is controlled by `IncrementalPrerelease` (sequential numbering) or `DatePrereleaseFormat` (.NET DateTime format string). + +**Tag format:** Releases are tagged with a configurable prefix (default `v`) — e.g., `v1.2.3`. + +### Configuration (`.github/PSModule.yml`) + +All settings have sensible defaults. An empty or missing file uses the defaults below: + +```yaml +Name: null # Defaults to the repository name + +ImportantFilePatterns: + - '^src/' + - '^README\.md$' + +Build: + Skip: false + Module: + Skip: false + Docs: + Skip: false + Site: + Skip: false + +Test: + Skip: false + Linux: + Skip: false + MacOS: + Skip: false + Windows: + Skip: false + SourceCode: + Skip: false + PSModule: + Skip: false + Module: + Skip: false + TestResults: + Skip: false + CodeCoverage: + Skip: false + PercentTarget: 0 + +Publish: + Module: + Skip: false + AutoCleanup: true # Delete prerelease tags after stable release + AutoPatching: true # Unlabeled PRs default to patch bump + IncrementalPrerelease: true # Sequential prerelease numbering + DatePrereleaseFormat: '' # Alternative: .NET DateTime format for prerelease + VersionPrefix: 'v' # Git tag prefix + MajorLabels: 'major, breaking' + MinorLabels: 'minor, feature' + PatchLabels: 'patch, fix' + IgnoreLabels: 'NoRelease' + UsePRTitleAsReleaseName: false + UsePRBodyAsReleaseNotes: true + UsePRTitleAsNotesHeading: true + +Linter: + Skip: false + env: {} # Additional env vars passed to super-linter +``` + +### Publishing + +The `Publish-Module` stage: + +1. Downloads the pre-built module artifact (identical to what was tested) +2. Reads the version from the compiled manifest (no recalculation) +3. Publishes to the PowerShell Gallery +4. Creates a GitHub Release with the module attached as a ZIP artifact +5. Comments on the PR with links to the Gallery package and GitHub Release +6. Cleans up old prerelease tags when publishing a stable release (if `AutoCleanup: true`) + +The publish step only runs when: + +- All tests and code coverage pass (or are skipped) +- The PR is merged to the default branch (stable release), or +- The PR carries the `Prerelease` label (prerelease from the feature/release branch) + +On abandoned (closed without merge) PRs, the pipeline cleans up any prerelease tags created for that branch. + +## Tests + +Keep Pester tests under `tests/`. Every public command's behavior must be covered, but commands do not require separate test files. The framework's `FunctionTest` check confirms that every public command is referenced somewhere under `tests/`, so a grouped suite satisfies the structural check; the suite must still assert the command's behavior. + +The profiles below are repository layout conventions, not Process-PSModule configuration modes. Use the simplest profile that keeps the suite readable: + +| Profile | Test layout | +| ------- | ----------- | +| **Simple** | One root `tests/.Tests.ps1` file covers the module. | +| **Standard** | One root `tests/.Tests.ps1` file per public function group. Ungrouped commands and cross-cutting scenarios may remain in separate root `*.Tests.ps1` files. | +| **Advanced** | Organize tests in recursive subdirectories. Process-PSModule discovers each directory independently using the precedence below. | + +For the Advanced profile, Process-PSModule traverses `tests/` and every subdirectory. Within each directory: + +1. Exactly one `*.Configuration.ps1` file is the only discovered test entry. More than one configuration file in the same directory is an error. +2. Otherwise, one or more `*.Container.ps1` files are all discovered, and `*.Tests.ps1` files in that directory are ignored. +3. Otherwise, all `*.Tests.ps1` files in that directory are discovered. + +Process-PSModule derives `TestName` from the file's basename before the first dot. For example, `Projects.Unit.Tests.ps1` produces `Projects`. Use a unique first-dot prefix for every discovered entry so matrix jobs and result artifacts are unambiguous. + +Each `*.Tests.ps1` file must declare the Pester 6 requirement: + +```powershell +#Requires -Modules @{ ModuleName = 'Pester'; ModuleVersion = '6.0.0'; MaximumVersion = '6.*' } +``` + +Tests run against the built module artifact installed locally, across a multi-OS matrix (Linux, macOS, Windows). The full suite must also remain runnable locally without mandatory cloud resources, special access, or secrets that cannot be mocked. + +Unit tests mock boundaries owned by the module, not third-party APIs or SDKs directly. Wrap external dependencies behind a thin module-owned interface and fake that interface. Where the external contract matters, back the unit tests with a small integration suite that exercises the live dependency. + +### Shared test infrastructure + +Tests run in parallel across multiple OS runners. When integration tests need shared infrastructure, provision it once rather than inside each test file: + +- `tests/BeforeAll.ps1` — special root workflow phase that runs once before the full test matrix. Create shared resources here. +- `tests/AfterAll.ps1` — special root workflow phase that runs once after the full test matrix. Remove shared resources here. + +These exact root files are detected separately and are not recursive test entries. Nested files with these names do not create workflow phases. + +Use `$env:GITHUB_RUN_ID` (stable per workflow run, shared across all runners) for deterministic resource names: + +```powershell +$resourceName = "Test-$($env:RUNNER_OS)-$($env:GITHUB_RUN_ID)" +``` + +Do not use `[guid]::NewGuid()` or `Get-Random` for shared names — these produce different values on each runner. Clean up stale resources at the start of `BeforeAll.ps1` before creating new ones. + +### Enforced framework tests + +The CI pipeline automatically tests every source file against the following rules. Violations fail the build: + +| Test ID | Rule | +| ------- | ---- | +| `NumberOfProcessors` | Use `[System.Environment]::ProcessorCount`, not `$env:NUMBER_OF_PROCESSORS` | +| `Verbose` | Do not pass `-Verbose` to commands unless explicitly silenced with `-Verbose:$false` | +| `OutNull` | Use `$null = ...` instead of `\| Out-Null` | +| `LowercaseKeywords` | All PowerShell keywords must be lowercase | +| `FunctionCount` | Each file must contain exactly one function or filter | +| `FunctionName` | Filename must match the function or filter name | +| `CmdletBinding` | Every function must have `[CmdletBinding()]` | +| `ParamBlock` | Every function must have a `param()` block | +| `FunctionTest` | Every public function must be referenced by the tests; its behavior must be covered whether the suite is per-command or grouped | + +To skip a specific rule for one file only, add a comment at the very top of that file: + +```powershell +#SkipTest:: +``` + +Use skip comments sparingly and always include a meaningful reason. Prefer refactoring to comply over skipping. + +### PSScriptAnalyzer linting + +Source code is linted with PSScriptAnalyzer using the repo-level settings at `.github/linters/.powershell-psscriptanalyzer.psd1`. Key enforced rules include: + +- `PSAlignAssignmentStatement` — aligned assignment operators in hashtables +- `PSAvoidLongLines` — maximum 150 characters per line +- `PSAvoidSemicolonsAsLineTerminators` — no trailing semicolons +- `PSPlaceOpenBrace` / `PSPlaceCloseBrace` — OTBS brace style +- `PSUseConsistentIndentation` — 4-space indentation +- `PSUseConsistentWhitespace` — consistent spacing around operators, pipes, and separators diff --git a/docs/content/Modules/Test-Specification.md b/docs/content/Modules/Test-Specification.md new file mode 100644 index 00000000..d08eb9b7 --- /dev/null +++ b/docs/content/Modules/Test-Specification.md @@ -0,0 +1,65 @@ +# PowerShell Function Test Specification + +## Overview + +This document defines the structure and guidelines for writing Pester tests for PowerShell functions. The goal is to ensure consistency and comprehensive test coverage while maintaining clarity. + +## Test Structure + +Each function is tested within a structured Pester `Describe` block that follows this hierarchy: + +### 1. Module-Level `Describe` + +- The outermost `Describe` block corresponds to the module. +- The module name is derived from the prefix of the function name's noun part. + +### 2. Function-Level `Describe` + +- Each function within the module gets its own `Describe` block. +- Named using the full function name. + +### 3. Use Case `Context` + +- Each use case is grouped within a `Context` block. +- The context name starts with the function name followed by a descriptive test case identifier. + - Example: `Get-Uri - simple usage` + - Example: `Get-Uri - Pipeline usage` + - Example: `Get-Uri - ParameterSet: Detailed` + +### 4. Functional `It` Statements + +- Each `It` block tests a specific aspect of the function's behavior. +- The name follows the format: `FunctionName - ` + - Example: `Get-Uri - gets the URI object when provided a string` + - Example: `Get-Uri - returns $null when input is empty` + - Example: `Get-Uri - throws error on invalid input` + +### 5. Test Guidelines + +- **No Mocks**: We do not use mocks in tests. +- **Real Inputs**: Tests should use actual inputs and expected outputs. +- **Comprehensive Coverage**: All function functionality, including edge cases, must be tested. + +## Example Test Structure + +```pwsh +Describe 'Uri' { + Describe 'Get-Uri' { + Context 'Get-Uri - simple usage' { + It 'Get-Uri - gets the URI object when provided a string' { + $result = Get-Uri -InputString 'https://example.com' + $result | Should -BeOfType [System.Uri] + } + } + + Context 'Get-Uri - Pipeline usage' { + It 'Get-Uri - processes input from the pipeline' { + $result = 'https://example.com' | Get-Uri + $result | Should -BeOfType [System.Uri] + } + } + } +} +``` + +This ensures our tests are structured, maintainable, and adhere to best practices. diff --git a/docs/content/Modules/Versioning.md b/docs/content/Modules/Versioning.md new file mode 100644 index 00000000..ee2eafdd --- /dev/null +++ b/docs/content/Modules/Versioning.md @@ -0,0 +1,167 @@ +# PowerShell Module Semantic Versioning Specification + +## Introduction + +This document defines how changes to a PowerShell module’s public interface determine updates to its version number under Semantic Versioning (SemVer). Semantic Versioning uses a three-part version format: **MAJOR.MINOR.PATCH**, where each part is incremented based on the nature of changes: + +- **MAJOR** version is incremented for incompatible API changes (breaking changes) ([Semantic Versioning 2.0.0 | Semantic Versioning](https://semver.org/#:~:text=Given%20a%20version%20number%20MAJOR,increment%20the)). +- **MINOR** version is incremented for added functionality that is backward compatible (new features) ([Semantic Versioning 2.0.0 | Semantic Versioning](https://semver.org/#:~:text=Given%20a%20version%20number%20MAJOR,increment%20the)). +- **PATCH** version is incremented for backward-compatible bugfixes or minor improvements ([Semantic Versioning 2.0.0 | Semantic Versioning](https://semver.org/#:~:text=Given%20a%20version%20number%20MAJOR,increment%20the)). + +In the context of a PowerShell module, the “public API” consists of all exported functions/cmdlets, public variables, classes, and enums that consumers of the module can use. Changes to these exported elements will dictate whether the version bump is major, minor, or patch. Internal changes that do not affect the exported interface are generally not reflected in the version. The following sections categorize changes and the required version update level, ensuring a consistent and predictable versioning strategy. + +## Major Version (X) – Breaking Changes + +A **major version bump** signifies a breaking change in the module’s public interface. Any modification that could cause existing scripts or code relying on the module to fail or change behavior in an incompatible way requires incrementing the MAJOR version. These include: + +- **Removal or Renaming of Exported Commands or Elements**: Removing an exported function/cmdlet, variable, class, or enum, or renaming any of these, is a breaking change. Consumers referencing the old name will encounter errors because the item no longer exists or has a different name. *(Example: `Get-ItemFoo` was exported in the previous version but is removed or renamed to `Get-FooItem` in the new version. Scripts calling `Get-ItemFoo` will break.)* This kind of change must increment the major version to signal the break. + +- **Changes to Function Signatures**: Modifying the signature of an exported function or cmdlet in a non-backward-compatible way triggers a major bump. This includes: + - Removing a parameter from a function. + - Renaming a parameter (existing scripts using the old parameter name would fail). + - Changing the type of a parameter or the return type in a way that could break callers. + - Changing a parameter from optional to mandatory (or otherwise altering a default value in a way that requires callers to change how they call the function). + - Reordering parameters **if** it affects how the function is invoked positionally (though in PowerShell named parameters are common, positional changes can break scripts that rely on positional invocation). + - Any other modification to a function’s definition that would make previously valid calls invalid. For example, if a new parameter is added *without* a default and thus is required, it would break calls that don’t provide that parameter – this is a breaking change requiring a major version update ([semantic versioning - Does adding a parameter to a function definition require a new major version? - Stack Overflow](https://stackoverflow.com/questions/31678403/does-adding-a-parameter-to-a-function-definition-require-a-new-major-version#:~:text=,compatible%20bug%20fixes)) ([semantic versioning - Does adding a parameter to a function definition require a new major version? - Stack Overflow](https://stackoverflow.com/questions/31678403/does-adding-a-parameter-to-a-function-definition-require-a-new-major-version#:~:text=,compatible%20bug%20fixes)). (If the new parameter is optional, see Minor changes below.) + + *Example:* An exported function `Invoke-ProcessData -Path ` is changed to `Invoke-ProcessData -Uri ` (parameter renamed) or an existing parameter is removed. Scripts using the old parameter name or expecting that parameter will fail, hence a major version increment is needed. + +- **Breaking Changes to Exported Classes**: If the module exports PowerShell classes (public classes meant for users to consume), any breaking change to those classes requires a major version bump. Breaking class changes include: + - Removing a public class or renaming a class. + - Removing or renaming public properties or methods of a class. + - Changing the signature of a class method (e.g. altering parameters or return type in an incompatible way). + - Changing property types or making a formerly public member non-public. + + *Example:* An exported class `FileClient` had a public property `Timeout` that is removed or renamed, or a method `Connect(string server)` is changed to `Connect(Uri server)`. Code instantiating `FileClient` or calling its methods would break, so the major version must increase. + +- **Breaking Changes to Exported Enums**: Enums (enumerations) define a set of constant values. Changing them in a breaking way includes: + - Removing an enum type that was exported. + - Renaming an enum type or an enum member. + - Removing an existing value from an enum, or altering its name or meaning. + - Changing the underlying type of an enum (if relevant in PowerShell, e.g. from int to another type). + + *Example:* An exported enum `LogLevel` had members `Info, Warning, Error`. If `Warning` is removed or renamed to `Warn`, any script using `LogLevel.Warning` will break. Such a change mandates a major version bump. (Adding a new enum value is not breaking – see Minor changes.) + +- **Other Incompatible Changes**: Any other change that alters the expected behavior or contract of the public API in a way that existing consumers would need to modify their code is considered breaking. For instance, if the module’s behavior changes in an incompatible manner (like a function now throws an error in a scenario where it previously quietly succeeded, or an output format changes such that scripts parsing the output would fail), the change should be treated as a breaking change. In general, **any removal or incompatible modification of functionality is a major change**. + +**Rationale:** According to Semantic Versioning, introducing changes that are not backward compatible requires a major version increment ([Semantic Versioning 2.0.0 | Semantic Versioning](https://semver.org/#:~:text=Given%20a%20version%20number%20MAJOR,increment%20the)). By increasing the major version, we communicate to users that they may need to adjust their scripts due to breaking changes. This aligns with the principle that *“MAJOR version when you make incompatible API changes”* ([Semantic Versioning 2.0.0 | Semantic Versioning](https://semver.org/#:~:text=Given%20a%20version%20number%20MAJOR,increment%20the)). For example, if a function or parameter that existed in version 1.x is no longer present in version 2.0, that is an incompatible API change. Our versioning policy follows this rule strictly: breaking changes will never be introduced in a minor or patch release, ensuring that patch and minor updates can be safely adopted without fear of script-breaking surprises. + +## Minor Version (Y) – Backward-Compatible Additions + +A **minor version bump** is used for new features and additions that are backward compatible with the existing public API. These changes enhance the module’s functionality without breaking any existing usage. In other words, existing scripts will continue to work as before, and new capabilities are introduced. Changes that trigger a MINOR version increase include: + +- **Adding New Exported Functions/Cmdlets**: Introducing a new function or cmdlet to the module is a backward-compatible addition. Since it does not remove or change existing functions, nothing breaks; users simply have an additional function available. *Example:* Adding a new cmdlet `New-Report` to the module (where it didn’t exist before) would be a new feature. This warrants a minor version bump because it’s an additive, non-breaking change. + +- **Adding New Parameters to Existing Functions**: If you extend an existing function’s capabilities by adding a new parameter **in a way that does not break existing calls**, it is a minor change. The key here is that the new parameter must be optional or have a default value such that any existing calls (which don’t pass this parameter) still work exactly as before. In semantic versioning terms, this is adding functionality in a backwards-compatible manner ([semantic versioning - Does adding a parameter to a function definition require a new major version? - Stack Overflow](https://stackoverflow.com/questions/31678403/does-adding-a-parameter-to-a-function-definition-require-a-new-major-version#:~:text=If%20your%20extra%20param%20is,requires%20a%20MAJOR%20version%20change)). + - If the new parameter is optional (or has a sensible default), existing scripts can ignore it and will not be affected ([semantic versioning - Does adding a parameter to a function definition require a new major version? - Stack Overflow](https://stackoverflow.com/questions/31678403/does-adding-a-parameter-to-a-function-definition-require-a-new-major-version#:~:text=If%20your%20extra%20param%20is,requires%20a%20MAJOR%20version%20change)). For example, adding an optional `-Force` switch or an optional `-TimeoutSeconds` parameter (with a default value) to a function is a minor update. Users can start using the new parameter if they want the new behavior, but all old usages remain valid. + - **Important:** If a new parameter is added as *required* (with no default), that breaks existing calls (which would now be missing a required argument), and thus **would** be a breaking change requiring a major bump ([semantic versioning - Does adding a parameter to a function definition require a new major version? - Stack Overflow](https://stackoverflow.com/questions/31678403/does-adding-a-parameter-to-a-function-definition-require-a-new-major-version#:~:text=,compatible%20bug%20fixes)). So, new parameters must be introduced in a backward-compatible way (optional or with defaults) to qualify as a minor version update. + +- **Adding New Exported Variables**: If the module begins to export a new public variable (for example, a new preference variable or a constant) that wasn’t present before, it’s an additive change. Since no existing variable is removed or changed, existing scripts are unaffected (they simply might not use or know about the new variable). This constitutes a minor version increment. *Example:* Adding a new `$PublicConfig` variable that scripts can read is a new feature, bumped in the minor version. + +- **Adding New Classes or Members**: Introducing a new public class (exported from the module) or adding new members to an existing exported class in a non-breaking way is a minor change: + - Adding a brand new class (e.g., a helper class or a new type for users to utilize) doesn’t impact existing code since nothing is removed or changed. + - Adding a new method or property to an existing exported class can be considered backward compatible *as long as it doesn’t conflict with existing members*. Existing scripts constructing or using the class will continue to work as before. (One caveat: if consumers have derived from this class, adding new abstract members would be breaking, but assuming typical usage where classes are used as-is, new members are just additional features.) + *Example:* A class `Connection` gains a new method `TestConnection()` in version 1.2. Previous scripts using `Connection` are not affected (they don’t call the new method), but now have the option to use `TestConnection`. This is a minor feature addition. + +- **Adding New Enum Values or New Enums**: If the module introduces a new enumeration type, or adds additional members to an existing enum, it’s generally treated as a backward-compatible addition: + - **New Enum Type:** Completely new enum (e.g., a new enum `Color` with values Red/Green/Blue) is additive. + - **New Value in Existing Enum:** Adding a value to an existing exported enum can be viewed as a new feature. Code that doesn’t know about the new value isn’t forced to use it. (Existing switch statements or logic that enumerate enum values may not account for it, but they won’t immediately break at runtime; however, developers should update their code to handle the new case if appropriate. Since it doesn’t outright break compilation or invocation in PowerShell, it’s considered backward compatible in this context.) + *Example:* The enum `LogLevel` had `Info, Warning, Error`. If we add `Verbose` as a new level in a minor release, existing scripts using `LogLevel` continue to run (they might not handle `Verbose` if encountered, but nothing crashes by the mere presence of the new value). It’s an additive feature, hence a minor bump. + +- **Non-Breaking Changes to Existing Features**: In some cases, a change might alter behavior but still be backward compatible. For instance, making an existing parameter accept a new type of input in addition to existing types (broadening what is accepted) could be considered a new capability that doesn’t break old usage. Such changes can fall under minor version if they extend functionality without removal or contradiction of the old behavior. *(However, caution is advised: changing behavior can sometimes surprise users. If in doubt, treat as major if it might disrupt assumptions.)* + +In summary, **any new functionality that does not force existing users to change their usage is a candidate for a minor version increment** ([Semantic Versioning 2.0.0 | Semantic Versioning](https://semver.org/#:~:text=Given%20a%20version%20number%20MAJOR,increment%20the)). Minor releases accumulate enhancements and new features, signalling to users that new capabilities are available, but all existing scripts should continue to work as they did in the previous version. Users can upgrade to the new minor version and gain new functions or options without needing to modify their existing code. This aligns with the SemVer guideline that *“MINOR version [increments] when you add functionality in a backward compatible manner”* ([Semantic Versioning 2.0.0 | Semantic Versioning](https://semver.org/#:~:text=Given%20a%20version%20number%20MAJOR,increment%20the)). + +## Patch Version (Z) – Bugfixes and Improvements + +A **patch version bump** is used for changes that do not affect the module's public API or add new features, but rather fix issues or improve internal implementation. Patch updates are meant to be safe, drop-in updates for users, with no risk of breaking functionality or changing how features are used. Scenarios for a PATCH version increment include: + +- **Bugfixes**: Correcting any bugs in existing functions or features, provided the fix does not alter the function's signature or expected input/output in a way that would break compatibility. The behavior might change (from incorrect to correct), but since the original behavior was unintended (a bug), this is considered a backward-compatible fix. For example: + - Fixing a logic error in a function so that it now produces the correct result. + - Correcting a typo in output or an error message. + - Resolving a minor issue where an enum value wasn’t handled in an internal function (assuming no public API change). + As long as the outward-facing contract remains the same (same function name, parameters, outputs), these are patch changes. Consumers might notice the bug is resolved, but they do not need to change their code – they just get the benefit of the fix. + +- **Performance Improvements**: Optimizations that improve the performance or efficiency of the module without affecting the external behavior or API. For instance, rewriting an algorithm inside a function to run faster or use less memory, but with the same input/output interface and results, qualifies as a patch. Users’ experience may improve (faster execution), but they don’t need to change anything in their usage. This is a non-breaking internal improvement. + +- **Refactoring and Internal Cleanup**: Changes to the internal code structure, organization, or quality that do not change any aspect of the public interface or observable behavior fall under patch (or possibly no version change at all – see the next section). If you release a new version that purely refactors code (improves maintainability, updates comments, reorganizes module files) and the module’s exported functions and behavior remain identical, it can be considered a patch update. From the user’s perspective, nothing changed functionally, so there’s no new feature (hence not a minor) and no break (hence not a major). Releasing it as a patch version indicates it’s a minor improvement or maintenance release. + +- **Documentation or Metadata Updates**: If you publish a new version to update documentation included in the module (e.g., help content) or to adjust module metadata (like author info, tags, etc.) without any code change affecting functionality, this would be a patch version increment. (However, often documentation changes alone might not necessitate a new release; if they do, patch is appropriate since the API is unchanged.) + +**Note:** A patch release should not introduce any new public surface area or change the meaning of anything in the public API. It is strictly for fixes and invisible improvements. According to SemVer rules, *"PATCH version when you make backwards-compatible bugfixes"* ([Semantic Versioning 2.0.0 | Semantic Versioning](https://semver.org/#:~:text=Given%20a%20version%20number%20MAJOR,increment%20the)) – this includes fixes and minor tweaks that do not affect compatibility. Users upgrading from one patch version to the next within the same minor series (e.g., 1.2.3 to 1.2.4) should notice no differences except the resolved issues or performance gains. They do not get new features (and thus don't need to learn anything new), and they do not have to worry about breaks. + +## Internal Changes with No Version Impact + +Certain changes do not require any version number increase at all, because they have no effect on the module’s outward-facing behavior or interface. In a disciplined development practice, if the only changes in a commit or release are purely internal and produce no difference in functionality or API, the version can remain the same. In practice, such changes are often bundled with other changes or released as patch versions if needed. But as a guideline, **metadata or refactoring changes that do not impact the exported interface should not influence the version number** (they are essentially “no-ops” as far as the user is concerned). Examples: + +- **Refactoring without Behavioral Changes**: If the code is refactored (e.g., splitting a large function into smaller private helper functions, renaming internal variables, improving readability) but the exported functions, classes, and variables all behave exactly the same and have the same signatures, then there’s no need for a version change. The module behaves identically from the consumer’s perspective. + +- **Build tool or Test Changes**: Updates to the module's build scripts, continuous integration configuration, or test suite do not require a version bump. These are changes for the developers/maintainers and do not ship to the user in a way that affects usage. + +- **Metadata Updates**: Changing non-functional metadata such as author name, project URL, licensing info, etc., in the module manifest (PSD1) doesn’t affect how the module is used at runtime. Such changes alone don’t merit a version increment. + +- **Formatting and Comments**: Modifying code formatting, comments, or other non-executable parts of the code has no effect on functionality. No version change is needed for these kinds of modifications alone. + +- **No-Op Rebuilds**: In some cases, a module might be rebuilt or repackaged without any code changes (for example, re-signing the module or packaging it differently). If the contents of the module’s public API and behavior are unchanged, the version number should ideally remain the same. (If a rebuild must be published, one might use the same version or a patch if required by tooling, but from a semantic standpoint, nothing changed.) + +In summary, if a change does **not** modify or add to the public interface and does not fix a user-facing bug, it should not cause any visible version change. The automated versioning scripts or maintainers should ignore such changes when determining how to bump the version. Essentially, **purely internal changes = no bump** (or at most a patch if a release is needed for some reason). This ensures the version number reflects meaningful changes that users care about. + +*(Note: In practice, every release must have a unique version. So if you are releasing changes that have no API impact, you might still increment the patch to publish it. But the key is that those internal changes by themselves never escalate the version beyond patch, and if they are truly no-ops, you might choose not to release until there’s a user-facing change.)* + +## Versioning Strategy and Update Expectations + +This module follows a **“latest version” support strategy**, meaning that only the most recent release is fully supported and maintained. When a new version is released, it supersedes previous versions. **Older versions are not maintained**, and users are expected to upgrade to the latest version to receive fixes and new features. As stated in Microsoft’s guidance for PowerShell modules: *“Only the latest major version receives full support, including new features, bugfixes, and updates. We strongly recommend upgrading to the latest version…”* ([Versioning, release cadence, and breaking changes - Microsoft Entra PowerShell | Microsoft Learn](https://learn.microsoft.com/en-us/powershell/entra-powershell/entraps-versioning-release-cadence?view=entra-powershell#:~:text=Only%20the%20latest%20major%20version,latest%20improvements%20and%20security%20updates)). In our context, this means: + +- We do not create maintenance releases for older major versions. For example, if the module is currently at 2.x, we will not typically release further updates for 1.x; any fixes or enhancements will go into a new 2.x (or later) release. The focus is on moving forward with the latest version of the module. + +- Breaking changes will be introduced only with a major version bump, and when we do increment the major version, users should plan to update their scripts to accommodate those changes. Since older majors won’t get back-ported fixes, upgrading is important to stay supported. + +- Minor and patch releases are intended to be safe to adopt (no breaking changes). Users should feel confident updating to a new minor version within the same major series to get new features, and applying patch updates for bugfixes. Given that we don't maintain parallel old versions, applying these updates is the primary way to get issues resolved. + +- Semantic versioning in this module serves as a contract with users: by looking at how the version changed, users can tell what to expect: + - A major jump (e.g. 1.4.0 → 2.0.0) signals **“There are breaking changes; read the release notes and be prepared to adjust your usage.”** + - A minor jump (e.g. 2.3.5 → 2.4.0) signals **“New features have been added, but everything you used before still works.”** + - A patch jump (e.g. 2.4.0 → 2.4.1) signals **“No new features, but some issues were fixed or minor improvements made; nothing should break.”** + +By adhering to this strategy, we ensure clarity and consistency. Users who always upgrade to the latest version will benefit from all improvements and will only need to make script changes when a major version increments (which will be clearly indicated by the version number change). + +## Automated Version Bump Determination (Implementation) + +To enforce the above rules, we will use custom PowerShell scripts as part of our build/release process to automatically determine the appropriate version bump based on the changes in the module. The automation will compare the module’s exported interface between the current release and the new version under development: + +- It will **detect additions** of public elements (functions, parameters, variables, classes, enum values) and **detect removals or changes** to public elements. +- Based on this comparison, the script will decide the version increment: + - If new public functions, new optional parameters, or other new exported members are found (and no breaking changes), the script will set the version bump to **Minor** (since new functionality has been added in a backwards-compatible way). + - If any exported function/variable/class/enum from the previous version is missing in the new version, or a function’s signature has changed incompatibly, etc., the script will flag a **Major** bump (since a breaking change was detected). + - If no differences in the public API are found (meaning no new features and no removed/changed features), then the changes must be purely fixes or internal improvements. In this case, the script will default to a **Patch** bump ([PowerShell: Automatic Module Semantic Versioning](https://powershellexplained.com/2017-10-14-Powershell-module-semantic-version/#:~:text=In%20this%20example%2C%20if%20there,version)). + +This approach mirrors the “fingerprint” strategy: generating a list (fingerprint) of all public API elements (e.g., `FunctionName:ParameterName` for each parameter of each function, plus entries for other exported members) for the current and previous version, and comparing them ([PowerShell: Automatic Module Semantic Versioning](https://powershellexplained.com/2017-10-14-Powershell-module-semantic-version/#:~:text=%27Detecting%20new%20features%27%20%24fingerprint%20,)) ([PowerShell: Automatic Module Semantic Versioning](https://powershellexplained.com/2017-10-14-Powershell-module-semantic-version/#:~:text=In%20this%20example%2C%20if%20there,version)). + +- If the new fingerprint has entries not present in the old fingerprint, those represent new capabilities (triggering a minor version increase) ([PowerShell: Automatic Module Semantic Versioning](https://powershellexplained.com/2017-10-14-Powershell-module-semantic-version/#:~:text=In%20this%20example%2C%20if%20there,version)). +- If the old fingerprint has entries not present in the new fingerprint, those represent removed or changed items (triggering a major version increase) ([PowerShell: Automatic Module Semantic Versioning](https://powershellexplained.com/2017-10-14-Powershell-module-semantic-version/#:~:text=In%20this%20example%2C%20if%20there,version)). +- If neither additions nor removals are detected, only patch-level changes exist ([PowerShell: Automatic Module Semantic Versioning](https://powershellexplained.com/2017-10-14-Powershell-module-semantic-version/#:~:text=In%20this%20example%2C%20if%20there,version)). + +Using automation ensures that our versioning rules are applied consistently on every release. However, we will also manually review changes for any subtleties that automation might miss. For example, a change in behavior that is technically backward-compatible in the API surface might still be communicated as a bigger change if it could impact users (the automation might view it as no API change, but maintainers might still choose to bump minor for a significant new behavior or even major if a fix alters expected outcomes in rare cases). The tooling serves as a baseline, and maintainers can override or augment the decision if necessary (but always adhering to the rule that breaking changes cannot be released under a non-major bump). + +**Example of the automated logic in practice:** +Suppose in version 1.3.0 the module has a function `Get-Data -Path ` and in the development code we change this to `Get-Data -Path -Filter `, where `-Filter` is a new optional parameter. The script compares the exported interface: + +- Old version fingerprint might include an entry like `Get-Data:Path` for the parameter. +- New version fingerprint includes `Get-Data:Path` and `Get-Data:Filter`. +The new fingerprint has an entry not in the old (`Get-Data:Filter`), indicating a new parameter. The old fingerprint has none that the new lacks (we didn’t remove anything; `Path` is still there). So the tool will identify a new feature addition with no removals, resulting in a **Minor** bump (1.4.0). This matches our expectation: adding an optional `-Filter` parameter is a backward-compatible enhancement. + +If instead we had removed `-Path` or renamed `Get-Data` to `Get-ItemData`, the old fingerprint would contain entries not in the new, flagging a breaking change. The script would then recommend a **Major** bump (to 2.0.0), aligning with our policy that such a removal/rename is breaking. + +If we only fixed a bug inside `Get-Data` but made no changes to its parameters or outputs, the fingerprints would be identical. The automation would not find any new or removed public element, and thus default to a **Patch** bump (1.3.1). This ensures even unseen internal changes result in at least a patch update if a release is made, but nothing more. + +## Conclusion + +This specification provides a clear framework for versioning a PowerShell module in accordance with Semantic Versioning principles, tailored to the specific elements of a PowerShell module’s public interface. By classifying changes into major, minor, or patch categories, we ensure that the module’s version number accurately communicates the impact of changes: + +- **Major** versions for breaking changes (removals, renames, incompatible alterations). +- **Minor** versions for new features and additions that are backward compatible. +- **Patch** versions for bugfixes and non-breaking improvements. + +Developers maintaining the module can use these guidelines to decide the appropriate version number for each release. Automated scripts will assist in detecting the scope of changes, but sound judgment will be applied in edge cases. Users of the module can rely on the version number to understand the significance of an update. Ultimately, this practice enables a predictable upgrade path where users only need to be concerned about potential breaking changes when the major version increases, and are otherwise free to take minor and patch updates confidently. + +By adhering to this specification, we uphold a contract of compatibility and improvement with our users, making module releases transparent and manageable in the long run. diff --git a/docs/content/Modules/index.md b/docs/content/Modules/index.md new file mode 100644 index 00000000..5ce52fb1 --- /dev/null +++ b/docs/content/Modules/index.md @@ -0,0 +1,19 @@ +# Modules + +Process-PSModule now hosts PSModule-org module documentation. + +This section is the local source of truth for: + +- module catalog and module-level specs +- Process-PSModule structure and build flow +- repository anatomy and template onboarding + +## Sections + +- [Repository Standard](Repository-Standard.md) +- [Standards](Standards.md) +- [Module types](Module-Types.md) +- [Test Specification](Test-Specification.md) +- [Versioning](Versioning.md) +- [Catalog](Catalog/index.md) (auto-generated from PSModule repo metadata, release data, and README summaries) +- [Process-PSModule](Process-PSModule/index.md) diff --git a/docs/content/PowerShell/DSC/index.md b/docs/content/PowerShell/DSC/index.md new file mode 100644 index 00000000..696e2c44 --- /dev/null +++ b/docs/content/PowerShell/DSC/index.md @@ -0,0 +1,10 @@ +# PowerShell DSC (Moved) + +Cross-org infrastructure and PowerShell guidance is now canonical in MSXOrg/docs. + +Use: + +- [MSX Coding Standards](https://msxorg.github.io/docs/Coding-Standards/) +- [MSX Capabilities](https://msxorg.github.io/docs/Capabilities/) + +This page is retained as a migration pointer only. diff --git a/docs/content/PowerShell/FunctionApps/index.md b/docs/content/PowerShell/FunctionApps/index.md new file mode 100644 index 00000000..2a84e569 --- /dev/null +++ b/docs/content/PowerShell/FunctionApps/index.md @@ -0,0 +1,10 @@ +# PowerShell Function Apps (Moved) + +Cross-org Function App guidance is now canonical in MSXOrg/docs. + +Use: + +- [MSX Capabilities](https://msxorg.github.io/docs/Capabilities/) +- [MSX Coding Standards](https://msxorg.github.io/docs/Coding-Standards/) + +This page is retained as a migration pointer only. diff --git a/docs/content/PowerShell/Scripts/index.md b/docs/content/PowerShell/Scripts/index.md new file mode 100644 index 00000000..caf158c2 --- /dev/null +++ b/docs/content/PowerShell/Scripts/index.md @@ -0,0 +1,9 @@ +# PowerShell Scripts (Moved) + +Cross-org PowerShell script standards are now canonical in MSXOrg/docs. + +Use: + +- [MSX Coding Standards / PowerShell / Scripts](https://msxorg.github.io/docs/Coding-Standards/PowerShell/Scripts/) + +This page is retained as a migration pointer only. diff --git a/docs/content/PowerShell/Standard/index.md b/docs/content/PowerShell/Standard/index.md new file mode 100644 index 00000000..e78a99d3 --- /dev/null +++ b/docs/content/PowerShell/Standard/index.md @@ -0,0 +1,12 @@ +# PowerShell Standards (Moved) + +Cross-org PowerShell standards are now canonical in MSXOrg/docs. + +Use: + +- [MSX Coding Standards / PowerShell](https://msxorg.github.io/docs/Coding-Standards/PowerShell/) + +PSModule-specific module construction guidance remains under: + +- [Modules](../../Modules/index.md) +- [Process-PSModule](../../Modules/Process-PSModule/index.md) diff --git a/docs/content/Solutions/index.md b/docs/content/Solutions/index.md new file mode 100644 index 00000000..0970c3bb --- /dev/null +++ b/docs/content/Solutions/index.md @@ -0,0 +1,10 @@ +# Solutions (Moved) + +Reusable solution architecture and system patterns are now canonical in MSXOrg/docs. + +Use: + +- [MSX Capabilities](https://msxorg.github.io/docs/Capabilities/) +- [MSX Initiatives](https://msxorg.github.io/docs/Initiatives/) + +This page is retained as a migration pointer only. diff --git a/docs/content/Style-Guides/index.md b/docs/content/Style-Guides/index.md new file mode 100644 index 00000000..b874da81 --- /dev/null +++ b/docs/content/Style-Guides/index.md @@ -0,0 +1,14 @@ +--- +title: Style Guides +description: Coding style guidelines for PSModule repositories. +--- + +# Style Guides (Moved) + +Style guide content is now canonical in MSXOrg/docs. + +Use: + +- [MSX Coding Standards](https://msxorg.github.io/docs/Coding-Standards/) + +This page is retained as a migration pointer only. diff --git a/docs/content/assets/images/icon.png b/docs/content/assets/images/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..be83fd5fd3914846c735d44415569b86c254ccad GIT binary patch literal 5882 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 OgRD;4;a-^H$^Qd{UB>JH literal 0 HcmV?d00001 diff --git a/docs/content/assets/images/module-catalog/githubissues.svg b/docs/content/assets/images/module-catalog/githubissues.svg new file mode 100644 index 00000000..9ff63642 --- /dev/null +++ b/docs/content/assets/images/module-catalog/githubissues.svg @@ -0,0 +1 @@ + diff --git a/docs/content/assets/images/module-catalog/githubpullrequests.svg b/docs/content/assets/images/module-catalog/githubpullrequests.svg new file mode 100644 index 00000000..21a334a9 --- /dev/null +++ b/docs/content/assets/images/module-catalog/githubpullrequests.svg @@ -0,0 +1 @@ + diff --git a/docs/content/assets/images/module-catalog/githubstars.svg b/docs/content/assets/images/module-catalog/githubstars.svg new file mode 100644 index 00000000..90731657 --- /dev/null +++ b/docs/content/assets/images/module-catalog/githubstars.svg @@ -0,0 +1 @@ + diff --git a/docs/content/assets/images/module-catalog/githubtags.svg b/docs/content/assets/images/module-catalog/githubtags.svg new file mode 100644 index 00000000..d20df966 --- /dev/null +++ b/docs/content/assets/images/module-catalog/githubtags.svg @@ -0,0 +1 @@ + diff --git a/docs/content/assets/images/module-catalog/package-variant-closed-check.svg b/docs/content/assets/images/module-catalog/package-variant-closed-check.svg new file mode 100644 index 00000000..f3dc43d1 --- /dev/null +++ b/docs/content/assets/images/module-catalog/package-variant-closed-check.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/docs/content/index.md b/docs/content/index.md new file mode 100644 index 00000000..29e82777 --- /dev/null +++ b/docs/content/index.md @@ -0,0 +1,50 @@ +--- +hide: + - navigation +--- + +# Make it **easy**, to move **fast**, in a **safe** way + +Every decision — from architecture to commit message — is filtered through those three words. + +## Easy + +Simplicity is a feature. Make the right thing the easy thing — for users, contributors, and agents alike. +Prefer the obvious and limited over the powerful and confusing. Automate what is mechanical so that human attention is saved for decisions that require judgment, context, and care. + +The safe and smart choice should be the default choice. Design systems, tools, and workflows so that doing the right thing requires no extra effort — while allowing a deliberate override when necessary. + +## Fast + +Shift left, ship early, learn fast, iterate. Work flows in small increments — draft PRs, micro-commits, quick reviews. +The faster we close the loop between intention and observation, the faster we improve and evolve what we are building. + +## Safe + +Every decision should be reversible. Every release should be observable. Every failure should teach. +Speed requires safety — we can only iterate fast when changes are cheap to undo, easy to monitor, and failures produce learning rather than blame. + +## A GitHub & PowerShell development framework + +PSModule **infuses GitHub with PowerShell** and brings more to the PowerShell ecosystem by using the GitHub platform to its fullest extent. +The framework is two things in one: a collection of reusable PowerShell modules, and a set of GitHub Actions workflows that automate the full PowerShell module (software) delivery lifecycle — the DevOps functionality that the ecosystem is missing. + +We empower PowerShell-savvy developers to effortlessly transform their ideas into impactful solutions, automating the repetitive tasks so developers — whether consumers or contributors — can concentrate on coding without distractions. + +Where we are headed is tracked as [objectives and initiatives](https://msxorg.github.io/docs/Ways-of-Working/Goal-Setting/). + +## What this docs site now owns + +This site is now focused on PSModule-org specific documentation: + +- [Modules](Modules/index.md): module catalog and module-specific standards +- [Process-PSModule](Modules/Process-PSModule/index.md): how modules are structured and built +- [Template onboarding](Modules/Process-PSModule/template-quickstart.md): how to start from the template + +## Cross-org guidance has moved + +Cross-org standards and reusable solution documentation are now canonical in [MSXOrg/docs](https://msxorg.github.io/docs/): + +- [Coding Standards](https://msxorg.github.io/docs/Coding-Standards/) +- [Capabilities](https://msxorg.github.io/docs/Capabilities/) +- [Frameworks](https://msxorg.github.io/docs/Frameworks/) diff --git a/docs/includes/abbreviations.md b/docs/includes/abbreviations.md new file mode 100644 index 00000000..e158693f --- /dev/null +++ b/docs/includes/abbreviations.md @@ -0,0 +1,50 @@ +*[API]: Application Programming Interface +*[Bitbucket]: Web-based version control repository hosting service +*[CD]: Continuous Deployment or Continuous Delivery +*[CDN]: Content Delivery Network +*[CI]: Continuous Integration +*[CI/CD]: Continuous Integration/Continuous Deployment +*[CLI]: Command Line Interface +*[CRUD]: Create, Read, Update, Delete +*[CSS]: Cascading Style Sheets +*[DevOps]: Development and Operations +*[DNS]: Domain Name System +*[Docker]: Platform for developing, shipping, and running applications in containers +*[GH]: GitHub +*[GHE]: GitHub Enterprise +*[GHEC]: GitHub Enterprise Cloud +*[GHECDR]: GitHub Enterprise Cloud with Data Residency +*[GHES]: GitHub Enterprise Server +*[Gist]: GitHub Gist +*[Git]: Version Control System +*[GitHub]: Web-based platform for version control using Git +*[GitLab]: Web-based DevOps lifecycle tool that provides a Git repository manager +*[HTML]: Hyper Text Markup Language +*[HTTP]: Hypertext Transfer Protocol +*[HTTPS]: Hypertext Transfer Protocol Secure +*[IaaS]: Infrastructure as a Service +*[IDE]: Integrated Development Environment +*[JS]: JavaScript +*[JSON]: JavaScript Object Notation +*[KPI]: Key Performance Indicator +*[Kubernetes]: Open-source system for automating deployment, scaling, and management of containerized applications +*[LTS]: Long-Term Servicing +*[OS]: Operating System +*[PaaS]: Platform as a Service +*[ps1]: PowerShell Script File +*[ps1xml]: PowerShell XML File +*[psd1]: PowerShell Data File +*[psm1]: PowerShell Module File +*[pwsh]: PowerShell Core (also known as PowerShell 7) +*[REST]: Representational State Transfer +*[SaaS]: Software as a Service +*[SEO]: Search Engine Optimization +*[SOAP]: Simple Object Access Protocol +*[SQL]: Structured Query Language +*[SSL]: Secure Sockets Layer +*[TLS]: Transport Layer Security +*[UI]: User Interface +*[UX]: User Experience +*[VPN]: Virtual Private Network +*[W3C]: World Wide Web Consortium +*[XML]: eXtensible Markup Language diff --git a/docs/overrides/assets/javascripts/tablesort.js b/docs/overrides/assets/javascripts/tablesort.js new file mode 100644 index 00000000..f6e0f3ea --- /dev/null +++ b/docs/overrides/assets/javascripts/tablesort.js @@ -0,0 +1,16 @@ +/* global document$, Tablesort */ +document$.subscribe(function () { + var tables = document.querySelectorAll("article table:not([class])"); + tables.forEach(function (table) { + // If table doesn't have a thead, create one from the first row + if (!table.querySelector("thead")) { + var firstRow = table.querySelector("tr"); + if (firstRow) { + var thead = document.createElement("thead"); + thead.appendChild(firstRow); + table.insertBefore(thead, table.firstChild); + } + } + new Tablesort(table); + }); +}); diff --git a/docs/zensical.toml b/docs/zensical.toml new file mode 100644 index 00000000..411bb3c3 --- /dev/null +++ b/docs/zensical.toml @@ -0,0 +1,157 @@ +# Zensical configuration +# https://zensical.org/docs/setup/basics/ + +[project] +site_name = "PSModule / Docs" +site_url = "https://psmodule.io/docs/" +docs_dir = "content" +repo_name = "PSModule/Process-PSModule" +repo_url = "https://github.com/PSModule/Process-PSModule" +edit_uri = "edit/main/docs/content/" +copyright = "Copyright © 2023 - 2025 PSModule" +extra_javascript = [ + "https://unpkg.com/tablesort@5.3.0/dist/tablesort.min.js", + "assets/javascripts/tablesort.js", +] +watch = ["includes"] +nav = [ + {"Modules" = [ + "Modules/index.md", + {"Repository Standard" = "Modules/Repository-Standard.md"}, + {"Standards" = "Modules/Standards.md"}, + {"Module types" = "Modules/Module-Types.md"}, + {"Test Specification" = "Modules/Test-Specification.md"}, + {"Versioning" = "Modules/Versioning.md"}, + {"Catalog" = [ + "Modules/Catalog/index.md", + ]}, + {"Process-PSModule" = [ + "Modules/Process-PSModule/index.md", + {"Repository Structure" = "Modules/Process-PSModule/repository-structure.md"}, + {"Module Anatomy" = "Modules/Process-PSModule/module-anatomy.md"}, + {"Module Build Validation" = "Modules/Process-PSModule/module-build-validation.md"}, + {"Build, Test, Pack, Publish" = "Modules/Process-PSModule/build-test-pack-publish.md"}, + {"Template Quickstart" = "Modules/Process-PSModule/template-quickstart.md"}, + {"Module Bootstrap" = "Modules/Process-PSModule/module-bootstrap.md"}, + ]}, + ]}, + {"Dictionary" = [ + "Dictionary/index.md", + ]}, +] + +[project.theme] +language = "en" +custom_dir = "overrides" +font.text = "Mona Sans" +font.code = "Source Code Pro" +logo = "assets/images/icon.png" +favicon = "assets/images/icon.png" +features = [ + "announce.dismiss", + "content.action.edit", + "content.action.view", + "content.code.annotate", + "content.code.copy", + "content.tooltips", + "navigation.footer", + "navigation.indexes", + "navigation.instant", + "navigation.instant.prefetch", + "navigation.instant.preview", + "navigation.instant.progress", + "navigation.tabs", + "navigation.top", + "navigation.tracking", + "search.highlight", + "search.share", + "search.suggest", + "toc.follow", +] + +[project.theme.icon] +repo = "material/github" +link = "material/link-variant" + +# Palette toggle for automatic mode +[[project.theme.palette]] +media = "(prefers-color-scheme)" +toggle.icon = "material/link" +toggle.name = "Switch to dark mode" + +# Palette toggle for dark mode +[[project.theme.palette]] +media = "(prefers-color-scheme: dark)" +scheme = "slate" +primary = "black" +accent = "light blue" +toggle.icon = "material/toggle-switch-off-outline" +toggle.name = "Switch to light mode" + +# Palette toggle for light mode +[[project.theme.palette]] +media = "(prefers-color-scheme: light)" +scheme = "default" +primary = "black" +accent = "light blue" +toggle.icon = "material/toggle-switch" +toggle.name = "Switch to system preference" + +[project.plugins.meta] + +[project.plugins.search] + +[project.markdown_extensions.toc] +permalink = "#" + +[project.markdown_extensions.attr_list] + +[project.markdown_extensions.abbr] + +[project.markdown_extensions.admonition] + +[project.markdown_extensions.def_list] + +[project.markdown_extensions.footnotes] + +[project.markdown_extensions.md_in_html] + +[project.markdown_extensions.tables] + +[project.markdown_extensions.pymdownx.details] + +[project.markdown_extensions.pymdownx.superfences] +custom_fences = [ + { name = "mermaid", class = "mermaid", format = "pymdownx.superfences.fence_code_format" }, +] + +[project.markdown_extensions.pymdownx.tasklist] +custom_checkbox = true + +[project.markdown_extensions.pymdownx.snippets] +auto_append = ["includes/abbreviations.md"] + +[project.extra.consent] +title = "Cookie consent" +description = """ + We use cookies to recognize your repeated visits and preferences, as well + as to measure the effectiveness of our documentation and whether users + find what they're searching for. With your consent, you're helping us to + make our documentation better. +""" +actions = ["accept", "reject"] + +[[project.extra.social]] +icon = "fontawesome/brands/discord" +link = "https://discord.gg/jedJWCPAhD" +name = "PSModule on Discord" + +[[project.extra.social]] +icon = "material/powershell" +link = "https://www.powershellgallery.com/profiles/PSModule.io" +name = "PSModule on the PowerShell Gallery" + +[[project.extra.social]] +icon = "fontawesome/brands/github" +link = "https://github.com/PSModule/" +name = "PSModule on GitHub" From f4fc1ca8363f522109415d3e9621e10506f247fd Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Sun, 9 Aug 2026 00:58:01 +0200 Subject: [PATCH 02/14] ci: publish documentation site and catalog Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .github/actions/update-index/action.yml | 19 + .github/actions/update-index/src/Helper.psm1 | 987 ++++++++++++++++++ .github/actions/update-index/src/main.ps1 | 23 + .../templates/module-catalog/v1-row.html | 17 + .../templates/module-catalog/v1-table.html | 9 + .../templates/module-catalog/v2-row.html | 9 + .../templates/module-catalog/v2-table.html | 12 + .github/workflows/Docs.yml | 112 ++ 8 files changed, 1188 insertions(+) create mode 100644 .github/actions/update-index/action.yml create mode 100644 .github/actions/update-index/src/Helper.psm1 create mode 100644 .github/actions/update-index/src/main.ps1 create mode 100644 .github/actions/update-index/templates/module-catalog/v1-row.html create mode 100644 .github/actions/update-index/templates/module-catalog/v1-table.html create mode 100644 .github/actions/update-index/templates/module-catalog/v2-row.html create mode 100644 .github/actions/update-index/templates/module-catalog/v2-table.html create mode 100644 .github/workflows/Docs.yml diff --git a/.github/actions/update-index/action.yml b/.github/actions/update-index/action.yml new file mode 100644 index 00000000..2eb4c755 --- /dev/null +++ b/.github/actions/update-index/action.yml @@ -0,0 +1,19 @@ +name: Update index +description: Run repository index update script via GitHub-Script. +inputs: + ClientID: + description: GitHub App client ID used by GitHub-Script. + required: true + PrivateKey: + description: GitHub App private key used by GitHub-Script. + required: true +runs: + using: composite + steps: + - name: Update index + uses: PSModule/GitHub-Script@8083ec1f733f00357ee4d0db0c6056686e483bc0 # v1.9.0 + with: + ClientID: ${{ inputs.ClientID }} + PrivateKey: ${{ inputs.PrivateKey }} + Script: | + ${{ github.action_path }}/src/main.ps1 diff --git a/.github/actions/update-index/src/Helper.psm1 b/.github/actions/update-index/src/Helper.psm1 new file mode 100644 index 00000000..627e3672 --- /dev/null +++ b/.github/actions/update-index/src/Helper.psm1 @@ -0,0 +1,987 @@ +#Requires -Modules GitHub + +[Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSAvoidLongLines', '', + Justification = 'Markdown templates' +)] +[CmdletBinding()] +param() + +function Connect-GitHubAppDefaultContext { + <# + .SYNOPSIS + Ensures a default GitHub App context is active. + + .DESCRIPTION + Uses Connect-GitHubApp with -Default so subsequent GitHub module calls + use an authenticated default context instead of anonymous access. + #> + [CmdletBinding()] + param( + [Parameter()] + [string] $Owner = $env:GITHUB_REPOSITORY_OWNER + ) + + if ([string]::IsNullOrWhiteSpace($Owner)) { + throw 'Owner is required to establish a default GitHub App context.' + } + + $currentContext = Get-GitHubContext -ErrorAction SilentlyContinue + if ($null -ne $currentContext -and $currentContext.AuthType -eq 'App' -and $currentContext.Name -eq $Owner) { + Write-Host "Using existing default GitHub App context [$($currentContext.Name)]" + return + } + + Write-Host "Connecting GitHub App context as default for organization [$Owner]" + Connect-GitHubApp -Organization $Owner -Default +} + +function Show-RepoList { + <# + .SYNOPSIS + Displays a formatted list of repositories for a GitHub organization. + + .DESCRIPTION + Connects to the specified GitHub organization, retrieves all repositories with their + descriptions and custom Type property, then outputs a sorted table grouped by type and name. + + .EXAMPLE + Show-RepoList + + Connects using the current repository's owner and prints a table of all repos with their type. + + .EXAMPLE + Show-RepoList -Owner 'PSModule' + + Connects to the PSModule organization and prints a table of all repos with their type. + #> + [CmdletBinding()] + param( + # Name of the organization to connect to. Defaults to the owner of the current repository. + [Parameter()] + [string] $Owner = $env:GITHUB_REPOSITORY_OWNER + ) + + LogGroup "Connect to organization [$Owner]" { + Connect-GitHubAppDefaultContext -Owner $Owner + Get-GitHubContext | Select-Object * | Format-List | Out-String + } + + LogGroup "Get repositories for organization [$Owner]" { + $rawRepos = Get-GitHubRepository -Owner $Owner + Write-Host "Found $($rawRepos.Count) repositories" + $repos = $rawRepos | ForEach-Object { + $rawRepo = $_ + $rawRepo.CustomProperties | Where-Object { $_.Name -eq 'Type' } | ForEach-Object { + $type = $_.Value + [pscustomobject]@{ + Name = $rawRepo.Name + Owner = $Owner + Type = $type + Description = $rawRepo.Description + DefaultBranch = $rawRepo.DefaultBranch + Stars = $rawRepo.Stargazers + OpenIssuesCount = $rawRepo.OpenIssues + HtmlUrl = $rawRepo.Url + } + } + } | Sort-Object Type, Name + $reposByType = $repos | Group-Object Type | Sort-Object Name + Write-Host 'Repository type distribution:' + $reposByType | ForEach-Object { + Write-Host " - $($_.Name): $($_.Count)" + } + Write-Host 'Repository table preview:' + $repos | Format-Table -AutoSize + } + + $repos +} + +function Update-MDSection { + <# + .SYNOPSIS + Updates a specific section within a markdown file. + + .DESCRIPTION + This function searches for a named section within a markdown file, identified by special comment markers. + It replaces the content between the markers with the provided new content. If the markers are not found, + an error is thrown. The function supports `-WhatIf` and `-Confirm` parameters for safety. + + .EXAMPLE + Update-MDSection -Path '.\profile\README.md' -Name 'MODULE_LIST' -Content 'New module list' + + Output: + ```powershell + (No explicit output, but the markdown file section is updated) + ``` + + Updates the section named 'MODULE_LIST' in the specified markdown file with the provided content. + #> + [outputType([void])] + [CmdletBinding(SupportsShouldProcess)] + param( + # Path to the markdown file where the section should be updated. + [Parameter()] + [string] $Path = 'src\docs\index.md', + + # Name of the section to be updated, used in comment markers. + [Parameter()] + [string] $Name = 'MODULE_LIST', + + # The new content to insert between the section markers. + [Parameter()] + [string] $Content + ) + + Write-Host "Preparing markdown section update [$Name] in [$Path]" + $startSegment = "" + $endSegment = "" + $currentContent = Get-Content -Path $Path + $startIndex = $currentContent.IndexOf($startSegment) + $endIndex = $currentContent.IndexOf($endSegment) + + if ($startIndex -lt 0) { + throw "[$Name] The start comment segment was not found in the file." + } + if ($endIndex -lt 0) { + throw "[$Name] The end comment segment was not found in the file." + } + if ($endIndex -lt $startIndex) { + throw "[$Name] The end comment segment was found before the start comment segment." + } + + $updatedContent = $currentContent[0..$startIndex] + $Content + $currentContent[($endIndex)..($currentContent.Length - 1)] + if ($PSCmdlet.ShouldProcess('Readme section', 'Update')) { + LogGroup "Update markdown section [$Name] in [$Path]" { + Set-Content -Path $Path -Value $updatedContent + Write-Host "Section [$Name] updated in [$Path]" + } + } +} + +function Get-TemplateContent { + <# + .SYNOPSIS + Reads a template file as raw text. + + .DESCRIPTION + Loads a UTF-8 template file from disk and returns the complete content as a string. + #> + [OutputType([string])] + [CmdletBinding()] + param( + [Parameter(Mandatory)] + [string] $Path + ) + + Get-Content -Path $Path -Raw +} + +function Get-PropertyValue { + <# + .SYNOPSIS + Gets the first non-empty property value from an object. + + .DESCRIPTION + Evaluates the provided property names in order and returns the first property + value that exists and is not null/whitespace. Returns the default value otherwise. + #> + [OutputType([object])] + [CmdletBinding()] + param( + [Parameter(Mandatory)] + [object] $InputObject, + + [Parameter(Mandatory)] + [string[]] $Names, + + [Parameter()] + [object] $Default = $null + ) + + foreach ($name in $Names) { + $property = $InputObject.PSObject.Properties[$name] + if ($null -eq $property) { + continue + } + if ($null -eq $property.Value) { + continue + } + $value = [string]$property.Value + if ([string]::IsNullOrWhiteSpace($value)) { + continue + } + return $property.Value + } + + return $Default +} + +function Invoke-GitHubApi { + <# + .SYNOPSIS + Invokes a GitHub REST API GET request. + + .DESCRIPTION + Calls the GitHub module API wrapper, ensures a default GitHub App context + for authenticated requests (unless -Anonymous is used), normalizes wrapped + responses, and treats HTTP 404 as missing data. + #> + [OutputType([object])] + [CmdletBinding()] + param( + [Parameter(Mandatory)] + [string] $Uri, + [Parameter()] + [switch] $Anonymous + ) + + try { + $apiParameters = @{ + Method = 'GET' + Uri = $Uri + ErrorAction = 'Stop' + } + + if ($Anonymous) { + Write-Host "Invoking GitHub API anonymously for [$Uri]" + $apiParameters.Anonymous = $true + } + + $rawResponse = GitHub\Invoke-GitHubAPI @apiParameters + if ($null -eq $rawResponse) { + return $null + } + + if ($rawResponse -is [array] -and $rawResponse.Count -gt 0 -and $rawResponse[0].PSObject.Properties['Response']) { + $payloadItems = @() + foreach ($item in $rawResponse) { + if ($null -eq $item.Response) { + continue + } + + if ($item.Response -is [array]) { + $payloadItems += $item.Response + } else { + $payloadItems += , $item.Response + } + } + + if ($payloadItems.Count -eq 0) { + return $null + } + if ($payloadItems.Count -eq 1) { + return $payloadItems[0] + } + + return $payloadItems + } + + if ($rawResponse.PSObject.Properties['Response']) { + return $rawResponse.Response + } + + return $rawResponse + } catch { + $statusCode = $null + if ($_.Exception.Response) { + $statusCode = $_.Exception.Response.StatusCode.value__ + } + $exceptionMessage = [string]$_.Exception.Message + $errorText = $_ | Out-String + if ( + $statusCode -eq 404 -or + $exceptionMessage -match '\b404\b' -or + $exceptionMessage -match 'Not Found' -or + $errorText -match 'StatusCode\s*:\s*404' -or + $errorText -match '\(404\)' + ) { + return $null + } + + Write-Warning "GitHub API call failed for [$Uri]: $($_.Exception.Message)" + return $null + } +} + +function Get-RepositoryReadmeContent { + <# + .SYNOPSIS + Gets a repository README as plain text. + + .DESCRIPTION + Downloads the repository README from GitHub and decodes the returned + Base64 content to UTF-8 text. + #> + [OutputType([string])] + [CmdletBinding()] + param( + [Parameter(Mandatory)] + [string] $Owner, + [Parameter(Mandatory)] + [string] $Name + ) + + $uri = "https://api.github.com/repos/$Owner/$Name/readme" + $response = Invoke-GitHubApi -Uri $uri + if ($null -eq $response) { + return '' + } + + $encodedContent = Get-PropertyValue -InputObject $response -Names @('content') + if ([string]::IsNullOrWhiteSpace([string]$encodedContent)) { + return '' + } + + $cleanContent = ([string]$encodedContent).Replace("`n", '').Replace("`r", '') + [Text.Encoding]::UTF8.GetString([Convert]::FromBase64String($cleanContent)) +} + +function Get-MarkdownSummary { + <# + .SYNOPSIS + Extracts a short summary from markdown content. + + .DESCRIPTION + Removes common markdown formatting and returns the first non-empty paragraph, + suitable for compact previews. + #> + [OutputType([string])] + [CmdletBinding()] + param( + [Parameter()] + [string] $Markdown + ) + + if ([string]::IsNullOrWhiteSpace($Markdown)) { + return '' + } + + $text = $Markdown + $text = [regex]::Replace($text, '(?s)```.*?```', ' ') + $text = [regex]::Replace($text, '(?m)^!\[[^\]]*\]\([^\)]*\)\s*$', '') + $text = [regex]::Replace($text, '(?m)^#+\s+', '') + $text = [regex]::Replace($text, '\[([^\]]+)\]\([^\)]+\)', '$1') + $text = [regex]::Replace($text, '(?m)^\s*>+\s*', '') + $text = [regex]::Replace($text, '[*_`~]', '') + $text = [regex]::Replace($text, '\r?\n', "`n") + + foreach ($paragraph in ($text -split "`n`n")) { + $candidate = [regex]::Replace($paragraph, '\s+', ' ').Trim() + if (-not [string]::IsNullOrWhiteSpace($candidate)) { + return $candidate + } + } + + '' +} + +function ConvertTo-HtmlAttributeValue { + <# + .SYNOPSIS + Converts text to a safe HTML attribute preview value. + + .DESCRIPTION + Normalizes whitespace, truncates to a max length, and escapes special HTML + characters for use in attributes such as title. + #> + [OutputType([string])] + [CmdletBinding()] + param( + [Parameter()] + [string] $Value, + [Parameter()] + [int] $MaxLength = 160 + ) + + if ([string]::IsNullOrWhiteSpace($Value)) { + return '' + } + + $singleLine = [regex]::Replace($Value, '\s+', ' ').Trim() + if ($singleLine.Length -gt $MaxLength) { + $singleLine = $singleLine.Substring(0, $MaxLength - 1).TrimEnd() + '...' + } + + $singleLine = $singleLine.Replace('&', '&') + $singleLine = $singleLine.Replace('"', '"') + $singleLine = $singleLine.Replace('<', '<') + $singleLine = $singleLine.Replace('>', '>') + $singleLine +} + +function Get-OpenItemCount { + <# + .SYNOPSIS + Gets the open issue or pull request count for a repository. + + .DESCRIPTION + Uses GitHub search API with a repository/type/state query and returns + the total count. + #> + [OutputType([int])] + [CmdletBinding()] + param( + [Parameter(Mandatory)] + [string] $Owner, + [Parameter(Mandatory)] + [string] $Name, + [Parameter(Mandatory)] + [ValidateSet('issue', 'pr')] + [string] $Type + ) + + $query = [uri]::EscapeDataString("repo:$Owner/$Name type:$Type state:open") + $uri = "https://api.github.com/search/issues?q=$query&per_page=1" + $response = Invoke-GitHubApi -Uri $uri + if ($null -eq $response) { + return 0 + } + + [int](Get-PropertyValue -InputObject $response -Names @('total_count') -Default 0) +} + +function Get-RepositoryVersion { + <# + .SYNOPSIS + Gets a repository's latest version identifier. + + .DESCRIPTION + Returns the latest release tag/name when available, otherwise falls back + to the most recent tag, or N/A. + #> + [OutputType([string])] + [CmdletBinding()] + param( + [Parameter(Mandatory)] + [string] $Owner, + [Parameter(Mandatory)] + [string] $Name + ) + + $latestReleaseUri = "https://api.github.com/repos/$Owner/$Name/releases/latest" + $latestRelease = Invoke-GitHubApi -Uri $latestReleaseUri + if ($null -ne $latestRelease) { + $releaseName = Get-PropertyValue -InputObject $latestRelease -Names @('tag_name', 'name') + if (-not [string]::IsNullOrWhiteSpace([string]$releaseName)) { + return [string]$releaseName + } + } + + $latestTagUri = "https://api.github.com/repos/$Owner/$Name/tags?per_page=1" + $latestTags = Invoke-GitHubApi -Uri $latestTagUri + if ($null -eq $latestTags -or $latestTags.Count -eq 0) { + return 'N/A' + } + + [string](Get-PropertyValue -InputObject $latestTags[0] -Names @('name') -Default 'N/A') +} + +function Get-WorkflowReference { + <# + .SYNOPSIS + Gets the Process-PSModule workflow reference used by a repository. + + .DESCRIPTION + Scans common workflow entry files on the default branch and extracts the + `@ref` from `uses: PSModule/Process-PSModule/...@ref` if present. + #> + [OutputType([string])] + [CmdletBinding()] + param( + [Parameter(Mandatory)] + [string] $Owner, + [Parameter(Mandatory)] + [string] $Name, + [Parameter(Mandatory)] + [string] $DefaultBranch + ) + + $encodedRef = [uri]::EscapeDataString($DefaultBranch) + $workflowFolderPath = '.github/workflows' + $workflowFolderUri = "https://api.github.com/repos/$Owner/$Name/contents/${workflowFolderPath}?ref=$encodedRef" + Write-Host "Discovering workflow files under [$workflowFolderPath] for [$Owner/$Name] on [$DefaultBranch]" + $workflowDiscoveryStrategy = 'folder-listing' + $workflowEntries = Invoke-GitHubApi -Uri $workflowFolderUri + if ($null -eq $workflowEntries) { + Write-Host "Workflow folder lookup failed with current auth; retrying anonymously for [$workflowFolderPath]" + $workflowEntries = Invoke-GitHubApi -Uri $workflowFolderUri -Anonymous + } + + $workflowFiles = @() + $prefetchedWorkflowResponses = @{} + if ($null -ne $workflowEntries) { + $workflowEntryItems = @($workflowEntries) + Write-Host "Workflow folder listing returned [$($workflowEntryItems.Count)] item(s)" + $workflowFiles = @( + $workflowEntryItems | + Where-Object { + (Get-PropertyValue -InputObject $_ -Names @('type') -Default '') -eq 'file' -and + [string](Get-PropertyValue -InputObject $_ -Names @('name') -Default '') -match '\.ya?ml$' + } | + Sort-Object { [string](Get-PropertyValue -InputObject $_ -Names @('name') -Default '') } + ) + } + + if ($workflowFiles.Count -eq 0) { + $workflowDiscoveryStrategy = 'canonical-fallback' + Write-Host "Workflow folder listing unavailable or empty; trying canonical workflow files directly" + foreach ($canonicalWorkflowPath in @('.github/workflows/Process-PSModule.yml', '.github/workflows/Process-PSModule.yaml')) { + $canonicalWorkflowUri = "https://api.github.com/repos/$Owner/$Name/contents/${canonicalWorkflowPath}?ref=$encodedRef" + $canonicalWorkflowResponse = Invoke-GitHubApi -Uri $canonicalWorkflowUri + if ($null -eq $canonicalWorkflowResponse) { + $canonicalWorkflowResponse = Invoke-GitHubApi -Uri $canonicalWorkflowUri -Anonymous + } + if ($null -eq $canonicalWorkflowResponse) { + continue + } + + Write-Host "Canonical workflow candidate found: [$canonicalWorkflowPath]" + $prefetchedWorkflowResponses[$canonicalWorkflowPath] = $canonicalWorkflowResponse + $workflowFiles += [pscustomobject]@{ + name = [IO.Path]::GetFileName($canonicalWorkflowPath) + path = $canonicalWorkflowPath + type = 'file' + } + } + } + + if ($workflowFiles.Count -eq 1) { + Write-Host "Single workflow file found: [$([string](Get-PropertyValue -InputObject $workflowFiles[0] -Names @('name') -Default 'unknown'))]" + } else { + Write-Host "Multiple workflow files found: $($workflowFiles.Count)" + $workflowFiles | ForEach-Object { + $workflowName = [string](Get-PropertyValue -InputObject $_ -Names @('name') -Default 'unknown') + Write-Host " - $workflowName" + } + } + Write-Host "Workflow discovery strategy used: [$workflowDiscoveryStrategy]" + + $candidateWorkflowFiles = $workflowFiles + if ($workflowFiles.Count -gt 1) { + $preferredFiles = @( + $workflowFiles | Where-Object { + [string](Get-PropertyValue -InputObject $_ -Names @('name') -Default '') -match '(?i)^process-psmodule\.ya?ml$' + } + ) + if ($preferredFiles.Count -eq 1) { + $preferredFileName = [string](Get-PropertyValue -InputObject $preferredFiles[0] -Names @('name') -Default 'unknown') + Write-Host "Multiple workflows detected; preferring canonical workflow file [$preferredFileName]" + $candidateWorkflowFiles = $preferredFiles + } + } + + $resolvedRefs = @() + $foundWorkflowFile = $false + foreach ($workflowFile in $candidateWorkflowFiles) { + $workflowPath = [string](Get-PropertyValue -InputObject $workflowFile -Names @('path') -Default '') + if ([string]::IsNullOrWhiteSpace($workflowPath)) { + continue + } + + Write-Host "Checking workflow path [$workflowPath] for [$Owner/$Name] on [$DefaultBranch]" + if ($prefetchedWorkflowResponses.ContainsKey($workflowPath)) { + $response = $prefetchedWorkflowResponses[$workflowPath] + } else { + $uri = "https://api.github.com/repos/$Owner/$Name/contents/${workflowPath}?ref=$encodedRef" + $response = Invoke-GitHubApi -Uri $uri + if ($null -eq $response) { + Write-Host "Workflow path lookup failed with current auth; retrying anonymously for [$workflowPath]" + $response = Invoke-GitHubApi -Uri $uri -Anonymous + if ($null -eq $response) { + Write-Host "Workflow path not found: [$workflowPath]" + continue + } + } + } + $foundWorkflowFile = $true + + $content = Get-PropertyValue -InputObject $response -Names @('content') + if ([string]::IsNullOrWhiteSpace([string]$content)) { + Write-Host "Workflow content is empty for path [$workflowPath]" + continue + } + + $decoded = [Text.Encoding]::UTF8.GetString([Convert]::FromBase64String(([string]$content).Replace("`n", '').Replace("`r", ''))) + $processLinePattern = '(?m)^\s*uses:\s*["'']?PSModule/Process-PSModule/.+$' + $processReferencePattern = '(?m)uses:\s*["'']?PSModule/Process-PSModule/.+?@(?[^"''\s#]+)' + $processLines = [regex]::Matches($decoded, $processLinePattern) + if ($processLines.Count -gt 0) { + foreach ($processLine in $processLines) { + Write-Host "Processing workflow line: $($processLine.Value.Trim())" + } + } else { + Write-Host "No Process-PSModule uses line found in [$workflowPath]" + } + + $match = [regex]::Match($decoded, $processReferencePattern) + if ($match.Success) { + $resolvedRef = $match.Groups['ref'].Value + Write-Host "Resolved Process-PSModule ref [$resolvedRef] from [$workflowPath]" + $resolvedRefs += $resolvedRef + } else { + Write-Host "Unable to parse Process-PSModule ref in [$workflowPath]" + } + } + + if (-not $foundWorkflowFile) { + Write-Host "No workflow files could be fetched for [$Owner/$Name]" + return 'N/A' + } + + $uniqueResolvedRefs = @($resolvedRefs | Select-Object -Unique) + if ($uniqueResolvedRefs.Count -eq 1) { + Write-Host "Workflow reference resolution succeeded using strategy [$workflowDiscoveryStrategy]" + return [string]$uniqueResolvedRefs[0] + } + if ($uniqueResolvedRefs.Count -gt 1) { + Write-Host "Multiple Process-PSModule refs resolved for [$Owner/$Name]: $($uniqueResolvedRefs -join ', ')" + Write-Host "Workflow reference resolution failed using strategy [$workflowDiscoveryStrategy]" + return 'N/A' + } + + Write-Host "No Process-PSModule workflow reference found for [$Owner/$Name]" + Write-Host "Workflow reference resolution failed using strategy [$workflowDiscoveryStrategy]" + 'N/A' +} + +function Get-ProcessReferenceStatus { + <# + .SYNOPSIS + Computes status of a Process-PSModule workflow reference. + + .DESCRIPTION + Classifies a workflow reference value relative to the latest available + Process-PSModule version. + #> + [OutputType([string])] + [CmdletBinding()] + param( + [Parameter()] + [string] $Reference, + [Parameter()] + [string] $LatestVersion + ) + + if ([string]::IsNullOrWhiteSpace($Reference) -or $Reference -eq 'N/A') { + return 'not-configured' + } + + if ($Reference -match '^[0-9a-f]{7,40}$') { + return 'sha-pinned' + } + + if ([string]::IsNullOrWhiteSpace($LatestVersion) -or $LatestVersion -eq 'N/A') { + return 'unknown' + } + + $referenceNormalized = $Reference.TrimStart('v') + $latestNormalized = $LatestVersion.TrimStart('v') + if ($referenceNormalized -eq $latestNormalized) { + return 'up-to-date' + } + + $referenceVersion = $null + $latestVersionParsed = $null + $hasReferenceVersion = [version]::TryParse(($referenceNormalized -replace '-.*$'), [ref]$referenceVersion) + $hasLatestVersion = [version]::TryParse(($latestNormalized -replace '-.*$'), [ref]$latestVersionParsed) + if ($hasReferenceVersion -and $hasLatestVersion) { + if ($referenceVersion -lt $latestVersionParsed) { + return 'behind' + } + return 'ahead' + } + + 'behind' +} + +function New-ModuleCatalogPage { + <# + .SYNOPSIS + Creates or updates a generated module catalog page. + + .DESCRIPTION + Builds markdown content from module metadata and writes it to the + target page path. + #> + [CmdletBinding(SupportsShouldProcess)] + param( + [Parameter(Mandatory)] + [string] $Path, + [Parameter(Mandatory)] + [pscustomobject] $ModuleData + ) + + $content = @" +# $($ModuleData.Name) + +> This page is generated from repository metadata by the docs index pipeline. + +- Repository: [$($ModuleData.Owner)/$($ModuleData.Name)](https://github.com/$($ModuleData.Owner)/$($ModuleData.Name)) +- Description: $($ModuleData.Description) +- Version: `$($ModuleData.Version)` +- Process-PSModule: `$($ModuleData.ProcessReference)` (`$($ModuleData.ProcessStatus)`) +- Open issues: [$($ModuleData.Issues)](https://github.com/$($ModuleData.Owner)/$($ModuleData.Name)/issues) +- Open PRs: [$($ModuleData.PullRequests)](https://github.com/$($ModuleData.Owner)/$($ModuleData.Name)/pulls) +- Stars: [$($ModuleData.Stars)](https://github.com/$($ModuleData.Owner)/$($ModuleData.Name)/stargazers) + +## About + +$($ModuleData.About) + +## README + +[View source README](https://github.com/$($ModuleData.Owner)/$($ModuleData.Name)#readme) +"@ + + if ($PSCmdlet.ShouldProcess($Path, 'Write module catalog page')) { + Set-Content -Path $Path -Value $content + } +} + +function Update-ActionList { + <# + .SYNOPSIS + Updates the GitHub Actions list section in the documentation. + + .DESCRIPTION + Generates an HTML table of all GitHub Actions repositories from the module's repo list + and updates the ACTION_LIST section in the GitHub Actions index markdown file. + + .EXAMPLE + Update-ActionList + + Regenerates the action table and writes it to docs\GitHub-Actions\index.md. + #> + [CmdletBinding(SupportsShouldProcess)] + param() + + $actionTableRowTemplate = @' + + {{ NAME_HYPHENED }} + + {{ DESCRIPTION }} +
+ GitHub Issues + GitHub Pull Requests + GitHub Stars + GitHub Watchers + GitHub Forks + + + GitHub release (with filter) + + +'@ + $actionTableRows = '' + $repos | Where-Object { $_.Type -eq 'Action' } | ForEach-Object { + $name_hyphened = ($_.Name).Replace('-', '‑') + $actionTableRow = $actionTableRowTemplate.replace('{{ OWNER }}', $_.Owner) + $actionTableRow = $actionTableRow.replace('{{ NAME }}', $_.Name) + $actionTableRow = $actionTableRow.replace('{{ NAME_HYPHENED }}', $name_hyphened) + $actionTableRow = $actionTableRow.replace('{{ DESCRIPTION }}', $_.Description) + $actionTableRow = $actionTableRow.TrimEnd() + $actionTableRow += [Environment]::NewLine + $actionTableRows += $actionTableRow + } + $actionTable = @" + + + + + + + +$actionTableRows
NameDescriptionVersion
+ +"@ + Update-MDSection -Path '.\src\docs\GitHub-Actions\index.md' -Name 'ACTION_LIST' -Content $actionTable +} + +function Update-ModuleList { + <# + .SYNOPSIS + Updates the PowerShell modules list section in the documentation. + + .DESCRIPTION + Generates an enriched HTML table for PSModule module repositories and writes + repository-specific catalog pages that summarize README and module metadata. + #> + [CmdletBinding(SupportsShouldProcess)] + param( + [Parameter()] + [object[]] $Repos = @() + ) + + if ($Repos.Count -eq 0) { + LogGroup 'Prepare module catalog generation' { + Write-Host 'No repository list was provided, retrieving repositories now' + } + $Repos = Show-RepoList + } + + $moduleCatalogTemplateVersion = 'v2' + $moduleCatalogTemplateFolder = Join-Path (Join-Path $PSScriptRoot '..') 'templates\module-catalog' + $moduleCatalogRowTemplatePath = Join-Path $moduleCatalogTemplateFolder "$moduleCatalogTemplateVersion-row.html" + $moduleCatalogTableTemplatePath = Join-Path $moduleCatalogTemplateFolder "$moduleCatalogTemplateVersion-table.html" + $moduleCatalogRowTemplate = Get-TemplateContent -Path $moduleCatalogRowTemplatePath + $moduleCatalogTableTemplate = Get-TemplateContent -Path $moduleCatalogTableTemplatePath + + $moduleRepos = $Repos | Where-Object { + $_.Type -eq 'Module' -and $_.Owner -eq 'PSModule' + } | Sort-Object Name + $catalogFolderPath = Join-Path 'docs\content\Modules\Catalog' 'Repositories' + if (-not (Test-Path $catalogFolderPath)) { + Write-Host "Creating catalog folder [$catalogFolderPath]" + $null = New-Item -Path $catalogFolderPath -ItemType Directory + } + + $processLatestVersion = Get-RepositoryVersion -Owner 'PSModule' -Name 'Process-PSModule' + $moduleTableRows = '' + LogGroup 'Prepare module catalog generation' { + Write-Host "Module repositories to process: $($moduleRepos.Count)" + Write-Host "Latest Process-PSModule version: $processLatestVersion" + } + + $moduleRepoTotal = $moduleRepos.Count + $moduleRepoIndex = 0 + foreach ($repo in $moduleRepos) { + $moduleRepoIndex++ + $owner = [string](Get-PropertyValue -InputObject $repo -Names @('Owner') -Default 'PSModule') + $name = [string](Get-PropertyValue -InputObject $repo -Names @('Name') -Default '') + if ([string]::IsNullOrWhiteSpace($name)) { + Write-Host "Skipping module at index [$moduleRepoIndex] because the repository name is empty" + continue + } + + LogGroup "Process module [$moduleRepoIndex/$moduleRepoTotal] [$owner/$name]" { + $description = [string](Get-PropertyValue -InputObject $repo -Names @('Description') -Default 'No description available.') + if ([string]::IsNullOrWhiteSpace($description)) { + $description = 'No description available.' + } + + $defaultBranch = [string](Get-PropertyValue -InputObject $repo -Names @('DefaultBranch', 'default_branch') -Default 'main') + if ([string]::IsNullOrWhiteSpace($defaultBranch)) { + $defaultBranch = 'main' + } + + Write-Host "Collecting metadata from branch [$defaultBranch]" + $readmeContent = Get-RepositoryReadmeContent -Owner $owner -Name $name + $aboutSummary = Get-MarkdownSummary -Markdown $readmeContent + if ([string]::IsNullOrWhiteSpace($aboutSummary)) { + $aboutSummary = $description + } + + $titleSummary = ConvertTo-HtmlAttributeValue -Value $aboutSummary + $version = Get-RepositoryVersion -Owner $owner -Name $name + $processReference = Get-WorkflowReference -Owner $owner -Name $name -DefaultBranch $defaultBranch + $processStatus = Get-ProcessReferenceStatus -Reference $processReference -LatestVersion $processLatestVersion + $issues = Get-OpenItemCount -Owner $owner -Name $name -Type issue + $pullRequests = Get-OpenItemCount -Owner $owner -Name $name -Type pr + $stars = [int](Get-PropertyValue -InputObject $repo -Names @('Stars', 'stargazers_count', 'StargazersCount') -Default 0) + + Write-Host "Version [$version], Process ref [$processReference], status [$processStatus]" + Write-Host "Open issues [$issues], open PRs [$pullRequests], stars [$stars]" + + $modulePageFileName = "$name.md" + $modulePagePath = Join-Path $catalogFolderPath $modulePageFileName + $modulePageRelativeLink = "./Repositories/$modulePageFileName" + + $moduleData = [pscustomobject]@{ + Owner = $owner + Name = $name + Description = $description + Version = $version + ProcessReference = $processReference + ProcessStatus = $processStatus + Issues = $issues + PullRequests = $pullRequests + Stars = $stars + About = $aboutSummary + } + New-ModuleCatalogPage -Path $modulePagePath -ModuleData $moduleData + Write-Host "Wrote repository page [$modulePagePath]" + + $moduleTableRow = $moduleCatalogRowTemplate + $moduleTableRow = $moduleTableRow.Replace('{{ MODULE_PAGE_LINK }}', $modulePageRelativeLink) + $moduleTableRow = $moduleTableRow.Replace('{{ TITLE_SUMMARY }}', $titleSummary) + $moduleTableRow = $moduleTableRow.Replace('{{ NAME }}', $name) + $moduleTableRow = $moduleTableRow.Replace('{{ VERSION }}', $version) + $moduleTableRow = $moduleTableRow.Replace('{{ PROCESS_REFERENCE }}', $processReference) + $moduleTableRow = $moduleTableRow.Replace('{{ PROCESS_STATUS }}', $processStatus) + $moduleTableRow = $moduleTableRow.Replace('{{ OWNER }}', $owner) + $moduleTableRow = $moduleTableRow.Replace('{{ ISSUES }}', [string]$issues) + $moduleTableRow = $moduleTableRow.Replace('{{ PULL_REQUESTS }}', [string]$pullRequests) + $moduleTableRow = $moduleTableRow.Replace('{{ STARS }}', [string]$stars) + $moduleTableRows += $moduleTableRow.TrimEnd() + $moduleTableRows += [Environment]::NewLine + } + } + + LogGroup 'Write module catalog table to docs index' { + $moduleTable = $moduleCatalogTableTemplate.Replace('{{ ROWS }}', $moduleTableRows.TrimEnd()) + Update-MDSection -Path '.\docs\content\Modules\Catalog\index.md' -Name 'MODULE_CATALOG' -Content $moduleTable + Write-Host 'Module catalog table update completed' + } +} + +function Update-FunctionAppList { + <# + .SYNOPSIS + Updates the Azure Function Apps list section in the documentation. + + .DESCRIPTION + Generates an HTML table of all Azure Function App repositories from the module's repo list + and updates the FUNCTIONAPP_LIST section in the PowerShell FunctionApps index markdown file. + + .EXAMPLE + Update-FunctionAppList + + Regenerates the function app table and writes it to docs\PowerShell\FunctionApps\index.md. + #> + [CmdletBinding(SupportsShouldProcess)] + param() + + $functionAppTableRowTemplate = @' + + {{ NAME_HYPHENED }} + + {{ DESCRIPTION }} +
+ GitHub Issues + GitHub Pull Requests + GitHub Stars + GitHub Watchers + GitHub Forks + + + GitHub release (with filter) + + +'@ + $functionAppTableRows = '' + $repos | Where-Object { $_.Type -eq 'FunctionApp' } | ForEach-Object { + $name_hyphened = ($_.Name).Replace('-', '‑') + $functionAppTableRow = $functionAppTableRowTemplate.replace('{{ OWNER }}', $_.Owner) + $functionAppTableRow = $functionAppTableRow.replace('{{ NAME }}', $_.Name) + $functionAppTableRow = $functionAppTableRow.replace('{{ NAME_HYPHENED }}', $name_hyphened) + $functionAppTableRow = $functionAppTableRow.replace('{{ DESCRIPTION }}', $_.Description) + $functionAppTableRow = $functionAppTableRow.TrimEnd() + $functionAppTableRow += [Environment]::NewLine + $functionAppTableRows += $functionAppTableRow + } + $functionAppTable = @" + + + + + + + +$functionAppTableRows
NameDescriptionVersion
+ +"@ + + Update-MDSection -Path '.\src\docs\PowerShell\FunctionApps\index.md' -Name 'FUNCTIONAPP_LIST' -Content $functionAppTable +} diff --git a/.github/actions/update-index/src/main.ps1 b/.github/actions/update-index/src/main.ps1 new file mode 100644 index 00000000..dbcdf259 --- /dev/null +++ b/.github/actions/update-index/src/main.ps1 @@ -0,0 +1,23 @@ +Import-Module -Name (Join-Path $PSScriptRoot 'Helper.psm1') + +LogGroup 'Initialize update-index run' { + Write-Host "Starting update-index in [$PSScriptRoot]" +} + +LogGroup 'Collect repositories' { + $repos = Show-RepoList + Write-Host "Repository collection complete: $($repos.Count) records" +} + +LogGroup 'Skipped generators' { + Write-Host 'Update-ActionList is currently disabled in main.ps1' + Write-Host 'Update-FunctionAppList is currently disabled in main.ps1' +} + +LogGroup 'Update module catalog docs' { + Update-ModuleList -Repos $repos +} + +LogGroup 'Finalize update-index run' { + Write-Host 'update-index run completed' +} diff --git a/.github/actions/update-index/templates/module-catalog/v1-row.html b/.github/actions/update-index/templates/module-catalog/v1-row.html new file mode 100644 index 00000000..5350c3ba --- /dev/null +++ b/.github/actions/update-index/templates/module-catalog/v1-row.html @@ -0,0 +1,17 @@ + + + {{ NAME }} + {{ DESCRIPTION }} +
+ GitHub Issues + GitHub Pull Requests + GitHub Stars + GitHub Watchers + GitHub Forks + PowerShell Gallery Downloads + + + GitHub release (with filter) + PowerShell Gallery Version + + diff --git a/.github/actions/update-index/templates/module-catalog/v1-table.html b/.github/actions/update-index/templates/module-catalog/v1-table.html new file mode 100644 index 00000000..932250e0 --- /dev/null +++ b/.github/actions/update-index/templates/module-catalog/v1-table.html @@ -0,0 +1,9 @@ + + + + + + + + +{{ ROWS }}
NameDescriptionVersion
diff --git a/.github/actions/update-index/templates/module-catalog/v2-row.html b/.github/actions/update-index/templates/module-catalog/v2-row.html new file mode 100644 index 00000000..4621275b --- /dev/null +++ b/.github/actions/update-index/templates/module-catalog/v2-row.html @@ -0,0 +1,9 @@ + + + {{ NAME }} + {{ VERSION }} + {{ PROCESS_REFERENCE }}
{{ PROCESS_STATUS }} + {{ ISSUES }} + {{ PULL_REQUESTS }} + {{ STARS }} + diff --git a/.github/actions/update-index/templates/module-catalog/v2-table.html b/.github/actions/update-index/templates/module-catalog/v2-table.html new file mode 100644 index 00000000..2429e73b --- /dev/null +++ b/.github/actions/update-index/templates/module-catalog/v2-table.html @@ -0,0 +1,12 @@ + + + + + + + + + + + +{{ ROWS }}
NameVersionProcess versionIssuesPull requestsStars
diff --git a/.github/workflows/Docs.yml b/.github/workflows/Docs.yml new file mode 100644 index 00000000..359ad2e5 --- /dev/null +++ b/.github/workflows/Docs.yml @@ -0,0 +1,112 @@ +name: Docs + +on: + workflow_dispatch: + schedule: + - cron: '*/5 * * * *' + push: + branches: + - main + paths: + - docs/** + - .github/actions/update-index/** + - .github/workflows/Docs.yml + pull_request: + branches: + - main + paths: + - docs/** + - .github/workflows/Docs.yml + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + lint: + name: Lint + if: github.event_name == 'pull_request' + runs-on: ubuntu-24.04 + permissions: + contents: read + packages: read # super-linter: read packages + statuses: write # super-linter: report status checks + pull-requests: write # super-linter: post PR summary comment + steps: + - name: Checkout repository + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + fetch-depth: 0 + persist-credentials: false + + - name: Lint code base + uses: super-linter/super-linter@4ce20838b8ab83717e78138c5b3a1407148e0918 # v8.7.0 + env: + GITHUB_TOKEN: ${{ github.token }} + ENABLE_GITHUB_ACTIONS_STEP_SUMMARY: true + ENABLE_GITHUB_PULL_REQUEST_SUMMARY_COMMENT: true + SAVE_SUPER_LINTER_SUMMARY: true + VALIDATE_BIOME_LINT: false + VALIDATE_BIOME_FORMAT: false + VALIDATE_JSCPD: false + VALIDATE_JSON_PRETTIER: false + VALIDATE_MARKDOWN_PRETTIER: false + VALIDATE_YAML_PRETTIER: false + VALIDATE_HTML_PRETTIER: false + + build: + name: Build + runs-on: ubuntu-24.04 + permissions: + contents: read + steps: + - name: Checkout repository + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + persist-credentials: false + + - name: Update index + uses: ./.github/actions/update-index + with: + ClientID: ${{ secrets.SCRIBBLER_BOT_CLIENT_ID }} # zizmor: ignore[secrets-outside-env] + PrivateKey: ${{ secrets.SCRIBBLER_BOT_PRIVATE_KEY }} # zizmor: ignore[secrets-outside-env] + + - name: Set up Python + uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0 + with: + python-version: 3.x + + - name: Install Zensical + run: pip install zensical + + - name: Build Zensical project + run: zensical build --clean + working-directory: docs + + - name: Upload pages artifact + uses: actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9 # v5.0.0 + with: + path: docs/site + + publish: + name: Publish + needs: [build, lint] + if: github.event_name != 'pull_request' + runs-on: ubuntu-24.04 + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + permissions: + contents: read + pages: write # deploy to GitHub Pages + id-token: write # OIDC token for actions/deploy-pages + steps: + - name: Configure pages + uses: actions/configure-pages@45bfe0192ca1faeb007ade9deae92b16b8254a0d # v6.0.0 + + - name: Deploy to GitHub Pages + uses: actions/deploy-pages@cd2ce8fcbc39b97be8ca5fce6e763baed58fa128 # v5.0.0 + id: deployment From bf3eb91d64d59dee927548b609c0475ea6d6a721 Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Sun, 9 Aug 2026 01:03:47 +0200 Subject: [PATCH 03/14] ci: scope docs workflow linting Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .github/workflows/Docs.yml | 2 ++ docs/.markdown-lint.yml | 27 +++++++++++++++++++++++++++ 2 files changed, 29 insertions(+) create mode 100644 docs/.markdown-lint.yml diff --git a/.github/workflows/Docs.yml b/.github/workflows/Docs.yml index 359ad2e5..56d989a3 100644 --- a/.github/workflows/Docs.yml +++ b/.github/workflows/Docs.yml @@ -48,6 +48,8 @@ jobs: GITHUB_TOKEN: ${{ github.token }} ENABLE_GITHUB_ACTIONS_STEP_SUMMARY: true ENABLE_GITHUB_PULL_REQUEST_SUMMARY_COMMENT: true + FILTER_REGEX_INCLUDE: '.*(docs/.*|\.github/actions/update-index/.*|\.github/workflows/Docs\.yml)$' + MARKDOWN_CONFIG_FILE: docs/.markdown-lint.yml SAVE_SUPER_LINTER_SUMMARY: true VALIDATE_BIOME_LINT: false VALIDATE_BIOME_FORMAT: false diff --git a/docs/.markdown-lint.yml b/docs/.markdown-lint.yml new file mode 100644 index 00000000..db8977fd --- /dev/null +++ b/docs/.markdown-lint.yml @@ -0,0 +1,27 @@ +########################### +## Markdown Linter rules ## +########################### + +# Linter rules doc: +# - https://github.com/DavidAnson/markdownlint + +############### +# Rules by id # +############### +MD004: false # Unordered list style +MD007: + indent: 2 # Unordered list indentation +MD013: + line_length: 3000 # Line length +MD025: false # Allow front-matter title + visible H1 on docs pages +MD026: + punctuation: '.,;:!。,;:' # List of not allowed +MD029: false # Ordered list item prefix +MD033: false # Allow inline HTML +MD036: false # Emphasis used instead of a heading +MD041: false # First line in file should be a top level heading, PULL_REQUEST_TEMPLATE.md is an exception + +################# +# Rules by tags # +################# +blank_lines: false # Error on blank lines From eb69f2a5a8cf49c5ad9090923f0c2c6e31765a3a Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Sun, 9 Aug 2026 01:08:32 +0200 Subject: [PATCH 04/14] ci: isolate Docs lint rules Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .github/workflows/Docs.yml | 2 +- docs/.github/linters/.codespellrc | 3 + docs/{ => .github/linters}/.markdown-lint.yml | 0 .../linters/.powershell-psscriptanalyzer.psd1 | 57 ++ docs/.github/linters/.textlintrc | 513 ++++++++++++++++++ 5 files changed, 574 insertions(+), 1 deletion(-) create mode 100644 docs/.github/linters/.codespellrc rename docs/{ => .github/linters}/.markdown-lint.yml (100%) create mode 100644 docs/.github/linters/.powershell-psscriptanalyzer.psd1 create mode 100644 docs/.github/linters/.textlintrc diff --git a/.github/workflows/Docs.yml b/.github/workflows/Docs.yml index 56d989a3..31d6f2be 100644 --- a/.github/workflows/Docs.yml +++ b/.github/workflows/Docs.yml @@ -49,7 +49,7 @@ jobs: ENABLE_GITHUB_ACTIONS_STEP_SUMMARY: true ENABLE_GITHUB_PULL_REQUEST_SUMMARY_COMMENT: true FILTER_REGEX_INCLUDE: '.*(docs/.*|\.github/actions/update-index/.*|\.github/workflows/Docs\.yml)$' - MARKDOWN_CONFIG_FILE: docs/.markdown-lint.yml + LINTER_RULES_PATH: docs/.github/linters SAVE_SUPER_LINTER_SUMMARY: true VALIDATE_BIOME_LINT: false VALIDATE_BIOME_FORMAT: false diff --git a/docs/.github/linters/.codespellrc b/docs/.github/linters/.codespellrc new file mode 100644 index 00000000..7858201d --- /dev/null +++ b/docs/.github/linters/.codespellrc @@ -0,0 +1,3 @@ +[codespell] +skip = ./docs/.github/linters +ignore-words-list = afterall,skelton,erformance diff --git a/docs/.markdown-lint.yml b/docs/.github/linters/.markdown-lint.yml similarity index 100% rename from docs/.markdown-lint.yml rename to docs/.github/linters/.markdown-lint.yml diff --git a/docs/.github/linters/.powershell-psscriptanalyzer.psd1 b/docs/.github/linters/.powershell-psscriptanalyzer.psd1 new file mode 100644 index 00000000..f6f9dd00 --- /dev/null +++ b/docs/.github/linters/.powershell-psscriptanalyzer.psd1 @@ -0,0 +1,57 @@ +@{ + Rules = @{ + PSAlignAssignmentStatement = @{ + Enable = $true + CheckHashtable = $true + } + PSAvoidLongLines = @{ + Enable = $true + MaximumLineLength = 150 + } + PSAvoidSemicolonsAsLineTerminators = @{ + Enable = $true + } + PSPlaceCloseBrace = @{ + Enable = $true + NewLineAfter = $false + IgnoreOneLineBlock = $true + NoEmptyLineBefore = $false + } + PSPlaceOpenBrace = @{ + Enable = $true + OnSameLine = $true + NewLineAfter = $true + IgnoreOneLineBlock = $true + } + PSProvideCommentHelp = @{ + Enable = $true + ExportedOnly = $false + BlockComment = $true + VSCodeSnippetCorrection = $false + Placement = 'begin' + } + PSUseConsistentIndentation = @{ + Enable = $true + IndentationSize = 4 + PipelineIndentation = 'IncreaseIndentationForFirstPipeline' + Kind = 'space' + } + PSUseConsistentWhitespace = @{ + Enable = $true + CheckInnerBrace = $true + CheckOpenBrace = $true + CheckOpenParen = $true + CheckOperator = $true + CheckPipe = $true + CheckPipeForRedundantWhitespace = $true + CheckSeparator = $true + CheckParameter = $true + IgnoreAssignmentOperatorInsideHashTable = $true + } + } + ExcludeRules = @( + 'PSAvoidUsingWriteHost', # Write-Host is acceptable in scripts used for docs tooling and guidance. + 'PSMissingModuleManifestField', # This rule is not applicable until the module is built. + 'PSUseToExportFieldsInManifest' + ) +} diff --git a/docs/.github/linters/.textlintrc b/docs/.github/linters/.textlintrc new file mode 100644 index 00000000..db48de80 --- /dev/null +++ b/docs/.github/linters/.textlintrc @@ -0,0 +1,513 @@ +{ + "filters": { + "comments": true + }, + "rules": { + "terminology": { + "defaultTerms": false, + "terms": [ + "Airbnb", + "Android", + "AppleScript", + "AppVeyor", + "AVA", + "BrowserStack", + "Browsersync", + "Codecov", + "CodePen", + "CodeSandbox", + "DefinitelyTyped", + "EditorConfig", + "ESLint", + "GitHub", + "GraphQL", + "GraphiQL", + "iOS", + "JavaScript", + "JetBrains", + "jQuery", + "LinkedIn", + "Lodash", + "MacBook", + "Markdown", + "OpenType", + "PayPal", + "PhpStorm", + "PowerShell", + "PlayStation", + "RubyMine", + "Sass", + "SemVer", + "TypeScript", + "UglifyJS", + "Wasm", + "WebAssembly", + "WebStorm", + "WordPress", + "YouTube", + [ + "Common[ .]js", + "CommonJS" + ], + [ + "JSDocs?", + "JSDoc" + ], + [ + "Node[ .]js", + "Node.js" + ], + [ + "React[ .]js", + "React" + ], + [ + "SauceLabs", + "Sauce Labs" + ], + [ + "StackOverflow", + "Stack Overflow" + ], + [ + "styled ?components", + "styled-components" + ], + [ + "HTTP[ /]2(?:\\.0)?", + "HTTP/2" + ], + [ + "OS X", + "macOS" + ], + [ + "Mac ?OS", + "macOS" + ], + [ + "a npm", + "an npm" + ], + "ECMAScript", + [ + "ES2015", + "ES6" + ], + [ + "ES7", + "ES2016" + ], + "3D", + [ + "3-D", + "3D" + ], + "Ajax", + "API", + "APIs", + "API's", + [ + "(? Date: Sun, 9 Aug 2026 01:14:58 +0200 Subject: [PATCH 05/14] fix: restrict Docs catalog updates to trusted events Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .github/workflows/Docs.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/Docs.yml b/.github/workflows/Docs.yml index 31d6f2be..c48a17a9 100644 --- a/.github/workflows/Docs.yml +++ b/.github/workflows/Docs.yml @@ -71,6 +71,7 @@ jobs: persist-credentials: false - name: Update index + if: github.event_name != 'pull_request' uses: ./.github/actions/update-index with: ClientID: ${{ secrets.SCRIBBLER_BOT_CLIENT_ID }} # zizmor: ignore[secrets-outside-env] @@ -95,7 +96,7 @@ jobs: publish: name: Publish - needs: [build, lint] + needs: build if: github.event_name != 'pull_request' runs-on: ubuntu-24.04 environment: From f471cfb8dcc428f6172db9bb9712c9fef09c5a26 Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Sun, 9 Aug 2026 01:18:31 +0200 Subject: [PATCH 06/14] fix: restrict Docs catalog updates to main Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .github/workflows/Docs.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/Docs.yml b/.github/workflows/Docs.yml index c48a17a9..a7b4737e 100644 --- a/.github/workflows/Docs.yml +++ b/.github/workflows/Docs.yml @@ -71,7 +71,7 @@ jobs: persist-credentials: false - name: Update index - if: github.event_name != 'pull_request' + if: github.event_name != 'pull_request' && github.ref == 'refs/heads/main' uses: ./.github/actions/update-index with: ClientID: ${{ secrets.SCRIBBLER_BOT_CLIENT_ID }} # zizmor: ignore[secrets-outside-env] From 27a51eb28b2861e2b4b079c1c4ae49a62baf6888 Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Sun, 9 Aug 2026 01:24:51 +0200 Subject: [PATCH 07/14] ci: exclude self-contained docs from generic lint Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .github/workflows/Linter.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/Linter.yml b/.github/workflows/Linter.yml index d86a841c..b29c06a1 100644 --- a/.github/workflows/Linter.yml +++ b/.github/workflows/Linter.yml @@ -29,7 +29,7 @@ jobs: uses: super-linter/super-linter@4ce20838b8ab83717e78138c5b3a1407148e0918 # v8.7.0 env: GITHUB_TOKEN: ${{ github.token }} - FILTER_REGEX_EXCLUDE: 'tests/src(TestRepo|WithManifestTestRepo)/src/classes/public/.*\.ps1$' + FILTER_REGEX_EXCLUDE: '(tests/src(TestRepo|WithManifestTestRepo)/src/classes/public/.*\.ps1|docs/.*)$' VALIDATE_BIOME_FORMAT: false VALIDATE_GITHUB_ACTIONS: false VALIDATE_GITHUB_ACTIONS_ZIZMOR: false From a1455e121e45659e53aa7487ae00f9347e2a978d Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Sun, 9 Aug 2026 01:29:45 +0200 Subject: [PATCH 08/14] ci: isolate Docs automation from generic lint Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .github/workflows/Linter.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/Linter.yml b/.github/workflows/Linter.yml index b29c06a1..3fbc8fd7 100644 --- a/.github/workflows/Linter.yml +++ b/.github/workflows/Linter.yml @@ -29,7 +29,7 @@ jobs: uses: super-linter/super-linter@4ce20838b8ab83717e78138c5b3a1407148e0918 # v8.7.0 env: GITHUB_TOKEN: ${{ github.token }} - FILTER_REGEX_EXCLUDE: '(tests/src(TestRepo|WithManifestTestRepo)/src/classes/public/.*\.ps1|docs/.*)$' + FILTER_REGEX_EXCLUDE: '(tests/src(TestRepo|WithManifestTestRepo)/src/classes/public/.*\.ps1|docs/.*|\.github/actions/update-index/.*|\.github/workflows/Docs\.yml)$' VALIDATE_BIOME_FORMAT: false VALIDATE_GITHUB_ACTIONS: false VALIDATE_GITHUB_ACTIONS_ZIZMOR: false From 76bbeeca4a19a28828ac9bb5d8a5e5d73241850d Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Sun, 9 Aug 2026 01:34:24 +0200 Subject: [PATCH 09/14] ci: ignore Docs scope in generic Biome lint Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- biome.json | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 biome.json diff --git a/biome.json b/biome.json new file mode 100644 index 00000000..f9f58a6a --- /dev/null +++ b/biome.json @@ -0,0 +1,10 @@ +{ + "$schema": "https://biomejs.dev/schemas/2.5.0/schema.json", + "files": { + "includes": [ + "**", + "!docs", + "!.github/actions/update-index" + ] + } +} From 771dde8ff39eab596d39356d79ad59a9a0348639 Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Sun, 9 Aug 2026 01:39:36 +0200 Subject: [PATCH 10/14] ci: trigger Docs checks for catalog changes Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .github/workflows/Docs.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/Docs.yml b/.github/workflows/Docs.yml index a7b4737e..6c32f340 100644 --- a/.github/workflows/Docs.yml +++ b/.github/workflows/Docs.yml @@ -16,6 +16,7 @@ on: - main paths: - docs/** + - .github/actions/update-index/** - .github/workflows/Docs.yml concurrency: From 02420bbe73af2f8863f7602fadea3bc312a79a65 Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Sun, 9 Aug 2026 08:46:52 +0200 Subject: [PATCH 11/14] docs: clarify documentation ownership Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- docs/content/Modules/Process-PSModule/index.md | 2 +- docs/content/Modules/Repository-Standard.md | 6 ------ docs/content/PowerShell/DSC/index.md | 6 ++---- docs/content/PowerShell/FunctionApps/index.md | 6 ++---- docs/content/PowerShell/Scripts/index.md | 6 ++---- docs/content/PowerShell/Standard/index.md | 4 ++-- docs/content/Solutions/index.md | 6 ++---- docs/content/Style-Guides/index.md | 6 ++---- docs/content/index.md | 6 +++--- 9 files changed, 16 insertions(+), 32 deletions(-) diff --git a/docs/content/Modules/Process-PSModule/index.md b/docs/content/Modules/Process-PSModule/index.md index 1b507c4b..3108a278 100644 --- a/docs/content/Modules/Process-PSModule/index.md +++ b/docs/content/Modules/Process-PSModule/index.md @@ -2,7 +2,7 @@ Process-PSModule is the module delivery framework used in PSModule repositories. -This section documents how module repositories are formed and how they move from source to published artifact. +This section documents module repository structure and the build and publication lifecycle. ## In this section diff --git a/docs/content/Modules/Repository-Standard.md b/docs/content/Modules/Repository-Standard.md index 851d9f63..28f0fad1 100644 --- a/docs/content/Modules/Repository-Standard.md +++ b/docs/content/Modules/Repository-Standard.md @@ -187,12 +187,6 @@ For PSModule module repositories, the requirements are: - Managed copies of those files are treated as generated distribution artifacts, not repository-specific source. - Standard changes to managed-file content are made in the distribution engine, not by patching generated copies in receiving repositories. -### Migration for existing repositories - -Repositories still aligned to `PSModule/Distributor` (the previous distribution engine) should migrate to Custo as the runtime implementation. - -This page defines the required target state (the file requirements). Runtime migration mechanics and rollout design (repository targeting, update flow, overwrite behavior, and deletion behavior) are owned by Custo and should be documented and executed from the Custo repository. - ## Supply-chain defaults Every module repository must include `.github/dependabot.yml`. Dependabot is part of the repository supply-chain control, not an optional convenience. diff --git a/docs/content/PowerShell/DSC/index.md b/docs/content/PowerShell/DSC/index.md index 696e2c44..94a6fa51 100644 --- a/docs/content/PowerShell/DSC/index.md +++ b/docs/content/PowerShell/DSC/index.md @@ -1,10 +1,8 @@ -# PowerShell DSC (Moved) +# PowerShell DSC -Cross-org infrastructure and PowerShell guidance is now canonical in MSXOrg/docs. +Cross-org infrastructure and PowerShell guidance is maintained in MSXOrg/docs. Use: - [MSX Coding Standards](https://msxorg.github.io/docs/Coding-Standards/) - [MSX Capabilities](https://msxorg.github.io/docs/Capabilities/) - -This page is retained as a migration pointer only. diff --git a/docs/content/PowerShell/FunctionApps/index.md b/docs/content/PowerShell/FunctionApps/index.md index 2a84e569..7208158f 100644 --- a/docs/content/PowerShell/FunctionApps/index.md +++ b/docs/content/PowerShell/FunctionApps/index.md @@ -1,10 +1,8 @@ -# PowerShell Function Apps (Moved) +# PowerShell Function Apps -Cross-org Function App guidance is now canonical in MSXOrg/docs. +Cross-org Function App guidance is maintained in MSXOrg/docs. Use: - [MSX Capabilities](https://msxorg.github.io/docs/Capabilities/) - [MSX Coding Standards](https://msxorg.github.io/docs/Coding-Standards/) - -This page is retained as a migration pointer only. diff --git a/docs/content/PowerShell/Scripts/index.md b/docs/content/PowerShell/Scripts/index.md index caf158c2..a0dbdfe4 100644 --- a/docs/content/PowerShell/Scripts/index.md +++ b/docs/content/PowerShell/Scripts/index.md @@ -1,9 +1,7 @@ -# PowerShell Scripts (Moved) +# PowerShell Scripts -Cross-org PowerShell script standards are now canonical in MSXOrg/docs. +Cross-org PowerShell script standards are maintained in MSXOrg/docs. Use: - [MSX Coding Standards / PowerShell / Scripts](https://msxorg.github.io/docs/Coding-Standards/PowerShell/Scripts/) - -This page is retained as a migration pointer only. diff --git a/docs/content/PowerShell/Standard/index.md b/docs/content/PowerShell/Standard/index.md index e78a99d3..5607b604 100644 --- a/docs/content/PowerShell/Standard/index.md +++ b/docs/content/PowerShell/Standard/index.md @@ -1,6 +1,6 @@ -# PowerShell Standards (Moved) +# PowerShell Standards -Cross-org PowerShell standards are now canonical in MSXOrg/docs. +Cross-org PowerShell standards are maintained in MSXOrg/docs. Use: diff --git a/docs/content/Solutions/index.md b/docs/content/Solutions/index.md index 0970c3bb..786941ba 100644 --- a/docs/content/Solutions/index.md +++ b/docs/content/Solutions/index.md @@ -1,10 +1,8 @@ -# Solutions (Moved) +# Solutions -Reusable solution architecture and system patterns are now canonical in MSXOrg/docs. +Reusable solution architecture and system patterns are maintained in MSXOrg/docs. Use: - [MSX Capabilities](https://msxorg.github.io/docs/Capabilities/) - [MSX Initiatives](https://msxorg.github.io/docs/Initiatives/) - -This page is retained as a migration pointer only. diff --git a/docs/content/Style-Guides/index.md b/docs/content/Style-Guides/index.md index b874da81..44140990 100644 --- a/docs/content/Style-Guides/index.md +++ b/docs/content/Style-Guides/index.md @@ -3,12 +3,10 @@ title: Style Guides description: Coding style guidelines for PSModule repositories. --- -# Style Guides (Moved) +# Style Guides -Style guide content is now canonical in MSXOrg/docs. +Style guide content is maintained in MSXOrg/docs. Use: - [MSX Coding Standards](https://msxorg.github.io/docs/Coding-Standards/) - -This page is retained as a migration pointer only. diff --git a/docs/content/index.md b/docs/content/index.md index 29e82777..9871be2f 100644 --- a/docs/content/index.md +++ b/docs/content/index.md @@ -33,15 +33,15 @@ We empower PowerShell-savvy developers to effortlessly transform their ideas int Where we are headed is tracked as [objectives and initiatives](https://msxorg.github.io/docs/Ways-of-Working/Goal-Setting/). -## What this docs site now owns +## PSModule documentation -This site is now focused on PSModule-org specific documentation: +This site covers PSModule organization-specific documentation: - [Modules](Modules/index.md): module catalog and module-specific standards - [Process-PSModule](Modules/Process-PSModule/index.md): how modules are structured and built - [Template onboarding](Modules/Process-PSModule/template-quickstart.md): how to start from the template -## Cross-org guidance has moved +## Cross-org guidance Cross-org standards and reusable solution documentation are now canonical in [MSXOrg/docs](https://msxorg.github.io/docs/): From c8f5e5a408979e23366808eddfb68eae2d10875a Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Sun, 9 Aug 2026 08:54:47 +0200 Subject: [PATCH 12/14] docs: add workflow reference documentation Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../Modules/Process-PSModule/configuration.md | 315 ++++++++++++++++++ .../Modules/Process-PSModule/design.md | 218 ++++++++++++ .../content/Modules/Process-PSModule/index.md | 12 +- .../media/Process-PSModule.png | Bin 0 -> 153514 bytes .../media/pagesEnvironment.png | Bin 0 -> 26530 bytes .../Process-PSModule/pipeline-stages.md | 297 +++++++++++++++++ .../principles-and-practices.md | 36 ++ .../Process-PSModule/repository-structure.md | 130 ++++++-- .../skipping-framework-tests.md | 205 ++++++++++++ docs/content/Modules/Process-PSModule/spec.md | 153 +++++++++ .../content/Modules/Process-PSModule/usage.md | 307 +++++++++++++++++ .../Process-PSModule/workflow-overview.md | 76 +++++ docs/zensical.toml | 8 + 13 files changed, 1734 insertions(+), 23 deletions(-) create mode 100644 docs/content/Modules/Process-PSModule/configuration.md create mode 100644 docs/content/Modules/Process-PSModule/design.md create mode 100644 docs/content/Modules/Process-PSModule/media/Process-PSModule.png create mode 100644 docs/content/Modules/Process-PSModule/media/pagesEnvironment.png create mode 100644 docs/content/Modules/Process-PSModule/pipeline-stages.md create mode 100644 docs/content/Modules/Process-PSModule/principles-and-practices.md create mode 100644 docs/content/Modules/Process-PSModule/skipping-framework-tests.md create mode 100644 docs/content/Modules/Process-PSModule/spec.md create mode 100644 docs/content/Modules/Process-PSModule/usage.md create mode 100644 docs/content/Modules/Process-PSModule/workflow-overview.md diff --git a/docs/content/Modules/Process-PSModule/configuration.md b/docs/content/Modules/Process-PSModule/configuration.md new file mode 100644 index 00000000..86e1b9e1 --- /dev/null +++ b/docs/content/Modules/Process-PSModule/configuration.md @@ -0,0 +1,315 @@ +--- +title: Configuration +description: The Process-PSModule settings file — every available setting, the full defaults, and worked examples for coverage, rapid testing, linting, and release notes. +--- + +# Configuration + +The workflow is configured using a settings file in the module repository. +The file can be a `JSON`, `YAML`, or `PSD1` file. By default, it will look for `.github/PSModule.yml`. + +The settings listed on this page are the user-facing configuration contract in `.github/PSModule.yml`. During the +Plan phase, Process-PSModule enriches this input into an internal runtime `Settings` object that downstream workflows +consume. Internal runtime paths in workflow docs (for example, `Settings.Publish.Module.Resolution.*`) describe that +enriched inter-workflow contract, not a different authoring format for repository settings files. + +Simple, Standard, and Advanced test profiles are repository conventions, not settings. `.github/PSModule.yml` has no +layout or suite-matrix selector; Process-PSModule [discovers the files under `tests/` recursively](pipeline-stages.md#module-local-test-discovery) +and computes its internal `Settings.Test.Module.Suites` matrix from them. + +Test discovery and change triggering are separate. The default `ImportantFilePatterns` match only `^src/` and +`^README\.md$`, so a test-only change does not enter the important-change build, test, and publish path. Repositories +that need test or automation changes to exercise that path must [add `^tests/` and any relevant settings or workflow +paths](usage.md#customizing-important-file-patterns) while retaining every default they still need. + +The following settings are available in the settings file: + +| Name | Type | Description | Default | +| ----------------------------------------- | --------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------- | +| `Name` | `String` | Name of the module to publish. Defaults to the repository name. | `null` | +| `ImportantFilePatterns` | `Array` | Regular expression patterns that identify important files. Changes matching these patterns trigger build, test, and publish stages. When set, fully replaces the defaults. | `['^src/', '^README\.md$']` | +| `Test.Skip` | `Boolean` | Skip all tests | `false` | +| `Test.Linux.Skip` | `Boolean` | Skip tests on Linux | `false` | +| `Test.MacOS.Skip` | `Boolean` | Skip tests on macOS | `false` | +| `Test.Windows.Skip` | `Boolean` | Skip tests on Windows | `false` | +| `Test.SourceCode.Skip` | `Boolean` | Skip source code tests | `false` | +| `Test.SourceCode.Linux.Skip` | `Boolean` | Skip source code tests on Linux | `false` | +| `Test.SourceCode.MacOS.Skip` | `Boolean` | Skip source code tests on macOS | `false` | +| `Test.SourceCode.Windows.Skip` | `Boolean` | Skip source code tests on Windows | `false` | +| `Test.PSModule.Skip` | `Boolean` | Skip PSModule framework tests | `false` | +| `Test.PSModule.Linux.Skip` | `Boolean` | Skip PSModule framework tests on Linux | `false` | +| `Test.PSModule.MacOS.Skip` | `Boolean` | Skip PSModule framework tests on macOS | `false` | +| `Test.PSModule.Windows.Skip` | `Boolean` | Skip PSModule framework tests on Windows | `false` | +| `Test.Module.Skip` | `Boolean` | Skip module tests | `false` | +| `Test.Module.Linux.Skip` | `Boolean` | Skip module tests on Linux | `false` | +| `Test.Module.MacOS.Skip` | `Boolean` | Skip module tests on macOS | `false` | +| `Test.Module.Windows.Skip` | `Boolean` | Skip module tests on Windows | `false` | +| `Test.TestResults.Skip` | `Boolean` | Skip test result processing | `false` | +| `Test.CodeCoverage.Skip` | `Boolean` | Skip code coverage tests | `false` | +| `Test.CodeCoverage.PercentTarget` | `Integer` | Target code coverage percentage | `0` | +| `Test.CodeCoverage.StepSummaryMode` | `String` | Step summary mode for code coverage reports | `'Missed, Files'` | +| `Build.Skip` | `Boolean` | Skip all build tasks | `false` | +| `Build.Module.Skip` | `Boolean` | Skip module build | `false` | +| `Build.Docs.Skip` | `Boolean` | Skip documentation build | `false` | +| `Build.Docs.ShowSummaryOnSuccess` | `Boolean` | Show super-linter summary on success for documentation linting | `false` | +| `Build.Site.Skip` | `Boolean` | Skip site build | `false` | +| `Publish.Module.Skip` | `Boolean` | Skip module publishing | `false` | +| `Publish.Module.AutoCleanup` | `Boolean` | Automatically clean up old prerelease tags when merging to main or when a PR is abandoned | `true` | +| `Publish.Module.AutoPatching` | `Boolean` | Automatically patch module version | `true` | +| `Publish.Module.IncrementalPrerelease` | `Boolean` | Use incremental prerelease versioning | `true` | +| `Publish.Module.DatePrereleaseFormat` | `String` | Format for date-based prerelease (uses [.NET DateTime format strings](https://learn.microsoft.com/dotnet/standard/base-types/standard-date-and-time-format-strings)) | `''` | +| `Publish.Module.VersionPrefix` | `String` | Prefix for version tags | `'v'` | +| `Publish.Module.MajorLabels` | `String` | Labels indicating a major version bump | `'major, breaking'` | +| `Publish.Module.MinorLabels` | `String` | Labels indicating a minor version bump | `'minor, feature'` | +| `Publish.Module.PatchLabels` | `String` | Labels indicating a patch version bump | `'patch, fix'` | +| `Publish.Module.IgnoreLabels` | `String` | Labels indicating no release | `'NoRelease'` | +| `Publish.Module.UsePRTitleAsReleaseName` | `Boolean` | Use the PR title as the GitHub release name instead of version string | `false` | +| `Publish.Module.UsePRBodyAsReleaseNotes` | `Boolean` | Use the PR body as the release notes content | `true` | +| `Publish.Module.UsePRTitleAsNotesHeading` | `Boolean` | Prepend PR title as H1 heading with PR number link before the body | `true` | +| `Linter.Skip` | `Boolean` | Skip repository linting | `false` | +| `Linter.ShowSummaryOnSuccess` | `Boolean` | Show super-linter summary on success for repository linting | `false` | +| `Linter.env` | `Object` | Environment variables for super-linter configuration | `{}` | + +
+`PSModule.yml` with all defaults + +```yaml +Name: null + +ImportantFilePatterns: + - '^src/' + - '^README\.md$' + +Build: + Skip: false + Module: + Skip: false + Docs: + Skip: false + ShowSummaryOnSuccess: false + Site: + Skip: false + +Test: + Skip: false + Linux: + Skip: false + MacOS: + Skip: false + Windows: + Skip: false + SourceCode: + Skip: false + Linux: + Skip: false + MacOS: + Skip: false + Windows: + Skip: false + PSModule: + Skip: false + Linux: + Skip: false + MacOS: + Skip: false + Windows: + Skip: false + Module: + Skip: false + Linux: + Skip: false + MacOS: + Skip: false + Windows: + Skip: false + TestResults: + Skip: false + CodeCoverage: + Skip: false + PercentTarget: 0 + StepSummaryMode: 'Missed, Files' + +Publish: + Module: + Skip: false + AutoCleanup: true + AutoPatching: true + IncrementalPrerelease: true + DatePrereleaseFormat: '' + VersionPrefix: 'v' + MajorLabels: 'major, breaking' + MinorLabels: 'minor, feature' + PatchLabels: 'patch, fix' + IgnoreLabels: 'NoRelease' + UsePRTitleAsReleaseName: false + UsePRBodyAsReleaseNotes: true + UsePRTitleAsNotesHeading: true + +Linter: + Skip: false + ShowSummaryOnSuccess: false + env: {} +``` + +
+ +## Example 1 - Defaults with Code Coverage target + +This example runs all steps and will require that code coverage is 80% before passing. + +```yaml +Test: + CodeCoverage: + PercentTarget: 80 +``` + +## Example 2 - Rapid testing + +This example ends up running Plan, Build-Module and Test-Module (tests from the module repo) on **ubuntu-latest** only. + +```yaml +Test: + SourceCode: + Skip: true + PSModule: + Skip: true + Module: + MacOS: + Skip: true + Windows: + Skip: true + TestResults: + Skip: true + CodeCoverage: + Skip: true +Build: + Docs: + Skip: true +``` + +## Example 3 - Configuring the Repository Linter + +The workflow uses [super-linter](https://github.com/super-linter/super-linter) to lint your repository code. +The linter runs on pull requests and provides status updates directly in the PR. + +### Disabling the Linter + +You can skip repository linting entirely: + +```yaml +Linter: + Skip: true +``` + +### Configuring Linter Validation Rules + +The workflow supports all environment variables that **super-linter** provides. You can configure these through the `Linter.env` object: + +```yaml +Linter: + env: + # Disable specific validations + VALIDATE_BIOME_FORMAT: false + VALIDATE_BIOME_LINT: false + VALIDATE_GITHUB_ACTIONS_ZIZMOR: false + VALIDATE_JSCPD: false + VALIDATE_JSON_PRETTIER: false + VALIDATE_MARKDOWN_PRETTIER: false + VALIDATE_YAML_PRETTIER: false + + # Or enable only specific validations + VALIDATE_YAML: true + VALIDATE_JSON: true + VALIDATE_MARKDOWN: true +``` + +### Additional Configuration + +Any super-linter environment variable can be set through the `Linter.env` object: + +```yaml +Linter: + env: + LOG_LEVEL: DEBUG + FILTER_REGEX_EXCLUDE: '.*test.*' + VALIDATE_ALL_CODEBASE: false +``` + +### Showing Linter Summary on Success + +By default, the linter only shows a summary when it finds issues. You can enable summary display on successful runs: + +```yaml +Linter: + ShowSummaryOnSuccess: true +``` + +This is useful for reviewing what was checked even when no issues are found. + +**Note:** The `GITHUB_TOKEN` is automatically provided by the workflow to enable status updates in pull requests. + +For a complete list of available environment variables and configuration options, see the +[super-linter environment variables documentation](https://github.com/super-linter/super-linter#environment-variables). + +## Example 4 - Configuring PR-based release notes + +The workflow can automatically generate GitHub release names and notes from your pull request content. +Three parameters control this behavior: + +| Parameter | Description | +|-----------|-------------| +| `UsePRTitleAsReleaseName` | Use the PR title as the GitHub release name instead of the version string | +| `UsePRBodyAsReleaseNotes` | Use the PR body as the release notes content | +| `UsePRTitleAsNotesHeading` | Prepend PR title as H1 heading with PR number link before the body | + +These parameters follow specific precedence rules when building release notes: + +1. **Heading + Body** (`UsePRTitleAsNotesHeading: true` + `UsePRBodyAsReleaseNotes: true`): Creates formatted notes with the PR title as an H1 heading followed by the PR body. The output format is `# PR Title (#123)\n\nPR body content`. Both the PR title and body must be present. +1. **Body only** (`UsePRBodyAsReleaseNotes: true`): Uses the PR body as-is for release notes. Takes effect when heading option is disabled or PR title is missing. +1. **Fallback**: When neither option is enabled or required PR content is missing, GitHub's auto-generated release notes are used via `--generate-notes`. + +### Default configuration (recommended) + +The defaults provide rich release notes with the PR title as a heading: + +```yaml +Publish: + Module: + UsePRTitleAsReleaseName: false + UsePRBodyAsReleaseNotes: true + UsePRTitleAsNotesHeading: true +``` + +This produces release notes like: + +```markdown +# 🚀 Add new authentication feature (#42) + +This PR adds OAuth2 support with the following changes: +- Added `Connect-OAuth2` function +- Updated documentation +``` + +### Version-only release names + +If you prefer version numbers as release names but still want PR-based notes: + +```yaml +Publish: + Module: + UsePRTitleAsReleaseName: false + UsePRBodyAsReleaseNotes: true + UsePRTitleAsNotesHeading: false +``` + +### Auto-generated notes + +To use GitHub's auto-generated release notes instead of PR content: + +```yaml +Publish: + Module: + UsePRTitleAsReleaseName: false + UsePRBodyAsReleaseNotes: false + UsePRTitleAsNotesHeading: false +``` diff --git a/docs/content/Modules/Process-PSModule/design.md b/docs/content/Modules/Process-PSModule/design.md new file mode 100644 index 00000000..cacf3626 --- /dev/null +++ b/docs/content/Modules/Process-PSModule/design.md @@ -0,0 +1,218 @@ +--- +title: Design +description: How Process-PSModule delivers the spec — a single reusable GitHub Actions workflow composing sub-workflows, a settings file contract, and the scenario matrix. +--- + +# Process-PSModule — Design + +The behaviour in the [spec](spec.md) is delivered by a **single reusable GitHub Actions workflow** at `PSModule/Process-PSModule/.github/workflows/workflow.yml`. A repository using the workflow provides a caller workflow and a minimal `.github/PSModule.yml` settings file; everything else uses sensible defaults. + +## Workflow architecture + +### Single entry point + +The reusable workflow accepts a caller workflow and minimal caller configuration: + +```yaml +# .github/workflows/Process-PSModule.yml in the module repository +name: Process-PSModule + +on: + workflow_dispatch: + schedule: + - cron: '0 0 * * *' + pull_request: + branches: + - main + types: + - closed + - opened + - reopened + - synchronize + - labeled + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: write + pull-requests: write + statuses: write + pages: write + id-token: write + +jobs: + Process-PSModule: + uses: PSModule/Process-PSModule/.github/workflows/workflow.yml@v5 + secrets: + APIKey: ${{ secrets.APIKey }} +``` + +### Composed reusable workflows + +The main workflow composes work across specialized reusable workflows, each owning a pipeline stage: + +- **Plan** — reads the settings file and event context, decides what runs, computes the next version +- **Lint-Repository** — validates repository structure and configuration +- **Build-Module** — compiles the module source and versions the manifest +- **Test-SourceCode** — validates source-code style and standards (PSScriptAnalyzer, framework tests) +- **Lint-SourceCode** — runs static analysis on source +- **Test-Module** — runs framework tests and module-local Pester tests in parallel per platform +- **Get-TestResults** — aggregates test results and enforces pass/fail +- **Get-CodeCoverage** — collects coverage from tests and enforces thresholds +- **Publish-Module** — publishes the module to the PowerShell Gallery +- **Publish-Site** — generates and publishes documentation to GitHub Pages + +Each workflow is reusable so it can be tested and versioned independently, invoked by name in the main orchestration workflow. + +## Settings file contract + +The caller provides `.github/PSModule.yml`: + +```yaml +# Minimal example — defaults apply for everything not specified +Linter: + Repository: + Enabled: true + +Build: + Module: + Enabled: true + +Test: + SourceCode: + Enabled: true + PSModule: + Enabled: true + Module: + Enabled: true + CodeCoverage: + Enabled: true + Threshold: 80 + +Publish: + Module: + Enabled: true + Site: + Enabled: true +``` + +The Plan job reads this settings file, enriches it with computed values (phase enables, test matrices, resolved version, release decision), and passes the enriched settings to downstream jobs as a JSON string in workflow outputs. + +### Runtime settings contract + +| Path | Meaning | +| --- | --- | +| `Settings.Linter.Repository.Enabled` | Whether repository linting runs. | +| `Settings.Build.Module.Enabled` | Whether module build runs. | +| `Settings.Test.SourceCode.Enabled` | Whether source-code tests run. | +| `Settings.Test.PSModule.Enabled` | Whether framework tests run. | +| `Settings.Test.Module.Enabled` | Whether module-local tests run. | +| `Settings.Test.TestResults.Enabled` | Whether test-results aggregation runs. | +| `Settings.Test.CodeCoverage.Enabled` | Whether code-coverage gates run. | +| `Settings.Publish.Module.Enabled` | Whether module publication runs. | +| `Settings.Publish.Site.Enabled` | Whether documentation publication runs. | +| `Settings.Test.SourceCode.Suites` | Computed source-code test matrix (platform × test suite). | +| `Settings.Test.PSModule.Suites` | Computed framework test matrix (platform × test suite). | +| `Settings.Test.Module.Suites` | Computed module-local test matrix (platform × test suite). | +| `Settings.Publish.Module.Resolution.Version` | Resolved semantic version (e.g., `v1.2.3`). | +| `Settings.Publish.Module.Resolution.Prerelease` | Whether the version is prerelease. | +| `Settings.Publish.Module.Resolution.FullVersion` | Full version string (e.g., `v1.2.3-pr.1.5`). | +| `Settings.Publish.Module.Resolution.ReleaseType` | `stable`, `prerelease`, or `none`. | +| `Settings.Publish.Module.Resolution.CreateRelease` | Whether to create a GitHub Release. | + +## Scenario matrix + +### Version labeling + +- **Major** — breaking change; bump `MAJOR` in SemVer +- **Minor** — new feature; bump `MINOR` +- **Patch** — bugfix; bump `PATCH` (default if no label) +- **Prerelease** — publish as prerelease, not promoted to latest +- **NoRelease** — run pipeline, skip publication + +Multiple SemVer labels or conflicting labels (e.g., `Major` + `NoRelease`) are rejected and block the merge. + +### Branch types + +- **Main (stable)** — publishes stable releases. A prerelease label publishes a prerelease from `main`. +- **Development** — optional prerelease branch (e.g., `dev`). Each push publishes a prerelease. +- **Feature branch** — optional feature branch. A prerelease label publishes a prerelease for testing. + +### Platform matrix + +Tests run on: + +- **Windows** (latest) +- **Linux** (Ubuntu latest) +- **macOS** (latest) + +Failures on any platform block the build. + +### Test suites + +Each platform runs in parallel: + +- **Source-code tests** — style, naming, structure (PSModule framework) +- **Framework tests** — module structure, common issues (PSModule framework) +- **Module-local tests** — Pester tests written by the module author +- **Linting** — PSScriptAnalyzer rules + +Test results are aggregated into a single pass/fail and reported to the PR. + +## Alternatives considered + +### Monolithic workflow vs. composable reusable workflows + +**Chosen: Composable reusable workflows** + +Each stage of the pipeline is a reusable workflow so it can be tested independently, versioned, and reused across the ecosystem. This trades orchestration complexity for testability and clarity. + +**Alternative: Single monolithic workflow** + +All logic in one workflow file. Pros: simpler to read end-to-end. Cons: harder to test, version, and reuse; changes in one stage risk all stages; every module repo copies the full logic. + +### Settings file format + +**Chosen: YAML with runtime enrichment** + +The caller provides a simple YAML file; the Plan job enriches it with computed values and passes the enriched settings to all downstream jobs. Pros: simple, readable, minimal to start. Cons: only the Plan job computes the settings; other jobs consume them. + +**Alternative: JSON in workflow outputs** + +Settings live only as workflow outputs, computed by Plan. Pros: single source of truth. Cons: harder to read and edit; no local file to inspect. + +### Version computation + +**Chosen: PR label + current version** + +The bump comes from the PR label; the next version is computed as `current_version + bump`. Pros: explicit, git-traceable (the label is recorded in the PR). Cons: must be re-computed if a PR is re-run or the base version changes. + +**Alternative: Conventional Commits** + +Parse commit messages for `feat:`, `fix:`, `BREAKING CHANGE:` to infer the bump. Pros: automatic. Cons: less explicit; easy to forget the convention; harder to override. + +## External dependencies + +The workflow relies on: + +- **[PSModule/Build-PSModule](https://github.com/PSModule/Build-PSModule)** — compiles and versions the module +- **[PSModule/Test-PSModule](https://github.com/PSModule/Test-PSModule)** — runs framework tests and style validation +- **[PSModule/Invoke-ScriptAnalyzer](https://github.com/PSModule/Invoke-ScriptAnalyzer)** — runs PSScriptAnalyzer linting +- **[Pester](https://pester.dev/)** — runs module tests +- **[GitHub Actions](https://github.com/features/actions)** — workflow engine +- **PowerShell Gallery API** — publishes module packages +- **GitHub Pages** — hosts documentation +- **Zensical** — generates documentation from source + +Each is versioned independently; the main workflow pins versions explicitly. + +## Where this connects + +- [Spec](spec.md) — the requirements this design delivers. +- [Pipeline stages](pipeline-stages.md) — detailed breakdown of each job. +- [Usage](usage.md) — how to invoke and configure. +- [Configuration](configuration.md) — the settings file reference. +- [Principles and practices](principles-and-practices.md) — the principles guiding this design. +- [Repository structure](repository-structure.md) — the repo layout the workflow expects. diff --git a/docs/content/Modules/Process-PSModule/index.md b/docs/content/Modules/Process-PSModule/index.md index 3108a278..719532a2 100644 --- a/docs/content/Modules/Process-PSModule/index.md +++ b/docs/content/Modules/Process-PSModule/index.md @@ -2,15 +2,21 @@ Process-PSModule is the module delivery framework used in PSModule repositories. -This section documents module repository structure and the build and publication lifecycle. +This section documents the Process-PSModule specification, design, configuration, implementation, and module repository practices. ## In this section +- [Workflow Overview](workflow-overview.md) +- [Specification](spec.md) +- [Design](design.md) +- [Usage](usage.md) +- [Configuration](configuration.md) +- [Pipeline Stages](pipeline-stages.md) - [Repository Structure](repository-structure.md) +- [Skipping Framework Tests](skipping-framework-tests.md) +- [Principles and Practices](principles-and-practices.md) - [Module Anatomy](module-anatomy.md) - [Module Build Validation](module-build-validation.md) - [Build, Test, Pack, Publish](build-test-pack-publish.md) - [Template Quickstart](template-quickstart.md) - [Module Bootstrap](module-bootstrap.md) - -For broader framework context, see [MSX Frameworks / Process-PSModule](https://msxorg.github.io/docs/Frameworks/Process-PSModule/). diff --git a/docs/content/Modules/Process-PSModule/media/Process-PSModule.png b/docs/content/Modules/Process-PSModule/media/Process-PSModule.png new file mode 100644 index 0000000000000000000000000000000000000000..d3d1f8552a89d1745b21700f0a65eed6c519cc46 GIT binary patch literal 153514 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 literal 0 HcmV?d00001 diff --git a/docs/content/Modules/Process-PSModule/media/pagesEnvironment.png b/docs/content/Modules/Process-PSModule/media/pagesEnvironment.png new file mode 100644 index 0000000000000000000000000000000000000000..59f411d052b340fe39143871c6214ffd7878e021 GIT binary patch literal 26530 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 literal 0 HcmV?d00001 diff --git a/docs/content/Modules/Process-PSModule/pipeline-stages.md b/docs/content/Modules/Process-PSModule/pipeline-stages.md new file mode 100644 index 00000000..c8e5c1eb --- /dev/null +++ b/docs/content/Modules/Process-PSModule/pipeline-stages.md @@ -0,0 +1,297 @@ +--- +title: Pipeline stages +description: The job-by-job breakdown of the Process-PSModule workflow, from Plan through Publish Docs. +--- + +# Pipeline stages + +The Process-PSModule workflow composes its work from a set of reusable jobs. Each +one is described below, in the order it runs, with a link to the workflow that +implements it. + +## Plan + +[workflow](https://github.com/PSModule/Process-PSModule/blob/main/.github/workflows/Plan.yml) + +The Plan job is the single decision point of the workflow. It reads the settings file (`.github/PSModule.yml`), +collects event context from GitHub, and decides what should happen in the rest of the process. Using that +situational awareness, it calculates the next module version. + +The user-facing settings file stays in `.github/PSModule.yml`. The workflow enriches that input into an internal runtime +`Settings` object passed between jobs. In this runtime contract, execution decisions are phase-owned (`*.Enabled`), test +suite matrices are computed under each owning test phase, and resolved version metadata is stored under +`Settings.Publish.Module.Resolution`. The `*.Suites` values are workflow outputs, not authorable layout settings. + +### Internal runtime settings contract + +| Runtime path | Meaning | +| --- | --- | +| `Settings.Linter.Repository.Enabled` | Whether repository linting runs. | +| `Settings.Build.Module.Enabled` | Whether module build runs. | +| `Settings.Test.SourceCode.Enabled` | Whether source-code tests run. | +| `Settings.Test.PSModule.Enabled` | Whether framework tests run. | +| `Settings.Test.Module.BeforeAllEnabled` | Whether setup scripts run before module-local tests. | +| `Settings.Test.Module.MainEnabled` | Whether module-local Pester tests run. | +| `Settings.Test.Module.AfterAllEnabled` | Whether teardown scripts run after module-local tests. | +| `Settings.Test.TestResults.Enabled` | Whether test results aggregation runs. | +| `Settings.Test.CodeCoverage.Enabled` | Whether code coverage aggregation/enforcement runs. | +| `Settings.Publish.Module.Enabled` | Whether module publication/release runs. | +| `Settings.Publish.Site.Enabled` | Whether documentation publication runs. | +| `Settings.Test.SourceCode.Suites` | Computed source-code test suite matrix. | +| `Settings.Test.PSModule.Suites` | Computed framework test suite matrix. | +| `Settings.Test.Module.Suites` | Computed module-local test suite matrix. | +| `Settings.Publish.Module.Resolution.Version` | Resolved semantic version used for build and publish. | +| `Settings.Publish.Module.Resolution.Prerelease` | Whether the resolved version is prerelease. | +| `Settings.Publish.Module.Resolution.FullVersion` | Resolved full version string. | +| `Settings.Publish.Module.Resolution.ReleaseType` | Resolved release classification for this run. | +| `Settings.Publish.Module.Resolution.CreateRelease` | Whether this run creates a release. | + +## Lint-Repository + +[workflow](https://github.com/PSModule/Process-PSModule/blob/main/.github/workflows/Lint-Repository.yml) + +## Build module + +[workflow](https://github.com/PSModule/Process-PSModule/blob/main/.github/workflows/Build-Module.yml) + +- Compiles the module source code into a PowerShell module, stamping the version from `Settings.Publish.Module.Resolution.Version` into the manifest. +- Uploads the built artifact. + +## Test source code + +[workflow](https://github.com/PSModule/Process-PSModule/blob/main/.github/workflows/Test-SourceCode.yml) + +- Tests the source code in parallel (matrix) using: + - [PSModule framework settings for style and standards for source code](https://github.com/PSModule/Test-PSModule?tab=readme-ov-file#sourcecode-tests) +- This produces a JSON-based report that is used by [Get-PesterTestResults](#get-test-results) evaluate the results of the tests. + +The [PSModule - SourceCode tests](https://github.com/PSModule/Process-PSModule/blob/main/scripts/tests/SourceCode/PSModule/PSModule.Tests.ps1) verifies the following coding practices that the framework enforces: + +| ID | Category | Description | +|---------------------|---------------------|--------------------------------------------------------------------------------------------| +| NumberOfProcessors | General | Should use `[System.Environment]::ProcessorCount` instead of `$env:NUMBER_OF_PROCESSORS`. | +| Verbose | General | Should not contain `-Verbose` unless it is explicitly disabled with `:$false`. | +| OutNull | General | Should use `$null = ...` instead of piping output to `Out-Null`. | +| NoTernary | General | Should not use ternary operations to maintain compatibility with PowerShell 5.1 and below. | +| LowercaseKeywords | General | All PowerShell keywords should be written in lowercase. | +| FunctionCount | Functions (Generic) | Each script file should contain exactly one function or filter. | +| FunctionName | Functions (Generic) | Script filenames should match the name of the function or filter they contain. | +| CmdletBinding | Functions (Generic) | Functions should include the `[CmdletBinding()]` attribute. | +| ParamBlock | Functions (Generic) | Functions should have a parameter block (`param()`). | +| FunctionTest | Functions (Public) | All public functions/filters should have corresponding tests. | + +## Lint source code + +[workflow](https://github.com/PSModule/Process-PSModule/blob/main/.github/workflows/Lint-SourceCode.yml) + +- Lints the source code in parallel (matrix) using: + - [PSScriptAnalyzer rules](https://github.com/PSModule/Invoke-ScriptAnalyzer) +- This produces a JSON-based report that is used by [Get-PesterTestResults](#get-test-results) evaluate the results of the linter. + +## Framework test + +[workflow](https://github.com/PSModule/Process-PSModule/blob/main/.github/workflows/Test-Module.yml) + +- Tests and lints the module in parallel (matrix) using: + - [PSModule framework settings for style and standards for modules](https://github.com/PSModule/Test-PSModule?tab=readme-ov-file#module-tests) + - [PSScriptAnalyzer rules](https://github.com/PSModule/Invoke-ScriptAnalyzer) +- This produces a JSON-based report that is used by [Get-PesterTestResults](#get-test-results) evaluate the results of the tests. +- **Code coverage for framework-generated code**: This step collects code coverage for framework-generated + boilerplate. During the [build step](#build-module), [Build-PSModule](https://github.com/PSModule/Build-PSModule) + injects boilerplate code into the compiled `.psm1` file — including type accelerator registration for public classes + and enums, and the `OnRemove` cleanup hook. The framework tests in + [Test-PSModule](https://github.com/PSModule/Test-PSModule) exercise these code paths and produce coverage artifacts + that are aggregated with coverage from [Test-ModuleLocal](#test-module) in the + [Get code coverage](#get-code-coverage) step. This keeps framework-generated lines from counting against the module + author's coverage report. + +## Test module + +[workflow](https://github.com/PSModule/Process-PSModule/blob/main/.github/workflows/Test-ModuleLocal.yml) + +- Imports and tests the module in parallel (matrix) using module-local Pester tests. +- Discovers module-local tests recursively under `tests/`, applying the [per-directory precedence](#module-local-test-discovery) independently at every level. +- Module test files declare a Pester **6.x** requirement via `#Requires -Modules @{ ModuleName = 'Pester'; ModuleVersion = '6.0.0'; MaximumVersion = '6.*' }` — a convention module authors add to each `*.Tests.ps1`, not something this pipeline injects. The [Invoke-Pester](https://github.com/PSModule/Invoke-Pester) action installs a matching `6.x`, so minor and patch updates flow in automatically while a new major stays a deliberate, reviewed change. +- Supports two special workflow phase scripts executed via separate dedicated jobs: + - `tests/BeforeAll.ps1`: Runs once before all module-local test matrix jobs to set up the test environment (e.g., deploy infrastructure, download test data). + - `tests/AfterAll.ps1`: Runs once after all module-local test matrix jobs complete to clean up the test environment (e.g., remove test resources, clean up databases). +- The workflow checks only those exact repository-root paths; phase detection is non-recursive. This is separate from the recursive discovery of ordinary module-local test entries described below; nested files named `BeforeAll.ps1` or `AfterAll.ps1` do not create workflow phases. +- The two phase scripts run with the same environment variables as the tests. +- This produces a JSON-based report that is used by [Get-PesterTestResults](#get-test-results) to evaluate the results of the tests. + +### Module-local test discovery + +Simple, Standard, and Advanced are [documentation profiles](https://msxorg.github.io/docs/Coding-Standards/PowerShell/Testing/#module-test-profiles), not selectable workflow modes. The same discovery engine handles every profile. `.github/PSModule.yml` has no test-layout or suite-matrix setting; `Settings.Test.Module.Suites` is computed internally from the repository files. + +Process-PSModule inspects `tests/` recursively. Within each directory it uses the first matching form: + +1. Exactly one `*.Configuration.ps1`. Discovery fails when a directory contains more than one. When selected, sibling `*.Container.ps1` and `*.Tests.ps1` files are not independently selected. +2. Otherwise, one or more `*.Container.ps1`. When selected, sibling `*.Tests.ps1` files are not independently selected. +3. Otherwise, all `*.Tests.ps1`. + +The selected form takes precedence only in that directory. Child directories are still inspected independently, so a repository may mix configurations, containers, and ordinary test files across different directories. + +Every discovered artifact needs a unique prefix before its first dot because that prefix becomes `TestName`. For example, `Users.Unit.Tests.ps1` and `Users.Integration.Tests.ps1` both become `Users`; use distinct prefixes such as `UsersUnit` and `UsersIntegration`. + +### Setup and Teardown Scripts + +The workflow supports automatic execution of setup and teardown scripts for module tests: + +- `tests/BeforeAll.ps1` and `tests/AfterAll.ps1` are special workflow phase files, not ordinary recursively discovered test entries. +- Each phase is enabled only when its exact file exists at the root of `tests/`. +- If either file is absent, the workflow skips that phase and continues normally. + +#### Setup - `BeforeAll.ps1` + +- Place at the exact repository-root path `tests/BeforeAll.ps1`. +- Runs once before all test matrix jobs to prepare the test environment. +- Deploy test infrastructure, download test data, initialize databases, or configure services. +- Has access to the same environment variables as your tests (secrets, GitHub token, etc.). + +##### Example - `BeforeAll.ps1` + +```powershell +Write-Host "Setting up test environment..." +# Deploy test infrastructure +# Download test data +# Initialize test databases +Write-Host "Test environment ready!" +``` + +#### Teardown - `AfterAll.ps1` + +- Place at the exact repository-root path `tests/AfterAll.ps1`. +- Runs once after all test matrix jobs complete to clean up the test environment. +- Remove test resources, clean up databases, stop services, or upload artifacts. +- Has access to the same environment variables as your tests. + +##### Example - `AfterAll.ps1` + +```powershell +Write-Host "Cleaning up test environment..." +# Remove test resources +# Clean up databases +# Stop services +Write-Host "Cleanup completed!" +``` + +#### Best practices for shared test infrastructure + +Tests run in parallel across multiple OS runners. To avoid rate limits or conflicts from excessive resource creation, +provision shared infrastructure once in `BeforeAll.ps1` and tear it down in `AfterAll.ps1`. Individual test files +should consume the shared infrastructure instead of creating their own. + +##### Use deterministic naming with `$env:GITHUB_RUN_ID` + +Use `$env:GITHUB_RUN_ID` (stable per workflow run, shared across OS runners) to build deterministic resource names. +This lets test files reference shared resources by name without passing state between jobs. + +```powershell +# BeforeAll.ps1 +$os = $env:RUNNER_OS +$id = $env:GITHUB_RUN_ID +$resourceName = "Test-$os-$id" +``` + +Do **not** use `[guid]::NewGuid()` or `Get-Random` for shared resource names — these produce different values on +each runner and cannot be referenced by other jobs. + +##### Clean up stale resources from previous failed runs + +If a previous workflow run failed before teardown completed, stale resources may remain. Start `BeforeAll.ps1` by +removing any resources matching your naming prefix before creating new ones: + +```powershell +# Remove stale resources from previous failed runs +Get-Resources -Filter "Test-$os-*" | Remove-Resource + +# Create fresh shared resources +New-Resource -Name "Test-$os-$id" +``` + +##### Tests reference shared resources — they do not create them + +Test files should fetch the shared resource by its deterministic name, not create new resources: + +```powershell +# Inside a test file +BeforeAll { + $os = $env:RUNNER_OS + $id = $env:GITHUB_RUN_ID + $resource = Get-Resource -Name "Test-$os-$id" +} +``` + +Test-specific ephemeral resources (for example, secrets, variables, or temporary items) can still be created and +cleaned up within each test file. Only long-lived or expensive resources should be shared. + +##### Naming conventions + +Use a consistent naming scheme so that resources are easy to identify and clean up. A recommended pattern: + +| Resource | Pattern | Example | +|-------------------|---------------------------------------|----------------------------| +| Shared resource | `Test-{OS}-{RunID}` | `Test-Linux-1234` | +| Extra resource | `Test-{OS}-{RunID}-{N}` | `Test-Linux-1234-1` | +| Secret / variable | `{TestName}_{OS}_{RunID}` | `Secrets_Linux_1234` | +| Environment | `{TestName}-{OS}-{RunID}` | `Secrets-Linux-1234` | + +When tests use multiple authentication contexts that share the same runner, include a token or context identifier in +the name to avoid collisions (for example, `Test-{OS}-{ContextID}-{RunID}`). + +### Module tests + +The [PSModule - Module tests](https://github.com/PSModule/Process-PSModule/blob/main/scripts/tests/Module/PSModule/PSModule.Tests.ps1) verifies the following coding practices that the framework enforces: + +| Name | Description | +| ------ | ----------- | +| Module Manifest exists | Verifies that a module manifest file is present. | +| Module Manifest is valid | Verifies that the module manifest file is valid. | + +## Get test results + +[workflow](https://github.com/PSModule/Process-PSModule/blob/main/.github/workflows/Get-TestResults.yml) + +- Gathers the test results from the previous steps and creates a summary of the results. +- If any tests have failed, the workflow will fail here. + +## Get code coverage + +[workflow](https://github.com/PSModule/Process-PSModule/blob/main/.github/workflows/Get-CodeCoverage.yml) + +- Gathers the code coverage from the previous steps and creates a summary of the results. +- Aggregates coverage from the [Framework test](#framework-test) step (framework-generated boilerplate) and the + [Test module](#test-module) step (module author code). A command executed in either step counts as covered, so + framework-generated lines do not count against the module author's coverage target. +- If the code coverage is below the target, the workflow will fail here. + +## Publish module + +[workflow](https://github.com/PSModule/Process-PSModule/blob/main/.github/workflows/Publish-Module.yml) + +- Publishes the artifact to the PowerShell Gallery exactly as built — no version mutation. +- Creates a GitHub Release using the version already stamped in the manifest. +- Attaches the built module as a `.zip` asset on the GitHub Release so consumers can download the exact bytes that were tested and pushed to the PowerShell Gallery. +- **Abandoned PR cleanup**: When a PR is closed without merging (abandoned), the workflow automatically cleans up any + prerelease versions and tags that were created for that PR. This ensures that abandoned work doesn't leave orphaned + prereleases in the PowerShell Gallery or repository. This behavior is controlled by the `Publish.Module.AutoCleanup` + setting. + +## Build docs + +[workflow](https://github.com/PSModule/Process-PSModule/blob/main/.github/workflows/Build-Docs.yml) + +- Generates documentation and lints the documentation using: + - [super-linter](https://github.com/super-linter/super-linter). + +## Build site + +[workflow](https://github.com/PSModule/Process-PSModule/blob/main/.github/workflows/Build-Site.yml) + +- Generates a static site using: + - [Zensical](https://zensical.org/). +- Uses `zensical.toml` as the site configuration contract. + +## Publish Docs + +[workflow](https://github.com/PSModule/Process-PSModule/blob/main/.github/workflows/Publish-Docs.yml) diff --git a/docs/content/Modules/Process-PSModule/principles-and-practices.md b/docs/content/Modules/Process-PSModule/principles-and-practices.md new file mode 100644 index 00000000..06e806d2 --- /dev/null +++ b/docs/content/Modules/Process-PSModule/principles-and-practices.md @@ -0,0 +1,36 @@ +--- +title: Principles and practices +description: The versioning, branching, and colocation principles behind Process-PSModule, and the development practices it is compatible with. +--- + +# Principles and practices + +## Linear versioning + +The contribution and release process is based on the idea that a PR is a release, and we only maintain a single linear ancestry of versions, not going +back to patch and update old versions of the modules. This means that if we are on version `2.1.3` of a module and there is a security issue, we only +patch the latest version with a fix, not releasing new versions based on older versions of the module, i.e. not updating the latest 1.x with the +patch. + +## Release and feature branches + +If you need to work forth a bigger release, create a branch representing the release (a release branch) and open a PR towards `main` for this branch. +For each topic or feature to add to the release, open a new branch representing the feature (a feature branch) and open a PR towards the release +branch. Optionally add the `Prerelease` label on the PR for the release branch, to release preview versions before merging and releasing a published +version of the PowerShell module. + +## Colocation of concerns + +Colocate concerns for long-term maintainability. For example, `#Requires -Modules` statements belong in the function files that use them, not in a +central manifest — this makes it immediately visible which functions drive each external dependency, and avoids silent drift between the manifest and +the actual code. Another example is how parameter descriptions are placed as comments in the `param()` block directly above each parameter +declaration, rather than in the comment-based help at the top of the function — this keeps the description next to the code it documents. + +## Compatibility + +The process is compatible with: + +- [Test-Driven Development](https://testdriven.io/test-driven-development/) using [Pester](https://pester.dev) and [PSScriptAnalyzer](https://learn.microsoft.com/powershell/utility-modules/psscriptanalyzer/overview) +- [GitHub Flow specifications](https://docs.github.com/en/get-started/using-github/github-flow) +- [SemVer 2.0.0 specifications](https://semver.org) +- [Continuous Delivery practices](https://en.wikipedia.org/wiki/Continuous_delivery) diff --git a/docs/content/Modules/Process-PSModule/repository-structure.md b/docs/content/Modules/Process-PSModule/repository-structure.md index a69acf2d..a187ef34 100644 --- a/docs/content/Modules/Process-PSModule/repository-structure.md +++ b/docs/content/Modules/Process-PSModule/repository-structure.md @@ -1,26 +1,116 @@ -# Process-PSModule Repository Structure +--- +title: Repository structure +description: The repository and module source layout Process-PSModule expects, and how to declare module dependencies with #Requires -Modules. +--- -A module repository in PSModule follows a predictable structure so the framework can build and validate it consistently. +# Repository structure -## Top-level contract +Process-PSModule expects repositories to follow the staged layout produced by Template-PSModule. The workflow inspects this structure to decide what to compile, document, and publish. -- `src/`: module source input for build -- `tests/`: Pester tests and test helpers -- `examples/`: usage examples for consumers -- `icon/`: module icon assets -- `.github/workflows/Process-PSModule.yml`: caller workflow that invokes the reusable `PSModule/Process-PSModule/.github/workflows/workflow.yml` -- `.github/PSModule.yml`: repository-level framework settings +```plaintext +/ +├── .github/ # Workflow config, doc/site templates, automation policy +│ ├── linters/ # Rule sets applied by shared lint steps +│ │ ├── .markdown-lint.yml # Markdown rules enforced via super-linter +│ │ ├── .powershell-psscriptanalyzer.psd1 # Analyzer profile for test jobs +│ │ └── .textlintrc # Text lint rules surfaced in Build Docs summaries +│ ├── workflows/ # Entry points for the reusable workflow +│ │ └── Process-PSModule.yml # Consumer hook into this workflow bundle +│ ├── CODEOWNERS # Default reviewers enforced by Process-PSModule checks +│ ├── dependabot.yml # Dependency update cadence handled by GitHub +│ ├── zensical.toml # Site config consumed during site builds +│ ├── PSModule.yml # Settings parsed to drive matrices +│ └── release.yml # Release automation template invoked on publish +├── examples/ # Samples referenced in generated documentation +│ └── General.ps1 # Example script ingested by Document-PSModule +├── icon/ # Icon assets linked from manifest and documentation +│ └── icon.png # Default module icon (PNG format) +├── src/ # Module source, see "Module source code structure" below +├── tests/ # Pester suites; the Simple layout is shown +│ ├── AfterAll.ps1 (optional) # Cleanup script for ModuleLocal runs +│ ├── BeforeAll.ps1 (optional) # Setup script for ModuleLocal runs +│ └── .Tests.ps1 # Simple: one root-level module suite +├── .gitattributes # Normalizes line endings across platforms +├── .gitignore # Excludes build artifacts from source control +├── LICENSE # License text surfaced in manifest metadata +└── README.md # Repository overview rendered on GitHub and docs landing +``` -## Source layout overview +The tree shows the [Simple PowerShell test profile](https://msxorg.github.io/docs/Coding-Standards/PowerShell/Testing/#simple), not an exclusive test-file shape. Standard keeps one root-level `tests/.Tests.ps1` file per public function group. Advanced uses recursively discovered subdirectories, and layouts may mix across directories. Process-PSModule defines the exact [per-directory precedence and sibling suppression](pipeline-stages.md#module-local-test-discovery). -- `src/functions/public/`: exported commands -- `src/functions/private/`: internal helpers -- `src/classes/public/`: user-facing classes -- `src/classes/private/`: internal classes -- `src/data/`: static data files -- `src/init/`: import-time initialization code -- `src/formats/`: formatting views -- `src/types/`: type metadata -- `src/variables/`: variables split by visibility +These names describe repository conventions, not settings. `.github/PSModule.yml` does not select a test profile. The optional `tests/BeforeAll.ps1` and `tests/AfterAll.ps1` files are root-only workflow phases and are not discovered recursively. -Detailed coding standards are canonical in [MSX Coding Standards](https://msxorg.github.io/docs/Coding-Standards/). +Key expectations: + +- Keep at least one exported function under `src/functions/public/` and corresponding tests in `tests/` using a [documented test profile](https://msxorg.github.io/docs/Coding-Standards/PowerShell/Testing/#module-test-profiles). +- Keep documentation site configuration in `.github/zensical.toml`. +- Optional folders (`assemblies`, `formats`, `types`, `variables`, and others) are processed automatically when present. +- Markdown files in `src/functions/public` subfolders become documentation pages alongside generated help. +- A group's overview page (`/.md` named after the folder, or `/index.md`) becomes that group's section landing page in the docs navigation. +- The build step compiles `src/` into a root module file and removes the original project layout from the artifact. +- Documentation generation mirrors the `src/functions/public` hierarchy so help content always aligns with source. + +## Module source code structure + +How the module is built. + +```plaintext +├── src/ # Module source compiled and documented by the pipeline +│ ├── assemblies/ # Bundled binaries copied into the build artifact +│ ├── classes/ # Class scripts merged into the root module +│ │ ├── private/ # Internal classes kept out of exports +│ │ │ └── SecretWriter.ps1 # Example internal class implementation +│ │ └── public/ # Public classes exported via type accelerators +│ │ └── Book.ps1 # Example public class documented for consumers +│ ├── data/ # Configuration loaded into `$script:` scope at runtime +│ │ ├── Config.psd1 # Example config surfaced in generated help +│ │ └── Settings.psd1 # Additional configuration consumed on import +│ ├── formats/ # Formatting metadata registered during build +│ │ ├── CultureInfo.Format.ps1xml # Example format included in manifest +│ │ └── Mygciview.Format.ps1xml # Additional format loaded at import +│ ├── functions/ # Function scripts exported by the module +│ │ ├── private/ # Helper functions scoped to the module +│ │ │ ├── Get-InternalPSModule.ps1 # Sample internal helper +│ │ │ └── Set-InternalPSModule.ps1 # Sample internal helper +│ │ └── public/ # Public commands documented and tested +│ │ ├── Category/ # Optional: organize commands into categories +│ │ │ ├── Get-CategoryCommand.ps1 # Command file within category +│ │ │ └── Category.md # Group overview -> section landing page (or index.md) +│ │ ├── Get-PSModuleTest.ps1 # Example command captured by Microsoft.PowerShell.PlatyPS +│ │ ├── New-PSModuleTest.ps1 # Example command exported and tested +│ │ ├── Set-PSModuleTest.ps1 # Example command exported and tested +│ │ └── Test-PSModuleTest.ps1 # Example command exported and tested +│ ├── init/ # Initialization scripts executed during module load +│ │ └── initializer.ps1 # Example init script included in build output +│ ├── modules/ # Nested modules packaged with the compiled output +│ │ └── OtherPSModule.psm1 # Example nested module staged for export +│ ├── scripts/ # Scripts listed in 'ScriptsToProcess' +│ │ └── loader.ps1 # Loader executed when the module imports +│ ├── types/ # Type data merged into the manifest +│ │ ├── DirectoryInfo.Types.ps1xml # Type definition registered on import +│ │ └── FileInfo.Types.ps1xml # Type definition registered on import +│ ├── variables/ # Variable scripts exported by the module +│ │ ├── private/ # Internal variables scoped to the module +│ │ │ └── PrivateVariables.ps1 # Example private variable seed +│ │ └── public/ # Public variables exported and documented +│ │ ├── Moons.ps1 # Example variable surfaced in generated docs +│ │ ├── Planets.ps1 # Example variable surfaced in generated docs +│ │ └── SolarSystems.ps1 # Example variable surfaced in generated docs +│ ├── finally.ps1 # Cleanup script appended to the root module +│ ├── header.ps1 # Optional header injected at the top of the module +│ ├── manifest.psd1 (optional) # Source manifest reused when present +│ └── README.md # Module-level docs ingested by Document-PSModule +``` + +### Declaring module dependencies + +Declare module dependencies using +[`#Requires -Modules`](https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_requires) +statements at the top of function files in `src/functions/public/` or `src/functions/private/` that genuinely require external modules. For modules we build, the default is to avoid third-party module, DLL, and package dependencies when PowerShell, the .NET base class library, or code we own can carry the feature with reasonable effort. +[Build-PSModule](https://github.com/PSModule/Build-PSModule) collects every `#Requires -Modules` declaration across all +source files, de-duplicates the list, and writes it into the `RequiredModules` field of the compiled manifest +automatically. For the full range of supported syntax variants, see the +[about_Requires](https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_requires) +documentation. + +> **Important:** Adding `RequiredModules` to `src/manifest.psd1` is **not** supported for this purpose. Those entries are silently ignored by the build and will not appear in the compiled manifest. Use `#Requires -Modules` in function files instead. diff --git a/docs/content/Modules/Process-PSModule/skipping-framework-tests.md b/docs/content/Modules/Process-PSModule/skipping-framework-tests.md new file mode 100644 index 00000000..8df9b62b --- /dev/null +++ b/docs/content/Modules/Process-PSModule/skipping-framework-tests.md @@ -0,0 +1,205 @@ +--- +title: Skipping framework tests +description: How to skip individual PSModule framework tests on a per-file basis, the available test IDs, and the broader configuration alternatives. +--- + +# Skipping Individual Framework Tests + +The PSModule framework tests run automatically as part of the `Test-Module` and `Test-SourceCode` jobs. While you can skip entire test categories using the configuration settings (e.g., `Test.PSModule.Skip`), you can also skip individual framework tests on a per-file basis when needed. + +## How to Skip Tests + +To skip an individual framework test for a specific file, add a special comment at the top of that file: + +```powershell +#SkipTest:: +``` + +- ``: The unique identifier of the test to skip (see list below) +- ``: A brief explanation of why the test is being skipped + +The skip comment will cause the framework to skip that specific test for that file only, and will log a warning in the build output with the reason provided. + +## Available Framework Tests + +### SourceCode Tests + +These tests run against your source code files in the `src` directory: + +| Test ID | Description | Example Skip Comment | +|---------|-------------|---------------------| +| `NumberOfProcessors` | Enforces use of `[System.Environment]::ProcessorCount` instead of `$env:NUMBER_OF_PROCESSORS` | `#SkipTest:NumberOfProcessors:Legacy code compatibility required` | +| `Verbose` | Ensures code does not pass `-Verbose` to other commands (which would override user preference), unless explicitly disabled with `-Verbose:$false` | `#SkipTest:Verbose:Required for debugging output` | +| `OutNull` | Enforces use of `$null = ...` instead of `... \| Out-Null` for better performance | `#SkipTest:OutNull:Pipeline processing required` | +| `NoTernary` | Prohibits ternary operators for PowerShell 5.1 compatibility (this test is skipped by default in the framework) | `#SkipTest:NoTernary:PowerShell 7+ only module` | +| `LowercaseKeywords` | Ensures all PowerShell keywords are lowercase | `#SkipTest:LowercaseKeywords:Generated code` | +| `FunctionCount` | Ensures each file contains exactly one function | `#SkipTest:FunctionCount:Helper functions included` | +| `FunctionName` | Ensures the filename matches the function name | `#SkipTest:FunctionName:Legacy naming convention` | +| `CmdletBinding` | Requires all functions to have `[CmdletBinding()]` attribute | `#SkipTest:CmdletBinding:Simple helper function` | +| `ParamBlock` | Requires all functions to have a `param()` block | `#SkipTest:ParamBlock:No parameters needed` | +| `FunctionTest` | Ensures all public functions have corresponding tests | `#SkipTest:FunctionTest:Test in development` | + +### Module Tests + +These tests run against the compiled module in the `outputs/module` directory: + +- Module import validation +- Module manifest validation + +Module tests typically don't need to be skipped as they validate the final built module. + +## Example Usage + +Here's an example of a function file that skips the `FunctionCount` test because it includes helper functions: + +```powershell +#SkipTest:FunctionCount:This file contains helper functions for the main function + +function Get-ComplexData { + <# + .SYNOPSIS + Get formatted data from a file. + + .DESCRIPTION + Read data from a file and format it as a structured object. + + .EXAMPLE + Get-ComplexData -Path '.\data.txt' + + Get the file content and its character count. + + .INPUTS + None + + You can't pipe objects to Get-ComplexData. + + .OUTPUTS + System.Management.Automation.PSCustomObject + + The formatted file data. + + .NOTES + This file intentionally skips only the FunctionCount framework test. + + .LINK + https://psmodule.io//Functions/Get-ComplexData + #> + [OutputType([PSCustomObject])] + [CmdletBinding()] + param( + # The path to the data file. + [Parameter(Mandatory)] + [ValidateNotNullOrEmpty()] + [string] $Path + ) + + $data = Get-RawData -Path $Path + Format-ComplexData -Data $data +} + +function Get-RawData { + <# + .SYNOPSIS + Get unformatted data from a file. + + .DESCRIPTION + Read the complete content of a data file as one string. + + .EXAMPLE + Get-RawData -Path '.\data.txt' + + Get the complete content of the data file. + + .INPUTS + None + + You can't pipe objects to Get-RawData. + + .OUTPUTS + System.String + + The unformatted file content. + + .NOTES + This function is a private helper for Get-ComplexData. + + .LINK + https://psmodule.io//Functions/Get-ComplexData + #> + [OutputType([string])] + [CmdletBinding()] + param( + # The path to the data file. + [Parameter(Mandatory)] + [ValidateNotNullOrEmpty()] + [string] $Path + ) + + Get-Content -LiteralPath $Path -Raw +} + +function Format-ComplexData { + <# + .SYNOPSIS + Format raw data as a structured object. + + .DESCRIPTION + Add useful metadata to raw data while preserving its content. + + .EXAMPLE + Format-ComplexData -Data 'example' + + Format the string and include its character count. + + .INPUTS + None + + You can't pipe objects to Format-ComplexData. + + .OUTPUTS + System.Management.Automation.PSCustomObject + + The formatted data and its character count. + + .NOTES + This function is a private helper for Get-ComplexData. + + .LINK + https://psmodule.io//Functions/Get-ComplexData + #> + [OutputType([PSCustomObject])] + [CmdletBinding()] + param( + # The raw content to format. + [Parameter(Mandatory)] + [ValidateNotNullOrEmpty()] + [string] $Data + ) + + [PSCustomObject] @{ + Content = $Data + CharacterCount = $Data.Length + } +} +``` + +Replace `` with the module's published name. If the public function belongs to a group, insert `/` between `Functions/` and `Get-ComplexData`. + +The skip exempts only `FunctionCount`. Every function in the file must still follow the [PowerShell function standard](https://msxorg.github.io/docs/Coding-Standards/PowerShell/Functions/), including complete comment-based help, matching `[OutputType()]` and `.OUTPUTS` metadata, typed parameters, and implicit output. + +## Best Practices + +- **Use skip comments sparingly**: Framework tests exist to maintain code quality and consistency. Only skip tests when absolutely necessary. +- **Provide clear reasons**: Always include a meaningful explanation in the skip comment to help reviewers understand why the test is being skipped. +- **Consider alternatives**: Before skipping a test, consider whether refactoring the code to comply with the test would be better for long-term maintainability. +- **Document exceptions**: If you skip a test, document the reason in your PR description or code comments. + +## Related Configuration + +For broader test control, use the configuration file settings: + +- Skip all framework tests: `Test.PSModule.Skip: true` +- Skip only source code tests: `Test.SourceCode.Skip: true` +- Skip framework tests on specific OS: `Test.PSModule.Windows.Skip: true` + +See the [Configuration](configuration.md) section for more details. diff --git a/docs/content/Modules/Process-PSModule/spec.md b/docs/content/Modules/Process-PSModule/spec.md new file mode 100644 index 00000000..bb4230ff --- /dev/null +++ b/docs/content/Modules/Process-PSModule/spec.md @@ -0,0 +1,153 @@ +--- +title: Spec +description: Requirements for Process-PSModule — an end-to-end PowerShell module pipeline that guarantees build, testing, quality gates, documentation generation, and versioned publication to package and docs registries. +--- + +# Process-PSModule — Spec + +## Premise + +A PowerShell module's lifecycle — from source code to versioned, published artifact — MUST be reliable, repeatable, and as automated as possible. Contributors focus on code and tests; the pipeline focuses on build, test, quality, documentation, and release. The pipeline MUST be driven entirely by pull-request labels and merge events, never by manual intervention or external tooling. The result is a versioned, immutable artifact — a module package in the PowerShell Gallery and its documentation site — paired with a GitHub Release and a git tag. + +### Principles + +This capability rests on the [MSX principles](https://msxorg.github.io/docs/Ways-of-Working/Principles/): + +- **[Everything as Code](https://msxorg.github.io/docs/Ways-of-Working/Principles/Engineering-Practices/#everything-as-code).** The pipeline and versioning are version-controlled, never a GUI action or manual tag. +- **[Decision before change](https://msxorg.github.io/docs/Ways-of-Working/Principles/AI-First-Development/#decision-before-change).** The pull request is the decision point; its review and labels encode both code acceptance and release intent. +- **[Extensible by default](https://msxorg.github.io/docs/Ways-of-Working/Principles/Software-Design/#extensible-by-default).** The pipeline is technology-agnostic at its core, configurable via a single settings file. + +## Scope + +Applies to any PowerShell module in the PSModule ecosystem that produces a versioned, publishable artifact. The pipeline does not govern module design, naming, or structure — only the lifecycle from source to published, versioned release. + +## Requirements + +### Functional Requirements + +### FR1 — Build the module from source { #fr1 } + +The pipeline MUST compile the PowerShell module source code into a module artifact, stamping it with the resolved semantic version and making it available for downstream testing and publication. + +### FR2 — Run cross-platform tests { #fr2 } + +The pipeline MUST execute the module's test suites against multiple platforms — at minimum Windows, Linux, and macOS — and fail the build if any platform's tests fail. Tests MUST include source-code validation (style, standards), framework tests (module structure, common issues), and module-local tests (user-written Pester tests). + +### FR3 — Enforce code quality and coverage gates { #fr3 } + +The pipeline MUST measure and enforce code coverage thresholds and static-analysis results. A build MUST NOT proceed to publication if quality or coverage targets are missed; the gate MUST prevent merge unless explicitly overridden by a label. + +### FR4 — Generate and publish documentation { #fr4 } + +The pipeline MUST generate module documentation from the source (cmdlet help, README, schema) and publish it to a static documentation site. Documentation MUST be versioned and deployable alongside the module release. + +### FR5 — Support label-driven versioning and publication { #fr5 } + +The pipeline MUST read pull-request labels (`Major`, `Minor`, `Patch`, `Prerelease`, `NoRelease`) to decide the semantic-version bump. It MUST compute the next version automatically, never reading or writing a hand-edited version file. A merge to the release branch MUST trigger publication to the PowerShell Gallery and documentation site; a prerelease label MUST result in a prerelease version available for testing before stable release. + +### FR6 — Produce immutable, linkable releases { #fr6 } + +Each publication MUST produce a GitHub Release, a git tag, and a PowerShell Gallery package version — all linked and versioned together so they are discoverable and pinnable for consumers. + +### Non-Functional Requirements + +### NFR1 — Semantic versioning compliance { #nfr1 } + +Versions MUST follow [SemVer 2.0.0](https://semver.org/) (`vMAJOR.MINOR.PATCH` or `vMAJOR.MINOR.PATCH-prerelease.N`). Breaking changes MUST increment `MAJOR`; new functionality MUST increment `MINOR`; bugfixes MUST increment `PATCH`. Prerelease versions MUST be obtainable but not promoted as the latest stable release. + +### NFR2 — Serialized releases { #nfr2 } + +Only one release process MUST run against a given version of the codebase at a time. Concurrent releases to the same ref MUST be prevented, so the tag, version counter, and published artifact remain consistent. + +### NFR3 — Single production authority { #nfr3 } + +Exactly one branch (typically `main`) MUST be authorized to publish stable releases. All other release branches MUST publish only prerelease versions. This ensures consumers have one unambiguous latest stable version. + +### NFR4 — Rapid feedback on failure { #nfr4 } + +Pipeline failures MUST be visible in the pull request and block merge. Contributors MUST know within minutes whether their changes pass quality and test gates, not hours or days later. + +### NFR5 — Reproducible and auditable { #nfr5 } + +The entire pipeline and its decisions MUST be stored in git, so the build is reproducible and auditable from the commit alone. No external configuration, API calls, or out-of-band decisions. + +## Success Criteria + +### Building and testing + +```gherkin +Scenario: Merge a valid pull request to main + Given a pull request with passing tests and quality gates + When the PR is merged to main + Then the module is built + And all tests pass on all configured platforms + And code coverage meets the configured threshold +``` + +### Version computation + +```gherkin +Scenario: Compute the next version from the PR label + Given a pull request with the label "Minor" + When the PR is merged to main and the current version is v1.2.3 + Then the new version is computed as v1.3.0 + +Scenario: Reject ambiguous version labels + Given a pull request with both "Major" and "Minor" labels + When the merge is attempted + Then the build fails and the merge is blocked +``` + +### Publication + +```gherkin +Scenario: Publish a module after a stable release + Given a merged PR to main with a version bump label + When the build completes successfully + Then a new version is published to the PowerShell Gallery + And a GitHub Release is created + And a git tag is pushed + And the documentation site is updated +``` + +### Prerelease workflow + +```gherkin +Scenario: Publish a prerelease version + Given a pull request with the label "Prerelease" + When the PR runs the pipeline + Then a prerelease version is published (e.g., v1.2.3-pr.1.N) + And it is available for testing before the PR is merged + +Scenario: Promote a prerelease to stable + Given a prerelease PR that is merged to main with a version label + When the PR is merged + Then a stable version is computed (e.g., v1.3.0) based on the label and the current main version + And the stable version is published +``` + +### Failure handling + +```gherkin +Scenario: Block merge on quality gate failure + Given a pull request failing code coverage requirements + When the PR is attempted to be merged + Then the build fails + And the merge is blocked + +Scenario: Handle documentation generation failure + Given a PR that breaks documentation generation + When the pipeline runs + Then the build fails + And feedback is provided in the PR + And the module is not published +``` + +## Where this connects + +- [Design](design.md) — how these requirements are delivered. +- [Pipeline stages](pipeline-stages.md) — the job-by-job breakdown of the workflow. +- [Usage](usage.md) — how to invoke and configure the workflow. +- [Configuration](configuration.md) — the settings file and its options. +- [Principles and practices](principles-and-practices.md) — versioning, branching, and development practices. +- [Documentation Model](https://msxorg.github.io/docs/Ways-of-Working/Documentation-Model/) — why this spec holds only the why and what. diff --git a/docs/content/Modules/Process-PSModule/usage.md b/docs/content/Modules/Process-PSModule/usage.md new file mode 100644 index 00000000..a0c4f092 --- /dev/null +++ b/docs/content/Modules/Process-PSModule/usage.md @@ -0,0 +1,307 @@ +--- +title: Usage +description: How to call the Process-PSModule workflow — inputs, secrets, permissions, the scenario matrix, and important-file change detection. +--- + +# Usage + +To use the workflow, create a new file in the `.github/workflows` directory of the module repository and add the following content. +For documentation site generation, use `zensical.toml` as the active site contract. + +
    +Workflow suggestion + +```yaml +name: Process-PSModule + +on: + workflow_dispatch: + schedule: + - cron: '0 0 * * *' + pull_request: + branches: + - main + types: + - closed + - opened + - reopened + - synchronize + - labeled + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: write + pull-requests: write + statuses: write + pages: write + id-token: write + +jobs: + Process-PSModule: + uses: PSModule/Process-PSModule/.github/workflows/workflow.yml@v5 + secrets: + APIKey: ${{ secrets.APIKey }} +``` + +
    + +## Inputs + +| Name | Type | Description | Required | Default | +| ---- | ---- | ----------- | -------- | ------- | +| `SettingsPath` | `string` | The path to the settings file. All workflow configuration is controlled through this settings file. | `false` | `.github/PSModule.yml` | +| `Debug` | `boolean` | Enable debug output. | `false` | `false` | +| `Verbose` | `boolean` | Enable verbose output. | `false` | `false` | +| `Version` | `string` | Specifies the version of the GitHub module to be installed. The value must be an exact version. | `false` | `''` | +| `Prerelease` | `boolean` | Whether to use a prerelease version of the 'GitHub' module. | `false` | `false` | +| `WorkingDirectory` | `string` | The path to the root of the repo. | `false` | `'.'` | +| `ImportantFilePatterns` | `string` | Newline-separated list of regular expression patterns that identify important files. Changes matching these patterns trigger build, test, and publish stages. When set, fully replaces the defaults. | `false` | `^src/\n^README\.md$` | + +## Secrets + +The reusable workflow at `.github/workflows/workflow.yml` declares only two workflow-call secrets, +which keeps the calling workflow in full control of the credentials that are exposed. +`secrets: inherit` is intentionally not required. + +| Name | Location | Description | Required | +| ---- | -------- | ----------- | -------- | +| `APIKey` | GitHub secrets | The API key for the PowerShell Gallery, used to publish the module. | Yes | +| `TestData` | GitHub secrets | A single-line JSON object with `secrets` and `variables` maps, exposed as environment variables to the module test jobs. Values under `secrets` are masked; values under `variables` are not. | No | + +### Breaking change: fixed test secrets use `TestData` + +The reusable workflow accepts test data through `TestData` and no longer declares or accepts the old fixed test-secret inputs: + +- `TEST_APP_ENT_CLIENT_ID` +- `TEST_APP_ENT_PRIVATE_KEY` +- `TEST_APP_ORG_CLIENT_ID` +- `TEST_APP_ORG_PRIVATE_KEY` +- `TEST_USER_ORG_FG_PAT` +- `TEST_USER_USER_FG_PAT` +- `TEST_USER_PAT` + +If a caller passed any of these secrets directly, place them in the `secrets` map inside `TestData`. +The environment variable names used by the tests can stay the same; only the workflow-call interface +changes: + +```yaml +jobs: + Process-PSModule: + uses: PSModule/Process-PSModule/.github/workflows/workflow.yml@v5 + secrets: + APIKey: ${{ secrets.APIKey }} + TestData: >- + { "secrets": { "TEST_USER_PAT": "${{ secrets.TEST_USER_PAT }}", + "TEST_APP_ORG_CLIENT_ID": "${{ secrets.TEST_APP_ORG_CLIENT_ID }}" } } +``` + +### Passing test phase data (secrets and variables) + +A single `TestData` secret lets a module expose any number of caller-defined values to its test jobs +(`BeforeAll-ModuleLocal`, `Test-ModuleLocal` and `AfterAll-ModuleLocal`) without changing the shared +workflow. It is one JSON object with two maps, so everything the tests need is visible in one place: + +```json +{ "secrets": { "NAME": "value" }, "variables": { "NAME": "value" } } +``` + +Values under `secrets` are masked in the logs; values under `variables` are not. Build it in the +calling workflow and pass it through the `secrets:` block (so the whole blob is masked). Reference each +secret directly as `"${{ secrets. }}"` and each variable as `${{ toJSON(vars.) }}`. A +folded `>-` scalar keeps the source readable while producing a single-line value, as long as the JSON +content lines stay at the same indentation level: + +```yaml +jobs: + Process-PSModule: + uses: PSModule/Process-PSModule/.github/workflows/workflow.yml@v5 + secrets: + APIKey: ${{ secrets.APIKey }} + TestData: >- + { "secrets": { "CONFLUENCE_API_TOKEN": "${{ secrets.CONFLUENCE_API_TOKEN }}" }, + "variables": { "CONFLUENCE_SITE": ${{ toJSON(vars.CONFLUENCE_SITE) }}, + "CONFLUENCE_USERNAME": ${{ toJSON(vars.CONFLUENCE_USERNAME) }}, + "CONFLUENCE_SPACE_KEY": ${{ toJSON(vars.CONFLUENCE_SPACE_KEY) }} } } +``` + +Each entry becomes an environment variable in the test jobs, so the module's Pester tests read the +values directly: + +```powershell +$env:CONFLUENCE_API_TOKEN # from the "secrets" map (masked in logs) +$env:CONFLUENCE_SITE # from the "variables" map (not masked) +``` + +The same `TestData` keys are exported before every module-local phase runs: + +- `BeforeAll-ModuleLocal` runs root `tests/BeforeAll.ps1` before the module-local test matrix. +- `Test-ModuleLocal` discovers and runs module-local Pester tests recursively. +- `AfterAll-ModuleLocal` runs root `tests/AfterAll.ps1` after the module-local test matrix, including cleanup paths. + +Setup and teardown detection is not recursive. These root scripts and the discovered tests should use the same environment variable names. +If `$env:` is available in one phase but missing in another, treat that as a Process-PSModule +propagation bug rather than a caller contract difference. + +Notes: + +- The names are caller-defined; no secret or variable names are hard-coded in the shared workflow. + Names must match `^[A-Za-z_][A-Za-z0-9_]*$` and must not override reserved variables such as `PATH`, + `CI`, `GITHUB_*`, `RUNNER_*` or `ACTIONS_*`. +- The `TestData` validation, masking and environment export logic is shared by the ModuleLocal workflows + through the [`PSModule/Install-PSModuleHelpers`](https://github.com/PSModule/Install-PSModuleHelpers) + action, which installs the `Import-TestData` command each workflow runs to expose the values. +- Reference secrets as `"${{ secrets. }}"` (quoted, directly) rather than + `toJSON(secrets.)`. The direct form keeps CodeQL's *excessive secrets exposure* check happy and + works for single-line secret values. It cannot carry values that contain `"`, `\` or newlines, so + base64-encode a multi-line or special-character secret and decode it in the test (for example + `[Text.Encoding]::UTF8.GetString([Convert]::FromBase64String($env:MY_KEY_B64))`). +- Variables use `toJSON(vars.)` so any characters are JSON-encoded safely; they are never masked. + You can use the same quoted direct form as secrets (`"${{ vars. }}"`) only for simple values + that do not contain `"`, `\` or newlines. +- Provide `TestData` as a single-line value (the folded `>-` block above does this). Avoid a literal + `|` block: GitHub registers every line of a multi-line secret as its own mask, which over-masks + unrelated log output. +- Do not pretty-print `TestData` with nested indentation. YAML preserves more-indented lines inside a + folded scalar, so a fully formatted JSON object can still become a multi-line secret. That makes + GitHub register each line as its own mask, including brace-only lines such as `{`, `}` or `},`, which + can turn unrelated log output into `***`. Keep the compact form above, or keep every JSON content + line at the same indentation level. +- Omit `TestData` entirely when the module needs no secrets or variables. Include only the map you + need (just `secrets`, just `variables`, or both). +- Because `secrets: inherit` is not used, only the values you list are ever exposed. +- If using `secrets: inherit` in a caller workflow, remember that GitHub only forwards secrets that + already exist by name. It does not assemble a `TestData` JSON payload from individual secrets such as + `TEST_USER_PAT`; the caller must still create and pass the `TestData` value explicitly. +- Organization, repository and GitHub *Environment* secrets and variables are supported when they are + visible to the calling job. For environment-scoped values, set `environment:` on the calling job and + explicitly include those values in `TestData`; they are not exposed automatically. + +## Permissions + +The following permissions are needed for the workflow to be able to perform all tasks. + +```yaml +permissions: + contents: write # to checkout the repo and create releases on the repo + pull-requests: write # to write comments to PRs + statuses: write # to update the status of the workflow from linter + pages: write # to deploy to Pages + id-token: write # to verify the Pages deployment originates from an appropriate source +``` + +For more info, see [Deploy GitHub Pages site](https://github.com/marketplace/actions/deploy-github-pages-site). + +## Scenario Matrix + +This table shows when each job runs based on the trigger scenario: + +| Job | Open/Updated PR | Merged PR | Abandoned PR | Manual Run | +| ------------------------- | --------------- | ---------- | ------------ | ---------- | +| **Plan** | ✅ Always | ✅ Always | ✅ Always | ✅ Always | +| **Lint-Repository** | ✅ Yes | ❌ No | ❌ No | ❌ No | +| **Build-Module** | ✅ Yes | ✅ Yes | ❌ No | ✅ Yes | +| **Build-Docs** | ✅ Yes | ✅ Yes | ❌ No | ✅ Yes | +| **Build-Site** | ✅ Yes | ✅ Yes | ❌ No | ✅ Yes | +| **Test-SourceCode** | ✅ Yes | ✅ Yes | ❌ No | ✅ Yes | +| **Lint-SourceCode** | ✅ Yes | ✅ Yes | ❌ No | ✅ Yes | +| **Test-Module** | ✅ Yes | ✅ Yes | ❌ No | ✅ Yes | +| **BeforeAll-ModuleLocal** | ✅ Yes | ✅ Yes | ❌ No | ✅ Yes | +| **Test-ModuleLocal** | ✅ Yes | ✅ Yes | ❌ No | ✅ Yes | +| **AfterAll-ModuleLocal** | ✅ Yes | ✅ Yes | ✅ Yes* | ✅ Yes | +| **Get-TestResults** | ✅ Yes | ✅ Yes | ❌ No | ✅ Yes | +| **Get-CodeCoverage** | ✅ Yes | ✅ Yes | ❌ No | ✅ Yes | +| **Publish-Site** | ❌ No | ✅ Yes | ❌ No | ❌ No | +| **Publish-Module** | ✅ Yes** | ✅ Yes** | ✅ Yes*** | ✅ Yes** | + +- \* Runs for cleanup if tests were started +- \*\* Only when all tests/coverage/build succeed +- \*\*\* Cleans up prerelease versions and tags created for the abandoned PR (when `Publish.Module.AutoCleanup` is + enabled) + +## Important file change detection + +The workflow automatically detects whether a pull request contains changes to "important" files that should enter the +build, test, and publish path. This prevents unnecessary work and releases when only files outside the configured +patterns are modified. + +### Files that trigger the important-change path + +By default, the following regular expression patterns identify important files: + +| Pattern | Description | +| :--- | :---------- | +| `^src/` | Module source code | +| `^README\.md$` | Module documentation | + +### Customizing important file patterns + +To override the default patterns, set `ImportantFilePatterns` in your settings file (`.github/PSModule.yml`): + +```yaml +ImportantFilePatterns: + - '^src/' + - '^README\.md$' + - '^tests/' + - '^\.github/PSModule\.yml$' + - '^\.github/workflows/' +``` + +When configured, the provided list fully replaces the defaults. Include the default patterns in your list if you still +want them to trigger the build, test, and publish path. + +Recursive [module-local test discovery](pipeline-stages.md#module-local-test-discovery) does not change this trigger. +With the defaults, a test-only change does not run the important-change build, test, and publish stages because +`^tests/` is not matched. Add `^tests/` when those changes must exercise the path, plus each settings, workflow, or +other automation path whose changes need the same validation. Include only paths that should trigger all three stages. + +To disable file-change triggering entirely (so that no file changes ever trigger a release), set an empty list in the +settings file: + +```yaml +ImportantFilePatterns: [] +``` + +You can also pass patterns via the workflow input: + +```yaml +jobs: + Process: + uses: PSModule/Process-PSModule/.github/workflows/workflow.yml@v5 + with: + ImportantFilePatterns: | + ^src/ + ^README\.md$ + ^examples/ +``` + +To disable triggering via the workflow input, pass an explicit empty string: + +```yaml +jobs: + process: + uses: PSModule/Process-PSModule/.github/workflows/workflow.yml@v5 + with: + ImportantFilePatterns: '' +``` + +Note that omitting the `ImportantFilePatterns` key entirely causes the workflow's default patterns (`^src/` and +`^README\.md$`) to be used. The settings file takes priority over the workflow input, so set +`ImportantFilePatterns: []` in `.github/PSModule.yml` to disable triggering regardless of the workflow input. + +Resolution order: settings file → workflow input → workflow input default values. + +### Behavior when no important files are changed + +When a pull request does not contain changes to important files: + +1. A comment is automatically added to the PR listing the configured patterns and explaining why build/test stages are + skipped +2. `Settings.Publish.Module.Resolution.ReleaseType` is `None` (and `Settings.Publish.Module.Resolution.CreateRelease` is `false`) +3. Build, test, and publish stages are skipped +4. The PR can still be merged for non-release changes (documentation updates, CI improvements, etc.) + +This behavior ensures that maintenance PRs (such as updating GitHub Actions versions or fixing typos in comments) +don't create unnecessary releases in the PowerShell Gallery. diff --git a/docs/content/Modules/Process-PSModule/workflow-overview.md b/docs/content/Modules/Process-PSModule/workflow-overview.md new file mode 100644 index 00000000..d47451f6 --- /dev/null +++ b/docs/content/Modules/Process-PSModule/workflow-overview.md @@ -0,0 +1,76 @@ +--- +title: Process-PSModule +description: The end-to-end PowerShell module pipeline — what it must guarantee and how it is built, alongside reference documentation for configuration and deployment. +--- + +# Process-PSModule + +An end-to-end PowerShell module pipeline that automates the entire lifecycle of a module: building from source, running cross-platform tests, enforcing code quality and coverage, generating documentation, and publishing the versioned module to the PowerShell Gallery and its documentation site to GitHub Pages. It is the core workflow used across all PowerShell modules in the [PSModule organization](https://github.com/PSModule), ensuring reliable, automated, and maintainable delivery of PowerShell projects. + + + +| Page | Description | +| --- | --- | +| [Spec](spec.md) | Requirements for Process-PSModule — an end-to-end PowerShell module pipeline that guarantees build, testing, quality gates, documentation generation, and versioned publication to package and docs registries. | +| [Design](design.md) | How Process-PSModule delivers the spec — a single reusable GitHub Actions workflow composing sub-workflows, a settings file contract, and the scenario matrix. | +| [Pipeline stages](pipeline-stages.md) | The job-by-job breakdown of the Process-PSModule workflow, from Plan through Publish Docs. | +| [Usage](usage.md) | How to call the Process-PSModule workflow — inputs, secrets, permissions, the scenario matrix, and important-file change detection. | +| [Configuration](configuration.md) | The Process-PSModule settings file — every available setting, the full defaults, and worked examples for coverage, rapid testing, linting, and release notes. | +| [Skipping framework tests](skipping-framework-tests.md) | How to skip individual PSModule framework tests on a per-file basis, the available test IDs, and the broader configuration alternatives. | +| [Repository structure](repository-structure.md) | The repository and module source layout Process-PSModule expects, and how to declare module dependencies with #Requires -Modules. | +| [Principles and practices](principles-and-practices.md) | The versioning, branching, and colocation principles behind Process-PSModule, and the development practices it is compatible with. | + + + +## How to get started + +1. [Create a repository from the Template-Module](https://github.com/new?template_name=Template-PSModule&template_owner=PSModule&description=Add%20a%20description%20(required)&name=%3CModule%20name%3E). +2. Configure the repository: + 1. Enable GitHub Pages in the repository settings. Set it to deploy from **GitHub Actions**. + 2. This will create an environment called `github-pages` that GitHub deploys your site to. +
    Within the github-pages environment, remove the branch protection for main. + Remove the branch protection on main +
    + 3. [Create an API key on the PowerShell Gallery](https://www.powershellgallery.com/account/apikeys). Give it permission to manage the module you + are working on. + 4. Create a new secret called `APIKEY` in the repository and set the API key for the PowerShell Gallery as its value. + 5. If you are planning on creating many modules, you could use a glob pattern for the API key permissions in PowerShell Gallery and store the + secret on the organization. +3. Clone the repo locally, create a branch, make your changes, push the changes, create a PR and let the workflow run. + - Adding a `Prerelease` label to the PR will create a prerelease version of the module. +4. Use `.github/zensical.toml` for site generation. Process-PSModule builds documentation with Zensical from this file. +5. When merging to `main`, the workflow automatically builds, tests, and publishes your module to the PowerShell Gallery and maintains the + documentation on GitHub Pages. By default the process releases a patch version, which you can change by applying labels like `minor` or `major` on + the PR to bump the version accordingly. + +## How it works + +Everything is packaged into this single workflow to simplify full configuration of the workflow via this repository. Simplifying management and +operations across all PowerShell module projects. A user can configure how it works by simply configuring settings using a single file. + +### Workflow overview + +The workflow is designed to be triggered on pull requests to the repository's default branch. +When a pull request is opened, closed, reopened, synchronized (push), or labeled, the workflow will run. +Depending on the labels in the pull requests, the [workflow will result in different outcomes](usage.md#scenario-matrix). + +![Process diagram](media/Process-PSModule.png) + +## Reference + +The spec and design own the what and how. The pages below are reference documentation for those who implement, configure, and operate the workflow. + +Process-PSModule composes its work from reusable workflows, actions, a container image, PowerShell modules, and Python packages. For the full dependency tree, including diagrams and a reference of every dependency, see [DEPENDENCIES.md](https://github.com/PSModule/Process-PSModule/blob/main/DEPENDENCIES.md). + + + +| Page | Description | +| --- | --- | +| [Pipeline stages](pipeline-stages.md) | The job-by-job breakdown of the Process-PSModule workflow, from Plan through Publish Docs. | +| [Usage](usage.md) | How to call the Process-PSModule workflow — inputs, secrets, permissions, the scenario matrix, and important-file change detection. | +| [Configuration](configuration.md) | The Process-PSModule settings file — every available setting, the full defaults, and worked examples for coverage, rapid testing, linting, and release notes. | +| [Skipping framework tests](skipping-framework-tests.md) | How to skip individual PSModule framework tests on a per-file basis, the available test IDs, and the broader configuration alternatives. | +| [Repository structure](repository-structure.md) | The repository and module source layout Process-PSModule expects, and how to declare module dependencies with #Requires -Modules. | +| [Principles and practices](principles-and-practices.md) | The versioning, branching, and colocation principles behind Process-PSModule, and the development practices it is compatible with. | + + diff --git a/docs/zensical.toml b/docs/zensical.toml index 411bb3c3..2af42dc6 100644 --- a/docs/zensical.toml +++ b/docs/zensical.toml @@ -27,7 +27,15 @@ nav = [ ]}, {"Process-PSModule" = [ "Modules/Process-PSModule/index.md", + {"Workflow Overview" = "Modules/Process-PSModule/workflow-overview.md"}, + {"Specification" = "Modules/Process-PSModule/spec.md"}, + {"Design" = "Modules/Process-PSModule/design.md"}, + {"Usage" = "Modules/Process-PSModule/usage.md"}, + {"Configuration" = "Modules/Process-PSModule/configuration.md"}, + {"Pipeline Stages" = "Modules/Process-PSModule/pipeline-stages.md"}, {"Repository Structure" = "Modules/Process-PSModule/repository-structure.md"}, + {"Skipping Framework Tests" = "Modules/Process-PSModule/skipping-framework-tests.md"}, + {"Principles and Practices" = "Modules/Process-PSModule/principles-and-practices.md"}, {"Module Anatomy" = "Modules/Process-PSModule/module-anatomy.md"}, {"Module Build Validation" = "Modules/Process-PSModule/module-build-validation.md"}, {"Build, Test, Pack, Publish" = "Modules/Process-PSModule/build-test-pack-publish.md"}, From 59d9ab33ad2aa98e108a2763bea1ec28f3113d2e Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Sun, 9 Aug 2026 08:57:49 +0200 Subject: [PATCH 13/14] ci: allow documentation table styles Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- docs/.github/linters/.markdown-lint.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/.github/linters/.markdown-lint.yml b/docs/.github/linters/.markdown-lint.yml index db8977fd..4110266d 100644 --- a/docs/.github/linters/.markdown-lint.yml +++ b/docs/.github/linters/.markdown-lint.yml @@ -20,6 +20,7 @@ MD029: false # Ordered list item prefix MD033: false # Allow inline HTML MD036: false # Emphasis used instead of a heading MD041: false # First line in file should be a top level heading, PULL_REQUEST_TEMPLATE.md is an exception +MD060: false # Documentation uses both compact and aligned table styles ################# # Rules by tags # From b93e8c84481374eaf829a9ba81d3a357a92c1297 Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Sun, 9 Aug 2026 09:01:01 +0200 Subject: [PATCH 14/14] docs: clarify published documentation guidance Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- docs/content/Modules/Repository-Standard.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/content/Modules/Repository-Standard.md b/docs/content/Modules/Repository-Standard.md index 28f0fad1..dab95f63 100644 --- a/docs/content/Modules/Repository-Standard.md +++ b/docs/content/Modules/Repository-Standard.md @@ -235,7 +235,7 @@ A PowerShell ecosystem is proposed in [dependabot/dependabot-core#15501](https:/ A module README is a start page, not the command reference or full manual. It brings a reader in, answers the first questions, and sends them to the right documentation surface. -Making the README shorter must not delete unique information. The README is published as the module's landing page on the documentation site (for example `psmodule.io/`); the per-command reference is generated separately from comment-based help. So the README is often the only published home for prerequisites, platform and dependency notes, authentication and setup guidance, operational behavior such as caching, state, or update and versioning semantics, and upstream attribution. Trimming the README must preserve that content: keep it on the landing page, or move it only to another surface that also publishes (a command group's overview page under `src/functions/public//.md`, comment-based help, or published documentation in `docs/`). Only remove content that is genuinely duplicated by the generated command reference. +Making the README shorter must not delete unique information. The README is published as the module's landing page on the documentation site (for example `psmodule.io/`); the per-command reference is generated separately from comment-based help. So the README is often the only published home for prerequisites, platform and dependency notes, authentication and setup guidance, operational behavior such as caching, state, or update and versioning semantics, and upstream attribution. Trimming the README must preserve that content: keep it on the landing page or in another surface that also publishes (a command group's overview page under `src/functions/public//.md`, comment-based help, or published documentation in `docs/`). Only remove content that is genuinely duplicated by the generated command reference. The README answers these questions, in this order: @@ -302,9 +302,9 @@ Keep, trim, or relocate content — do not delete it: - **Keep on the landing page:** the overview, prerequisites and requirements (PowerShell version, supported platforms, module or native dependencies), installation, the capabilities showcase, and the short operational notes a reader needs before first use. - **Trim:** exhaustive command inventories, parameter tables, and repetitive examples that differ only by a parameter. These come from comment-based help — point to `Get-Help` and the documentation site instead of restating them. -- **Relocate only to a published home — never drop:** long-form guides and unique conceptual content (authentication and setup walkthroughs, deep operational detail, end-to-end scenarios) may move out of the README only into a surface that is actually published: a command group's overview page under `src/functions/public//.md`, comment-based help, or published documentation in `docs/` or `examples/`. Only relocate to unpublished areas if there is no published home for it yet; keep the full content in the README to ensure it reaches users. A longer landing page is acceptable and expected for feature-rich modules; do not shorten by deleting. +- **Keep content in a published home — never drop it:** long-form guides and unique conceptual content (authentication and setup walkthroughs, deep operational detail, end-to-end scenarios) belong in a surface that is actually published: a command group's overview page under `src/functions/public//.md`, comment-based help, or published documentation in `docs/` or `examples/`. When no published home exists, keep the full content in the README to ensure it reaches users. A longer landing page is acceptable and expected for feature-rich modules; do not shorten by deleting. -Retain upstream attribution and licensing context. Credit, acknowledgements, donation notes, and third-party license notices for wrapped or bundled work must stay in the README, or move to a clearly linked place. The rule below about community and policy sections does not apply to attribution the project is expected to carry. +Retain upstream attribution and licensing context. Credit, acknowledgements, donation notes, and third-party license notices for wrapped or bundled work must stay in the README or appear in a clearly linked location. The rule below about community and policy sections does not apply to attribution the project is expected to carry. README pages should not duplicate generated command documentation. Do not add full command inventories, parameter tables, or long reference sections when those details are already produced from comment-based help.