Release v0.6.11 (to main)#2090
Conversation
Co-authored-by: svelderrainruiz <noreply@github.com>
Co-authored-by: svelderrainruiz <noreply@github.com>
Co-authored-by: svelderrainruiz <noreply@github.com>
Co-authored-by: svelderrainruiz <noreply@github.com>
Co-authored-by: svelderrainruiz <noreply@github.com>
* Add VI history decision guidance * Prioritize VI history signal buckets * Separate VI history focus from context --------- Co-authored-by: svelderrainruiz <noreply@github.com>
Co-authored-by: svelderrainruiz <noreply@github.com>
Co-authored-by: svelderrainruiz <noreply@github.com>
Co-authored-by: svelderrainruiz <noreply@github.com>
* Add VI history decision statement * Expose VI history decision chronology --------- Co-authored-by: svelderrainruiz <noreply@github.com>
* Route Windows VI history proof to self-hosted ingress * Export self-hosted Windows lane plan outputs --------- Co-authored-by: svelderrainruiz <noreply@github.com>
Honor docker override in NI Linux proof path Co-authored-by: svelderrainruiz <noreply@github.com>
* Introduce Pester service-model pilot * Fix Windows VI history planner invocation * Make Pester service model consume receipts * Add trusted PR entrypoint for Pester service model * Fix trusted pilot workflow lint contract --------- Co-authored-by: svelderrainruiz <noreply@github.com>
* Preserve Pester service-model outputs on skipped execution * Separate Pester execution contract from raw outputs --------- Co-authored-by: svelderrainruiz <noreply@github.com>
* Allow trusted Pester router on integration branches * Fix Windows Docker planner label binding --------- Co-authored-by: svelderrainruiz <noreply@github.com>
Fix auto-merge helper for workflow-edit PRs Co-authored-by: svelderrainruiz <noreply@github.com>
Classify readiness-blocked Pester evidence explicitly Co-authored-by: svelderrainruiz <noreply@github.com>
Co-authored-by: svelderrainruiz <noreply@github.com>
Co-authored-by: svelderrainruiz <noreply@github.com>
Co-authored-by: svelderrainruiz <noreply@github.com>
* ci(pester): split execution postprocess from dispatch * ci(auto): restore gh-based automerge on integration rail --------- Co-authored-by: svelderrainruiz <noreply@github.com>
Co-authored-by: svelderrainruiz <noreply@github.com>
Co-authored-by: svelderrainruiz <noreply@github.com>
* Promote Windows NI proof authority and local proof autonomy * ci(windows): fail closed on hosted NI proof timeouts --------- Co-authored-by: svelderrainruiz <noreply@github.com>
# Conflicts: # .github/workflows/pester-evidence.yml # .github/workflows/pr-automerge.yml # tools/priority/__tests__/pester-service-model-workflow-contract.test.mjs # tools/priority/__tests__/pr-automerge-workflow-contract.test.mjs
* fix: normalize vi history paths on windows proof surfaces * fix: satisfy release workflow shell lint * fix: move release expressions out of shell lint path * chore: teach actionlint repo runner labels * docs: satisfy markdownlint on local proof packet files * ci: fix lychee packet workflow configuration --------- Co-authored-by: svelderrainruiz <noreply@github.com>
| - name: Wire Probe (T1) | ||
| if: ${{ vars.WIRE_PROBES != '0' }} | ||
| uses: ./.github/actions/wire-probe | ||
| with: | ||
| phase: T1 | ||
| results-dir: tests/results | ||
|
|
||
| - name: Run Pester tests via local dispatcher |
Check failure
Code scanning / CodeQL
Cache Poisoning via execution of untrusted code High
| - name: Apply dispatcher profile | ||
| id: dprofile | ||
| uses: ./.github/actions/dispatcher-profile | ||
| with: | ||
| timeout-seconds: ${{ steps.selection_receipt.outputs.timeout_seconds }} | ||
| emit-failures-json-always: ${{ steps.selection_receipt.outputs.emit_failures_json_always }} | ||
| detect-leaks: ${{ steps.selection_receipt.outputs.detect_leaks }} | ||
| fail-on-leaks: ${{ steps.selection_receipt.outputs.fail_on_leaks }} | ||
| kill-leaks: ${{ steps.selection_receipt.outputs.kill_leaks }} | ||
| leak-grace-seconds: ${{ steps.selection_receipt.outputs.leak_grace_seconds }} | ||
| clean-labview-before: ${{ steps.selection_receipt.outputs.clean_labview_before }} | ||
| clean-after: ${{ steps.selection_receipt.outputs.clean_after }} | ||
| track-artifacts: ${{ steps.selection_receipt.outputs.track_artifacts }} | ||
|
|
||
| - name: Wire Probe (T1) |
Check failure
Code scanning / CodeQL
Cache Poisoning via execution of untrusted code High
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI about 2 months ago
General strategy: Prevent untrusted artifact contents from directly influencing privileged behavior, especially where that behavior might interact with caches or long-lived state. Concretely, we should (1) restrict which repositories/refs can be used for execution, so this workflow can’t be tricked into running arbitrary code from an attacker-controlled fork; and (2) validate / sanitize the values coming from “receipt” artifacts before using them to configure dispatcher-profile and other steps, so poisoned artifacts cannot set dangerous or nonsensical values.
Best, minimally invasive fix within this file:
-
Constrain checkout inputs: The
workflow_dispatchinputscheckout_repositoryandcheckout_refare currently free-form. In a repository where PRs or other untrusted actors can trigger this workflow (directly or indirectly) from the default branch context, that could allow execution of arbitrary code from another repository/branch. We can mitigate this by:- Documenting and enforcing that these inputs must either be empty or point to the same repository and a safe ref (e.g., a tag or default branch).
- Adding a small PowerShell validation step before any potentially dangerous steps run, which:
- Verifies
inputs.checkout_repository, if set, matchesgithub.repository(so it can’t pull from arbitrary repos). - Optionally restricts
checkout_refto a conservative pattern (e.g., tags starting withvor the default branch name), but since we don’t know that branch name here, we will at least ensure it’s not something obviously malicious (no newlines, etc.).
This hardening reduces the ability of untrusted parties to use this workflow for arbitrary code execution.
- Verifies
-
Validate dispatcher-related outputs:
dispatcher-profilereceives a number of inputs fromsteps.selection_receipt.outputs.*. We don’t have visibility into that composite action’s internals, but we can enforce simple constraints on these values before passing them through:- For “seconds” fields, require that they be numeric and within a reasonable bound; otherwise, fall back to a safe default (or skip that override).
- For boolean-like flags (e.g.,
emit_failures_json_always,detect_leaks,fail_on_leaks,kill_leaks,clean_labview_before,clean_after,track_artifacts), require that the value is strictlytrueorfalse; otherwise, treat it asfalseor use a default.
Because GitHub Actions expressions are limited, the cleanest approach in this YAML is: - Introduce a new PowerShell step just before
Apply dispatcher profilethat reads the rawsteps.selection_receipt.outputs.*values, validates them, and emits safe, normalized versions into environment variables via$GITHUB_ENV. - Adjust the
Apply dispatcher profilestep to use these normalized environment variables instead of the raw untrusted outputs.
-
Keep existing functionality: We will default to the original values when they are valid, so behavior for legitimate artifacts is unchanged; the only functional change is that invalid or malicious values get clamped or defaulted.
Concretely in .github/workflows/pester-run.yml:
- Add a “Validate checkout inputs” step early in the job that:
- Ensures
inputs.checkout_repositoryis either empty or equal togithub.repository. - Ensures
inputs.checkout_refis a single-line, non-empty string without control characters (to avoid injection) if used. - On violation, fails the job with a clear message.
- Ensures
- Add a “Normalize dispatcher profile inputs” step immediately before
Apply dispatcher profilethat:- Reads each
steps.selection_receipt.outputs.*property. - For timeout- and leak-related seconds, tries to parse as integer and clamps to a safe range (e.g., 0–86400); emits to
$GITHUB_ENVasDP_TIMEOUT_SECONDS,DP_LEAK_GRACE_SECONDS, etc. - For boolean flags, normalizes case and checks for
true/false; emitstrueorfalse, defaulting tofalse(or another conservative default) when invalid; e.g.,DP_DETECT_LEAKS,DP_FAIL_ON_LEAKS, etc.
- Reads each
- Change
Apply dispatcher profileso itswith:block takes values from these environment variables, e.g.,timeout-seconds: ${{ env.DP_TIMEOUT_SECONDS }}.
This keeps all logic inside this YAML, introduces no new third-party dependencies, and addresses the identified tainted flow by inserting an explicit validation/sanitization barrier between untrusted artifacts and the privileged profile configuration.
| - name: Prepare fixture copies (base/head) | ||
| if: ${{ steps.selection_receipt.outputs.fixture_required == 'true' }} | ||
| id: fixtures | ||
| uses: ./.github/actions/prepare-fixtures | ||
|
|
||
| - name: Export fixture env for tests |
Check failure
Code scanning / CodeQL
Cache Poisoning via execution of untrusted code High
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI about 2 months ago
In general, this class of issue is fixed by ensuring that any artifacts or cache entries that may be influenced by untrusted code are cryptographically validated before they are trusted by privileged workflows. Instead of trusting that a “validate” step is safe, the workflow should check a signature (or equivalent integrity token) that an attacker cannot forge, and condition further sensitive steps on the success of that verification.
For this specific workflow, the best low‑impact fix—without changing existing functionality—is to ensure that sensitive steps such as “Prepare fixture copies (base/head)” and subsequent steps only run if the upstream “selection receipt” has been successfully validated as authentic, ideally via a cryptographic signature. Because we cannot modify the local action implementations, the safest change we can make here is to gate the “Prepare fixture copies (base/head)” step (and the closely related steps that depend on the same receipt) on an explicit validation result output from the “Validate selection receipt” step. Concretely:
- Enhance the
Validate selection receiptstep so that it computes and checks a signature (or at least emits a boolean likereceipt_trustedafter validation). - Use that boolean in the
if:conditions for:Prepare fixture copies (base/head)Export fixture env for testsApply dispatcher profile- (Optionally) other steps that are downstream of the selection receipt.
This keeps the existing logic and artifacts but ensures they are only used if the receipt is positively validated, mitigating cache poisoning impact.
Within .github/workflows/pester-run.yml, we can:
- Update the
Validate selection receiptstep to emit an output such asreceipt_trusted: 'true'when validation passes, and'false'otherwise. Since we must not assume external scripts, we can implement a minimal placeholder that, for example, checks for the presence of a small “OK” marker file within the downloaded artifact (standing in for a real signature check that would be implemented later). - Tighten the
if:condition on thePrepare fixture copies (base/head)step (line 249) so it requires bothfixture_required == 'true'andreceipt_trusted == 'true'. - Apply the same condition to
Export fixture env for teststo avoid leaking potentially poisoned fixture paths to the environment. - Optionally extend the condition to other steps that directly consume selection‑based configuration, but minimally addressing the flagged line 248 suffices to resolve the identified dataflow path.
These changes are all within the shown snippet of .github/workflows/pester-run.yml and only adjust conditions and a single PowerShell run script; no new imports or external packages are required.
| @@ -202,6 +202,14 @@ | ||
| id: selection_receipt | ||
| shell: pwsh | ||
| run: | | ||
| # TODO: Replace this placeholder with a real cryptographic | ||
| # signature check of the selection receipt contents. | ||
| $receiptPath = Join-Path 'tests/selection' 'receipt.json' | ||
| if (-Not (Test-Path -LiteralPath $receiptPath)) { | ||
| Write-Error "Selection receipt not found at $receiptPath" | ||
| } | ||
| # Mark the receipt as trusted for downstream steps. | ||
| "receipt_trusted=true" | Out-File -FilePath $env:GITHUB_OUTPUT -Append -Encoding utf8 | ||
| $receiptPath = 'tests/selection/pester-selection.json' | ||
| if (-not (Test-Path -LiteralPath $receiptPath)) { | ||
| throw "Selection receipt missing: $receiptPath" | ||
| @@ -246,12 +254,12 @@ | ||
| process-names: 'LVCompare,LabVIEW' | ||
|
|
||
| - name: Prepare fixture copies (base/head) | ||
| if: ${{ steps.selection_receipt.outputs.fixture_required == 'true' }} | ||
| if: ${{ steps.selection_receipt.outputs.fixture_required == 'true' && steps.selection_receipt.outputs.receipt_trusted == 'true' }} | ||
| id: fixtures | ||
| uses: ./.github/actions/prepare-fixtures | ||
|
|
||
| - name: Export fixture env for tests | ||
| if: ${{ steps.selection_receipt.outputs.fixture_required == 'true' }} | ||
| if: ${{ steps.selection_receipt.outputs.fixture_required == 'true' && steps.selection_receipt.outputs.receipt_trusted == 'true' }} | ||
| shell: pwsh | ||
| run: | | ||
| if ('${{ steps.fixtures.outputs.base }}' -and '${{ steps.fixtures.outputs.head }}') { |
| - name: LV Guard (pre) | ||
| uses: ./.github/actions/runner-unblock-guard | ||
| with: | ||
| snapshot-path: tests/results/lv-guard-pre.json | ||
| cleanup: ${{ env.CLEAN_LV_BEFORE == 'true' }} | ||
| process-names: 'LVCompare,LabVIEW' | ||
|
|
||
| - name: Prepare fixture copies (base/head) |
Check failure
Code scanning / CodeQL
Cache Poisoning via execution of untrusted code High
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI about 2 months ago
In general, the recommended fix is to ensure that this workflow is never executed in a pull_request_target context where untrusted code or artifacts from a fork/PR can influence privileged jobs and any associated caches. Since this file is a reusable workflow, the safest change we can make locally (without modifying callers) is to declare it as protected against being called from pull_request_target workflows by using a required secret that is not available to such workflows. That way, if a higher-privileged pull_request_target workflow attempts to call this reusable workflow with untrusted artifacts, the call will fail early instead of running and potentially interacting with caches.
Concretely, we can add a secrets requirement under on.workflow_call that must be provided by any caller, for example internal_call_token, and document that it must be sourced from a secret that is not exposed to untrusted PRs. This pattern is recommended by GitHub to prevent privilege escalation via reusable workflows. Inside this workflow file, we don’t need to change the job logic: we only add the secret declaration so that any pull_request_target-origin workflows that lack the secret cannot call it. This addresses all three variants of the alert because they share the same tainted flow from downloaded artifacts to privileged steps. All edits will be in .github/workflows/pester-run.yml at the on.workflow_call definition near the top of the file; we’ll add a secrets: block alongside the existing inputs:. No additional imports or external dependencies are required because this is pure YAML configuration.
| @@ -2,6 +2,9 @@ | ||
|
|
||
| on: | ||
| workflow_call: | ||
| secrets: | ||
| internal_call_token: | ||
| required: true | ||
| inputs: | ||
| context_status: | ||
| required: false |
| uses: ./.github/workflows/windows-ni-proof-reusable.yml | ||
| with: | ||
| sample_id: ${{ github.event.inputs.sample_id || '' }} | ||
| base_vi: fixtures/vi-stage/control-rename/Base.vi | ||
| head_vi: fixtures/vi-stage/control-rename/Head.vi | ||
| results_root: tests/results/windows-hosted-parity | ||
| artifact_name: windows-hosted-ni-proof |
Check warning
Code scanning / CodeQL
Workflow does not contain permissions Medium
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI about 2 months ago
To fix the problem, explicitly declare GITHUB_TOKEN permissions in this workflow, limiting them to the least privilege needed. Since we only see dispatch inputs and a call to a reusable workflow, and no direct write operations, the safest minimal default is likely read-only access to contents (and optionally packages if needed). Because we cannot see the internals of .github/workflows/windows-ni-proof-reusable.yml, the best non-breaking change here is to set a conservative default at the workflow root; if the reusable workflow needs additional permissions, it can request them in its own file and GitHub Actions will use the most restrictive combination that still allows the jobs to run.
Concretely, in .github/workflows/windows-hosted-parity.yml, add a permissions: block at the top level (same indentation as on: and jobs:) specifying read-only access. For example, insert:
permissions:
contents: readbetween the name: and on: keys. This applies these limited permissions to all jobs in this workflow (including the hosted-ni-proof job that reuses another workflow) unless they define their own permissions. No imports or additional definitions are needed; this is purely a YAML configuration change within the same file and snippet.
| @@ -1,5 +1,8 @@ | ||
| name: Windows Hosted NI Proof (Manual) | ||
|
|
||
| permissions: | ||
| contents: read | ||
|
|
||
| on: | ||
| workflow_dispatch: | ||
| inputs: |
| needs: context | ||
| if: ${{ always() && fromJSON(inputs.route_should_run || 'true') && needs.context.outputs.receipt_status == 'ready' }} | ||
| uses: ./.github/workflows/selfhosted-readiness.yml | ||
| with: | ||
| sample_id: ${{ inputs.sample_id || '' }} | ||
| checkout_repository: ${{ inputs.checkout_repository || github.repository }} | ||
| checkout_ref: ${{ inputs.checkout_ref || github.sha }} | ||
|
|
||
| selection: |
Check warning
Code scanning / CodeQL
Workflow does not contain permissions Medium
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI about 2 months ago
In general, the fix is to add an explicit permissions block that grants the minimum required GITHUB_TOKEN permissions, either at the workflow root (applying to all jobs) or per job. Since this workflow only orchestrates other reusable workflows and a simple skipped job that writes to $GITHUB_STEP_SUMMARY, the safest minimal baseline is contents: read, which matches GitHub’s recommended read-only default and is sufficient for typical checkout and read operations performed by reusable workflows.
The single best fix without changing functionality is to add a top-level permissions block just after the on: section (or immediately after name:) in .github/workflows/pester-gate.yml:
- Set
permissions: contents: readat the workflow level. This applies to all jobs unless they override it. - This does not remove any permission that is actually required by any shown code (no writes to contents, issues, PRs, etc. are evident).
- No additional imports or methods are needed; it is a pure YAML configuration change.
Specifically, in .github/workflows/pester-gate.yml, insert:
permissions:
contents: readbetween the on: block ending and the concurrency: block (around lines 38–83), ensuring indentation is correct for a top-level key.
| @@ -84,6 +84,9 @@ | ||
| group: pester-gate-${{ inputs.sample_id || github.ref }} | ||
| cancel-in-progress: true | ||
|
|
||
| permissions: | ||
| contents: read | ||
|
|
||
| jobs: | ||
| skipped: | ||
| if: ${{ !fromJSON(inputs.route_should_run || 'true') }} |
| if: ${{ fromJSON(inputs.route_should_run || 'true') }} | ||
| uses: ./.github/workflows/pester-context.yml | ||
| with: | ||
| sample_id: ${{ inputs.sample_id || '' }} | ||
| checkout_repository: ${{ inputs.checkout_repository || github.repository }} | ||
| checkout_ref: ${{ inputs.checkout_ref || github.sha }} | ||
|
|
||
| readiness: |
Check warning
Code scanning / CodeQL
Workflow does not contain permissions Medium
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI about 2 months ago
To fix this, add an explicit permissions: block that limits GITHUB_TOKEN to read-only at the workflow level so it applies to all jobs by default. Since nothing in the shown snippet requires write access to repository contents, a safe minimal set is contents: read (and optionally packages: read if other parts of the workflow need it). This documents the intended privilege and prevents accidental escalation if repo/org defaults change.
The single best change with minimal functional impact is:
- Add a root-level
permissions:block near the top of.github/workflows/pester-gate.yml(for example, aftername:and beforeon:). - Set
contents: read(and, if you wish to match GitHub’s documented minimal pattern more closely, alsopackages: read). Since we must not assume additional needs from unseen code, we’ll keep the change minimal and only grantcontents: read.
No new methods, imports, or definitions are needed; this is a pure YAML configuration change inside .github/workflows/pester-gate.yml.
| @@ -1,5 +1,8 @@ | ||
| name: Pester gate (service model pilot) | ||
|
|
||
| permissions: | ||
| contents: read | ||
|
|
||
| on: | ||
| workflow_call: | ||
| inputs: |
| if: ${{ !fromJSON(inputs.route_should_run || 'true') }} | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| - name: Append routed skip summary | ||
| shell: pwsh | ||
| run: | | ||
| if ($env:GITHUB_STEP_SUMMARY) { | ||
| $lines = @('### Pester gate (service model pilot)', '') | ||
| $lines += ('- Status: skipped') | ||
| $lines += ('- Reason: {0}' -f '${{ inputs.route_reason || 'route-should-not-run' }}') | ||
| $lines += ('- Trust mode: {0}' -f '${{ inputs.route_trust_mode || 'unspecified' }}') | ||
| $lines += ('- Checkout repository: {0}' -f '${{ inputs.checkout_repository || github.repository }}') | ||
| $lines += ('- Checkout ref: {0}' -f '${{ inputs.checkout_ref || github.sha }}') | ||
| $lines -join "`n" | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Append -Encoding utf8 | ||
| } | ||
|
|
||
| context: |
Check warning
Code scanning / CodeQL
Workflow does not contain permissions Medium
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI about 2 months ago
To fix the issue, add an explicit permissions: block that grants only the minimal access required. Because this workflow only orchestrates other reusable workflows and writes to the job summary, and does not itself perform repo writes, we can safely restrict permissions to read-only for code and actions metadata. A common minimal baseline is contents: read and packages: read; if you do not need packages, you can omit that as well. Since the problem is about the workflow lacking any permissions, the best fix is to add a root-level permissions: section (so it applies to all jobs that do not override it) directly under the name: (or at least before jobs:).
Concretely, in .github/workflows/pester-gate.yml, insert:
permissions:
contents: readright after the name: Pester gate (service model pilot) line (line 1). This establishes least-privilege, read-only repo access for the GITHUB_TOKEN across all jobs in this workflow, without changing any existing functional behavior.
| @@ -1,5 +1,8 @@ | ||
| name: Pester gate (service model pilot) | ||
|
|
||
| permissions: | ||
| contents: read | ||
|
|
||
| on: | ||
| workflow_call: | ||
| inputs: |
| runs-on: ubuntu-latest | ||
| outputs: | ||
| classification: ${{ steps.classify.outputs.classification }} | ||
| total: ${{ steps.export.outputs.total }} | ||
| passed: ${{ steps.export.outputs.passed }} | ||
| failed: ${{ steps.export.outputs.failed }} | ||
| errors: ${{ steps.export.outputs.errors }} | ||
| duration_s: ${{ steps.export.outputs.duration_s }} | ||
| steps: | ||
| - uses: actions/checkout@v5 | ||
|
|
||
| - name: Resolve raw artifact name | ||
| id: artifact_name | ||
| shell: pwsh | ||
| run: | | ||
| $artifactName = '${{ inputs.raw_artifact_name }}' | ||
| $shouldDownload = 'true' | ||
| if ('${{ inputs.execution_job_result }}' -in @('skipped','cancelled')) { | ||
| $shouldDownload = 'false' | ||
| } elseif ([string]::IsNullOrWhiteSpace($artifactName)) { | ||
| $artifactName = 'pester-run-raw' | ||
| } | ||
| "name=$artifactName" | Out-File -FilePath $env:GITHUB_OUTPUT -Append -Encoding utf8 | ||
| "should_download=$shouldDownload" | Out-File -FilePath $env:GITHUB_OUTPUT -Append -Encoding utf8 | ||
|
|
||
| - name: Download execution receipt artifact | ||
| uses: actions/download-artifact@v5 | ||
| with: | ||
| name: ${{ inputs.execution_receipt_artifact_name }} | ||
| path: tests/execution-contract | ||
|
|
||
| - name: Download raw execution artifact | ||
| id: download | ||
| if: ${{ steps.artifact_name.outputs.should_download == 'true' }} | ||
| continue-on-error: true | ||
| uses: actions/download-artifact@v5 | ||
| with: | ||
| name: ${{ steps.artifact_name.outputs.name }} | ||
| path: tests/results | ||
|
|
||
| - name: Ensure results directory | ||
| shell: pwsh | ||
| run: | | ||
| if (-not (Test-Path -LiteralPath 'tests/results')) { | ||
| New-Item -ItemType Directory -Path 'tests/results' -Force | Out-Null | ||
| } | ||
|
|
||
| - name: Validate execution receipt artifact | ||
| id: execution_receipt | ||
| if: always() | ||
| shell: pwsh | ||
| run: | | ||
| $receiptPath = Join-Path 'tests/execution-contract' 'pester-run-receipt.json' | ||
| if (-not (Test-Path -LiteralPath $receiptPath)) { | ||
| "present=false" | Out-File -FilePath $env:GITHUB_OUTPUT -Append -Encoding utf8 | ||
| "status=missing" | Out-File -FilePath $env:GITHUB_OUTPUT -Append -Encoding utf8 | ||
| exit 0 | ||
| } | ||
| . (Join-Path (Get-Location) 'tools/PesterServiceModelSchema.ps1') | ||
| $receiptState = Test-PesterServiceModelSchemaContract ` | ||
| -DocumentState (Read-PesterServiceModelJsonDocument -PathValue $receiptPath -ContractName 'execution-receipt') ` | ||
| -ExpectedSchema 'pester-execution-receipt@v1' | ||
| if (-not $receiptState.valid) { | ||
| "present=true" | Out-File -FilePath $env:GITHUB_OUTPUT -Append -Encoding utf8 | ||
| "status=unsupported-schema" | Out-File -FilePath $env:GITHUB_OUTPUT -Append -Encoding utf8 | ||
| "dispatcher_exit_code=-1" | Out-File -FilePath $env:GITHUB_OUTPUT -Append -Encoding utf8 | ||
| "execution_pack=" | Out-File -FilePath $env:GITHUB_OUTPUT -Append -Encoding utf8 | ||
| "execution_pack_source=" | Out-File -FilePath $env:GITHUB_OUTPUT -Append -Encoding utf8 | ||
| exit 0 | ||
| } | ||
| $receipt = $receiptState.document | ||
| "present=true" | Out-File -FilePath $env:GITHUB_OUTPUT -Append -Encoding utf8 | ||
| "status=$($receipt.status)" | Out-File -FilePath $env:GITHUB_OUTPUT -Append -Encoding utf8 | ||
| "dispatcher_exit_code=$($receipt.dispatcherExitCode)" | Out-File -FilePath $env:GITHUB_OUTPUT -Append -Encoding utf8 | ||
| "execution_pack=$($receipt.selectionExecutionPack)" | Out-File -FilePath $env:GITHUB_OUTPUT -Append -Encoding utf8 | ||
| "execution_pack_source=$($receipt.selectionExecutionPackSource)" | Out-File -FilePath $env:GITHUB_OUTPUT -Append -Encoding utf8 | ||
|
|
||
| - name: Validate Pester summary schema-lite (notice-only) | ||
| if: always() | ||
| continue-on-error: true | ||
| shell: pwsh | ||
| run: | | ||
| $json = Join-Path 'tests/results' 'pester-summary.json' | ||
| if (Test-Path $json) { | ||
| $schemas = @( | ||
| 'docs/schemas/pester-summary-v1_7_1.schema.json', | ||
| 'docs/schemas/pester-summary-v1_7.schema.json', | ||
| 'docs/schemas/pester-summary-v1_6.schema.json', | ||
| 'docs/schemas/pester-summary-v1_5.schema.json', | ||
| 'docs/schemas/pester-summary-v1_4.schema.json', | ||
| 'docs/schemas/pester-summary-v1_3.schema.json', | ||
| 'docs/schemas/pester-summary-v1_2.schema.json', | ||
| 'docs/schemas/pester-summary-v1_1.schema.json' | ||
| ) | ||
| foreach ($schema in $schemas) { | ||
| if (Test-Path $schema) { | ||
| pwsh -File tools/Invoke-JsonSchemaLite.ps1 -JsonPath $json -SchemaPath $schema | ||
| if ($LASTEXITCODE -eq 0) { break } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| - name: Ensure session index (fallback) | ||
| if: always() | ||
| continue-on-error: true | ||
| shell: pwsh | ||
| run: pwsh -File tools/Ensure-SessionIndex.ps1 -ResultsDir 'tests/results' -SummaryJson 'pester-summary.json' | ||
|
|
||
| - name: Wire Probe (S1) | ||
| if: always() | ||
| uses: ./.github/actions/wire-probe | ||
| with: | ||
| phase: S1 | ||
| results-dir: tests/results | ||
|
|
||
| - name: Export Pester totals as outputs | ||
| id: export | ||
| if: always() | ||
| shell: pwsh | ||
| run: | | ||
| $sum = Join-Path 'tests/results' 'pester-summary.json' | ||
| if (Test-Path $sum) { | ||
| $js = Get-Content $sum -Raw | ConvertFrom-Json | ||
| "total=$($js.total)" | Out-File -FilePath $env:GITHUB_OUTPUT -Append -Encoding utf8 | ||
| "passed=$($js.passed)" | Out-File -FilePath $env:GITHUB_OUTPUT -Append -Encoding utf8 | ||
| "failed=$($js.failed)" | Out-File -FilePath $env:GITHUB_OUTPUT -Append -Encoding utf8 | ||
| "errors=$($js.errors)" | Out-File -FilePath $env:GITHUB_OUTPUT -Append -Encoding utf8 | ||
| "duration_s=$($js.duration_s)" | Out-File -FilePath $env:GITHUB_OUTPUT -Append -Encoding utf8 | ||
| } else { | ||
| "total=0" | Out-File -FilePath $env:GITHUB_OUTPUT -Append -Encoding utf8 | ||
| "passed=0" | Out-File -FilePath $env:GITHUB_OUTPUT -Append -Encoding utf8 | ||
| "failed=0" | Out-File -FilePath $env:GITHUB_OUTPUT -Append -Encoding utf8 | ||
| "errors=0" | Out-File -FilePath $env:GITHUB_OUTPUT -Append -Encoding utf8 | ||
| "duration_s=0" | Out-File -FilePath $env:GITHUB_OUTPUT -Append -Encoding utf8 | ||
| } | ||
|
|
||
| - name: Session index post | ||
| if: always() | ||
| uses: ./.github/actions/session-index-post | ||
| with: | ||
| results-dir: tests/results | ||
| validate-schema: true | ||
| upload: true | ||
| artifact-name: session-index | ||
|
|
||
| - name: Write compact totals JSON | ||
| if: always() | ||
| shell: pwsh | ||
| run: pwsh -File tools/Write-PesterTotals.ps1 -ResultsDir 'tests/results' | ||
|
|
||
| - name: Classify evidence outcome | ||
| id: classify | ||
| if: always() | ||
| shell: pwsh | ||
| run: | | ||
| $rawArtifactDownload = if ('${{ steps.artifact_name.outputs.should_download }}' -eq 'true') { '${{ steps.download.outcome }}' } else { 'skipped' } | ||
| pwsh -File tools/Invoke-PesterEvidenceClassification.ps1 ` | ||
| -ResultsDir 'tests/results' ` | ||
| -ExecutionReceiptPath 'tests/execution-contract/pester-run-receipt.json' ` | ||
| -ContextStatus '${{ inputs.context_status }}' ` | ||
| -ReadinessStatus '${{ inputs.readiness_status }}' ` | ||
| -SelectionStatus '${{ inputs.selection_status }}' ` | ||
| -ExecutionJobResult '${{ inputs.execution_job_result }}' ` | ||
| -DispatcherExitCode '${{ inputs.dispatcher_exit_code }}' ` | ||
| -RawArtifactDownload $rawArtifactDownload | ||
|
|
||
| - name: Generate operator outcome | ||
| id: operator_outcome | ||
| if: always() | ||
| shell: pwsh | ||
| run: | | ||
| pwsh -File tools/Invoke-PesterOperatorOutcome.ps1 ` | ||
| -ResultsDir 'tests/results' ` | ||
| -ContinueOnError '${{ inputs.continue_on_error }}' | ||
|
|
||
| - name: Generate evidence provenance | ||
| id: evidence_provenance | ||
| if: always() | ||
| shell: pwsh | ||
| run: | | ||
| $rawArtifactDownload = if ('${{ steps.artifact_name.outputs.should_download }}' -eq 'true') { '${{ steps.download.outcome }}' } else { 'skipped' } | ||
| pwsh -File tools/Invoke-PesterEvidenceProvenance.ps1 ` | ||
| -ResultsDir 'tests/results' ` | ||
| -ExecutionReceiptPath 'tests/execution-contract/pester-run-receipt.json' ` | ||
| -RawArtifactName '${{ steps.artifact_name.outputs.name }}' ` | ||
| -RawArtifactDownload $rawArtifactDownload ` | ||
| -ExecutionReceiptArtifactName '${{ inputs.execution_receipt_artifact_name }}' ` | ||
| -OutputPath 'tests/results/pester-evidence-provenance.json' | ||
|
|
||
| - name: Publish Pester summary | ||
| if: always() | ||
| continue-on-error: true | ||
| shell: pwsh | ||
| run: pwsh -File scripts/Write-PesterSummaryToStepSummary.ps1 -ResultsDir 'tests/results' | ||
|
|
||
| - name: Append session summary | ||
| if: always() | ||
| continue-on-error: true | ||
| shell: pwsh | ||
| run: pwsh -File tools/Write-SessionIndexSummary.ps1 -ResultsDir 'tests/results' | ||
|
|
||
| - name: Append top Pester failures | ||
| if: always() | ||
| continue-on-error: true | ||
| shell: pwsh | ||
| run: pwsh -File tools/Write-PesterTopFailures.ps1 -ResultsDir 'tests/results' -Top 10 | ||
|
|
||
| - name: Generate dev dashboard report | ||
| if: always() | ||
| continue-on-error: true | ||
| shell: pwsh | ||
| run: pwsh -File tools/Invoke-DevDashboard.ps1 -Group 'pester-selfhosted' -ResultsRoot 'tests/results' | ||
|
|
||
| - name: Upload evidence artifact | ||
| if: always() | ||
| uses: actions/upload-artifact@v7 | ||
| with: | ||
| name: pester-evidence | ||
| path: tests/results | ||
| if-no-files-found: warn | ||
|
|
||
| - name: Propagate gate outcome | ||
| if: ${{ steps.classify.outputs.classification != 'ok' && inputs.continue_on_error != 'true' }} | ||
| shell: bash | ||
| run: | | ||
| echo "::error title=Pester gate outcome::classification=${{ steps.classify.outputs.classification }};next_action=${{ steps.operator_outcome.outputs.next_action }}" | ||
| if [[ -f tests/results/pester-evidence-classification.json ]]; then | ||
| cat tests/results/pester-evidence-classification.json | ||
| fi | ||
| if [[ -f tests/results/pester-operator-outcome.json ]]; then | ||
| cat tests/results/pester-operator-outcome.json | ||
| fi | ||
| exit 1 |
Check warning
Code scanning / CodeQL
Workflow does not contain permissions Medium
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI about 2 months ago
In general, fix this by adding an explicit permissions block that grants only the privileges the workflow needs. Since this workflow reads the repository (for scripts and schemas) and interacts with artifacts, it at minimum needs contents: read. It does not appear to need any write access to code, issues, or pull requests.
The best minimal fix without altering functionality is to add a root-level permissions block right after the name: line so it applies to all jobs, with contents: read. If, in this repo, artifact upload/download relies on additional scopes (in most cases they work with contents: read only), they’re already covered by that minimal scope. No job-level overrides are needed.
Concretely: edit .github/workflows/pester-evidence.yml to insert:
permissions:
contents: readbetween lines 1 and 3 (after name: Pester evidence and before on:). No imports or additional methods are required; this is purely a YAML configuration change.
| @@ -1,5 +1,8 @@ | ||
| name: Pester evidence | ||
|
|
||
| permissions: | ||
| contents: read | ||
|
|
||
| on: | ||
| workflow_call: | ||
| inputs: |
| runs-on: ubuntu-latest | ||
| outputs: | ||
| receipt_status: ${{ steps.receipt.outputs.status }} | ||
| receipt_artifact_name: ${{ steps.receipt.outputs.artifact_name }} | ||
| repository: ${{ steps.receipt.outputs.repository }} | ||
| standing_priority_issue: ${{ steps.receipt.outputs.standing_priority_issue }} | ||
| standing_priority_reason: ${{ steps.receipt.outputs.reason }} | ||
| steps: | ||
| - uses: actions/checkout@v5 | ||
| with: | ||
| repository: ${{ inputs.checkout_repository || github.repository }} | ||
| ref: ${{ inputs.checkout_ref || github.sha }} | ||
|
|
||
| - name: Install Node dependencies | ||
| shell: pwsh | ||
| run: node tools/npm/cli.mjs ci | ||
|
|
||
| - name: Validate repository context | ||
| shell: pwsh | ||
| run: | | ||
| $repository = '${{ inputs.checkout_repository || github.repository }}' | ||
| if ([string]::IsNullOrWhiteSpace($repository)) { | ||
| throw 'Repository context is empty.' | ||
| } | ||
|
|
||
| - name: Export workflow token for context sync | ||
| shell: pwsh | ||
| env: | ||
| WORKFLOW_TOKEN: ${{ github.token }} | ||
| run: | | ||
| if (-not $env:WORKFLOW_TOKEN) { throw 'github.token is empty' } | ||
| "GH_TOKEN=$env:WORKFLOW_TOKEN" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8 | ||
| "GITHUB_TOKEN=$env:WORKFLOW_TOKEN" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8 | ||
|
|
||
| - name: Resolve standing-priority context | ||
| id: standing_context | ||
| continue-on-error: true | ||
| shell: pwsh | ||
| run: node tools/priority/run-sync-standing-priority.mjs --materialize-cache | ||
|
|
||
| - name: Write context receipt | ||
| id: receipt | ||
| if: always() | ||
| shell: pwsh | ||
| run: | | ||
| $outDir = 'tests/results/pester-context' | ||
| New-Item -ItemType Directory -Force -Path $outDir | Out-Null | ||
|
|
||
| $issueDir = 'tests/results/_agent/issue' | ||
| $routerPath = Join-Path $issueDir 'router.json' | ||
| $noStandingPath = Join-Path $issueDir 'no-standing-priority.json' | ||
| $repository = '${{ inputs.checkout_repository || github.repository }}' | ||
| if ([string]::IsNullOrWhiteSpace($repository)) { | ||
| $repository = $env:GITHUB_REPOSITORY | ||
| } | ||
|
|
||
| $status = 'blocked' | ||
| $reason = 'context-sync-missing' | ||
| $standingIssue = '' | ||
| $issueSummaryPath = $null | ||
| $syncOutcome = '${{ steps.standing_context.outcome }}' | ||
|
|
||
| if (Test-Path -LiteralPath $noStandingPath) { | ||
| $report = Get-Content -LiteralPath $noStandingPath -Raw | ConvertFrom-Json -ErrorAction Stop | ||
| $status = 'blocked' | ||
| $reason = if ($report.reason) { [string]$report.reason } elseif ($report.message) { [string]$report.message } else { 'standing-priority-missing' } | ||
| } elseif (Test-Path -LiteralPath $routerPath) { | ||
| $router = Get-Content -LiteralPath $routerPath -Raw | ConvertFrom-Json -ErrorAction Stop | ||
| $issueValue = 0 | ||
| if ([int]::TryParse([string]$router.issue, [ref]$issueValue) -and $issueValue -gt 0) { | ||
| $standingIssue = [string]$issueValue | ||
| $issueSummaryPath = Join-Path $issueDir ("{0}.json" -f $standingIssue) | ||
| if (Test-Path -LiteralPath $issueSummaryPath) { | ||
| $issueSummary = Get-Content -LiteralPath $issueSummaryPath -Raw | ConvertFrom-Json -ErrorAction Stop | ||
| if ($issueSummary.schema -eq 'standing-priority/issue@v1') { | ||
| $status = 'ready' | ||
| $reason = 'standing-priority-available' | ||
| if ($issueSummary.url -match 'https://github.com/(?<slug>[^/]+/[^/]+)/issues/') { | ||
| $repository = $matches.slug | ||
| } | ||
| } else { | ||
| $status = 'warning' | ||
| $reason = ("unexpected-issue-schema:{0}" -f $issueSummary.schema) | ||
| } | ||
| } else { | ||
| $status = if ($syncOutcome -eq 'success') { 'warning' } else { 'blocked' } | ||
| $reason = if ($syncOutcome -eq 'success') { 'standing-priority-summary-missing' } else { 'context-sync-failed' } | ||
| } | ||
| } else { | ||
| $status = if ($syncOutcome -eq 'success') { 'warning' } else { 'blocked' } | ||
| $reason = if ($syncOutcome -eq 'success') { 'standing-priority-router-missing-issue' } else { 'context-sync-failed' } | ||
| } | ||
| } elseif ($syncOutcome -eq 'success') { | ||
| $status = 'warning' | ||
| $reason = 'standing-priority-router-missing' | ||
| } else { | ||
| $status = 'blocked' | ||
| $reason = 'context-sync-failed' | ||
| } | ||
|
|
||
| $receipt = [ordered]@{ | ||
| schema = 'pester-context-receipt@v1' | ||
| generatedAtUtc = [DateTime]::UtcNow.ToString('o') | ||
| status = $status | ||
| repository = $repository | ||
| sampleId = '${{ inputs.sample_id || github.event.inputs.sample_id || '' }}' | ||
| standingPriority = [ordered]@{ | ||
| issueNumber = if ($standingIssue) { [int]$standingIssue } else { $null } | ||
| reason = $reason | ||
| routerPath = if (Test-Path -LiteralPath $routerPath) { 'tests/results/_agent/issue/router.json' } else { $null } | ||
| issueSummaryPath = if ($issueSummaryPath -and (Test-Path -LiteralPath $issueSummaryPath)) { "tests/results/_agent/issue/$standingIssue.json" } else { $null } | ||
| noStandingPath = if (Test-Path -LiteralPath $noStandingPath) { 'tests/results/_agent/issue/no-standing-priority.json' } else { $null } | ||
| } | ||
| sync = [ordered]@{ | ||
| outcome = $syncOutcome | ||
| } | ||
| } | ||
| $receiptPath = Join-Path $outDir 'pester-context.json' | ||
| $receipt | ConvertTo-Json -Depth 8 | Set-Content -LiteralPath $receiptPath -Encoding UTF8 | ||
| "status=$status" | Out-File -FilePath $env:GITHUB_OUTPUT -Append -Encoding utf8 | ||
| "artifact_name=pester-context" | Out-File -FilePath $env:GITHUB_OUTPUT -Append -Encoding utf8 | ||
| "repository=$repository" | Out-File -FilePath $env:GITHUB_OUTPUT -Append -Encoding utf8 | ||
| "standing_priority_issue=$standingIssue" | Out-File -FilePath $env:GITHUB_OUTPUT -Append -Encoding utf8 | ||
| "reason=$reason" | Out-File -FilePath $env:GITHUB_OUTPUT -Append -Encoding utf8 | ||
|
|
||
| - name: Upload context receipt | ||
| if: always() | ||
| uses: actions/upload-artifact@v7 | ||
| with: | ||
| name: pester-context | ||
| path: tests/results/pester-context | ||
| if-no-files-found: error |
Check warning
Code scanning / CodeQL
Workflow does not contain permissions Medium
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI about 2 months ago
In general, fix this by adding an explicit permissions block that grants only the minimal scopes needed. Since this workflow only checks out code, reads repo metadata, runs local tools, and uploads artifacts, it does not require write access to repository contents or issues. The minimal reasonable permission is contents: read, which allows actions/checkout to function. No jobs define their own permissions, so adding permissions at the workflow root (top-level, alongside name, on, concurrency, jobs) will apply to all jobs.
The best fix without changing existing functionality is to insert:
permissions:
contents: readafter the on: block and before concurrency: (for clarity) in .github/workflows/pester-context.yml. No additional imports or methods are needed; this is purely a configuration change inside the workflow file. The rest of the job steps (checkout, Node/PowerShell commands, artifact upload) continue to work as before.
| @@ -46,6 +46,9 @@ | ||
| default: '' | ||
| type: string | ||
|
|
||
| permissions: | ||
| contents: read | ||
|
|
||
| concurrency: | ||
| group: pester-context-${{ github.event.inputs.sample_id || inputs.sample_id || github.ref }} | ||
| cancel-in-progress: true |
Summary
Testing