Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
11 changes: 11 additions & 0 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,16 @@ jobs:
contents: read
id-token: write

# Terraform documentation freshness check
terraform-docs-check:
name: Terraform Docs Check
uses: ./.github/workflows/terraform-docs-check.yml
with:
soft-fail: true
permissions:
contents: read


# CodeQL security analysis
codeql-analysis:
name: CodeQL Analysis
Expand Down Expand Up @@ -213,6 +223,7 @@ jobs:
- terraform-tests
- go-lint
- go-tests
- terraform-docs-check
- codeql-analysis
name: Release Please
runs-on: ubuntu-latest
Expand Down
9 changes: 9 additions & 0 deletions .github/workflows/pester-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,15 @@ jobs:
$testPaths += $testFile
}
}

# Map scripts/ root source to test: scripts/Foo.ps1 -> shared/ci/tests/scripts/Foo.Tests.ps1
if ($file -match '^scripts/([^/]+)\.psm?1$') {
$relativePath = $Matches[1]
$testFile = "shared/ci/tests/scripts/$relativePath.Tests.ps1"
if (Test-Path $testFile) {
$testPaths += $testFile
}
}
}

$uniquePaths = $testPaths | Sort-Object -Unique
Expand Down
11 changes: 11 additions & 0 deletions .github/workflows/pr-validation.yml
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,16 @@ jobs:
permissions:
contents: read

# Terraform documentation freshness check
terraform-docs-check:
name: Terraform Docs Check
uses: ./.github/workflows/terraform-docs-check.yml
with:
soft-fail: true
changed-files-only: true
permissions:
contents: read

# Go tests
go-tests:
name: Go Tests
Expand All @@ -206,6 +216,7 @@ jobs:
contents: read
id-token: write


# CodeQL security analysis
codeql-analysis:
name: CodeQL Analysis
Expand Down
87 changes: 87 additions & 0 deletions .github/workflows/terraform-docs-check.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
name: Terraform Docs Check

on:
workflow_call:
inputs:
soft-fail:
description: 'Whether to continue on terraform-docs drift detection'
required: false
type: boolean
default: false
changed-files-only:
description: 'Only check directories with changed Terraform files'
required: false
type: boolean
default: false
terraform-docs-version:
description: 'terraform-docs version to install'
required: false
type: string
default: 'v0.21.0'
terraform-docs-sha256:
description: 'SHA256 checksum of terraform-docs linux-amd64 tarball'
required: false
type: string
default: '2fdd81b8d21ff1498cd559af0dcc5d155835f84600db06d3923e217124fc735a'

permissions:
contents: read

defaults:
run:
shell: pwsh

jobs:
terraform-docs-check:
name: Terraform Docs Check
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
fetch-depth: ${{ inputs.changed-files-only && '0' || '1' }}

- name: Create logs directory
run: New-Item -ItemType Directory -Force -Path logs | Out-Null

- name: Setup Node.js and install dependencies
uses: ./.github/actions/setup-node-deps

- name: Install terraform-docs
run: |
$version = '${{ inputs.terraform-docs-version }}'
$expectedSha = '${{ inputs.terraform-docs-sha256 }}'
$tarball = 'terraform-docs.tar.gz'
$url = "https://github.com/terraform-docs/terraform-docs/releases/download/${version}/terraform-docs-${version}-linux-amd64.tar.gz"

Invoke-WebRequest -Uri $url -OutFile $tarball

$actualSha = (Get-FileHash -Path $tarball -Algorithm SHA256).Hash.ToLowerInvariant()
if ($actualSha -ne $expectedSha) {
throw "SHA256 mismatch for terraform-docs ${version}: expected '${expectedSha}', got '${actualSha}'"
}
Write-Output "SHA256 verified: ${actualSha}"

tar -xzf $tarball
sudo mv terraform-docs /usr/local/bin/
terraform-docs --version

- name: Run terraform-docs check
continue-on-error: ${{ inputs.soft-fail }}
run: |
$params = @{}
if ('${{ inputs.changed-files-only }}' -eq 'true') {
$params['ChangedFilesOnly'] = $true
}
shared/ci/linting/Invoke-TerraformDocsCheck.ps1 @params

- name: Upload terraform-docs check results
if: always()
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: terraform-docs-check-results
path: logs/terraform-docs-check-results.json
retention-days: 30
44 changes: 43 additions & 1 deletion setup-dev.ps1
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
#!/usr/bin/env pwsh
#!/usr/bin/env pwsh
Comment thread
WilliamBerryiii marked this conversation as resolved.
# Copyright (c) Microsoft Corporation.
# SPDX-License-Identifier: MIT

Expand Down Expand Up @@ -89,6 +89,48 @@ Write-Host ''
Write-Host 'If this script fails, the devcontainer is your fallback.'
Write-Host ''

Write-Section 'Git Symlink Resolution'

# Git symlinks are stored as text files on Windows when core.symlinks=false.
# Replace broken symlinks with junctions (directories) or hard links (files).
$symlinkEntries = git ls-files -s 2>$null | Select-String '120000' | ForEach-Object {
($_ -split '\s+', 4)[3]
}
$repairedCount = 0
foreach ($entry in $symlinkEntries) {
$fullPath = Join-Path $ScriptDir $entry
if (-not (Test-Path $fullPath)) { continue }

$item = Get-Item $fullPath -Force
# Already a junction/symlink — nothing to fix
if ($item.LinkType) { continue }
# Only fix plain text files (broken symlink placeholders)
if ($item.PSIsContainer) { continue }

$target = (Get-Content $fullPath -Raw).Trim()
$resolvedTarget = Resolve-Path (Join-Path (Split-Path $fullPath) $target) -ErrorAction SilentlyContinue
if (-not $resolvedTarget) {
Write-Warn "Symlink target not found: $entry -> $target"
continue
}

Remove-Item $fullPath -Force
$targetItem = Get-Item $resolvedTarget.Path
if ($targetItem.PSIsContainer) {
New-Item -ItemType Junction -Path $fullPath -Target $resolvedTarget.Path | Out-Null
}
else {
New-Item -ItemType HardLink -Path $fullPath -Target $resolvedTarget.Path | Out-Null
}
$repairedCount++
}
if ($repairedCount -gt 0) {
Write-Info "Repaired $repairedCount broken git symlink(s) (junctions/hard links)"
}
else {
Write-Info 'All git symlinks are intact'
}

Write-Section 'Tool Verification'

Assert-Tools az, terraform, kubectl, helm, jq
Expand Down
172 changes: 172 additions & 0 deletions shared/ci/linting/Invoke-TerraformDocsCheck.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
#!/usr/bin/env pwsh
# Copyright (c) Microsoft Corporation.
# SPDX-License-Identifier: MIT

#Requires -Version 7.0

<#
.SYNOPSIS
Checks that terraform-docs generated documentation is up to date.
.DESCRIPTION
Runs npm run docs:tf -- --check to compare generated documentation against committed
files. Reports drift via CI annotations and writes JSON results to logs/.
.PARAMETER OutputPath
Path for JSON results. Defaults to logs/terraform-docs-check-results.json.
.PARAMETER TerraformDir
Root directory containing Terraform files. Defaults to infrastructure/terraform.
.PARAMETER ChangedFilesOnly
When set, only check if directories containing changed .tf files have doc drift.
#>

[CmdletBinding()]
param(
[string]$OutputPath,
[string]$TerraformDir,
[switch]$ChangedFilesOnly
)

Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'

Import-Module (Join-Path $PSScriptRoot "Modules/LintingHelpers.psm1") -Force
Import-Module (Join-Path $PSScriptRoot "../../../scripts/lib/Modules/CIHelpers.psm1") -Force

function Invoke-TerraformDocsCheckCore {
[CmdletBinding()]
param(
[string]$OutputPath,
[string]$TerraformDir,
[switch]$ChangedFilesOnly
)

$repoRoot = & git rev-parse --show-toplevel 2>$null
if (-not $repoRoot) {
$repoRoot = (Get-Item $PSScriptRoot).Parent.Parent.Parent.FullName
}

if (-not $OutputPath) { $OutputPath = Join-Path $repoRoot 'logs/terraform-docs-check-results.json' }
if (-not $TerraformDir) { $TerraformDir = Join-Path $repoRoot 'infrastructure/terraform' }
Comment thread
WilliamBerryiii marked this conversation as resolved.
Outdated

$outputDir = Split-Path $OutputPath -Parent
if (-not (Test-Path $outputDir)) {
New-Item -ItemType Directory -Force -Path $outputDir | Out-Null
}

if (-not (Get-Command terraform-docs -ErrorAction SilentlyContinue)) {
Write-CIAnnotation -Level Error -Message 'terraform-docs is not installed or not in PATH'
return 1
}

$tdVersion = (& terraform-docs --version 2>&1 | Out-String).Trim()

# Skip if no relevant files changed
if ($ChangedFilesOnly) {
$changedTf = @(Get-ChangedFilesFromGit -FileExtensions @('*.tf', '*.tfvars'))
$changedConfig = @(Get-ChangedFilesFromGit -FileExtensions @('*.yml') | Where-Object { $_ -match '\.terraform-docs\.yml$' })

if ($changedTf.Count -eq 0 -and $changedConfig.Count -eq 0) {
Write-Host 'No Terraform or terraform-docs config files changed — skipping docs check'

$results = @{
timestamp = (Get-Date -Format 'o')
terraform_docs_version = $tdVersion
skipped = $true
drift_detected = $false
drifted_files = @()
summary = @{
files_drifted = 0
overall_passed = $true
}
}

$results | ConvertTo-Json -Depth 10 | Out-File -FilePath $OutputPath -Encoding utf8
Write-Host "Results written to $OutputPath"

$summaryContent = "### Terraform Docs Check Results`n`n**Status:** ⏭️ Skipped (no relevant files changed)"
Write-CIStepSummary -Content $summaryContent
Write-Host $summaryContent
return 0
}
}

# Run terraform-docs check via npm script
Write-Host 'Running npm run docs:tf -- --check...'
$output = & npm run docs:tf -- --check 2>&1 | ForEach-Object { $_.ToString() }
Comment thread
WilliamBerryiii marked this conversation as resolved.
Outdated
$exitCode = $LASTEXITCODE
$driftDetected = ($exitCode -ne 0)
$driftedFiles = @()

if ($driftDetected) {
# Parse output for drifted file paths from git diff output
$driftedFiles = @($output | ForEach-Object {
if ($_ -match 'diff --git a/(.+) b/') { $Matches[1] }
} | Where-Object { $_ } | Sort-Object -Unique)

foreach ($file in $driftedFiles) {
Write-CIAnnotation -Level Error -Message "Documentation is out of date: $file. Run 'npm run docs:tf' to regenerate." -File $file
}

if ($driftedFiles.Count -eq 0) {
Write-CIAnnotation -Level Error -Message "terraform-docs detected documentation drift. Run 'npm run docs:tf' to regenerate."
}
}

# Build results
$results = @{
timestamp = (Get-Date -Format 'o')
terraform_docs_version = $tdVersion
skipped = $false
drift_detected = $driftDetected
drifted_files = $driftedFiles
output = ($output -join "`n")
summary = @{
files_drifted = $driftedFiles.Count
overall_passed = (-not $driftDetected)
}
}

$results | ConvertTo-Json -Depth 10 | Out-File -FilePath $OutputPath -Encoding utf8
Write-Host "Results written to $OutputPath"

# Step summary
$summaryLines = @()
$summaryLines += '### Terraform Docs Check Results'
$summaryLines += ''

if ($driftDetected) {
$summaryLines += '**Status:** ❌ Documentation drift detected'
$summaryLines += ''
$summaryLines += 'Run `npm run docs:tf` to regenerate documentation.'
$summaryLines += ''
if ($driftedFiles.Count -gt 0) {
$summaryLines += '| File | Status |'
$summaryLines += '|------|--------|'
foreach ($file in $driftedFiles) {
$summaryLines += "| ``$file`` | ❌ Out of date |"
}
}
}
else {
$summaryLines += '**Status:** ✅ All documentation is up to date'
}

$summaryContent = $summaryLines -join "`n"
Write-CIStepSummary -Content $summaryContent
Write-Host $summaryContent

if ($driftDetected) { return 1 } else { return 0 }
}

#region Main Execution
if ($MyInvocation.InvocationName -ne '.') {
try {
$exitCode = Invoke-TerraformDocsCheckCore @PSBoundParameters
exit $exitCode
}
catch {
Write-Error -ErrorAction Continue "Invoke-TerraformDocsCheck failed: $($_.Exception.Message)"
Write-CIAnnotation -Level Error -Message $_.Exception.Message
exit 1
}
}
#endregion Main Execution
Loading
Loading