Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 65 additions & 0 deletions powershell/internal/Get-MtGitHubRepoFromGit.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
function Get-MtGitHubRepoFromGit {
<#
.SYNOPSIS
Detects the current GitHub repository from the local git remote (origin).

.DESCRIPTION
Used by Add-MtMaesterAppFederatedCredential and New-MtMaesterApp to remove
the need for the user to specify -GitHubOrganization and -GitHubRepository
when running the command from inside a git working tree whose `origin` remote
points at GitHub.

Supports HTTPS, scp-style SSH, and ssh:// remote URL formats:
https://github.com/owner/repo.git
https://github.com/owner/repo
https://www.github.com/owner/repo.git
git@github.com:owner/repo.git
Comment thread
SamErde marked this conversation as resolved.
ssh://git@github.com/owner/repo.git

Returns $null when:
* git is not installed / not on PATH
* the current directory is not inside a git working tree
* the origin remote is not a GitHub URL
* the URL cannot be parsed

.OUTPUTS
[pscustomobject] with Organization, Repository, RemoteUrl properties, or $null.
#>
[CmdletBinding()]
[OutputType([pscustomobject])]
param(
# Optional override for the git remote name. Defaults to 'origin'.
[string] $RemoteName = 'origin'
)

if (-not (Get-Command git -ErrorAction SilentlyContinue)) {
Write-Verbose "git is not available on PATH; cannot auto-detect GitHub repo."
return $null
}

try {
$remoteUrl = (& git remote get-url $RemoteName 2>$null) | Select-Object -First 1
} catch {
Write-Verbose "Failed to read git remote '$RemoteName': $($_.Exception.Message)"
return $null
}

if ([string]::IsNullOrWhiteSpace($remoteUrl)) {
Write-Verbose "git remote '$RemoteName' is not configured."
return $null
}

# Match HTTPS, SSH, and scp-style GitHub URLs. Host anchored to github.com (optional 'www.')
# so lookalikes such as 'evilgithub.com' or 'github.com.attacker.com' do not match.
$pattern = '^(?:https?://(?:www\.)?github\.com/|git@github\.com:|ssh://git@github\.com/)([^/]+)/([^/]+?)(?:\.git)?/?\s*$'
if ($remoteUrl -notmatch $pattern) {
Write-Verbose "Remote URL '$remoteUrl' is not a recognised GitHub URL."
return $null
}

return [pscustomobject]@{
Organization = $Matches[1]
Repository = $Matches[2]
RemoteUrl = $remoteUrl.Trim()
}
}
72 changes: 72 additions & 0 deletions powershell/internal/Set-MtGitHubActionsSecret.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
function Set-MtGitHubActionsSecret {
<#
.SYNOPSIS
Sets AZURE_CLIENT_ID and AZURE_TENANT_ID as GitHub Actions repository secrets via the GitHub CLI.

.DESCRIPTION
Used by Add-MtMaesterAppFederatedCredential when -SetGitHubSecrets is specified.
Returns $true when both secrets were set successfully, $false otherwise (caller
should fall back to printing manual setup instructions).

Requires the GitHub CLI (`gh`) to be installed and authenticated. Will validate
both before attempting any state-changing call.

.OUTPUTS
[bool] - $true on success, $false if gh is missing/unauthenticated or any
`gh secret set` call fails.
#>
[CmdletBinding()]
[OutputType([bool])]
[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingWriteHost', '', Justification = 'Colors are beautiful')]
[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Justification = 'User opted in via -SetGitHubSecrets switch on the calling cmdlet')]
param(
# Target repository in 'owner/repo' format.
[Parameter(Mandatory = $true)]
[string] $GitHubRepository,

# Application (Client) ID to store as AZURE_CLIENT_ID.
[Parameter(Mandatory = $true)]
[string] $ClientId,

# Entra Tenant ID to store as AZURE_TENANT_ID.
[Parameter(Mandatory = $true)]
[string] $TenantId
)

if (-not (Get-Command gh -ErrorAction SilentlyContinue)) {
Write-Warning "GitHub CLI ('gh') is not installed or not on PATH. Falling back to manual instructions."
Write-Host "Install gh from https://cli.github.com/ to enable -SetGitHubSecrets." -ForegroundColor DarkGray
return $false
}

# Validate gh auth - 'gh auth status' exits 0 when authenticated.
& gh auth status 2>&1 | Out-Null
if ($LASTEXITCODE -ne 0) {
Write-Warning "GitHub CLI is not authenticated. Run 'gh auth login' first. Falling back to manual instructions."
return $false
Comment thread
SamErde marked this conversation as resolved.
}

Write-Host "Setting GitHub Actions secrets on $GitHubRepository via gh CLI..." -ForegroundColor Yellow

$secrets = [ordered]@{
AZURE_CLIENT_ID = $ClientId
AZURE_TENANT_ID = $TenantId
Comment thread
SamErde marked this conversation as resolved.
}

foreach ($name in $secrets.Keys) {
$value = $secrets[$name]
# Pipe the value via stdin so the secret never appears on the gh command line
# (process listings, audit logs, shell history would otherwise capture it).
$output = $value | & gh secret set $name --repo $GitHubRepository 2>&1
if ($LASTEXITCODE -ne 0) {
Write-Warning "Failed to set $name on $GitHubRepository : $output"
return $false
}
Write-Host " ✓ $name set" -ForegroundColor Green
}

Write-Host ""
Write-Host "✅ AZURE_CLIENT_ID and AZURE_TENANT_ID configured on $GitHubRepository." -ForegroundColor Green
Write-Host ""
return $true
}
46 changes: 46 additions & 0 deletions powershell/internal/Write-MtGitHubSecretsManualInstruction.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
function Write-MtGitHubSecretsManualInstruction {
<#
.SYNOPSIS
Prints the manual GitHub Actions secrets setup instructions used as a fallback by
Add-MtMaesterAppFederatedCredential when -SetGitHubSecrets is not used or when the
GitHub CLI (`gh`) is unavailable / unauthenticated / fails.
#>
[CmdletBinding()]
[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingWriteHost', '', Justification = 'Colors are beautiful')]
param(
[Parameter(Mandatory = $true)] [string] $GitHubOrganization,
[Parameter(Mandatory = $true)] [string] $GitHubRepository,
[Parameter(Mandatory = $true)] [string] $ClientId,
[Parameter(Mandatory = $true)] [string] $TenantId,

# When $true the caller already attempted -SetGitHubSecrets and the gh CLI path
# failed (missing / unauthenticated / call failure). In that case suggesting the
# user re-run with the same switch is misleading - show a `gh` troubleshooting tip
# instead.
[switch] $AttemptedAutomatic
)

$githubSecretsUrl = "https://github.com/$GitHubOrganization/$GitHubRepository/settings/secrets/actions"

Write-Host "GitHub Actions Configuration:" -ForegroundColor Yellow
Write-Host "Add these secrets to your GitHub repository ($GitHubOrganization/$GitHubRepository):" -ForegroundColor White
Write-Host ""
Write-Host "1. Browse to $githubSecretsUrl" -ForegroundColor Cyan
Write-Host "2. Click on 'New repository secret'" -ForegroundColor Cyan
Write-Host "3. Create the following secrets:" -ForegroundColor Cyan
Write-Host ""
Write-Host " Name: AZURE_CLIENT_ID" -ForegroundColor Cyan
Write-Host " Value: $ClientId" -ForegroundColor Cyan
Write-Host " Name: AZURE_TENANT_ID" -ForegroundColor Cyan
Write-Host " Value: $TenantId" -ForegroundColor Cyan
Write-Host ""
if ($AttemptedAutomatic) {
Write-Host "Tip: -SetGitHubSecrets was requested but the GitHub CLI ('gh') was unavailable or" -ForegroundColor DarkGray
Write-Host " failed. Install gh from https://cli.github.com/, run 'gh auth login', then re-run" -ForegroundColor DarkGray
Write-Host " this command to push the secrets automatically." -ForegroundColor DarkGray
} else {
Write-Host "Tip: re-run with -SetGitHubSecrets to push these via the GitHub CLI automatically." -ForegroundColor DarkGray
}
Write-Host "See https://maester.dev/docs/monitoring/github#add-entra-tenant-info-to-github-repos for details." -ForegroundColor Yellow
Write-Host ""
}
139 changes: 107 additions & 32 deletions powershell/public/core/Add-MtMaesterAppFederatedCredential.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,28 @@
The Application (Client) ID of the Maester application to add the federated credential to.

.PARAMETER GitHubRepository
The GitHub repository in the format 'owner/repo' (e.g., 'myorg/myrepo').
The GitHub repository name (without the organization). E.g. maester-tests.
If both -GitHubOrganization and -GitHubRepository are omitted and the current working
directory is inside a git repository whose 'origin' remote points at GitHub, both
values are auto-detected from `git remote get-url origin`. Specifying one without
the other is not supported - either pass both explicitly, or pass neither and rely
on auto-detection.

.PARAMETER GitHubBranch
The GitHub branch that can use this credential. Defaults to 'main'.

.PARAMETER Name
The name for the federated credential. Defaults to 'maester-devops'.
The name for the federated credential. Defaults to 'maester-devops-<org>-<repo>'.

.PARAMETER Force
Skip the confirmation prompt if a similar credential already exists.
.PARAMETER SetGitHubSecrets
If specified, sets the AZURE_CLIENT_ID and AZURE_TENANT_ID secrets on the target
GitHub repository using the GitHub CLI (`gh`). Requires `gh` to be installed and
authenticated (`gh auth login`). When the secrets cannot be set automatically the
cmdlet falls back to printing the manual setup instructions.
Comment thread
SamErde marked this conversation as resolved.

Re-running the cmdlet with -SetGitHubSecrets against an app that already has a
matching federated credential will skip the credential creation step and proceed
directly to (re)setting the secrets.

.EXAMPLE
Add-MtMaesterAppFederatedCredential -AppId "12345678-1234-1234-1234-123456789012" -GitHubOrganization "myorg" -GitHubRepository "myrepo"
Expand All @@ -37,6 +49,13 @@

Adds a federated credential for the develop branch with a custom name.

.EXAMPLE
Add-MtMaesterAppFederatedCredential -AppId "12345678-1234-1234-1234-123456789012" -SetGitHubSecrets

Auto-detects the GitHub organization and repository from the current git remote, adds
the federated credential, and pushes AZURE_CLIENT_ID / AZURE_TENANT_ID to the repo's
Actions secrets via the GitHub CLI.

.LINK
https://maester.dev/docs/commands/Add-MtMaesterAppFederatedCredential
#>
Expand All @@ -53,14 +72,15 @@
[Alias('ClientId')]
[string] $AppId,

# Your GitHub organization name or GitHub username. E.g. jasonf
[Parameter(Mandatory = $true, ParameterSetName = 'ById')]
[Parameter(Mandatory = $true, ParameterSetName = 'ByApplicationId')]
# Your GitHub organization name or GitHub username. E.g. jasonf.
# If omitted (together with -GitHubRepository) the value is auto-detected from
# the local git remote ('origin') when the current directory is a git repo.
[Parameter(Mandatory = $false)]
[string] $GitHubOrganization,

# Your GitHub repository name where the GitHub Actions workflow is located. E.g. maester-tests
[Parameter(Mandatory = $true, ParameterSetName = 'ById')]
[Parameter(Mandatory = $true, ParameterSetName = 'ByApplicationId')]
# Your GitHub repository name where the GitHub Actions workflow is located. E.g. maester-tests.
# Auto-detected from the local git remote ('origin') when omitted.
[Parameter(Mandatory = $false)]
[string] $GitHubRepository,

# The GitHub branch that can use this credential
Expand All @@ -69,13 +89,39 @@

# The name for the federated credential
[Parameter(Mandatory = $false)]
[string] $Name
[string] $Name,

# If set, also pushes AZURE_CLIENT_ID and AZURE_TENANT_ID to the GitHub repo's
# Actions secrets using the GitHub CLI (`gh`). Falls back to printing manual
# instructions if `gh` is not installed or not authenticated.
[Parameter(Mandatory = $false)]
[switch] $SetGitHubSecrets
)

if (-not (Test-MtAzContext)) {
return
}

# Auto-detect GitHub org/repo from the local git remote. Only triggers when BOTH
# parameters were omitted - mixing an explicit value with an auto-detected one is
# ambiguous (which repo did the caller really mean?) so we require both-or-neither.
if (-not $GitHubOrganization -and -not $GitHubRepository) {
$detected = Get-MtGitHubRepoFromGit
if ($detected) {
$GitHubOrganization = $detected.Organization
$GitHubRepository = $detected.Repository
Write-Host "Auto-detected GitHub repository from git remote: $GitHubOrganization/$GitHubRepository" -ForegroundColor Cyan
}
} elseif (-not $GitHubOrganization -or -not $GitHubRepository) {
Write-Error "Specify both -GitHubOrganization and -GitHubRepository, or omit both to auto-detect from the local git remote."
return
}

if (-not $GitHubOrganization -or -not $GitHubRepository) {
Write-Error "GitHubOrganization and GitHubRepository are required. They can be auto-detected when the current directory is a git working tree whose 'origin' remote points at GitHub."
return
}

try {
if ($Id) {
$params = @{ Id = $Id }
Expand Down Expand Up @@ -111,20 +157,46 @@
}

if ($duplicateName -or $duplicateSubject) {
if($duplicateSubject) {
Write-Error "A federated credential for this repository already exists:"
$duplicateCred += $duplicateSubject
# duplicateSubject = an existing credential already grants the requested
# repo/branch. That is not a failure - the desired end state already exists,
# so we treat re-runs as idempotent (and continue to secrets setup when asked).
#
# duplicateName without duplicateSubject = the name is taken by a credential
# for a DIFFERENT repo/branch. The requested credential cannot be created,
# so this remains a hard error so callers / automation detect the failure.
if ($duplicateSubject) {
Write-Warning "A federated credential for this repository already exists:"
$duplicateCred = $duplicateSubject
}
elseif($duplicateName) {
Write-Error "A federated credential with this name already exists:"
$duplicateCred = $duplicateName
elseif ($duplicateName) {
Write-Error "A federated credential with the name '$Name' already exists for a different repository or branch. Choose a different -Name or remove the existing credential."
$duplicateName | ForEach-Object {
Write-Host " Name: $($_.name)" -ForegroundColor Yellow
Write-Host " Subject: $($_.subject)" -ForegroundColor Yellow
Write-Host ""
}
return
}

$duplicateCred | ForEach-Object {
Write-Host " Name: $($_.name)" -ForegroundColor Yellow
Write-Host " Subject: $($_.subject)" -ForegroundColor Yellow
Write-Host ""
}

# If the existing credential already matches the requested repo/branch and the
# caller asked us to also set secrets, do that work instead of silently returning.
# This makes `-SetGitHubSecrets` idempotent on re-runs.
if ($duplicateSubject -and $SetGitHubSecrets) {
Write-Host "Existing credential matches - proceeding to (re)set GitHub Actions secrets." -ForegroundColor Cyan
$tenantId = (Get-AzContext).Tenant.Id
$secretsConfigured = Set-MtGitHubActionsSecret -GitHubRepository "$GitHubOrganization/$GitHubRepository" -ClientId $app.AppId -TenantId $tenantId
if (-not $secretsConfigured) {
Write-MtGitHubSecretsManualInstruction -GitHubOrganization $GitHubOrganization -GitHubRepository $GitHubRepository -ClientId $app.AppId -TenantId $tenantId -AttemptedAutomatic
}
return $duplicateSubject
}

return
}

Expand All @@ -145,24 +217,27 @@
throw "Failed to create federated credential. Error: $($createdCredential.error.message)"
}

$githubSecretsUrl = "https://github.com/$GitHubOrganization/$GitHubRepository/settings/secrets/actions"
$tenantId = (Get-AzContext).Tenant.Id

Write-Host ""
Write-Host "🎉 Federated credential created successfully!" -ForegroundColor Green
Write-Host ""
Write-Host "GitHub Actions Configuration:" -ForegroundColor Yellow
Write-Host "Add these secrets to your GitHub repository ($GitHubOrganization/$GitHubRepository):" -ForegroundColor White
Write-Host ""
Write-Host "1. Browse to $githubSecretsUrl" -ForegroundColor Cyan
Write-Host "2. Click on 'New repository secret'" -ForegroundColor Cyan
Write-Host "3. Create the following secrets:" -ForegroundColor Cyan
Write-Host ""
Write-Host " Name: AZURE_CLIENT_ID" -ForegroundColor Cyan
Write-Host " Value: $($app.AppId)" -ForegroundColor Cyan
Write-Host " Name: AZURE_TENANT_ID" -ForegroundColor Cyan
Write-Host " Value: $((Get-AzContext).Tenant.Id)" -ForegroundColor Cyan
Write-Host ""
Write-Host "See https://maester.dev/docs/monitoring/github#add-entra-tenant-info-to-github-repos for details." -ForegroundColor Yellow
Write-Host ""

$secretsConfigured = $false
if ($SetGitHubSecrets) {
$secretsConfigured = Set-MtGitHubActionsSecret -GitHubRepository "$GitHubOrganization/$GitHubRepository" -ClientId $app.AppId -TenantId $tenantId
}
Comment thread
SamErde marked this conversation as resolved.

if (-not $secretsConfigured) {
$manualParams = @{
GitHubOrganization = $GitHubOrganization
GitHubRepository = $GitHubRepository
ClientId = $app.AppId
TenantId = $tenantId
}
if ($SetGitHubSecrets) { $manualParams['AttemptedAutomatic'] = $true }
Write-MtGitHubSecretsManualInstruction @manualParams
}

return $createdCredential

Expand Down
Loading
Loading