You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
ci: enforce actionlint for handwritten workflows - #20610
Handwritten GitHub Actions workflow errors can currently merge without a blocking schema and expression check.
What the Actionlint gate does
This adds Actionlint v1.7.12 directly to the main CI workflow. The job:
Runs on ubuntu-latest on every normal CI invocation, not only when workflow files change.
Lints all 59 handwritten top-level workflow files under .github/workflows through lint-handwritten-workflows.sh, which both CI and the updater call so they always lint the same file set. The script fails if no files match.
Excludes the nine generated *.lock.yml and agentics-maintenance workflows, which remain owned by validate-agentic-workflows.yml.
Validates referenced local action metadata while checking the consuming workflows.
Disables ShellCheck and pyflakes so this first gate is limited to core GitHub Actions schema, expression, event, input/output, dependency, and runner-label validation.
Retries transient GitHub release-asset failures before verifying the repository-pinned archive checksum.
Runs independently of prepare_for_ci, so skip_workflow can still avoid expensive build/test work but cannot hide an Actionlint failure.
Participates in Final Results and the red-main CI_GREEN contract.
The installation pattern follows dotnet/skills: download a versioned upstream release archive and verify a checksum pinned in the repository before extraction. Actionlint's v1.7.12 usage documentation explicitly recommends preparing the executable with its download script for GitHub Actions. This uses the same direct upstream-release model while additionally pinning the expected archive bytes in this repository.
Failures found in the current baseline
Running Actionlint v1.7.12 against the intended handwritten workflow set on origin/main reproduced these failures:
.github/workflows/specialized-test-runner.yml:25:18:
input "enablePlaywrightInstall" of workflow_call event has the default value
"false", but it is also required. if an input is marked as required, its
default value will never be used [events]
.github/workflows/specialized-test-runner.yml:111:28:
property "generate_tests_matrix" is not defined in object type
{check_cli_archive: ...; check_nugets: ...; generate_tests_matrix_raw: ...;
inject_properties: ...} [expression]
.github/workflows/deployment-tests.yml:73:15:
could not parse action metadata in ".github/actions/enumerate-tests":
line 4: unexpected key "type" for definition of input "buildArgs" [action]
.github/workflows/deployment-tests.yml:111:14:
label "8-core-ubuntu-latest" is unknown. if it is a custom label for
self-hosted runner, set list of labels in actionlint.yaml config file
[runner-label]
The runner-label failure was also reported for other existing workflows that use the same repository-specific runner.
The specialized-test-runner.yml fixes are behavior-neutral:
The removed Check if any test requires GitHub token step read steps.generate_tests_matrix.outputs.runsheet. That step id does not exist (the real one is generate_tests_matrix_raw), so it always evaluated to false.
Its requiresGitHubToken job output had no consumers.
Per-test tokens are unaffected: run-tests.yml reads each matrix entry's own requiresGitHubToken property.
Both callers (tests-outerloop.yml, tests-quarantine.yml) pass enablePlaywrightInstall explicitly, so the dropped default was never used.
Fixes for the baseline failures
Failure
Fix
enumerate-tests/action.yml declared type: string on a composite-action input. Action metadata inputs are strings and do not support a type field.
Remove the unsupported field. This does not change the input's runtime value or behavior.
enablePlaywrightInstall was both required and given default: false. A required caller must always provide the value, so the default was unreachable.
Remove the default and retain the explicit required contract. Both callers already pass true.
check_github_token referenced nonexistent steps.generate_tests_matrix, queried the wrong canonical-matrix path, and exposed an output that no downstream job consumed.
Remove the dead aggregate step and output. Per-test token behavior remains unchanged: the matrix passes properties to run-tests.yml, which still sets GH_TOKEN when that test's requiresGitHubToken or requiresCliArchive property is true.
Existing workflows use the repository-specific 8-core-ubuntu-latest runner label, which Actionlint cannot discover.
Register the label in .github/actionlint.yaml. Actionlint itself still runs on ubuntu-latest; this configuration only teaches the parser that the existing label is valid.
The two existing Infrastructure.Tests contracts for Final Results and red-main tracking are updated to include Actionlint. A newly added test that only repeated the Actionlint job condition was removed.
Updating Actionlint
The version and Linux AMD64 archive SHA-256 live in .github/actionlint-version.json. CI reads them with jq and validates the version format before downloading. The pin lives outside .github/workflows, so a bot PR only needs Contents:write. The Aspire bot app does not have workflow-write access.
update-actionlint.yml runs weekly on a schedule. Manual workflow_dispatch defaults to mode=validate, which runs only check and is safe from a fork. mode=propose runs update only from the upstream default branch. Scheduled and propose runs share a concurrency group:
check job (no secrets): reads the latest published release from the GitHub API.
Exits as a no-op when the pin is current.
Fails on a downgrade, a malformed tag, a draft or prerelease, a missing or duplicate linux_amd64 asset, an unexpected download URL, or a non-sha256: digest.
Downloads the archive, verifies its bytes against the API digest, then runs the candidate through the same lint-handwritten-workflows.sh as CI.
update job: rewrites the pin and opens a draft PR through ./.github/actions/create-pull-request with an Aspire bot token scoped to contents and pull-requests write.
A create-issue-on-failure job files or updates one deduplicated automation-broken issue when any step of a scheduled run fails. It reuses the shared report-pipeline-failure.js reporter from deployment-tests.yml and tests-daily-smoke.yml. The scheduled-workflow watchdog lists the updater as a selfReports backstop, so it only covers startup_failure and timed_out runs and never double-files.
The shared reporter's issue title is now Scheduled run failing: <name> instead of Nightly run failing: <name>, because it now also serves a weekly workflow. This also changes the titles of new deployment E2E and daily smoke failure issues. Dedup keys off the hidden per-workflow marker, so already-open issues are still found. .github/actionlint-version.json routes to Infrastructure.Tests.
Security considerations
Pinning a third-party GitHub Action to a commit SHA pins the wrapper source, but does not by itself pin the Actionlint binary that the wrapper downloads. This workflow verifies the expected SHA-256 before extracting or executing the archive, so replaced or substituted release bytes fail closed. It also avoids introducing another third-party Action and the corresponding repository/enterprise Action allow-list coordination.
The pinned checksum was verified against the upstream v1.7.12 checksum manifest and GitHub release asset digest. The updater's API digest comes from the same upstream release, so it is not an independent trust anchor. A reviewer still approves each bump, and the committed hash makes CI detect any later substitution of the archive.
The local create-pull-request action reference carries a zizmor: ignore[self-repository] because Actionlint v1.7.12 rejects zizmor's preferred $/ syntax. With that, zizmor 1.30.1 reports no findings for update-actionlint.yml, and every changed existing workflow or action has the same or fewer findings than on main.
Validation
./restore.sh — passed.
bash .github/workflows/lint-handwritten-workflows.sh — passed for all 59 handwritten workflows with Actionlint v1.7.12.
actionlint -shellcheck= -pyflakes= on every changed workflow — passed.
uvx zizmor@1.30.1 --offline on every changed workflow and action, compared per file with main — no findings in update-actionlint.yml; ci.yml and enumerate-tests/action.yml unchanged; specialized-test-runner.yml has one fewer finding.
curl --fail --location --retry 3 --retry-all-errors ... followed by pinned SHA-256 verification — passed for the upstream Linux AMD64 archive.
Actionlint v1.7.12 upstream usage and installation documentation — verified that executable download is the recommended GitHub Actions/shell-script installation model.
dotnet/skills workflow at 44ea5f1ffca45206f66df7730327a920b20d645c — verified the same versioned-archive and repository-pinned-checksum pattern.
PR-triggered Outerloop Tests and Quarantined Tests runs, which exercise specialized-test-runner.yml — passed.
RUNNER_TEMP=<tmp> GITHUB_OUTPUT=<tmp>/out node .github/workflows/update-actionlint.js check against the live API — passed: updated=false, previous-version=1.7.12.
Removing the updater's digest verification makes the harness accept a mismatched archive. CheckFailsWhenDownloadDoesNotMatchDigest covers that regression.
Removing the *.lock.yml exclusion from lint-handwritten-workflows.sh fails two LintHandwrittenWorkflowsScriptTests cases.
End-to-end run of update-actionlint.yml in a fork. The validation-only branch changed only the owner guard, the triggers, the PR base, and the token source (github.token instead of the bot app), and started from a pin of 1.7.11:
Success path: found 1.7.12, verified actionlint_1.7.12_linux_amd64.tar.gz against the release digest, linted 59 handwritten workflows with the candidate, and opened a draft PR whose only change was the pin.
A second success run force-updated the same branch and logged Pull request #119 already exists; no duplicate PR was created.
Failure path: app-token minting failed because the fork has no bot secrets, and create-issue-on-failure filed one Scheduled run failing: Update actionlint issue labeled automation-broken and area-engineering-systems. A second failure run commented on that issue instead of filing a new one.
monitor-scheduled-workflows.yml dry run in a fork, with the PR's config checked out: passed and evaluated update-actionlint.yml (noop, no scheduled runs yet). Upstream dispatch cannot test this, because the monitor checks out main.
Not covered by the fork runs: whether the Aspire bot app has contents and pull-requests write on microsoft/aspire. The first scheduled run after merge will show that; if minting or PR creation fails, the failure-issue path opens an automation-broken issue.
Update documentation to describe both shared actionlint configuration paths
.github/workflows/README.md:43
This now contradicts the added self-hosted-runner.labels configuration: .github/actionlint.yaml no longer only scopes the generated-workflow workaround; it also registers the repository-specific runner label. Update this paragraph so maintainers know both lint paths depend on the shared configuration.
Clarify actionlint.yaml config is shared by both lint paths
.github/workflows/README.md:43
This paragraph still says .github/actionlint.yamlonly scopes the generated-workflow workaround, but this PR also adds the repository runner label there. Update the description so maintainers know the config is shared by both lint paths.
Prevent update-actionlint.yml from being skipped by broad workflow pattern
.github/workflows/update-actionlint.yml:7
This new workflow is still matched by eng/github-ci/ci-skip-entirely-patterns.txt:80 (.github/workflows/update-*.yml). A future PR that changes only this YAML will therefore set skip_workflow=true and skip Infrastructure.Tests, so the new schedule/permissions/lint-parity contract tests will not run despite the trigger-map route. Carve update-actionlint.yml out of the top-level skip set (or replace the broad glob with explicit scheduled workflows) so changes to this tested workflow select Infrastructure.Tests.
The reason will be displayed to describe this comment to others. Learn more.
Copilot review overview
🔵 Needs a closer look
A fixed updater branch can leave an existing draft PR advertising a stale release version and checksum.
Review effort: Balanced Findings: None
Previously missed (1)
In code that hasn't changed since last review
Update existing PR metadata when force-pushing security branch
.github/workflows/update-actionlint.yml:81
This fixed branch can make the security metadata stale. The local create-pull-request action force-pushes an existing branch but, when an open PR already exists, returns without updating its title or body (.github/actions/create-pull-request/action.yml:81-109). If another actionlint release appears before the draft is merged, the branch moves to the new pin while the PR still advertises the old release and checksum. Use a versioned branch (or update existing PR metadata).
actionlint 1.7.12 reports 7 errors in the handwritten workflows on
main:
- build-packages.yml, ci.yml, deployment-tests.yml (x2): unknown
runner label "8-core-ubuntu-latest" (a larger-runner label).
- enumerate-tests action, used by deployment-tests.yml: unexpected
key "type" on a composite-action input.
- specialized-test-runner.yml: required workflow_call input
"enablePlaywrightInstall" also declares a default that is never used.
- specialized-test-runner.yml: the "Check if any test requires GitHub
token" step reads steps.generate_tests_matrix, which does not exist,
so its requiresGitHubToken job output was always false. Nothing
consumes that output; test lanes read requiresGitHubToken from each
runsheet entry instead.
Declare the runner label in .github/actionlint.yaml, drop the invalid
keys, and remove the broken step and its unused output.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Script tests need to run a child process with PATH shims and extra
variables. Add an optional environment parameter to the shared
ProcessRunner that layers overrides on top of the inherited
environment, so tests reuse it instead of hand-rolling Process code.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Workflow mistakes such as invalid expressions, unknown inputs, or
shellcheck errors were only caught when the workflow ran. Add a
required 'Lint handwritten workflows' job to CI that installs a
pinned actionlint, verifies the archive SHA-256, and lints the
handwritten workflows and local actions. gh-aw generated output is
excluded because it has its own validation.
The version and checksum live in .github/actionlint-version.json,
outside .github/workflows, so they can be bumped without
workflow-write access. Final Results and the CI failure tracker now
depend on the lint job, and it stays required even when build and
test jobs are skipped.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
When the branch already has an open PR, the action only reported its
number, so a recurring updater could leave a stale title and body
after force-pushing a newer change. Add an opt-in
update-existing-pr-metadata input that edits the existing PR's title
and body and reports pull-request-operation=updated. The default
behavior is unchanged.
The action now has behavioral tests, so route its path to
Infrastructure.Tests instead of skipping CI for it.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
The pinned actionlint version in .github/actionlint-version.json would
otherwise go stale silently. Add update-actionlint.yml, which runs
weekly and on demand:
- Resolves the latest actionlint release and verifies the Linux
archive against the sha256 digest published by the release API.
- Lints the handwritten workflows with the candidate binary through
the same script the CI gate uses, so a bump that would break the
gate is never proposed.
- validate mode (manual default) stops after linting and is safe on
forks. propose mode, used by the schedule and restricted to the
default branch, opens or refreshes a draft PR.
- A concurrency group keeps overlapping runs from racing on the
update branch.
Scheduled failures are reported through report-pipeline-failure and
watched by monitor-scheduled-workflows. The reporter's issue title and
body now say "Scheduled" instead of "Nightly" because this workflow is
weekly.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
🟡 ISSUES — non-blocking attention: PR CI and the updater's validation mode passed. The existing-PR metadata edit has behavioral coverage and correct permissions, but has not run inside the updater with its bot token.
🟡 Existing-PR metadata edit: optional live integration evidence
Gap: The new opt-in title/body refresh has not executed inside the updater with the bot token. This is an advisory integration gap, not a demonstrated permission or code defect.
Authorization: The updater token requests pull-requests: write, matching GitHub's App permission requirement. extension-release.yml already uses this App and gh pr edit --title --body-file; that is source precedent, not claimed live execution.
Command probe: With gh 2.101.0 and a user token, gh pr edit --title "$PR_TITLE" --body-file - <<< "$PR_BODY" succeeded against radical/aspire#119, preserving literal backticks and $HOME. Original title/body were restored. This proves the CLI/API shape, not bot-token or updater execution.
Resolution: No rerun is required solely to prove the existing token can edit a PR. The fixture-covered composite-action change is classified as an advisory live-service gap, not as verified execution or maintainer risk acceptance.
Existing machinery:Azure VM Sizes and Foundry Models already exercise the same App-token PR-creation path. The latter created #20551. That pre-existing machinery is not a new validation requirement.
specialized-test-runner.yml execution: Final PR definition exercised through the Outerloop and Quarantined callers. Removed output had no consumers; both callers explicitly supply the input whose unreachable default was removed.
update-actionlint.yml execution:Final-head fork validation loaded the final definition, completed check, and accepted workflow-scoped concurrency. update and failure reporting were intentionally skipped. This verifies validation-mode execution, not those skipped jobs; prior trials below provide supporting mutation/failure-path evidence, with the new edit branch separately advisory.
actionlint: Repository-pinned 1.7.12; base/head scan found no new or worsened errors. Final CI lint passed; seven baseline errors resolved.
zizmor: Version 1.30.1; matching base/head scans found zero new/worsened findings and zero blocking candidates.
ShellCheck: Version 0.11.0; matching base/head scans of the new standalone shell script found zero new/worsened findings. This does not claim ShellCheck coverage of every embedded workflow script.
Targeted CI security review: Changed event contexts, owner/default-branch guards, token scopes, fork behavior, mutation targets, download trust, and shell/expression injection inspected separately from scanners. No new high-confidence security defects found. Unrun integration behavior is not itself a security finding.
Behavioral infrastructure coverage:Infrastructure.Tests passed. Script tests exercise updater validation/error handling, lint file selection and exit propagation, metadata-edit argv/stdin/operation outputs, and selector routing. Reverting the metadata opt-in branch makes the enabled-case updated and edit-call assertions fail.
Internal definition 1602: No official-pipeline, Arcade, signing, packaging, template-test plumbing, or release-artifact changes.
Deployment E2E: No deployment scenario/runtime changes; removed invalid action metadata does not change action execution.
gh-aw compile/lint: No agentic sources, generated locks, or compiler pins changed.
Changed action-pin approval: No existing third-party action pin changed; actionlint is a binary download, not an action publisher.
PSScriptAnalyzer: No PowerShell source or embedded PowerShell changed.
Scope and method
Scope: Actionlint gate/updater; shared PR action; specialized-runner schema/dead-output fixes; CI skip/selector routing; infrastructure tests and scheduled-failure reporting.
Revision: Head c61dd6a162ed0a05cfbc4067c8dcd164ed236309, base e9579221c0f57492b6a385fde7b80af972988c6d, unchanged since the independent audit. Scan evidence remains applicable.
Method: Prior independent agent collected PR comments, reviews, inline threads and linked issues, inventoried workflow runs, and ran python3 -B "$S/scan_ci_delta.py" --facts "$A/facts/facts.json" --repo-dir "$R" --scratch-dir "$A/scan-<tool>" --tool <tool> for actionlint, zizmor, and ShellCheck. Outcomes above are scanner deltas, not proof every runtime path ran.
Classification: The final updater workflow ran in validation mode. Remaining live evidence for the fixture-covered composite-action edit is advisory. No absent execution is called verified and no formal maintainer risk acceptance is claimed.
History: Automatic retry cleared the two first-attempt failures: CLI TrayWatchStreamTests.StalledResourceWatchDoesNotBlockReconciliationOrQuit and Infrastructure TemplateRestorePlanTests.PreservesItemDeclarationOrderDuringRestore. No unrelated tests were edited or disabled.
Publication: Unpublished draft. Local artifacts retained. No further privileged trial is prescribed solely for the edit; pr-testing is unnecessary for that advisory boundary.
Title: ci: enforce actionlint for handwritten workflows
Head commit:c61dd6a162ed0a05cfbc4067c8dcd164ed236309
Tested at: 2026-10-06T15:54:22-05:00
Execution: Windows host, Git Bash, isolated temporary fixtures, and inspection of latest-head Linux CI evidence.
Source identity: The clean testing branch was moved to the PR head without changing the PR. Canonical upstream/main was freshly fetched; the PR was not rebased onto it.
Installed CLI: N/A; this is an infrastructure-only PR. CLI dogfood and generated-app scenarios would not exercise these changes.
Source checkout: Exact PR head, verified again at completion.
CI identity:CI run 37059288782, attempt 2, reports that exact head_sha. Its checkout log identifies the synthetic merge of this head into base commit e9579221c0f57492b6a385fde7b80af972988c6d.
Lint tool: Local Actionlint v1.7.12, built from source for Windows AMD64. Separately, the actual Linux CI gate downloaded the pinned release archive and logged actionlint_1.7.12_linux_amd64.tar.gz: OK before extracting and executing it.
Status: Verified for the tested source and latest-head CI evidence. No claim that the locally installed Windows binary is the Linux archive.
Changes Analyzed
Files Changed
All 29 PR files were accounted for:
.github\actionlint-version.json - new version/checksum pin.
Hosting, dashboard, templates, or client/components
VS Code extension
Azure DevOps pipelines
Test Scenarios Executed
Scenario 1: Verify actual latest-head CI execution
Objective: Confirm the new gate and relevant tests ran, rather than inferring coverage from a green check.
Coverage type: Existing CI execution and artifact inspection.
Status: Passed.
Downloaded the Actionlint and Infrastructure job logs and the Infrastructure result artifact:
gh run view 37059288782--repo microsoft/aspire --job 111030279347--log
gh run view 37059288782--repo microsoft/aspire --job 111030309057--log
gh run download 37059288782--repo microsoft/aspire --name logs-Infrastructure-ubuntu-latest --dir <temporary-artifact-directory>
Observed results: The pinned Linux archive checksum passed, the lint step evaluated 60 handwritten workflows, and the Infrastructure TRX contains 1,515 passed, 0 failed, 0 not executed. Its 260 directly relevant cases all passed:
Test class
Passed
CreatePullRequestActionTests
1
ExtensionWorkflowTests
16
LintHandwrittenWorkflowsScriptTests
3
MonitorScheduledWorkflowsIntegrationTests
16
MonitorScheduledWorkflowsTests
20
ProcessRunnerTests
3
ReportCiFailureIntegrationTests
12
ReportCiFailureTests
5
ReportPipelineFailureIntegrationTests
9
ReportPipelineFailureTests
7
TestTriggerMapTests
137
UpdateActionlintTests
31
These tests were inspected from the CI artifact, not rerun locally. The passing updater cases include malformed pins/tags, missing assets/digests, unexpected download URLs, prerelease rejection, downgrade rejection, digest mismatch, candidate outputs, and pin preservation after failed apply. The shared-action case checks that metadata updates are opt-in and preserve a multiline body. Reporter/watchdog integration cases use isolated API fakes, not real issue mutations.
Objective: Confirm the current-head script can call the live release API and produce workflow-compatible outputs without editing the pin.
Coverage type: Live happy path / no-op boundary.
Status: Passed for the live no-op path.
Executed:
node .github\workflows\update-actionlint.js check
RUNNER_TEMP and GITHUB_OUTPUT pointed into the isolated temporary directory. No authentication-profile directories were overridden.
Observed output:
actionlint 1.7.12 is the latest release; nothing to update.
Observed step outputs:
updated=false
previous-version=1.7.12
The committed pin was unchanged. This did not exercise a new release, Linux candidate extraction/linting, token minting, branch pushing, or real PR creation/update. Candidate/rejection logic has passing CI fixture coverage as described in Scenario 1.
Scenario 4: Aggregation, routing, shell syntax, and coverage preservation
Objective: Check the wiring that can silently skip validation or drop coverage.
Coverage type: Structural audit and focused fixtures.
Status: Passed for inspected/fixture-covered behavior.
Parsed the three changed workflow YAML files and both changed composite actions. Syntax-checked their 29 Bash blocks with bash -n, substituting GitHub expression placeholders without executing the commands.
Executed the actual changed-file action's glob-conversion function and pattern loader against nine paths. README-only edits remain skippable; the updater workflow, shared PR action, and Actionlint pin are not skippable; all five existing scheduled-updater exclusions remain skippable.
Evaluated the extracted Final Results failure condition against seven string-valued result fixtures. Successful lint permits intentionally skipped builds; failed, cancelled, or unexpectedly skipped lint still fails the gate. Test failure and stabilization cancellation also fail the gate. This is local boolean-condition coverage, not a live GitHub skipped-build run.
Structurally compared the specialized runner to freshly fetched upstream/main. Test enumeration, matrix/invocation, package/CLI/image producers, artifact shape, and result aggregation are unchanged. Only the unused aggregate-token step is removed from the generation steps. Per-test token handling remains in run-tests.yml.
Inspected actual Outerloop and Quarantine runs. Each emitted the expected Acquisition project exactly once on Linux, macOS, and Windows; all three consumer jobs succeeded. The logs show enablePlaywrightInstall: true injected into the consumed runsheet.
git diff --check upstream/main...HEAD passed. The repository working tree remained clean.
The PR-specialized runs deliberately cover one project, not the full scheduled suites. Since this PR does not change enumeration or invocation, no full-suite dispatch was performed.
Evidence:pr20610-audit.py, wiring-audit.log, wiring-audit-results.json, local-fixture-results.json, outerloop-runsheet.log, quarantine-runsheet.log, both *-combined-runsheet.json, both *-jobs.jsonl.
CI Infrastructure Validation
GitHub Actions
What runs on this PR: Latest-head CI, Actionlint, Infrastructure.Tests, Final Results, Outerloop plumbing, and Quarantine plumbing were inspected. The PR's agentic-workflow validation check also passed; generated-workflow files themselves were not changed.
Automated tests: Latest-head CI artifact confirms the 1,515-test Infrastructure run and the 260 relevant cases above. No redundant local .NET build/restore or test run was performed.
Manual triggers: None. No upstream or fork branches, issues, PRs, or workflow runs were created. Querying the upstream update-actionlint.yml workflow endpoint returned HTTP 404: the new workflow is not yet registered on the default branch. The existing monitor explicitly checks out main, so dispatching it would not test this PR's watch-list change.
Dependency graph:
Changed producer/helper
Consumers and validation
Actionlint pin/config/shared selector
ci.yml lint job and updater candidate lint; real latest-head CI and local script execution
Lint job result
Final Results and push-only red-main tracker; latest-head CI, passing YAML contracts, local aggregation fixtures; push execution remains unvalidated
Updater check outputs
Updater update job, pin apply, shared PR action; source wiring plus passing updater/action fixtures; real current-head write path remains unvalidated
create-pull-request composite action
generate-api-diffs.yml, generate-ats-diffs.yml, refresh-manifests.yml, release-github-tasks.yml (two call sites), update-ai-foundry-models.yml, update-aspire-skills-bundle.yml, update-azure-vm-sizes.yml, update-dependencies.yml, update-nix-cli-flake.yml, and new update-actionlint.yml; existing callers retain the default false, only the new updater opts in; behavior fixture passed
enumerate-tests composite action
tests.yml and deployment-tests.yml; CI exercised normal tests. Removing unsupported type metadata does not alter input value/runtime behavior. No deployment run was requested
Specialized runner
Outerloop/Quarantine callers and their package/archive/image/test consumers; both real PR runs and downloaded runsheets inspected; execution/producer graph unchanged
Scheduled failure reporter
Deployment, daily CLI smoke, and new updater; reporter/integration cases passed; no real failure issues were filed
Watchdog config
monitor-scheduled-workflows.yml; selfReports backstop logic covered by passing CI fixtures; actual current-head watch-list execution on main remains unvalidated
CI skip patterns / trigger map
CI changed-file gate and selector; real latest-head Infrastructure selection, 137 trigger-map cases, and nine exact glob-converter fixtures
Coverage-loss audit: No trigger removal, narrower test matrix, new continue-on-error, or artifact rename. The required lint dependency is added rather than replacing build/test dependencies. Source comparison confirms no specialized test consumer was removed. The newest available successful main-CI runs returned by the listing were from May, so no misleading current-vs-old full-main job-count comparison was used; conservation was established structurally and against actual PR runsheets.
Failure-mode scan:
The lint job is independent of prepare_for_ci; build-skip paths cannot hide a lint failure.
All introduced external action references are first-party actions/* pins already used in the repository; no new third-party action SHA requires an allow-list update.
Updater check has read-only permissions and no app secrets; the update job mints its token in the same job that consumes it, not across job outputs.
The updater write path is restricted to upstream default-branch scheduled/propose runs. App contents/pull-requests write grants were not verified.
Failure issues are schedule-only; watchdog selfReports handling preserves the existing per-workflow dedup marker.
Existing shared-action callers do not opt into changed metadata behavior.
Required Playwright input is explicitly supplied by both specialized callers; matrix properties and per-test token handling remain intact.
No gh-aw sources or lock files changed. No compilation/regeneration was required.
Historical author validation: The author-linked fork success/failure/watchdog runs were queried read-only. All used validation commit 35b8f12b669996fe97e2daca08be7212cf828c0a, not current PR head c61dd6a...; they are therefore not counted as current-head end-to-end verification.
Azure DevOps
N/A. No Azure DevOps pipeline, signing, packaging, or publishing behavior changed.
Summary
Scenario
Result
Scope
Latest-head CI evidence
Passed
1,515 Infrastructure tests; 260 relevant cases; pinned Linux archive verified
Actual handwritten lint gate
Passed
60 real workflows and targeted negative/exclusion fixtures
Live updater release check
Passed
Read-only no-op; pin unchanged
Syntax, routing, aggregation, coverage
Passed
29 Bash blocks, nine routing paths, seven aggregation cases, three OS variants per specialized run
Current-head scheduled/propose write path
Not verified
New workflow not registered on upstream main; bot permissions and real metadata update untested
Live push/red-main and skipped-build run
Not verified
Source/contracts/fixtures inspected, no protected-branch run performed
Overall Result
Focused validation passed; no functional regression found in the tested scope. Full scheduled automation verification remains incomplete.
The latest source and real CI lint 60 handwritten workflows, not the 59 still stated in the PR description. This is a stale description count, not a lint selection failure.
Recommendations
Update the description's workflow count. After merge, verify upstream updater registration, bot token permissions, and the first real scheduled/propose execution; do not treat historical modified-fork runs as verification of the current write path.
The exact temporary fixture/download directory C:\Users\dapine\AppData\Local\Temp\aspire-pr-20610-v4firf0g.q0i was removed after relevant TRX, runsheets, logs, and metadata were copied to persistent session storage. No server or container was started. No repository files were edited.
The temporary syntax audit initially encountered Windows subprocess newline/executable-resolution issues. Using UTF-8 LF input and the explicitly resolved Git Bash executable corrected the harness; the final audit passed. Those initial harness errors are not PR failures.
Resolve ci.yml and ExtensionWorkflowTests conflicts: keep main's
STABILIZATION_ENABLED gating and push-only tracker semantics while
retaining the required actionlint result in Final Results and CI_GREEN.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
The reason will be displayed to describe this comment to others. Learn more.
🟢 Approval recommended
The workflow gate, updater safeguards, routing, baseline fixes, and focused regression coverage are internally consistent with no unresolved defects found.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
Handwritten GitHub Actions workflow errors can currently merge without a blocking schema and expression check.
What the Actionlint gate does
This adds Actionlint v1.7.12 directly to the main CI workflow. The job:
ubuntu-lateston every normal CI invocation, not only when workflow files change..github/workflowsthroughlint-handwritten-workflows.sh, which both CI and the updater call so they always lint the same file set. The script fails if no files match.*.lock.ymland agentics-maintenance workflows, which remain owned byvalidate-agentic-workflows.yml.prepare_for_ci, soskip_workflowcan still avoid expensive build/test work but cannot hide an Actionlint failure.Final Resultsand the red-mainCI_GREENcontract.The installation pattern follows
dotnet/skills: download a versioned upstream release archive and verify a checksum pinned in the repository before extraction. Actionlint's v1.7.12 usage documentation explicitly recommends preparing the executable with its download script for GitHub Actions. This uses the same direct upstream-release model while additionally pinning the expected archive bytes in this repository.Failures found in the current baseline
Running Actionlint v1.7.12 against the intended handwritten workflow set on
origin/mainreproduced these failures:The runner-label failure was also reported for other existing workflows that use the same repository-specific runner.
The
specialized-test-runner.ymlfixes are behavior-neutral:Check if any test requires GitHub tokenstep readsteps.generate_tests_matrix.outputs.runsheet. That step id does not exist (the real one isgenerate_tests_matrix_raw), so it always evaluated tofalse.requiresGitHubTokenjob output had no consumers.run-tests.ymlreads each matrix entry's ownrequiresGitHubTokenproperty.tests-outerloop.yml,tests-quarantine.yml) passenablePlaywrightInstallexplicitly, so the droppeddefaultwas never used.Fixes for the baseline failures
enumerate-tests/action.ymldeclaredtype: stringon a composite-action input. Action metadata inputs are strings and do not support atypefield.enablePlaywrightInstallwas both required and givendefault: false. A required caller must always provide the value, so the default was unreachable.true.check_github_tokenreferenced nonexistentsteps.generate_tests_matrix, queried the wrong canonical-matrix path, and exposed an output that no downstream job consumed.propertiestorun-tests.yml, which still setsGH_TOKENwhen that test'srequiresGitHubTokenorrequiresCliArchiveproperty is true.8-core-ubuntu-latestrunner label, which Actionlint cannot discover..github/actionlint.yaml. Actionlint itself still runs onubuntu-latest; this configuration only teaches the parser that the existing label is valid.The two existing Infrastructure.Tests contracts for
Final Resultsand red-main tracking are updated to include Actionlint. A newly added test that only repeated the Actionlint job condition was removed.Updating Actionlint
The version and Linux AMD64 archive SHA-256 live in
.github/actionlint-version.json. CI reads them withjqand validates the version format before downloading. The pin lives outside.github/workflows, so a bot PR only needs Contents:write. The Aspire bot app does not have workflow-write access.update-actionlint.ymlruns weekly on a schedule. Manualworkflow_dispatchdefaults tomode=validate, which runs onlycheckand is safe from a fork.mode=proposerunsupdateonly from the upstream default branch. Scheduled and propose runs share a concurrency group:checkjob (no secrets): reads the latest published release from the GitHub API.linux_amd64asset, an unexpected download URL, or a non-sha256:digest.lint-handwritten-workflows.shas CI.updatejob: rewrites the pin and opens a draft PR through./.github/actions/create-pull-requestwith an Aspire bot token scoped tocontentsandpull-requestswrite.A
create-issue-on-failurejob files or updates one deduplicatedautomation-brokenissue when any step of a scheduled run fails. It reuses the sharedreport-pipeline-failure.jsreporter fromdeployment-tests.ymlandtests-daily-smoke.yml. The scheduled-workflow watchdog lists the updater as aselfReportsbackstop, so it only coversstartup_failureandtimed_outruns and never double-files.The shared reporter's issue title is now
Scheduled run failing: <name>instead ofNightly run failing: <name>, because it now also serves a weekly workflow. This also changes the titles of new deployment E2E and daily smoke failure issues. Dedup keys off the hidden per-workflow marker, so already-open issues are still found..github/actionlint-version.jsonroutes to Infrastructure.Tests.Security considerations
Pinning a third-party GitHub Action to a commit SHA pins the wrapper source, but does not by itself pin the Actionlint binary that the wrapper downloads. This workflow verifies the expected SHA-256 before extracting or executing the archive, so replaced or substituted release bytes fail closed. It also avoids introducing another third-party Action and the corresponding repository/enterprise Action allow-list coordination.
The pinned checksum was verified against the upstream v1.7.12 checksum manifest and GitHub release asset digest. The updater's API digest comes from the same upstream release, so it is not an independent trust anchor. A reviewer still approves each bump, and the committed hash makes CI detect any later substitution of the archive.
The local
create-pull-requestaction reference carries azizmor: ignore[self-repository]because Actionlint v1.7.12 rejects zizmor's preferred$/syntax. With that, zizmor 1.30.1 reports no findings forupdate-actionlint.yml, and every changed existing workflow or action has the same or fewer findings than onmain.Validation
./restore.sh— passed.bash .github/workflows/lint-handwritten-workflows.sh— passed for all 59 handwritten workflows with Actionlint v1.7.12.actionlint -shellcheck= -pyflakes=on every changed workflow — passed.uvx zizmor@1.30.1 --offlineon every changed workflow and action, compared per file withmain— no findings inupdate-actionlint.yml;ci.ymlandenumerate-tests/action.ymlunchanged;specialized-test-runner.ymlhas one fewer finding.curl --fail --location --retry 3 --retry-all-errors ...followed by pinned SHA-256 verification — passed for the upstream Linux AMD64 archive.actionlint_1.7.12_linux_amd64.tar.gzchecksum verification — passed.dotnet/skillsworkflow at44ea5f1ffca45206f66df7730327a920b20d645c— verified the same versioned-archive and repository-pinned-checksum pattern.dotnet test --project tests/Infrastructure.Tests/Infrastructure.Tests.csproj --no-launch-profile -- --filter-class "*.UpdateActionlintTests" --filter-class "*.LintHandwrittenWorkflowsScriptTests" --filter-class "*.ExtensionWorkflowTests" --filter-class "*.MonitorScheduledWorkflows*" --filter-class "*.ReportPipelineFailure*" --filter-class "*.TestTriggerMapTests" --filter-not-trait "quarantined=true" --filter-not-trait "outerloop=true"— passed, 228 tests (the reporter wording change re-ranReportPipelineFailure*,MonitorScheduledWorkflows*andUpdateActionlintTests: 83/83).Outerloop TestsandQuarantined Testsruns, which exercisespecialized-test-runner.yml— passed.RUNNER_TEMP=<tmp> GITHUB_OUTPUT=<tmp>/out node .github/workflows/update-actionlint.js checkagainst the live API — passed:updated=false,previous-version=1.7.12.CheckFailsWhenDownloadDoesNotMatchDigestcovers that regression.*.lock.ymlexclusion fromlint-handwritten-workflows.shfails twoLintHandwrittenWorkflowsScriptTestscases.dotnet test --project tests/Infrastructure.Tests/Infrastructure.Tests.csproj --no-launch-profile -- --filter-method "*.EveryLocalActionUsedByAWorkflowIsAccountedFor" --filter-method "*.EnumerateTestsActionSelectsManagedTestProjectsButNotNonDotnetJobs" --filter-not-trait "quarantined=true" --filter-not-trait "outerloop=true"— passed, 2 tests.update-actionlint.ymlin a fork. The validation-only branch changed only the owner guard, the triggers, the PR base, and the token source (github.tokeninstead of the bot app), and started from a pin of 1.7.11:actionlint_1.7.12_linux_amd64.tar.gzagainst the release digest, linted 59 handwritten workflows with the candidate, and opened a draft PR whose only change was the pin.Pull request #119 already exists; no duplicate PR was created.create-issue-on-failurefiled oneScheduled run failing: Update actionlintissue labeledautomation-brokenandarea-engineering-systems. A second failure run commented on that issue instead of filing a new one.monitor-scheduled-workflows.ymldry run in a fork, with the PR's config checked out: passed and evaluatedupdate-actionlint.yml(noop, no scheduled runs yet). Upstream dispatch cannot test this, because the monitor checks outmain.contentsandpull-requestswrite onmicrosoft/aspire. The first scheduled run after merge will show that; if minting or PR creation fails, the failure-issue path opens anautomation-brokenissue.Refs #18036
Checklist
<remarks />and<code />elements on your triple slash comments?