diff --git a/.github/workflows/guardrail-gate-nightly.yml b/.github/workflows/guardrail-gate-nightly.yml new file mode 100644 index 0000000..fc4efdc --- /dev/null +++ b/.github/workflows/guardrail-gate-nightly.yml @@ -0,0 +1,79 @@ +name: Guardrail Gate (Nightly Drift Check) + +on: + schedule: + - cron: "17 3 * * *" + workflow_dispatch: + +permissions: + contents: read + +jobs: + guardrail-nightly: + name: Guardrail Gate Nightly + strategy: + matrix: + os: [ubuntu-latest, windows-latest] + runs-on: ${{ matrix.os }} + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install PowerShell (Ubuntu) + if: matrix.os == 'ubuntu-latest' + run: | + sudo apt-get update + sudo apt-get install -y wget apt-transport-https software-properties-common + wget -q https://packages.microsoft.com/config/ubuntu/22.04/packages-microsoft-prod.deb + sudo dpkg -i packages-microsoft-prod.deb + sudo apt-get update + sudo apt-get install -y powershell + + - name: Generate lane artifacts + shell: pwsh + run: | + pwsh scripts/lanes/check-infra.ps1 + pwsh scripts/lanes/check-sec.ps1 + pwsh scripts/lanes/check-web-build.ps1 + pwsh scripts/lanes/check-data.ps1 + pwsh scripts/lanes/check-obs.ps1 + pwsh scripts/lanes/check-perf.ps1 + pwsh scripts/lanes/check-a11y.ps1 + pwsh scripts/lanes/check-gov.ps1 + + - name: Run guardrail enforcer + shell: pwsh + run: pwsh -File scripts/run-guardrail-enforcer.ps1 -OutDir ai/out + + - name: Append report to job summary + if: always() + shell: pwsh + run: | + if (Test-Path "ai/out/GUARDRAIL_ENFORCER_REPORT.md") { + $content = Get-Content "ai/out/GUARDRAIL_ENFORCER_REPORT.md" -Raw + Write-Host "$content" + echo "$content" >> $env:GITHUB_STEP_SUMMARY + } else { + Write-Host "Report missing" + } + + - name: Upload artifacts + if: failure() || success() + uses: actions/upload-artifact@v4 + with: + name: guardrail-artifacts-nightly-${{ matrix.os }} + path: | + ai/out/LANE_*.md + ai/out/GUARDRAIL_ENFORCER_REPORT.md + + - name: Slack notify on failure (optional) + if: failure() && env.SLACK_WEBHOOK_URL != '' + uses: slackapi/slack-github-action@v1.24.0 + with: + payload: | + { + "text": "Nightly Guardrail Gate failed on ${{ matrix.os }} for ${{ github.repository }}@${{ github.ref }}. See artifacts and report." + } + env: + SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} + diff --git a/.github/workflows/guardrail-gate.yml b/.github/workflows/guardrail-gate.yml new file mode 100644 index 0000000..b93f9e9 --- /dev/null +++ b/.github/workflows/guardrail-gate.yml @@ -0,0 +1,80 @@ +name: Guardrail Gate (PR & Push) + +on: + push: + branches: ["**"] + pull_request: + branches: ["**"] + +permissions: + contents: read + +jobs: + guardrail: + name: Guardrail Gate + strategy: + matrix: + os: [ubuntu-latest, windows-latest] + runs-on: ${{ matrix.os }} + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install PowerShell (Ubuntu) + if: matrix.os == 'ubuntu-latest' + run: | + sudo apt-get update + sudo apt-get install -y wget apt-transport-https software-properties-common + wget -q https://packages.microsoft.com/config/ubuntu/22.04/packages-microsoft-prod.deb + sudo dpkg -i packages-microsoft-prod.deb + sudo apt-get update + sudo apt-get install -y powershell + + - name: Generate lane artifacts + shell: pwsh + run: | + pwsh scripts/lanes/check-infra.ps1 + pwsh scripts/lanes/check-sec.ps1 + pwsh scripts/lanes/check-web-build.ps1 + pwsh scripts/lanes/check-data.ps1 + pwsh scripts/lanes/check-obs.ps1 + pwsh scripts/lanes/check-perf.ps1 + pwsh scripts/lanes/check-a11y.ps1 + pwsh scripts/lanes/check-gov.ps1 + + - name: Run guardrail enforcer + shell: pwsh + run: pwsh -File scripts/run-guardrail-enforcer.ps1 -OutDir ai/out + + - name: Append report to job summary + if: always() + shell: pwsh + run: | + if (Test-Path "ai/out/GUARDRAIL_ENFORCER_REPORT.md") { + $content = Get-Content "ai/out/GUARDRAIL_ENFORCER_REPORT.md" -Raw + Write-Host "$content" + echo "$content" >> $env:GITHUB_STEP_SUMMARY + } else { + Write-Host "Report missing" + } + + - name: Upload artifacts + if: failure() || success() + uses: actions/upload-artifact@v4 + with: + name: guardrail-artifacts-${{ matrix.os }} + path: | + ai/out/LANE_*.md + ai/out/GUARDRAIL_ENFORCER_REPORT.md + + - name: Slack notify on failure (optional) + if: failure() && env.SLACK_WEBHOOK_URL != '' + uses: slackapi/slack-github-action@v1.24.0 + with: + payload: | + { + "text": "Guardrail Gate failed on ${{ matrix.os }} for ${{ github.repository }}@${{ github.ref }}. See artifacts and report." + } + env: + SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} + diff --git a/ai/out/GUARDRAIL_ENFORCER_REPORT.md b/ai/out/GUARDRAIL_ENFORCER_REPORT.md new file mode 100644 index 0000000..6e962da --- /dev/null +++ b/ai/out/GUARDRAIL_ENFORCER_REPORT.md @@ -0,0 +1,11 @@ +# Guardrail Enforcer Report + +OutDir: ai/out +Generated: 2025-11-27 20:10:10 + +Lanes: +- LANE_INFRA: INFRA_OK +- LANE_SEC: SEC_OK + + +Decision: RELEASE_GO diff --git a/ai/out/LANE_INFRA.md b/ai/out/LANE_INFRA.md new file mode 100644 index 0000000..c86ee0d --- /dev/null +++ b/ai/out/LANE_INFRA.md @@ -0,0 +1,10 @@ +# Lane: INFRA +Date: 2025-11-27 20:10:09 + +Summary: +INFRA baseline OK (placeholder). + +Checks: +- Placeholder pass + +Result: INFRA_OK diff --git a/ai/out/LANE_SEC.md b/ai/out/LANE_SEC.md new file mode 100644 index 0000000..9eb7287 --- /dev/null +++ b/ai/out/LANE_SEC.md @@ -0,0 +1,10 @@ +# Lane: SEC +Date: 2025-11-27 20:10:10 + +Summary: +SEC baseline OK (placeholder). + +Checks: +- Placeholder pass + +Result: SEC_OK diff --git a/docs/guardrail-gate.md b/docs/guardrail-gate.md new file mode 100644 index 0000000..6617b04 Binary files /dev/null and b/docs/guardrail-gate.md differ diff --git a/scripts/lanes/_lane-helper.ps1 b/scripts/lanes/_lane-helper.ps1 new file mode 100644 index 0000000..3e7ecac --- /dev/null +++ b/scripts/lanes/_lane-helper.ps1 @@ -0,0 +1,17 @@ +function New-Lane { + param([Parameter(Mandatory=$true)][string]$Name,[Parameter(Mandatory=$true)][string]$Summary) + $lanePath = "ai/out/LANE_{0}.md" -f $Name + @" +# Lane: $Name +Date: $(Get-Date -Format "yyyy-MM-dd HH:mm:ss") + +Summary: +$Summary + +Checks: +- Placeholder pass + +Result: ${Name}_OK +"@ | Set-Content $lanePath +} + diff --git a/scripts/lanes/check-a11y.ps1 b/scripts/lanes/check-a11y.ps1 new file mode 100644 index 0000000..41270e3 --- /dev/null +++ b/scripts/lanes/check-a11y.ps1 @@ -0,0 +1,2 @@ +. "$(Join-Path $PSScriptRoot '_lane-helper.ps1')" +New-Lane -Name "A11Y" -Summary "A11Y baseline OK (placeholder)." diff --git a/scripts/lanes/check-data.ps1 b/scripts/lanes/check-data.ps1 new file mode 100644 index 0000000..96787a0 --- /dev/null +++ b/scripts/lanes/check-data.ps1 @@ -0,0 +1,2 @@ +. "$(Join-Path $PSScriptRoot '_lane-helper.ps1')" +New-Lane -Name "DATA" -Summary "DATA baseline OK (placeholder)." diff --git a/scripts/lanes/check-gov.ps1 b/scripts/lanes/check-gov.ps1 new file mode 100644 index 0000000..3b062ea --- /dev/null +++ b/scripts/lanes/check-gov.ps1 @@ -0,0 +1,2 @@ +. "$(Join-Path $PSScriptRoot '_lane-helper.ps1')" +New-Lane -Name "GOV" -Summary "GOV baseline OK (placeholder)." diff --git a/scripts/lanes/check-infra.ps1 b/scripts/lanes/check-infra.ps1 new file mode 100644 index 0000000..2c882b7 --- /dev/null +++ b/scripts/lanes/check-infra.ps1 @@ -0,0 +1,2 @@ +. "$(Join-Path $PSScriptRoot '_lane-helper.ps1')" +New-Lane -Name "INFRA" -Summary "INFRA baseline OK (placeholder)." diff --git a/scripts/lanes/check-obs.ps1 b/scripts/lanes/check-obs.ps1 new file mode 100644 index 0000000..3fc658c --- /dev/null +++ b/scripts/lanes/check-obs.ps1 @@ -0,0 +1,2 @@ +. "$(Join-Path $PSScriptRoot '_lane-helper.ps1')" +New-Lane -Name "OBS" -Summary "OBS baseline OK (placeholder)." diff --git a/scripts/lanes/check-perf.ps1 b/scripts/lanes/check-perf.ps1 new file mode 100644 index 0000000..c1d2f4a --- /dev/null +++ b/scripts/lanes/check-perf.ps1 @@ -0,0 +1,2 @@ +. "$(Join-Path $PSScriptRoot '_lane-helper.ps1')" +New-Lane -Name "PERF" -Summary "PERF baseline OK (placeholder)." diff --git a/scripts/lanes/check-sec.ps1 b/scripts/lanes/check-sec.ps1 new file mode 100644 index 0000000..0fd26b0 --- /dev/null +++ b/scripts/lanes/check-sec.ps1 @@ -0,0 +1,2 @@ +. "$(Join-Path $PSScriptRoot '_lane-helper.ps1')" +New-Lane -Name "SEC" -Summary "SEC baseline OK (placeholder)." diff --git a/scripts/lanes/check-web-build.ps1 b/scripts/lanes/check-web-build.ps1 new file mode 100644 index 0000000..13faacb --- /dev/null +++ b/scripts/lanes/check-web-build.ps1 @@ -0,0 +1,2 @@ +. "$(Join-Path $PSScriptRoot '_lane-helper.ps1')" +New-Lane -Name "WEB_BUILD" -Summary "WEB_BUILD baseline OK (placeholder)." diff --git a/scripts/run-guardrail-enforcer.ps1 b/scripts/run-guardrail-enforcer.ps1 new file mode 100644 index 0000000..f135eff --- /dev/null +++ b/scripts/run-guardrail-enforcer.ps1 @@ -0,0 +1,42 @@ +param([string]$OutDir = $env:GUARDRAIL_OUT_DIR) + +if (-not $OutDir) { $OutDir = "ai/out" } + +$ErrorActionPreference = "Stop" + +if (-not (Test-Path $OutDir)) { New-Item -ItemType Directory -Force -Path $OutDir | Out-Null } + +$laneFiles = Get-ChildItem -Path $OutDir -Filter "LANE_*.md" -ErrorAction SilentlyContinue + +$results = @() + +foreach ($file in $laneFiles) { + $text = Get-Content $file.FullName -Raw + $match = [regex]::Match($text, "(?m)^Result:\s*(\S+)") + if ($match.Success) { $results += [PSCustomObject]@{ Lane = $file.BaseName; Result = $match.Groups[1].Value } } + else { $results += [PSCustomObject]@{ Lane = $file.BaseName; Result = "UNKNOWN" } } +} + +$overall = "RELEASE_GO" + +foreach ($r in $results) { if ($r.Result -notmatch "OK|RELEASE_GO") { $overall = "RELEASE_HOLD"; break } } + +$report = @" +# Guardrail Enforcer Report + +OutDir: $OutDir +Generated: $(Get-Date -Format "yyyy-MM-dd HH:mm:ss") + +Lanes: +$( + $results | ForEach-Object { "- $($_.Lane): $($_.Result)" } | Out-String +) + +Decision: $overall +"@ + +$reportPath = Join-Path $OutDir "GUARDRAIL_ENFORCER_REPORT.md" +Set-Content -Path $reportPath -Value $report + +if ($overall -eq "RELEASE_GO") { exit 0 } else { Write-Error "Release gated: $overall"; exit 1 } +