# Contributing

Source: https://maester.dev/docs/contributing

# Contributing

## Introduction

This guide is for anyone who wants to contribute to the Maester project. Whether you want to contribute to the code, documentation, or just have an idea for a new feature, we welcome your input.

Follow the guide below to set up Maester for development on your local machine, make changes, and submit a pull request.

If you work with GitHub Copilot or Claude Code, the repository ships agents and skills that know these conventions, for example `maester-test-expert` for writing a check. [Agents and skills](./agents-and-skills.md) lists them, who each one is for, and how to change them.

## Repository layout

Maester has a fixed folder layout so that anyone, person or AI agent, can tell where a file belongs from what it
does. The rules below are checked by `powershell/tests/general/RepositoryLayout.Tests.ps1`, which runs with the
other unit tests, so a pull request that breaks them fails CI. [AGENTS.md](https://github.com/maester365/maester/blob/main/AGENTS.md)
carries the same rules for AI coding and review agents.

### The rules

1. **Folder names are lowercase kebab-case**: `tests/maester/global-secure-access`, `powershell/internal/services/azure-devops`.
   This applies to every folder under `tests/`, `powershell/` and `build/`. Files keep their own conventions:
   `Test.<ID>.ps1` and `Test.<ID>.md` for checks, `Verb-Noun.ps1` for functions.
2. **One name per service.** A service folder has the same name wherever it appears:
   `ad`, `azure`, `azure-devops`, `copilot`, `copilot-studio`, `defender`, `entra`, `exchange`, `fabric`, `foundry`, `github`,
   `global-secure-access`, `graph`, `intune`, `purview`, `sharepoint`, `teams`, `xspm`. Adding a service means adding
   it to this list and to the layout test.
3. **A file goes in the group that matches its job**, not in the suite that first needed it (see the trees below).
4. **Generated code lives in `generated/` folders**, so it is obvious from the path that it must not be edited by hand.

```text
tests/                              The security checks: Test.<ID>.ps1 + Test.<ID>.md, plus suite.json, README.md and framework files
  <suite>/frameworks/<key>.json     The catalogue of a benchmark, one entry per requirement (asd/, cis/, cisa/ and disa/)
  maester/<service>/                MT.xxxx checks, one folder per service (plus drift/)
  maester/xspm/                     Exposure management; has its own suite.json (the nearest one applies)
  cis/                              CIS benchmark checks
  cisa/<service>/                   CISA SCuBA checks
  asd/<service>/                    ASD Essential Eight and ISM checks; frameworks generated by build/asd
  disa/                             DISA STIG checks for Entra ID and Intune
  ad/<area>/                        Active Directory checks
  eidsca/  orca/                    Generated by build/eidsca and build/orca
  custom/                           Reserved for users' own tests; nothing is committed here

packs/<name>/                       Maester Packs that ship with Maester: maester-pack.json, suites, helpers, connectors

powershell/
  public/                           Exported commands, grouped by what the user is doing
    run/                            Run, list, create and convert tests (Invoke-Maester, Get-MtTest, New-MtTest, ...)
    connect/                        Connect, check the session, clear caches (Connect-Maester, Test-MtConnection, ...)
    report/                         Reports and result files (Get-MtHtmlReport, Send-MtMail, Merge-MtMaesterResult, ...)
    app/                            The Maester Entra app (New-MtMaesterApp, ...)
    packs/                          Install, restore, check and create Maester Packs (Install-MtPack, Get-MtPack, ...)
    services/<service>/             Data helpers that custom tests call (Get-MtUser, Get-MtExo, Invoke-MtGraphRequest, ...)
  internal/                         Not exported
    engine/                         Discovery, selection, planning and execution of tests
    session/                        Module state, configuration, versions, progress and telemetry
    connect/                        Connection checks and consent help
    report/                         Result conversion, Markdown reports, affected objects
    app/                            Helpers for the Maester Entra app
    packs/                          Pack manifests, the lock file and store, the loader gate and connectors
    services/<service>/             Reusable data access for one service (the GitHub client, Graph caching, ...)
    checks/<suite>/<service>/       Helpers used by the checks of one suite
    generated/eidsca/  orca/        Generated; regenerate, never edit
    utility/                        Small helpers with no domain (query strings, object properties, prompts)
  assets/  lib/  tools/             Data files, the compiled engine, developer tools
  tests/                            Unit tests of the module itself
```

### Why this layout

- **Lowercase kebab-case avoids case bugs.** Windows and macOS ignore case in paths, Linux does not. Mixed case
  (`Maester/Entra` beside `cisa/entra`) leads to code that passes locally and fails in CI on Linux, and to
  case-only renames that Git on Windows and macOS handles badly. All-lowercase names also match the website URLs
  (`/docs/tests/cis/...`) and the rest of the module (`public`, `internal`, `assets`). Prowler, the closest open
  source project to Maester, uses the same convention.
- **Suites are found by `suite.json`, not by folder name**, so a folder name can follow the convention without
  changing a suite's ID or documentation URLs. That is what lets `XSPM` live in `maester/xspm`.
- **Grouping by job, then by service, gives every file one obvious home.** Before 3.0, Exchange code could be in four
  places (a suite folder under `public/`, a suite folder under `internal/checks/`, a loose file, or a service
  folder). Well-run projects separate the layers the same way: ScubaGear splits connection, per-product data
  providers, evaluation and reporting; Prowler keeps each service's client beside that service's checks and shared
  code in `lib/`; PSFramework and dbatools split exported and internal functions, then group by feature.
- **`public/` is grouped by what the user is doing**, so the folder tells you which commands belong together in the
  [command reference](./commands/readme.md).
- **Folders name products, areas name themes.** A check goes in the folder of the product whose settings it reads,
  because that is what an author knows when adding a check and what an admin knows when looking for one. Themes
  that cut across products, such as AI, are the `Area` attribute of a test: Copilot Studio agent checks live in
  `maester/copilot-studio/`, an Entra agent identity check lives in `maester/entra/`, and both declare
  `Area = 'ai'`, so `Invoke-Maester -Area ai` runs them together. A theme folder such as `ai/` would end up
  holding checks for Copilot, Copilot Studio, Foundry, Entra Agent ID and Purview, each with its own API, portal
  and permissions.
- **Data access is shared, check logic is not.** Code that reads a service (`services/`) is reused by many checks and
  by custom tests. Code that only one suite's checks need stays in `checks/<suite>/`.
  The one exception is a benchmark test that asks for what a test in another suite already checks: it is a shim
  that returns that test's result, not a copy of its logic. See
  [Tests that another suite already has](./writing-tests/index.mdx#tests-that-another-suite-already-has).

### Making an internal helper public

Custom tests are loaded into their own private module, so they can call only the commands Maester exports. When
users need one of the internal helpers in their own tests, promote it instead of copying it:

1. Move the file from `powershell/internal/services/<service>/` to `powershell/public/services/<service>/` with
   `git mv`, so its history follows it.
2. Add the function to `FunctionsToExport` in `powershell/Maester.psd1`.
3. Give it complete comment-based help: synopsis, description, every parameter, at least one example and a
   `.LINK` to `https://maester.dev/docs/commands/<Name>`. The command reference page is generated from it.
4. Review the name and parameters before you merge. Once a command is exported it is public API: renaming it,
   removing a parameter or changing what it returns is a breaking change for users' tests.

`Manifest.Tests.ps1` fails if a file in `public/` is not exported or an internal function is, and `Help.Tests.ps1`
fails if an exported command's help is incomplete, so a half-finished promotion does not pass CI. Helpers that
belong to one suite's checks (`internal/checks/`) are not candidates: move the reusable part into
`services/<service>/` first.

## Maester PowerShell module dev guide

### Build and load Maester locally

After changing files under `./powershell` or `./tests`, run the following command
from the repository root:

```powershell
./build/Build-LocalMaester.ps1
```

This command:

- Builds the consolidated, publishable module in `./module`.
- Generates the companion test metadata used by reports.
- Validates the build output.
- Unloads any other Maester module and imports the local build into the current PowerShell session.

Run the packaged tests with `Invoke-Maester`; the built-in tests run from the loaded module. Run
`Build-LocalMaester.ps1` again whenever you change module or test source.

After changing files under `./report`, include `-BuildReport` to build the report,
copy its generated template into the PowerShell assets, and include it in the
local module:

```powershell
./build/Build-LocalMaester.ps1 -BuildReport
```

### Build and load with VS Code

Press F5 with the **PowerShell: Build and Load Local Maester** launch
configuration selected. VS Code runs `Build-LocalMaester.ps1` and leaves the
local build loaded in its interactive PowerShell session.

The consolidated module is the closest match to the artifact published in a
release. If you specifically need source-file breakpoints, use the source-only
workflow below; source breakpoints do not map to the generated
`./module/Maester.psm1` file.

### Source-only debugging

- Source files live in `./powershell` (module functions) and `./tests` (bundled test suites). Never edit files in `./module` — it is generated build output.
- For source-level breakpoints or quick iteration, load the PowerShell module directly from source. Repeat this whenever you change code in `./powershell`.
  - `Import-Module ./powershell/Maester.psd1 -Force`
- Run Maester
  - `Invoke-Maester`
- Before submitting a change, use `./build/Build-LocalMaester.ps1` to verify the consolidated module.

### Building the module

The publishable Maester module is produced by a build script that consolidates the source files into an optimized module for faster import:

- For normal development, run `./build/Build-LocalMaester.ps1` to build, validate, and import the publishable module in one step.
- To produce the artifact without validating or importing it, run `./build/Build-MaesterModule.ps1`.
- `./module` is a build artifact: it is ignored by git and never committed to source control. Built modules are attached to GitHub Releases and published to the PowerShell Gallery from CI.
- The `FunctionsToExport` list in the built `Maester.psd1` is auto-generated at build time from the public source files. Do not edit the built manifest manually.
- The built-in tests are built from `./tests`: native tests go into the module and its test catalog, and the remaining files are copied to `./module/builtin-pester`. Do not edit build output manually.

### Code style

- Use the One True Brace Style (OTBS): the opening brace goes on the same line as the statement.
- Use Pascal Case for all variable and parameter names.
- Use 4-space indentation. No tabs.
- Save all `.ps1`, `.psm1` and `.psd1` files as UTF-8 with BOM (`utf8BOM`). Maester runs only on PowerShell 7, which reads
  UTF-8 either way, so this is a consistency rule: one encoding for every PowerShell file, whichever editor or tool
  wrote it. The repository's `.editorconfig` and `.vscode/settings.json` set it, and
  `powershell/tests/general/FileEncoding.Tests.ps1` checks every tracked file. A script or generator that writes a
  PowerShell file must write the BOM too (`-Encoding utf8BOM`, or `[System.Text.UTF8Encoding]::new($true)`).
  Markdown (including each test's `Test.<ID>.md` guide) and JSON files have no BOM; the same test checks Markdown.
- Use approved PowerShell verbs only. Run `Get-Verb` for the authoritative list.
- Avoid trailing whitespace. End every file with one blank line (a single trailing newline).
- Place comment-based help inside the function body (immediately after the opening brace), not above the `function` keyword.

### File organization

- One function per file, and the filename must match the function name exactly.
- Exported functions go in a group folder under `./powershell/public`, internal ones under `./powershell/internal`. See [Repository layout](#repository-layout) for which folder.
- Built-in security checks are native tests in `./tests/<suite>/[<service>/]Test.<ID>.ps1`, each with a `Test.<ID>.md` beside it. They are module source: the build defines their functions in the module.

### Pester Tests

- QA tests for the Maester module framework itself are at `./powershell/tests`. These are distinct from the bundled security test suites in `./tests`.
- When making changes to the module you can run the QA tests locally by running `./powershell/tests/pester.ps1`
- `pester.ps1` runs the test files in batches of eight, each batch in its own pwsh process, two processes at a time. Each process needs up to about 1.3 GB of memory. On a small VM use `-ThrottleLimit 1`; on a machine with memory to spare, `-ThrottleLimit 4` runs the suite in about half the time.
- While you work, run only the files that cover your change. `-Include` and `-Exclude` take file name patterns:

  ```powershell
  # The unit tests of the commands you changed
  ./powershell/tests/pester.ps1 -TestGeneral $false -Include 'Get-MtUser*', 'Test-MtCa*'

  # The checks that a new or changed native test must pass
  ./powershell/tests/pester.ps1 -TestFunctions $false -Include 'NativeTest*', 'RepositoryLayout*', 'MaesterTestTags*', 'LegacyIds*'

  # Everything except the slowest files, which regenerate the golden fixtures and the ORCA and EIDSCA tests
  ./powershell/tests/pester.ps1 -Exclude 'Golden*', 'GeneratedFileFormatting*', 'OrcaGenerator*', 'EidscaGenerator*'
  ```

- Run the whole suite once before you push, or let the pull request's build run it.
- On Windows, Microsoft Defender scans every script the module loads. If your policy allows it, excluding the folder of your clone from real-time scanning can make the run much faster.
- The **PSScriptAnalyzer**, **PSFramework** and **PSModuleDevelopment** modules are required to run the tests, install them with `Install-Module PSFramework, PSModuleDevelopment, PSScriptAnalyzer`
- The tests are run automatically on PRs and commits to the main branch and will fail if the tests do not pass

## Contributing new tests and updating existing tests

### Test folder convention

Every built-in check is a [native test](./writing-tests/index.mdx): `Test.<ID>.ps1` plus `Test.<ID>.md`, in the [tests](https://github.com/maester365/maester/tree/main/tests) folder of its suite:

- `/maester` - Maester tests (`MT.xxxx`), with a subfolder per service such as `/entra`, `/exchange`, `/teams`
- `/maester/xspm` - Exposure management tests (MT IDs, with their own suite)
- `/cisa` - CISA tests, with a subfolder per service
- `/cis` - CIS tests
- `/asd` - ASD Essential Eight and ISM tests (`ASD.ISM-xxxx`), with a subfolder per service
- `/disa` - DISA STIG tests (`DISA.V-xxxxxx`, named after the STIG rule), one folder for every product
- `/eidsca` - EIDSCA tests (generated, see below)
- `/orca` - ORCA tests (generated by `build/orca/Update-OrcaTests.ps1`)
- `/ad` - Active Directory tests
- `/custom` - Folder for users to add tests (do not add any tests to this folder).

Each suite folder has a `suite.json` with the suite's ID, name and source. Folder names follow the
[repository layout](#repository-layout).

A test says which requirement of a framework it implements in its attribute, `Control = 'cis-m365:5.1.5.1'` or
`Control = 'cisa-scuba:MS.AAD.3.1'` or `Control = 'essential-eight:ISM-1504'`. This is the same for a CIS, CISA or ASD
test and for a Maester test that checks what one of their requirements asks for.

The catalogue of each framework is a file beside the `suite.json` that declares it (`Frameworks`),
`frameworks/<key>.json`. It holds the name and version of the framework and every requirement with its facts: the
CIS level and profiles in `tests/cis/frameworks/`, the CISA criticality, upstream status and NIST SP 800-53
references in `tests/cisa/frameworks/cisa-scuba.json`, and the Essential Eight maturity level and strategy and the
ISM guideline, section and applicability in `tests/asd/frameworks/` (generated from the ISM OSCAL release by
`build/asd/Update-AsdFramework.ps1`). It does not list tests. The module build and the unit tests
fail when a `Control` names one of these frameworks and a requirement its file does not have, and when a test in
`tests/cis`, `tests/cisa` or `tests/asd` has no `Control` for its suite's framework.
See [Benchmarks and framework files](./writing-tests/index.mdx#benchmarks-and-framework-files) and
[Mapping a test to a framework](./writing-tests/index.mdx#mapping-a-test-to-a-framework).

### Checklist for writing good tests

When contributing tests, please ensure the following:

#### Before writing the test

- [x] Check if your new idea is not already covered by an existing test (you can also jump on the [Maester Discord](https://discord.entra.news) to discuss). `Get-MtTest` lists every test.
- [x] For a benchmark test (CIS, CISA, ASD), look for a test in any suite that reads the same setting, and read the benchmark's own audit or assessment guidance. If it asks for what that test checks, write a shim that returns that test's result instead of a second implementation; see [Tests that another suite already has](./writing-tests/index.mdx#tests-that-another-suite-already-has).
- [x] Reserve a unique id for your test. See [#️⃣ Pick next Maester test sequence number](https://github.com/maester365/maester/issues/697) on how to reserve a unique id for your new test.

#### Writing the test

- [x] Scaffold the files in the right suite folder, for example `New-MtTest -Id MT.1234 -Title '...' -Service Graph -Product entra -Feature conditional-access -Area authentication -Path ./tests/maester/entra`, then follow [Writing native tests](./writing-tests/index.mdx).
- [x] Built-in tests must set `Product`, `Feature`, `Area`, `ResourceType`, `Severity`, `Mitre`, `Service` (or `None`) and `Author` (your GitHub handle) in the `[MaesterTest]` attribute, plus `UserImpact` and `ImplementationCost` unless `Severity` is `Info`, and describe every parameter. Choose the values with the [attribute rubric](./writing-tests/attribute-rubric.md), using keys from `powershell/assets/MaesterTaxonomy.psd1`. They are checked on the pull request; see [Attribute checks](#attribute-checks).
- [x] Do not add a `Tag` for the product, feature, area, suite, benchmark, licence or severity of a test, or for its ID: each of those has an attribute or is derived.
- [x] Do not check connections or licences in the test and do not wrap it in a `try`/`catch` that only reports `-SkippedBecause Error`: declare `Service` and `License` and let the engine handle it. The unit tests enforce this.
- [x] For a CIS, CISA or ASD test, set `Control` to the requirement it implements (`Control = 'cis-m365:5.1.5.1'`, `'cis-github:1.2.2'`, `'cisa-scuba:MS.AAD.3.1'` or `'essential-eight:ISM-1504'`), and add the requirement to the framework file if it is new (`tests/cis/frameworks/cis-m365.json`, `tests/cis/frameworks/cis-github.json` or `tests/cisa/frameworks/cisa-scuba.json`): its `Id`, and its level and profiles or its criticality. The ASD files are generated from the ISM OSCAL release by `build/asd/Update-AsdFramework.ps1`, so a missing ISM control means the release has to be updated, not the file. Do not copy the benchmark's text into the file.
- [x] Tunable values (thresholds, objects to exclude) are parameters of the test function, not hard-coded.
- [x] The test's `.md` file explains the test in detail and provides all the context required for the user to resolve the issue, including deep links to the admin portal page to resolve the issue. This will be shown to the user when they view the test report. The file should include:
  - [x] Link to the admin portal blade where the setting can be configured
  - [x] A `#### Remediation action` section, and `<!--- Results --->` followed by `%TestResult%` at the end
  - [x] If there are multiple objects (e.g. list of CA policies, Users, etc) then use the `GraphObjects` and `GraphObjectType` parameters in [Add-MtTestResultDetail](https://github.com/maester365/maester/blob/main/powershell/public/Add-MtTestResultDetail.ps1). These include deep links to the admin portal. If the object type you wish is not available you can add it to [Get-GraphObjectMarkdown.ps1](https://github.com/maester365/maester/blob/main/powershell/internal/Get-GraphObjectMarkdown.ps1). Feel free to ask on Discord if you need help with this.
- [x] The test's page under `website/docs/tests/` is generated from the test files; do not write it by hand.
- [x] Check the test with `Get-MtTest <ID>` after `Import-Module ./powershell/Maester.psd1 -Force`, and run it with `Invoke-MtTest -Path <file>` against a tenant. `Get-MtTest -Path <file>` applies the rules of a custom test, so it does not report a missing attribute or a key that is not in the taxonomy.

When in doubt always check the existing tests for the conventions used, feel free to discuss on [Discord](https://discord.entra.news) or [GitHub Issues](https://github.com/maester365/maester/issues).

#### Before creating the Pull Request

##### Test your code

- We run a bunch of pester tests on your code when a PR is raised and will block merge until the issues are addressed.
- Before you submit the PR, run the tests locally by running `./powershell/tests/pester.ps1`. See [Pester Tests](#pester-tests) for running a subset while you work.
- Fix any issues that are reported. If you need help see the `Common Pester test failures and how to fix` section below. Reach out on Discord if you need help.
- The **PSScriptAnalyzer**, **PSFramework** and **PSModuleDevelopment** modules are required to run the tests, install them with `Install-Module PSFramework, PSModuleDevelopment, PSScriptAnalyzer`
- If a test is not applicable (e.g. it says not to use plural but the product name is AzureDevOps then you can add a `SuppressMessageAttribute` tag (see Invoke-Maester.ps1 which has suppression tags at the beginning of the function).

### Attribute checks

A pull request that adds a test, or changes `Product`, `RelatedProduct`, `Feature`, `Area`, `ResourceType`, `Severity`, `UserImpact`, `ImplementationCost` or `Mitre` on an existing test, has those values checked against the [attribute rubric](./writing-tests/attribute-rubric.md) in two ways:

- The unit tests in [`NativeTestAttributes.Tests.ps1`](https://github.com/maester365/maester/blob/main/powershell/tests/general/NativeTestAttributes.Tests.ps1) fail when a built-in test misses a required attribute, uses a key that is not in the [taxonomy](https://github.com/maester365/maester/blob/main/powershell/assets/MaesterTaxonomy.psd1), lists a retired or unknown ATT&CK technique, or carries a tag for something an attribute already says.
- The [attribute-review](https://github.com/maester365/maester/blob/main/.github/workflows/attribute-review.yaml) check has a reviewer model read the test, its Markdown file and the rubric, together with the attributes of the other tests with the same product and feature. It fails when a value does not fit the rubric or differs from the same check in another suite in area, resource type or severity (user impact and implementation cost only when both plainly recommend the same fix; MITRE techniques and related products are not compared), and comments on the pull request with the value it expected and the reason. Change the value, or explain in the pull request why the declared value fits the rubric. A maintainer can overrule the check.

The maintainer or agent who reviews the pull request applies the same rubric; [AGENTS.md](https://github.com/maester365/maester/blob/main/AGENTS.md) describes that step.

### Updating EIDSCA tests and documentation

The EIDSCA tests and documentation are maintained in the [EIDSCA repository → EidscaConfig.json](https://github.com/Cloud-Architekt/AzureAD-Attack-Defense/blob/AADSCAv4/config/EidscaConfig.json) file.

The [/build/eidsca/Update-EidscaTests.ps1](https://github.com/maester365/maester/blob/main/build/eidsca/Update-EidscaTests.ps1) script is used to generate the EIDSCA commands in the Maester module along with the EIDSCA tests and documentation.

The [EIDSCA sync page](/eidsca-sync) shows which values come from the EIDSCA config and which Maester adds, and compares the two live. The weekly `eidsca-sync` workflow makes the same comparison. While they differ, it keeps one open issue and one pull request that runs the script with `-Download`, both assigned to the EIDSCA maintainer, and updates them every week. Merging the pull request closes the issue; the next difference starts a new issue and pull request. To run the script yourself, use `./build/eidsca/Update-EidscaTests.ps1 -Download`.

The EIDSCA config is the source of every value of a check except the Maester classification, which it does not carry. Severity, title, description, the recommended value and the remediation come from the EIDSCA repository; change them there. The Maester classification of each check (`Feature`, `Area`, `ResourceType`, `UserImpact`, `ImplementationCost`, `Mitre`, `Control` and `RelatedProduct`) is in [/build/eidsca/eidsca-test-metadata.json](https://github.com/maester365/maester/blob/main/build/eidsca/eidsca-test-metadata.json), chosen with the [attribute rubric](./writing-tests/attribute-rubric.md). When the EIDSCA repository adds a check, the generator warns that it has no row in that file and the unit tests fail until the row is added. Add the row in a separate pull request to `main`, not on the sync pull request's branch, which each weekly run rebuilds from `main`.

The illustration below shows the workflow for integrating EIDSCA tests and documentation into Maester.

![EIDSCA and Maester integration workflow](assets/eidcsa-maester-workflow.png)

When generating the EIDSCA commands and tests, manual verification should be performed to ensure the EIDSCA tests are being run correctly and the results are accurate.

## Common Pester test failures and how to fix

### PSUseBOMForUnicodeEncodedFile, or a file without a BOM

The quick fix for this is to run this script. Make sure you give the right path to the affected file.

```powershell
$affectedFilePath = '/path/to/maester/powershell/public/run/Invoke-Maester.ps1'
$content = Get-Content $affectedFilePath -Raw; Set-Content $affectedFilePath -Value $content -NoNewline -Encoding utf8BOM
```

## Contributing to Maester docs and blog posts

Simple edits can be made in the GitHub UI by selecting the `Edit this page` link at the bottom of each page or you can browse to the [docs](https://github.com/maester365/maester/tree/main/website/docs) folder on GitHub.

For more complex changes, you can fork the repository and submit a pull request.

The docs/commands folder is auto-generated based on the comments in the PowerShell cmdlets. If you want to update the documentation for a command, you will need to update the comment-based help in the .ps1 file for the command.

### Running documentation locally

The [Maester.dev](https://maester.dev) website is built with [Astro](https://astro.build) and [Starlight](https://starlight.astro.build).

#### Pre-requisites

[Node.js](https://nodejs.org/en/download/) 22.22 or later (`node -v`). The CI builds use Node.js 24.

#### Installation

In the `./website` folder, install the dependencies:

```shell
npm install
```

#### Starting the site

While in the `./website` folder, start the site. It runs at [http://localhost:4321/](http://localhost:4321/) and reloads as you edit:

```shell
npm start
```

`npm run build` builds the whole site into `website/dist` and then checks that every URL the site has ever had still works (see [URLs](#urls) below).

#### Editing content

Guides are Markdown files in `./website/docs`. A file's path is its URL: `docs/monitoring/github.md` is
`https://maester.dev/docs/monitoring/github`, and a `readme.md` or `index.md` is its folder's page.

- Link to other pages by their file, such as `[Configuration](configuration/overview.md)`. The site turns these
  links into URLs, and they also work when you read the docs on GitHub.
- Notes and warnings use Starlight's syntax: `:::note`, `:::tip`, `:::caution` or `:::danger`, with an optional
  title in brackets (`:::tip[Before you start]`).
- A page that uses components (tabs, icons, explainers) is an `.mdx` file and imports them, for example
  `import { Tabs, TabItem } from '@astrojs/starlight/components';` or
  `import IIcon from '@components/IIcon.astro';` for an [Iconify](https://icon-sets.iconify.design/) icon. See
  [Starlight's components](https://starlight.astro.build/components/using-components/).
- The pages under `Commands` are generated from each command's comment-based help: edit the `.ps1` file, not the page.
- The test pages are built from each test's `[MaesterTest]` attribute and `Test.<ID>.md`: edit those, not the
  website.
- The first `npm start` or `npm run build` generates the test catalog and the command pages from your local
  source, which needs PowerShell 7 and the `Microsoft.Graph.Authentication` module. After changing a test or a
  command's help, run `npm run refresh-data` to see the change. These files aren't committed: maester.dev is built
  from the source each time a change is merged.

#### Stable and preview

The docs describe the preview, which is published to the PowerShell Gallery each time a change is merged. Checks,
commands and parameters that the stable release doesn't have are marked **Preview** automatically, from the test
catalog and `website/src/data/versions.json`; you don't mark them yourself.

In a guide, wrap a section that only applies from the next release in `<Since version="3.1">`, or describe a change
in behaviour with `<Changed in="3.1">` (from `@components/Since.astro` and `@components/Changed.astro`). The build compares the version with
`website/version-config.js` and shows **Preview 3.1** or **New in 3.1**; the release workflows keep both files up to
date. Write the docs in the same pull request as the change.

#### Explainers

Short animated explainers (in the intro and the applicability guide, and on the home page) are
[HyperFrames](https://hyperframes.heygen.com) compositions: HTML and CSS with a GSAP timeline, in
`website/public/explainers/<name>/index.html`. Their chapters and transcript are in `website/src/data/explainers.json`;
keep the two in step. Preview one with `npx hyperframes preview` in its folder. Each composition lists the source
files it describes (`Watches:` in its header comment). If your change alters what an explainer shows, such as a gate,
a reason code or a command's output, update the explainer in the same pull request.

#### URLs

`website/src/data/url-contract.txt` lists every URL the site had when it moved to Astro, and `npm run build` fails if
any of them no longer opens a page, directly or through a redirect in `website/src/lib/redirects.mjs`. When you move or
rename a page, add a redirect from its old URL.

#### Blog post images

Every blog post has a card image (`img/card.png` in the post's folder) that is shown on the [blog](https://maester.dev/blog) and in link previews on social media. The images all use one template, so don't make one by hand.

You don't have to do anything: after a new post is merged, the `Blog card images` workflow makes its card and opens a pull request with it. If the post has no headline and label, Claude suggests them, and the maintainer checks them in that pull request.

To choose the text yourself, add a short headline and a label to the post's front matter:

```yaml
card_headline: "Maester *3.0*: a new test engine" # *words* are highlighted
card_label: Release # Release, Guide, Feature, New tests, Integration, Event, Video, News, ...
```

To see the card before you open your pull request, generate it from the ./website folder. The first time, install the browser that renders it with `npx playwright install chromium`.

```shell
npm run blog-cards                                # posts that don't have a card yet
npm run blog-cards -- 2026-10-24-maester-packs    # one post, by its folder name
npm run blog-cards -- --all                       # every post, after a change to the template
```

The script saves the image and sets `image: ./img/card.png` in the post's front matter. Without `card_headline` the card uses the post's title. The template is `website/scripts/blog-cards/template.html`.

### Site versioning

There is one website, built from `main`, so a docs change is live once it is merged. It describes the preview of
Maester, and marks what the stable release doesn't have yet (see [Stable and preview](#stable-and-preview) above).
The docs of Maester 2.x are kept, frozen, at [maester.dev/docs/2.x](https://maester.dev/docs/2.x/intro), and are not
indexed by search engines.

#### Version numbers

A preview is a prerelease of the release it leads to, and the docs show that release's number.

| | Module on the PowerShell Gallery | Shown on the preview site |
| --- | --- | --- |
| Previews of 3.0 | `3.0.0-preview1` to `3.0.0-preview9`, then `3.0.0-rc01`, `3.0.0-rc02`, ... | `3.0.0-preview` |
| The release | `3.0.0` | `3.0.0` |
| Previews after it | `3.1.0-preview01`, `3.1.0-preview02`, ... | `3.1.0-preview` |

- A preview never uses up a release number, and every preview sorts below its release, so people on a preview are
  offered the release.
- The PowerShell Gallery compares prerelease labels as text, so the preview number has two digits: `preview10`
  sorts above `preview09` but below `preview9`. That is why the 3.0 previews after `preview9` are `rc01` and on.
- The numbers are worked out by `.github/workflows/Get-NextModuleVersion.ps1` from what is on the gallery. To start
  a new major version, raise `$major` in `.github/workflows/minor-version-update.ps1`.
- PowerShell installs `3.0.0-preview2` and `3.0.0` into the same folder. Someone who has a preview moves to the
  release with `Install-Module Maester -Force`; Maester tells them so when it starts.
