# Set up Maester in GitLab

Source: https://maester.dev/docs/monitoring/gitlab

# <IIcon icon="mdi:gitlab" height="48" /> Set up Maester in GitLab

This guide will walk you through setting up Maester in GitLab and automate the running of tests using GitLab Pipelines (jobs).

## Set up a GitLab project for Maester

### Pre-requisites

- If you are new to GitLab, create an account at [gitlab.com](https://gitlab.com/-/trial_registrations/new) for organizations
- If you use GitLab.com, you can run your CI/CD jobs on [GitLab-hosted runners](https://docs.gitlab.com/runner/).

### Create a blank project for the Maester pipeline

- On the left sidebar, at the top, select 'Create new ()' and 'New project/repository'.
- Select [Create a blank project](https://docs.gitlab.com/ee/user/project/index.html#create-a-blank-project)
  - **Repository name**: E.g. `maester-monitoring`
  - **Private**: Select this option to keep your pipeline and results private

The built-in Maester tests ship inside the Maester PowerShell module, and the pipeline below installs the latest module on every run. The project only holds the pipeline. Later, you can add a `maester-config.json` to change settings (see [Configuration](../configuration/overview.md)) and a `custom` folder for your own tests (see [Writing custom tests](../writing-tests/index.mdx)).

There are many ways to authenticate with Microsoft Entra from GitHub Actions. We recommend using [**workload identity federation**](https://learn.microsoft.com/entra/workload-id/workload-identity-federation) as it is more secure, requires less maintenance and is the easiest to set up.

If you’re unable to use more advanced options like certificates stored in Azure Key Vault, which need an Azure subscription, there’s also guidance available for using client secrets.

- <IIcon icon="gravity-ui:nut-hex" height="18" /> **Workload identity federation** (recommended) uses OpenID Connect (OIDC) to authenticate with Microsoft Entra protected resources without using secrets.
- <IIcon icon="material-symbols:password" height="18" /> **Client secret** uses a secret to authenticate with Microsoft Entra protected resources.

<Tabs>

<TabItem label="GitLab Pipline Workload identity federation (recommended)">

<CreateEntraApp/>

### Add federated credentials

- Select **Certificates & secrets**
- Select **Federated credentials**, select **Add credential**
- For **Federated credential scenario**, select **Other issuer**
- Fill in the following fields
  - **Issuer**: Your GitLab organization url or standard gitlab url `https://gitlab.com`
  - **Type**: Select **Explicit subject identifier**
  - **Value**: `project_path:<mygroup>/<myproject>:ref_type:branch:ref:<branch>` E.g. `project_path:maester/maester-monitoring:ref_type:branch:ref:main`
  - **Name**: Credential name E.g. `gitlab-federated-identity`
  - **Description**: Credential name E.g. `GitLab service account federated identity`
  - **Audience**: Your GitLab organization url or standard gitlab url `https://gitlab.com`
- Select **Add**

> **📖 For detailed Azure integration guidance:** GitLab provides comprehensive documentation on integrating with Azure services. See the [GitLab Azure integration guide](https://docs.gitlab.com/ci/cloud_services/azure/) for advanced authentication patterns, best practices, and troubleshooting tips.

### Create GitLab variables

- Open your Maester GitLab project and go to **Settings**
- Select **CI/CD** > **Variables** > **CI/CD Variables**
- Add the three secrets listed below by selecting **Add variable**
- To look up these values you will need to use the Entra portal, open the application you created earlier and copy the following values from the **Overview** page:
  - Visibility: **Visible**, Key: `AZURE_TENANT_ID`, Value: The Directory (tenant) ID of the Entra tenant
  - Visibility: **Visible**, Key: `AZURE_CLIENT_ID`, Value: The Application (client) ID of the Entra application you created
- Define which services should be connected using the other variables in order to run the corresponding tests.
  - Visibility: **Visible**, Key: `CONNECTION_EXCHANGE`, Value: "true" if you want to connect to the service and execute tests for this service too, "false" if not
  - Visibility: **Visible**, Key: `CONNECTION_PURVIEW`, Value: "true" if you want to connect to the service and execute tests for this service too, "false" if not
  - Visibility: **Visible**, Key: `CONNECTION_TEAMS`, Value: "true" if you want to connect to the service and execute tests for this service too, "false" if not
- Save each secret by selecting **Add variable** at the bottom.

### Create .gitlab-ci.yml file (or use pipeline editor)

```yaml
stages:
  - test

run_maester_tests_inline:
  stage: test
  image: mcr.microsoft.com/microsoftgraph/powershell:latest
  id_tokens:
    AZURE_FEDERATED_TOKEN:
      aud: https://gitlab.com
  variables:
    TENANTID: $AZURE_TENANT_ID
    CLIENTID: $AZURE_CLIENT_ID
    CONNECTION_EXCHANGE: $CONNECTION_EXCHANGE
    CONNECTION_PURVIEW: $CONNECTION_PURVIEW
    CONNECTION_TEAMS: $CONNECTION_TEAMS

  before_script:
    - mkdir test-results
    - pwsh -c 'Write-host "Running in project $env:CI_PROJECT_NAME with results at $env:CI_JOB_URL ($env:CI_JOB_URL)."'
  script:
    - |
      pwsh -Command '
        #region prepare execution
        # Install Maester
        #Install-Module Maester -AllowPrerelease -Force
        Install-Module Maester -Force
        Install-Module Az.Accounts -Force

        # Declare functions
        function Get-AccessToken {
            param([string]$Scope)

            $TokenBody = @{
                grant_type            = "client_credentials"
                client_id             = $env:AZURE_CLIENT_ID
                client_assertion_type = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer"
                client_assertion      = $env:AZURE_FEDERATED_TOKEN.Trim()
                scope                 = $Scope
            }

            $Response = Invoke-RestMethod -Uri "https://login.microsoftonline.com/$env:AZURE_TENANT_ID/oauth2/v2.0/token" -Method POST -Body $TokenBody -ContentType "application/x-www-form-urlencoded"
            return $Response.access_token
        }

        # Configure the run. The built-in tests ship inside the Maester module;
        # Path adds the custom tests and maester-config.json in this project, if any.
        $MaesterParameters = @{
            Path                 = "."
            Verbosity            = "None"
            OutputFolder         = "test-results"
            OutputFolderFileName = "test-results"
            PassThru             = $true
        }

        $MaesterParameters.Add("DisableTelemetry", $false )
        Write-Host "Disable Maester telemetry set to: $($MaesterParameters.DisableTelemetry)"

        $AdditionalConnections = @{
            Exchange      = [System.Convert]::ToBoolean($env:CONNECTION_EXCHANGE)
            Purview       = [System.Convert]::ToBoolean($env:CONNECTION_PURVIEW)
            Teams         = [System.Convert]::ToBoolean($env:CONNECTION_TEAMS)
        }

        Write-Host "Additional connections configuration:"
        $AdditionalConnections.GetEnumerator() | ForEach-Object {
            Write-Host "  $($_.Key): $($_.Value)"
        }

        #endregion prepare execution
        #region connect to services

        # Connect as service principal
        Write-Host "Connect service principal"
        Connect-AzAccount -ServicePrincipal -Tenant $env:AZURE_TENANT_ID -ApplicationId $env:AZURE_CLIENT_ID -FederatedToken $env:AZURE_FEDERATED_TOKEN | Out-Null

        # Get Graph token and connect to Microsoft Graph
        Write-Host "Connect Graph"
        $graphToken = (Get-AzAccessToken -ResourceUrl "https://graph.microsoft.com").Token
        Connect-MgGraph -AccessToken $graphToken -NoWelcome

        # Connect to Exchange Online and Purview
        if ($AdditionalConnections.Exchange -eq $true -or $AdditionalConnections.Purview -eq $true) {
            Install-Module -Name ExchangeOnlineManagement -Force

            # Get Exchange Online token using Az authentication
            $exchangeToken = Get-AccessToken -Scope "https://outlook.office365.com/.default"

            if ($AdditionalConnections.Exchange -eq $true) {
                Write-Host "Connect Exchange"
                Connect-ExchangeOnline -AccessToken $exchangeToken -Organization $env:AZURE_TENANT_ID -ShowBanner:$false
            }

            if ($AdditionalConnections.Purview -eq $true) {
              $domains = Invoke-MgGraphRequest -Uri "https://graph.microsoft.com/v1.0/domains?`$select=id,isDefault"
              $primaryDomain = $domains.value | Where-Object { $_.isDefault -eq $true } | Select-Object -ExpandProperty id

              Write-Host "Connect Purview"
              Connect-IPPSSession -AccessToken $exchangeToken -Organization $primaryDomain -ShowBanner:$false
            }
        }

        # Connect to Microsoft Teams
        if ($AdditionalConnections.Teams -eq $true) {
            Install-Module -Name MicrosoftTeams -Force

            Write-Host "Connect Teams"

            # Get Graph token using federated credentials
            $graphToken = Get-AccessToken -Scope "https://graph.microsoft.com/.default"

            # Get Teams token using federated credentials
            $teamsToken = Get-AccessToken -Scope "48ac35b8-9aa8-4d74-927d-1f4a14a0b239/.default"  # Microsoft Teams Application ID

            # Connect to Microsoft Teams with both tokens
            Connect-MicrosoftTeams -AccessTokens @("$graphToken", "$teamsToken")
        }

        #endregion connect to services

        #region run tests

        # Run Maester tests
        $results = Invoke-Maester @MaesterParameters

        #endregion run tests
        #region end script

        # View summary report
        $results | Format-List Result, FailedCount, PassedCount, SkippedCount, TotalCount, TenantId, TenantName, CurrentVersion, LatestVersion

        # Flag status to GitLab
        if ($results.Result -ne "Passed") {
            Write-Warning "Status = $($results.Result): see Maester Test Report for details."
        }
        #endregion end script
      '
  after_script:
    - pwsh -c 'Write-host "Report can be opened at ($env:CI_JOB_URL/artifacts/external_file/test-results/test-results.html)."'
  artifacts:
    when: on_success
    paths:
      - test-results/
    expire_in: 1 week

  rules:
    - if: '$CI_PIPELINE_SOURCE == "schedule" || $CI_PIPELINE_SOURCE == "web"'
```
</TabItem>
<TabItem label="Custom workflow using client secret">

:::caution
Client secret authentication is not recommended for long-term automation, and we recommend using workload identity federation when possible. Client secrets expire, require manual rotation and manual updates in your pipeline/repository variables.
:::

<CreateEntraApp/>

<CreateEntraClientSecret/>

### Create GitLab variables

- Open your Maester GitLab project and go to **Settings**
- Select **CI/CD** > **Variables** > **CI/CD Variables**
- Add the three secrets listed below by selecting **Add variable**
- To look up these values you will need to use the Entra portal, open the application you created earlier and copy the following values from the **Overview** page:
  - Visibility: **Visible**, Key: `AZURE_TENANT_ID`, Value: The Directory (tenant) ID of the Entra tenant
  - Visibility: **Visible**, Key: `AZURE_CLIENT_ID`, Value: The Application (client) ID of the Entra application you created
  - Visibility: **Masked and hidden**, Key: `AZURE_CLIENT_SECRET`, Value: The client secret you copied in the previous step
- Define which services should be connected using the other variables in order to run the corresponding tests.
  - Visibility: **Visible**, Key: `CONNECTION_EXCHANGE`, Value: "true" if you want to connect to the service and execute tests for this service too, "false" if not
  - Visibility: **Visible**, Key: `CONNECTION_PURVIEW`, Value: "true" if you want to connect to the service and execute tests for this service too, "false" if not
  - Visibility: **Visible**, Key: `CONNECTION_TEAMS`, Value: "true" if you want to connect to the service and execute tests for this service too, "false" if not
- Save each secret by selecting **Add variable** at the bottom.

### Create .gitlab-ci.yml file (or use pipeline editor)

```yaml
stages:
  - test

run_maester_tests_inline:
  stage: test
  image: mcr.microsoft.com/microsoftgraph/powershell:latest
  variables:
    TENANTID: $AZURE_TENANT_ID
    CLIENTID: $AZURE_CLIENT_ID
    CLIENTSECRET: $AZURE_CLIENT_SECRET
    CONNECTION_EXCHANGE: $CONNECTION_EXCHANGE
    CONNECTION_PURVIEW: $CONNECTION_PURVIEW
    CONNECTION_TEAMS: $CONNECTION_TEAMS

  before_script:
    - mkdir test-results
    - pwsh -c 'Write-host "Running in project $env:CI_PROJECT_NAME with results at $env:CI_JOB_URL ($env:CI_JOB_URL)."'
  script:
    - |
      pwsh -Command '
        #region prepare execution
        # Install Maester
        #Install-Module Maester -AllowPrerelease -Force
        Install-Module Maester -Force

        # Declare functions
        function Get-AccessToken {
            param([string]$Scope)

            $TokenBody = @{
                Grant_Type    = "client_credentials"
                Scope         = $Scope
                Client_Id     = $env:AZURE_CLIENT_ID
                Client_Secret = $env:AZURE_CLIENT_SECRET
            }
            $Response = Invoke-RestMethod -Uri "https://login.microsoftonline.com/$env:AZURE_TENANT_ID/oauth2/v2.0/token" -Method POST -Body $TokenBody -ContentType "application/x-www-form-urlencoded"
            return $Response.access_token
        }

        # Configure the run. The built-in tests ship inside the Maester module;
        # Path adds the custom tests and maester-config.json in this project, if any.
        $MaesterParameters = @{
            Path                 = "."
            Verbosity            = "None"
            OutputFolder         = "test-results"
            OutputFolderFileName = "test-results"
            PassThru             = $true
        }

        $MaesterParameters.Add("DisableTelemetry", $false )
        Write-Host "Disable Maester telemetry set to: $($MaesterParameters.DisableTelemetry)"

        $AdditionalConnections = @{
            Exchange      = [System.Convert]::ToBoolean($env:CONNECTION_EXCHANGE)
            Purview       = [System.Convert]::ToBoolean($env:CONNECTION_PURVIEW)
            Teams         = [System.Convert]::ToBoolean($env:CONNECTION_TEAMS)
        }

        Write-Host "Additional connections configuration:"
        $AdditionalConnections.GetEnumerator() | ForEach-Object {
            Write-Host "  $($_.Key): $($_.Value)"
        }

        # Connect to Microsoft Graph
        Write-Host "Connect Graph"
        $clientSecret = ConvertTo-SecureString -AsPlainText $env:AZURE_CLIENT_SECRET -Force
        [pscredential] $clientSecretCredential = New-Object System.Management.Automation.PSCredential($env:AZURE_CLIENT_ID, $clientSecret)
        Connect-MgGraph -TenantId $env:AZURE_TENANT_ID -ClientSecretCredential $clientSecretCredential -NoWelcome

        # Connect to Exchange Online and Purview
        if ($AdditionalConnections.Exchange -eq $true -or $AdditionalConnections.Purview -eq $true) {
            Install-Module -Name ExchangeOnlineManagement -Force

            # Get Exchange Online token using
            $exchangeToken = Get-AccessToken -Scope "https://outlook.office365.com/.default"

            if ($AdditionalConnections.Exchange -eq $true) {
                Write-Host "Connect Exchange"
                Connect-ExchangeOnline -AccessToken $exchangeToken -Organization $env:AZURE_TENANT_ID -ShowBanner:$false
            }

            if ($AdditionalConnections.Purview -eq $true) {
              $domains = Invoke-MgGraphRequest -Uri "https://graph.microsoft.com/v1.0/domains?`$select=id,isDefault"
              $primaryDomain = $domains.value | Where-Object { $_.isDefault -eq $true } | Select-Object -ExpandProperty id

              Write-Host "Connect Purview"
              Connect-IPPSSession -AccessToken $exchangeToken -Organization $primaryDomain -ShowBanner:$false
            }

        }

        # Connect to Microsoft Teams
        if ($AdditionalConnections.Teams -eq $true) {
            Install-Module -Name MicrosoftTeams -Force

            Write-Host "Connect Teams"

            # Get Graph token using federated credentials
            $graphToken = Get-AccessToken -Scope "https://graph.microsoft.com/.default"

            # Get Teams token using federated credentials
            $teamsToken = Get-AccessToken -Scope "48ac35b8-9aa8-4d74-927d-1f4a14a0b239/.default"  # Microsoft Teams Application ID

            Connect-MicrosoftTeams -AccessTokens @("$graphToken", "$teamsToken")
        }

        #endregion connect to services
        #region run tests

        # Run Maester tests
        $results = Invoke-Maester @MaesterParameters

        #endregion run tests
        #region end script

        # View summary report
        $results | Format-List Result, FailedCount, PassedCount, SkippedCount, TotalCount, TenantId, TenantName, CurrentVersion, LatestVersion

        # Flag status to GitLab
        if ($results.Result -ne "Passed") {
            Write-Warning "Status = $($results.Result): see Maester Test Report for details."
        }
        #endregion end script
      '
  after_script:
    - pwsh -c 'Write-host "Report can be opened at ($env:CI_JOB_URL/artifacts/external_file/test-results/test-results.html)."'
  artifacts:
    when: on_success
    paths:
      - test-results/
    expire_in: 1 week

  rules:
    - if: '$CI_PIPELINE_SOURCE == "schedule" || $CI_PIPELINE_SOURCE == "web"'
```

  </TabItem>
  </Tabs>

:::tip
- Maester 3.0 requires PowerShell 7.4 or later, which the `mcr.microsoft.com/microsoftgraph/powershell` image provides.
- If your custom tests include [Pester-format tests](../writing-tests/pester-format-tests.md) (`*.Tests.ps1`), add `Install-Module Pester -MinimumVersion 5.7.1 -Force` before running Maester. Pester is not needed for the built-in tests.
- To show the results in GitLab's test report, add `PesterConfiguration = @{ TestResult = @{ Enabled = $true; OutputPath = "test-results/junit.xml"; OutputFormat = "JUnitXml" } }` to `$MaesterParameters` (Maester writes the file itself, Pester is not required) and add a `reports` entry with `junit: test-results/junit.xml` under `artifacts`.
- To keep a fixed Maester version, replace `Install-Module Maester -Force` with `Install-Module Maester -RequiredVersion <version> -Force`. The built-in tests update with the module. If you set up this pipeline with Maester 2.x, see [Upgrading from 2.x](../upgrading-from-2x.md).
:::

## Manually running the Maester tests

To manually run the Maester tests workflow

- Open your Maester GitLab project and go to **Build**
- Select **Piplines** from the left pane
- Select **New pipline** on the right top button
- And again **New pipline** to run new pipeline

## Viewing the test results

- Open your Maester GitLab project and go to **Build**
- Select **Artifacts** from the left pane
- Search a artifact to view the results e.g. `run_maester_tests_*`
- Select browse on the right to open the folder `test-results`

> **Summary view**
> A summary view is not available with GitLab in comparison to GitHub.
