Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
78 commits
Select commit Hold shift + click to select a range
7598bd3
Require an execute bit before selecting a Unix apphost
nohwnd Aug 18, 2026
114e602
Merge remote-tracking branch 'origin/main' into nohwnd-restore-unix-e…
nohwnd Aug 20, 2026
01d27ff
Fence the Unix apphost execute-bit check on NET7_0_OR_GREATER
nohwnd Aug 20, 2026
f122241
docs: fix typo refrence -> reference (#10672)
vaibhav8a Aug 24, 2026
57204bd
Add DisposeHelper unit tests (#10649)
Evangelink Aug 24, 2026
8393f40
[main] Bump Microsoft.CodeAnalysis.BannedApiAnalyzers from 5.10.0-1.2…
dependabot[bot] Aug 24, 2026
39bf3c0
Reduce ServerMode notification allocations (#10670)
Evangelink Aug 24, 2026
e287fd7
Cache regex assertion patterns safely (#10661)
Evangelink Aug 24, 2026
2538d01
Add StackTraceRegexHelper unit tests (#10655)
Evangelink Aug 24, 2026
808198c
Optimize TRX reparse point confinement checks (#10648)
Evangelink Aug 24, 2026
d52effb
[main] Bump github/gh-aw/actions/setup from 0.86.0 to 0.86.2 (#10657)
dependabot[bot] Aug 24, 2026
966e2a2
[main] Bump Azure.Core from 1.61.0 to 1.62.0 (#10663)
dependabot[bot] Aug 24, 2026
396ebe4
Add report file retry helper tests (#10635)
Evangelink Aug 24, 2026
71b22b5
Split AzureDevOpsSummaryReporter into focused modules (#10644)
Evangelink Aug 24, 2026
456af5a
Update bundled Playwright for .NET to 1.62.0 (#10624)
nohwnd-bot Aug 24, 2026
39a9175
[main] Bump DavidAnson/markdownlint-cli2-action from 24.1.0 to 24.2.0…
dependabot[bot] Aug 24, 2026
d499b22
[main] Update dependencies from devdiv/DevDiv/vs-code-coverage, dotne…
dotnet-maestro[bot] Aug 24, 2026
77d47b4
Test MSTEST0001 adapter flag parsing (#10631)
Evangelink Aug 24, 2026
6c7846a
Fix broken changelog comparison links (#10676)
Evangelink Aug 24, 2026
eb36b1e
Localized file check-in by OneLocBuild Task: Build definition ID 1218…
dotnet-oneloc-localization[bot] Aug 24, 2026
0981e6d
Fix apphost DOTNET_ROOT path assertion (#10626)
nohwnd Aug 24, 2026
2661467
Read acceptance-test binlogs behind a lock (#10658)
nohwnd Aug 24, 2026
c59d540
Localized file check-in by OneLocBuild Task: Build definition ID 1218…
dotnet-oneloc-localization[bot] Aug 24, 2026
c1279d5
[main] Bump github/codeql-action/upload-sarif from 4.37.6 to 4.37.7 (…
dependabot[bot] Aug 24, 2026
73397da
Remove duplicate XLF target entries (#10680)
Evangelink Aug 24, 2026
9c6cc0e
Pin expert review threat detector model (#10684)
Evangelink Aug 24, 2026
e77b6cb
Fix TRX lifetime handshake race (#10682)
Evangelink Aug 24, 2026
9516a1c
Fix server-mode protocol capability documentation (#10688)
Evangelink Aug 24, 2026
1d2d090
Upgrade agentic workflows to gh-aw 0.86.2 (#10678)
Evangelink Aug 24, 2026
95901a7
Enforce AwesomeAssertions in source generation tests (#10677)
Evangelink Aug 24, 2026
85b1aaf
Fix agentic workflow activation failures (#10686)
Evangelink Aug 24, 2026
100b3d9
Normalize punctuation in MTP help descriptions (#10683)
Evangelink Aug 24, 2026
6fb5373
Deduplicate report identity key encoding (#10687)
Evangelink Aug 24, 2026
034514c
Avoid false threat detection in review workflows (#10692)
Evangelink Aug 24, 2026
df20227
Fix MSTEST0082 follow-up gaps (#10637)
Evangelink Aug 24, 2026
25010a3
Fix ambiguous localization lock markers (#10690)
Evangelink Aug 24, 2026
31e1cbf
Cache MSTest MTP node properties (#10694)
Evangelink Aug 24, 2026
348da37
Localized file check-in by OneLocBuild Task: Build definition ID 1218…
dotnet-oneloc-localization[bot] Aug 24, 2026
a0177c7
Avoid expert review detection false positives (#10705)
Evangelink Aug 24, 2026
14f0b0a
Report code coverage in CI summaries (#10693)
Evangelink Aug 24, 2026
6c06604
Label Azure DevOps retry attempt results (#10704)
Evangelink Aug 24, 2026
0b3d85c
Add hierarchical configuration sections (#10699)
Evangelink Aug 24, 2026
a7f3466
Document and test custom retry extensibility (#10703)
Evangelink Aug 24, 2026
f8992c1
[main] Update dependencies from microsoft/testfx (#10713)
dotnet-maestro[bot] Aug 25, 2026
99c5496
[main] Update dependencies from dotnet/arcade (#10712)
dotnet-maestro[bot] Aug 25, 2026
5115024
[main] Update dependencies from devdiv/DevDiv/vs-code-coverage (#10714)
dotnet-maestro[bot] Aug 25, 2026
f0bcffa
Localized file check-in by OneLocBuild Task: Build definition ID 1218…
dotnet-oneloc-localization[bot] Aug 25, 2026
c6b529b
Preserve generated attributes for async tests (#10700)
Evangelink Aug 25, 2026
9bbfc32
Fix Azure DevOps published test identity (#10706)
Evangelink Aug 25, 2026
111e68f
Add GitHub Actions summary section selection (#10697)
Evangelink Aug 25, 2026
b525be9
Deduplicate report post-processing helpers (#10717)
Evangelink Aug 25, 2026
66ec411
Add failure-only GitHub step summaries (#10695)
Evangelink Aug 25, 2026
af78e8b
Fix agentic workflow incident handling (#10722)
Evangelink Aug 25, 2026
85b7f8f
Split CI run summary aggregation into focused modules (#10719)
Evangelink Aug 25, 2026
88063ea
Localized file check-in by OneLocBuild Task: Build definition ID 1218…
dotnet-oneloc-localization[bot] Aug 25, 2026
359e1d9
Update Aspire.Hosting.Testing to 13.5.2 (#10724)
Evangelink Aug 25, 2026
5ac412c
Add tests for artifact reparse point detection (#10721)
Evangelink Aug 25, 2026
52b767e
Attach retry artifacts to Azure DevOps sub-results (#10723)
Evangelink Aug 25, 2026
c005c21
Declare PlatformServices as non-packable (#10728)
Evangelink Aug 25, 2026
491b236
Pin the threat-detection model on every agentic workflow (#10729)
nohwnd Aug 25, 2026
4f64688
Test that the HTML post-processor refuses to merge through a symlink …
nohwnd Aug 25, 2026
a6f1a7d
Run the Markdown Linter on markdownlint-cli2 instead of Super Linter …
nohwnd Aug 25, 2026
3986221
Complete MSTest 4.4 and Platform 2.4 changelogs (#10742)
Evangelink Aug 25, 2026
57ea162
Add nightly performance regression detection (#10720)
Evangelink Aug 25, 2026
432ae5e
Handle report merge directory creation failures (#10734)
Evangelink Aug 25, 2026
4c5f67e
Deduplicate published-results parsing in Azure DevOps client (#10735)
Evangelink Aug 25, 2026
86e88e3
Document IDataConsumer UID filtering (#10740)
Evangelink Aug 25, 2026
bdc92c4
[main] Update dependencies from dotnet/arcade (#10747)
dotnet-maestro[bot] Aug 26, 2026
dfd5b7c
[main] Update dependencies from devdiv/DevDiv/vs-code-coverage (#10749)
dotnet-maestro[bot] Aug 26, 2026
a89f97b
Localized file check-in by OneLocBuild Task: Build definition ID 1218…
dotnet-oneloc-localization[bot] Aug 26, 2026
0708690
[main] Update dependencies from microsoft/testfx (#10748)
dotnet-maestro[bot] Aug 26, 2026
cd572ae
[main] Bump Microsoft.TestPlatform.Filter.Source from 18.8.0-preview-…
dependabot[bot] Aug 26, 2026
366d968
[main] Bump Microsoft.TestPlatform.ObjectModel from 18.8.0 to 18.9.0 …
dependabot[bot] Aug 26, 2026
16b27e3
[main] Bump Microsoft.CodeAnalysis.VisualBasic.Analyzer.Testing from …
dependabot[bot] Aug 26, 2026
190ac31
Require an execute bit before selecting a Unix apphost
nohwnd Aug 18, 2026
3ef8a35
Fence the Unix apphost execute-bit check on NET7_0_OR_GREATER
nohwnd Aug 20, 2026
a43021a
Retry Unix apphost launches through dotnet
Evangelink Aug 26, 2026
fffb558
Merge PR branch before review fixes
Evangelink Aug 26, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
2 changes: 1 addition & 1 deletion .gitattributes
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@
#*.rtf diff=astextplain
#*.RTF diff=astextplain

.github/workflows/*.lock.yml linguist-generated=true merge=ours
.github/workflows/*.lock.yml linguist-generated=true

###############################################################################
# Localization resource files
Expand Down
94 changes: 37 additions & 57 deletions .github/aw/actions-lock.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,20 +40,20 @@
"version": "v7.0.1",
"sha": "043fb46d1a93c77aae656e7c1c64a875d1fc6a0a"
},
"github/codeql-action/upload-sarif@v4.37.6": {
"github/codeql-action/upload-sarif@v4.37.8": {
"repo": "github/codeql-action/upload-sarif",
"version": "v4.37.6",
"sha": "5595ccaf912efad79be6eef63a5619ff05969be3"
"version": "v4.37.8",
"sha": "db488ddef3bf6cb639b32c2e9a7c0a7ea8271d28"
},
"github/gh-aw-actions/setup-cli@v0.84.3": {
"github/gh-aw-actions/setup-cli@v0.86.2": {
"repo": "github/gh-aw-actions/setup-cli",
"version": "v0.84.3",
"sha": "c863074b673419603d146aab585e2986ef08deec"
"version": "v0.86.2",
"sha": "6aab9e5b5c91c615506061f09bedd81a23babe3c"
},
"github/gh-aw-actions/setup@v0.84.3": {
"github/gh-aw-actions/setup@v0.86.2": {
"repo": "github/gh-aw-actions/setup",
"version": "v0.84.3",
"sha": "c863074b673419603d146aab585e2986ef08deec"
"version": "v0.86.2",
"sha": "6aab9e5b5c91c615506061f09bedd81a23babe3c"
},
"super-linter/super-linter@v8.7.0": {
"repo": "super-linter/super-linter",
Expand All @@ -68,60 +68,40 @@
"digest": "sha256:28bd5fe8b56d1bd048e5babf5b10710ebe0bae67db86916198a6eec434943f8b",
"pinned_image": "alpine:latest@sha256:28bd5fe8b56d1bd048e5babf5b10710ebe0bae67db86916198a6eec434943f8b"
},
"ghcr.io/github/gh-aw-firewall/agent:0.27.42": {
"image": "ghcr.io/github/gh-aw-firewall/agent:0.27.42",
"digest": "sha256:26a8af4e5566485b02f52af59ee03803ae798271a9619d4767e94d07806deb9b",
"pinned_image": "ghcr.io/github/gh-aw-firewall/agent:0.27.42@sha256:26a8af4e5566485b02f52af59ee03803ae798271a9619d4767e94d07806deb9b"
},
"ghcr.io/github/gh-aw-firewall/agent:0.27.43": {
"image": "ghcr.io/github/gh-aw-firewall/agent:0.27.43",
"digest": "sha256:04e2d1987a565000a8f114b89d806ae7a3864dd4f944be65275b28c93d8690e6",
"pinned_image": "ghcr.io/github/gh-aw-firewall/agent:0.27.43@sha256:04e2d1987a565000a8f114b89d806ae7a3864dd4f944be65275b28c93d8690e6"
},
"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.42": {
"image": "ghcr.io/github/gh-aw-firewall/api-proxy:0.27.42",
"digest": "sha256:944f2686c9ab9bec338fd14b662461662f77cd12cd0ea8a3e7cb8c0987cd1607",
"pinned_image": "ghcr.io/github/gh-aw-firewall/api-proxy:0.27.42@sha256:944f2686c9ab9bec338fd14b662461662f77cd12cd0ea8a3e7cb8c0987cd1607"
},
"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.43": {
"image": "ghcr.io/github/gh-aw-firewall/api-proxy:0.27.43",
"digest": "sha256:d85f57975af5ea23af4996e41ed73fbc8f5b4a47402472bfe82e508f352cb0c1",
"pinned_image": "ghcr.io/github/gh-aw-firewall/api-proxy:0.27.43@sha256:d85f57975af5ea23af4996e41ed73fbc8f5b4a47402472bfe82e508f352cb0c1"
},
"ghcr.io/github/gh-aw-firewall/squid:0.27.42": {
"image": "ghcr.io/github/gh-aw-firewall/squid:0.27.42",
"digest": "sha256:42dfeb649c680a8558cd5423dbc530b653a69413e35ffbe5e71da5d48c94bdf0",
"pinned_image": "ghcr.io/github/gh-aw-firewall/squid:0.27.42@sha256:42dfeb649c680a8558cd5423dbc530b653a69413e35ffbe5e71da5d48c94bdf0"
},
"ghcr.io/github/gh-aw-firewall/squid:0.27.43": {
"image": "ghcr.io/github/gh-aw-firewall/squid:0.27.43",
"digest": "sha256:26be5e0b8c8f4c41c8a59126b29bb5d80b07253597472ded2a16bdd75abcbf9d",
"pinned_image": "ghcr.io/github/gh-aw-firewall/squid:0.27.43@sha256:26be5e0b8c8f4c41c8a59126b29bb5d80b07253597472ded2a16bdd75abcbf9d"
},
"ghcr.io/github/gh-aw-mcpg:v0.4.6": {
"image": "ghcr.io/github/gh-aw-mcpg:v0.4.6",
"digest": "sha256:fecabec51bbc41f2ad61076d6bcd9a36ef23b142e672a444e054d37fc29de93c",
"pinned_image": "ghcr.io/github/gh-aw-mcpg:v0.4.6@sha256:fecabec51bbc41f2ad61076d6bcd9a36ef23b142e672a444e054d37fc29de93c"
},
"ghcr.io/github/gh-aw-mcpg:v0.4.7": {
"image": "ghcr.io/github/gh-aw-mcpg:v0.4.7",
"digest": "sha256:7545220a9aca134b71e51193ee0eaf4c50756ebf8fbd25a63ae7556e62815c00",
"pinned_image": "ghcr.io/github/gh-aw-mcpg:v0.4.7@sha256:7545220a9aca134b71e51193ee0eaf4c50756ebf8fbd25a63ae7556e62815c00"
"ghcr.io/github/gh-aw-firewall/agent:0.27.44": {
"image": "ghcr.io/github/gh-aw-firewall/agent:0.27.44",
"digest": "sha256:0d727725c737b58c7bdf51f640cffb928385ec46517e0917c7f1a02f1bada8b4",
"pinned_image": "ghcr.io/github/gh-aw-firewall/agent:0.27.44@sha256:0d727725c737b58c7bdf51f640cffb928385ec46517e0917c7f1a02f1bada8b4"
},
"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.44": {
"image": "ghcr.io/github/gh-aw-firewall/api-proxy:0.27.44",
"digest": "sha256:b50fbadba138f6e9aba94aca09711335c489bb3b15861220cb66f6092e042dc7",
"pinned_image": "ghcr.io/github/gh-aw-firewall/api-proxy:0.27.44@sha256:b50fbadba138f6e9aba94aca09711335c489bb3b15861220cb66f6092e042dc7"
},
"ghcr.io/github/gh-aw-firewall/cli-proxy:0.27.44": {
"image": "ghcr.io/github/gh-aw-firewall/cli-proxy:0.27.44",
"digest": "sha256:c064d15974f7c933ec7d3f7b4038f4fd203547b3154bdc821afd379144887eff",
"pinned_image": "ghcr.io/github/gh-aw-firewall/cli-proxy:0.27.44@sha256:c064d15974f7c933ec7d3f7b4038f4fd203547b3154bdc821afd379144887eff"
},
"ghcr.io/github/gh-aw-firewall/squid:0.27.44": {
"image": "ghcr.io/github/gh-aw-firewall/squid:0.27.44",
"digest": "sha256:83e48bbe12c634be8c228a576832fe45f66c529ac3659db92bddbcf2eeb6d627",
"pinned_image": "ghcr.io/github/gh-aw-firewall/squid:0.27.44@sha256:83e48bbe12c634be8c228a576832fe45f66c529ac3659db92bddbcf2eeb6d627"
},
"ghcr.io/github/gh-aw-mcpg:v0.4.9": {
"image": "ghcr.io/github/gh-aw-mcpg:v0.4.9",
"digest": "sha256:e5a1569aeaf41820fa7bdee3e94468cae448133cdbf00119ad24f5b74db1ab9f",
"pinned_image": "ghcr.io/github/gh-aw-mcpg:v0.4.9@sha256:e5a1569aeaf41820fa7bdee3e94468cae448133cdbf00119ad24f5b74db1ab9f"
},
"ghcr.io/github/gh-aw-node": {
"image": "ghcr.io/github/gh-aw-node",
"digest": "sha256:0d9f1fb5fd6610c0ac1f5194a38e45a8a1e81f8a390d5142d8e4e6f26a4b3196",
"pinned_image": "ghcr.io/github/gh-aw-node@sha256:0d9f1fb5fd6610c0ac1f5194a38e45a8a1e81f8a390d5142d8e4e6f26a4b3196"
},
"ghcr.io/github/github-mcp-server:v1.7.0": {
"image": "ghcr.io/github/github-mcp-server:v1.7.0",
"digest": "sha256:c491ffdf6f4c85cb5397021bc655edb8ab825c6f5f568e7597d77a1bd7c4d308",
"pinned_image": "ghcr.io/github/github-mcp-server:v1.7.0@sha256:c491ffdf6f4c85cb5397021bc655edb8ab825c6f5f568e7597d77a1bd7c4d308"
},
"ghcr.io/github/github-mcp-server:v1.8.0": {
"image": "ghcr.io/github/github-mcp-server:v1.8.0",
"digest": "sha256:d5a18c04b92714c309eb46a2305087e91a4dbd80420f6e462656699f95093520",
"pinned_image": "ghcr.io/github/github-mcp-server:v1.8.0@sha256:d5a18c04b92714c309eb46a2305087e91a4dbd80420f6e462656699f95093520"
"ghcr.io/github/github-mcp-server:v1.9.0": {
"image": "ghcr.io/github/github-mcp-server:v1.9.0",
"digest": "sha256:881b53d6f75f69bdbc1b5b10fc2f1361717c19054143b3a8529fb5c32061a50e",
"pinned_image": "ghcr.io/github/github-mcp-server:v1.9.0@sha256:881b53d6f75f69bdbc1b5b10fc2f1361717c19054143b3a8529fb5c32061a50e"
},
"mcr.microsoft.com/dotnet-buildtools/prereqs:azurelinux-3.0-binlog-mcp-amd64": {
"image": "mcr.microsoft.com/dotnet-buildtools/prereqs:azurelinux-3.0-binlog-mcp-amd64",
Expand Down
2 changes: 2 additions & 0 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,8 @@ When making change to resource files, you MUST:

When you add a new CLI option, rename an existing one, or change the description/arguments of an existing one (typically by editing an `ICommandLineOptionsProvider` implementation such as `PlatformCommandLineProvider`, `TerminalTestReporterCommandLineOptionsProvider`, `MSTestExtension`'s options provider, or a `*CommandLineOptionsProvider`), you MUST update the corresponding `--help` and `--info` acceptance test expectations so they keep matching the actual output.

All CLI option and extension descriptions must end with terminal punctuation (normally a period).

The wildcard-match expectations live in:

- [`test/IntegrationTests/Microsoft.Testing.Platform.Acceptance.IntegrationTests/HelpInfoTests.cs`](../test/IntegrationTests/Microsoft.Testing.Platform.Acceptance.IntegrationTests/HelpInfoTests.cs) — MTP help/info with no extensions registered.
Expand Down
10 changes: 9 additions & 1 deletion .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,14 +37,22 @@ updates:
schedule:
interval: weekly
ignore:
# The github/gh-aw-actions/* actions are pinned by `gh aw compile` and
# The github/gh-aw-actions/* and github/gh-aw/actions/setup actions are
# pinned by `gh aw compile` and
# MUST move in lockstep with the gh-aw CLI version that generated the
# `.lock.yml` files (and `.github/aw/actions-lock.json`). Letting
# Dependabot bump just the `uses:` SHAs drifts them away from the
# generator, which breaks the threat-detection job with
# `awf: command not found` -> parse_error (see #10203). These are
# updated via `gh aw upgrade` / the Agentic Maintenance workflow instead.
- dependency-name: "github/gh-aw-actions/*"
- dependency-name: "github/gh-aw/actions/setup"
# DavidAnson/markdownlint-cli2-action pins the CLI version it bundles, and
# `.github/workflows/markdown-linter.md` installs that same version by hand so the
# scheduled report and the pull request gate apply identical rules. Dependabot can
# only see the action, so letting it bump alone would silently give the two different
# rule sets. Move both pins together, by hand, when updating the action.
- dependency-name: "DavidAnson/markdownlint-cli2-action"
commit-message:
prefix: '[main] '
cooldown:
Expand Down
141 changes: 136 additions & 5 deletions .github/scripts/check_action_pins.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
along unnoticed in an unrelated pull request (see microsoft/testfx#10258).

This script is a deterministic, offline guard for exactly that failure mode.
It enforces five rules:
It enforces six rules:

R1 (generated files) Every `uses:` reference must be pinned to a 40-character
commit SHA. A mutable tag or branch is rejected. Generated files are
Expand All @@ -37,6 +37,10 @@
aliases hide the pin and any `# vX.Y.Z` label from review; a reference the
recognizers cannot parse would fall out of every rule. Both fail here
instead.
R6 (lock files) Every action reference declared in a generated
`gh-aw-manifest` must use that manifest's SHA. This catches partial
dependency updates that change executable `uses:` lines without
recompiling the workflow.

Comparisons are made on a canonical form. GitHub resolves owner/repository names
case-insensitively, so `Actions/Checkout` would otherwise look like an untracked
Expand All @@ -51,9 +55,9 @@

References inside a workflow's generated header (the `# Custom actions used:`
block) are audited alongside the live `uses:` steps, because the corruption shows
up there too. The machine-readable `# gh-aw-manifest:` JSON header is *not*
audited: it is compiler bookkeeping copied from `actions-lock.json` at compile
time rather than an executable reference, so it self-heals on the next recompile.
up there too. The machine-readable `# gh-aw-manifest:` JSON header is also checked
against those references so a partial dependency update cannot combine generated
steps from one gh-aw version with setup scripts from another.

Executable collection
---------------------
Expand Down Expand Up @@ -116,7 +120,7 @@
HEADER_ACTION_RE = re.compile(
r"""^#\s+-\s+(?P<ref>\S+@\S+?)\s*(?:\#\s*(?P<comment>.*?))?\s*$"""
)
MANIFEST_RE = re.compile(r"^#\s*gh-aw-manifest:")
MANIFEST_RE = re.compile(r"^#\s*gh-aw-manifest:\s*(?P<json>.*)$")
# A file is "generated" when its name says so -- filename classification is
# deterministic and cannot be defeated by a compiler changing its header text.
# gh-aw emits several distinct banners (`generated by gh-aw`,
Expand Down Expand Up @@ -347,6 +351,71 @@ def collect_header_references(path: Path, text: str, generated: bool) -> list[Re
return references


def collect_manifest_pins(path: Path, text: str) -> tuple[dict[str, str], list[str]]:
"""Collect action SHAs from a generated gh-aw manifest."""
manifests: list[tuple[int, re.Match[str]]] = []
for lineno, line in enumerate(text.splitlines(), start=1):
if manifest_match := MANIFEST_RE.match(line):
manifests.append((lineno, manifest_match))

if not manifests:
return {}, []

relative_path = path.relative_to(REPO_ROOT).as_posix()
if len(manifests) > 1:
return {}, [f"{relative_path}: contains more than one `gh-aw-manifest` header."]

lineno, manifest_match = manifests[0]
try:
manifest = json.loads(manifest_match.group("json"))
except json.JSONDecodeError as error:
return {}, [f"{relative_path}:{lineno}: `gh-aw-manifest` is not valid JSON: {error}"]

if not isinstance(manifest, dict):
return {}, [f"{relative_path}:{lineno}: `gh-aw-manifest` must be a JSON object."]

actions = manifest.get("actions")
if not isinstance(actions, list):
return {}, [f"{relative_path}:{lineno}: `gh-aw-manifest.actions` must be a JSON array."]

pins: dict[str, str] = {}
errors: list[str] = []
for action in actions:
if not isinstance(action, dict):
errors.append(
f"{relative_path}:{lineno}: every `gh-aw-manifest.actions` entry must be a JSON object."
)
continue

repo = action.get("repo")
sha = action.get("sha")
if not isinstance(repo, str) or not isinstance(sha, str):
errors.append(
f"{relative_path}:{lineno}: every `gh-aw-manifest.actions` entry must contain "
"string `repo` and `sha` fields."
)
continue

parsed_ref = split_ref(f"{repo}@{sha}")
if parsed_ref is None or not SHA_RE.match(parsed_ref[1]):
errors.append(
f"{relative_path}:{lineno}: `gh-aw-manifest.actions` entry `{repo}@{sha}` must "
"name a valid action and a 40-character commit SHA."
)
continue

canonical, normalized_sha = parsed_ref
existing = pins.get(canonical)
if existing is not None and existing != normalized_sha:
errors.append(
f"{relative_path}:{lineno}: `gh-aw-manifest` records multiple SHAs for "
f"`{canonical}`: {existing} and {normalized_sha}."
)
pins[canonical] = normalized_sha

return pins, errors


def is_scalar_named(node: yaml.nodes.Node, name: str) -> bool:
return isinstance(node, yaml.nodes.ScalarNode) and node.value == name

Expand Down Expand Up @@ -555,6 +624,56 @@ def audit(references: list[Reference], locked: dict[str, tuple[str, str]]) -> li
return errors


def audit_manifest_consistency(
references: list[Reference],
manifest_pins: dict[Path, dict[str, str]],
) -> list[str]:
"""Ensure each generated workflow uses the action SHAs in its own manifest."""
by_path_and_repo: dict[Path, dict[str, list[Reference]]] = defaultdict(lambda: defaultdict(list))
for reference in references:
if not reference.is_docker and reference.is_sha_pinned:
by_path_and_repo[reference.path][reference.repo].append(reference)

errors: list[str] = []
for path, pins in manifest_pins.items():
references_by_repo = by_path_and_repo.get(path, {})
manifest_repos = set(pins)
reference_repos = set(references_by_repo)

for repo in sorted(manifest_repos - reference_repos):
errors.append(
f"{path.relative_to(REPO_ROOT).as_posix()}: `gh-aw-manifest` records `{repo}`, "
"but the generated workflow has no matching action reference."
)

for repo in sorted(reference_repos - manifest_repos):
locations = ", ".join(reference.location for reference in references_by_repo[repo][:3])
errors.append(
f"{path.relative_to(REPO_ROOT).as_posix()}: generated action `{repo}` is missing "
f"from `gh-aw-manifest`. References: {locations}."
)

for repo in sorted(manifest_repos & reference_repos):
expected_sha = pins[repo]
mismatches = [
reference
for reference in references_by_repo[repo]
if reference.ref != expected_sha
]
if not mismatches:
continue

actual_shas = ", ".join(sorted({reference.ref for reference in mismatches}))
locations = ", ".join(reference.location for reference in mismatches[:3])
more = f", +{len(mismatches) - 3} more" if len(mismatches) > 3 else ""
errors.append(
f"{path.relative_to(REPO_ROOT).as_posix()}: `{repo}` uses {actual_shas}, but its "
f"`gh-aw-manifest` records {expected_sha}. Mismatches: {locations}{more}."
)

return errors


def print_pin_table(references: list[Reference]) -> None:
by_repo: dict[str, set[tuple[str, str]]] = defaultdict(set)
for reference in references:
Expand All @@ -578,9 +697,20 @@ def main(argv: list[str] | None = None) -> int:

references: list[Reference] = []
structural_errors: list[str] = []
manifest_pins: dict[Path, dict[str, str]] = {}
for path in files:
text = path.read_text(encoding="utf-8")
file_manifest_pins, manifest_errors = collect_manifest_pins(path, text)
file_references, file_errors = collect_references(path)
has_manifest = any(MANIFEST_RE.match(line) for line in text.splitlines())
if has_manifest:
manifest_pins[path] = file_manifest_pins
elif path.name.endswith(GENERATED_SUFFIX):
manifest_errors.append(
f"{path.relative_to(REPO_ROOT).as_posix()}: generated lock file has no `gh-aw-manifest` header."
)
references.extend(file_references)
structural_errors.extend(manifest_errors)
structural_errors.extend(file_errors)

if args.list:
Expand All @@ -600,6 +730,7 @@ def main(argv: list[str] | None = None) -> int:
return 1

errors = structural_errors + audit(references, locked)
errors.extend(audit_manifest_consistency(references, manifest_pins))
if errors:
print(f"Action pin audit failed with {len(errors)} problem(s):\n", file=sys.stderr)
for error in errors:
Expand Down
Loading