# ----------------------------------------------------------------------------- # unknowns-budget-gate.yml # Sprint: SPRINT_5100_0004_0001_unknowns_budget_ci_gates # Task: T2 - CI Budget Gate Workflow # Description: Enforces unknowns budgets on PRs and pushes # ----------------------------------------------------------------------------- name: Unknowns Budget Gate on: pull_request: paths: - 'src/**' - 'Dockerfile*' - '*.lock' - 'etc/policy.unknowns.yaml' push: branches: [main] paths: - 'src/**' - 'Dockerfile*' - '*.lock' env: DOTNET_NOLOGO: 1 DOTNET_CLI_TELEMETRY_OPTOUT: 1 TZ: UTC STELLAOPS_BUDGET_CONFIG: ./etc/policy.unknowns.yaml jobs: scan-and-check-budget: runs-on: ubuntu-22.04 permissions: contents: read pull-requests: write steps: - name: Checkout uses: actions/checkout@v4 with: fetch-depth: 0 - name: Setup .NET uses: actions/setup-dotnet@v4 with: dotnet-version: '10.0.100' include-prerelease: true - name: Cache NuGet packages uses: actions/cache@v4 with: path: | ~/.nuget/packages local-nugets/packages key: budget-gate-nuget-${{ runner.os }}-${{ hashFiles('**/*.csproj') }} - name: Restore and Build CLI run: | dotnet restore src/Cli/StellaOps.Cli/StellaOps.Cli.csproj --configfile nuget.config dotnet build src/Cli/StellaOps.Cli/StellaOps.Cli.csproj -c Release --no-restore - name: Determine environment id: env run: | if [[ "${{ github.ref }}" == "refs/heads/main" ]]; then echo "environment=prod" >> $GITHUB_OUTPUT echo "enforce=true" >> $GITHUB_OUTPUT elif [[ "${{ github.event_name }}" == "pull_request" ]]; then echo "environment=stage" >> $GITHUB_OUTPUT echo "enforce=false" >> $GITHUB_OUTPUT else echo "environment=dev" >> $GITHUB_OUTPUT echo "enforce=false" >> $GITHUB_OUTPUT fi - name: Create sample verdict for testing id: scan run: | mkdir -p out # In a real scenario, this would be from stella scan # For now, create a minimal verdict file cat > out/verdict.json << 'EOF' { "unknowns": [] } EOF echo "verdict_path=out/verdict.json" >> $GITHUB_OUTPUT - name: Check unknowns budget id: budget continue-on-error: true run: | set +e dotnet run --project src/Cli/StellaOps.Cli/StellaOps.Cli.csproj -- \ unknowns budget check \ --verdict ${{ steps.scan.outputs.verdict_path }} \ --environment ${{ steps.env.outputs.environment }} \ --output json \ --fail-on-exceed > out/budget-result.json EXIT_CODE=$? echo "exit_code=$EXIT_CODE" >> $GITHUB_OUTPUT if [ -f out/budget-result.json ]; then # Compact JSON for output RESULT=$(cat out/budget-result.json | jq -c '.') echo "result=$RESULT" >> $GITHUB_OUTPUT fi exit $EXIT_CODE - name: Upload budget report uses: actions/upload-artifact@v4 if: always() with: name: budget-report-${{ github.run_id }} path: out/budget-result.json retention-days: 30 - name: Post PR comment if: github.event_name == 'pull_request' && always() uses: actions/github-script@v7 with: script: | const fs = require('fs'); let result = { isWithinBudget: true, totalUnknowns: 0 }; try { const content = fs.readFileSync('out/budget-result.json', 'utf8'); result = JSON.parse(content); } catch (e) { console.log('Could not read budget result:', e.message); } const status = result.isWithinBudget ? ':white_check_mark:' : ':x:'; const env = '${{ steps.env.outputs.environment }}'; let body = `## ${status} Unknowns Budget Check | Metric | Value | |--------|-------| | Environment | ${env} | | Total Unknowns | ${result.totalUnknowns || 0} | | Budget Limit | ${result.totalLimit || 'Unlimited'} | | Status | ${result.isWithinBudget ? 'PASS' : 'FAIL'} | `; if (result.violations && result.violations.length > 0) { body += ` ### Violations `; for (const v of result.violations) { body += `- **${v.reasonCode}**: ${v.count}/${v.limit}\n`; } } if (result.message) { body += `\n> ${result.message}\n`; } body += `\n---\n_Generated by StellaOps Unknowns Budget Gate_`; // Find existing comment const { data: comments } = await github.rest.issues.listComments({ owner: context.repo.owner, repo: context.repo.repo, issue_number: context.issue.number, }); const botComment = comments.find(c => c.body.includes('Unknowns Budget Check') && c.user.type === 'Bot' ); if (botComment) { await github.rest.issues.updateComment({ owner: context.repo.owner, repo: context.repo.repo, comment_id: botComment.id, body: body }); } else { await github.rest.issues.createComment({ owner: context.repo.owner, repo: context.repo.repo, issue_number: context.issue.number, body: body }); } - name: Fail if budget exceeded (prod) if: steps.env.outputs.environment == 'prod' && steps.budget.outputs.exit_code == '2' run: | echo "::error::Production unknowns budget exceeded!" exit 1 - name: Warn if budget exceeded (non-prod) if: steps.env.outputs.environment != 'prod' && steps.budget.outputs.exit_code == '2' run: | echo "::warning::Unknowns budget exceeded for ${{ steps.env.outputs.environment }}"