Skip to content

ci: enforce actionlint for handwritten workflows - #20610

Merged
Ankit Jain (radical) merged 6 commits into
microsoft:mainfrom
radical:core-actionlint-gate
Oct 7, 2026
Merged

Ankit Jain (radical) merged 6 commits into
microsoft:mainfrom
radical:core-actionlint-gate

Conversation

@radical

@radical Ankit Jain (radical) commented Sep 30, 2026 •

Copy link
Copy Markdown
Member

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:

  • 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.
  • Upstream actionlint_1.7.12_linux_amd64.tar.gz checksum verification — passed.
  • 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.
  • 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-ran ReportPipelineFailure*, MonitorScheduledWorkflows* and UpdateActionlintTests: 83/83).
  • 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.
  • 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.
  • 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.

Refs #18036

Checklist

  • Is this feature complete?
    • Yes. Ready to ship.
    • No. Follow-up changes expected.
  • Are you including unit tests for the changes and scenario tests if relevant?
    • Yes
    • No
  • Did you add public API?
    • Yes
      • If yes, did you have an API Review for it?
        • Yes
        • No
      • Did you add <remarks /> and <code /> elements on your triple slash comments?
        • Yes
        • No
    • No
  • Does the change make any security assumptions or guarantees?
    • Yes
      • If yes, have you done a threat model and had a security review?
        • Yes
        • No
    • No

@github-actions

Copy link
Copy Markdown
Contributor

🚀 Dogfood this PR with:

⚠️ WARNING: Do not do this without first carefully reviewing the code of this PR to satisfy yourself it is safe.

curl -fsSL https://raw.githubusercontent.com/microsoft/aspire/main/eng/scripts/get-aspire-cli-pr.sh | bash -s -- 20610

Or

  • Run remotely in PowerShell:
iex "& { $(irm https://raw.githubusercontent.com/microsoft/aspire/main/eng/scripts/get-aspire-cli-pr.ps1) } 20610"

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

The mandatory actionlint download lacks retries, creating an avoidable CI-wide transient failure mode.

Review effort: Balanced
Findings: 1 Medium severity

Open (1)
What changed in this PR

Adds blocking actionlint validation for handwritten workflows while preserving generated-workflow validation ownership.

Changes:

  • Adds checksum-pinned actionlint validation and integrates it with final CI results.
  • Removes invalid or unused workflow metadata.
  • Updates workflow tests, runner configuration, and documentation.
File Description
.github/​workflows/​ci.yml Adds and gates CI on actionlint.
.github/​workflows/​specialized-test-runner.yml Removes unreachable defaults and dead output.
.github/​actions/​enumerate-tests/​action.yml Removes unsupported input metadata.
.github/​actionlint.yaml Registers the custom runner label.
.github/​workflows/​README.md Documents handwritten workflow linting.
tests/​Infrastructure.Tests/​WorkflowScripts/​ExtensionWorkflowTests.cs Tests actionlint’s CI dependencies and result handling.

💡 Add a code-review agent skill for context-aware, tailored reviews. Learn more in the docs.

Comment thread .github/workflows/ci.yml Resolved
@github-actions

Copy link
Copy Markdown
Contributor

Retrying the failed CI jobs for this pull request from the CI run attempt. The rerun is being tracked in the rerun attempt.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🔵 Needs a closer look

The blocking download lacks retry handling, and the README misstates the shared Actionlint configuration.

Review effort: Balanced
Findings: 1 Medium severity

Open (1)
Previously missed (1)

In code that hasn't changed since last review

Low severity 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.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🔵 Needs a closer look

It changes blocking CI and red-main aggregation behavior whose operational impact warrants final human review.

Review effort: Balanced
Findings: None

Resolved since last review (1)
Previously missed (1)

In code that hasn't changed since last review

Low severity Clarify actionlint.yaml config is shared by both lint paths

.github/​workflows/​README.md:43

This paragraph still says .github/actionlint.yaml only 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.

@github-actions

Copy link
Copy Markdown
Contributor

Retrying the failed CI jobs for this pull request from the CI run attempt. The rerun is being tracked in the rerun attempt.

@aspire-repo-bot
aspire-repo-bot Bot requested a balanced review from Copilot October 1, 2026 19:25

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

The tests hard-code the current pin, causing the first automated version-bump PR to fail Infrastructure.Tests.

Review effort: Balanced
Findings: 1 High severity

Open (1)

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🔵 Needs a closer look

The updater workflow remains covered by the top-level update-*.yml skip pattern, preventing its contract tests from running on YAML-only changes.

Review effort: Balanced
Findings: None

Resolved since last review (1)
Previously missed (1)

In code that hasn't changed since last review

Medium severity 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.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Medium severity 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).

@aspire-repo-bot
aspire-repo-bot Bot requested a balanced review from Copilot October 1, 2026 22:56
Ankit Jain (radical) and others added 5 commits October 2, 2026 16:10
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>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🔵 Needs a closer look

It changes blocking CI, supply-chain validation, scheduled automation, and shared PR creation behavior, warranting final human review.

Review effort: Balanced
Findings: None

Resolved since last review (1)

@github-actions

github-actions Bot commented Oct 2, 2026

Copy link
Copy Markdown
Contributor

Retrying the failed CI jobs for this pull request from the CI run attempt. The rerun is being tracked in the rerun attempt.

@radical

Copy link
Copy Markdown
Member Author

CI confidence report

🟡 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.

Required validation: 🔴 0 missing · 🟡 1 attention · 🟢 10 verified · ⚪ 5 not applicable

Evidence

🟡 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.
  • Evidence: ExistingPullRequestMetadataIsUpdatedOnlyWhenEnabled executes the action script against a fake gh, asserting disabled/enabled operation outputs, exact edit arguments, and stdin body. Infrastructure.Tests passed.
  • 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.
🟢 Verified checks (10)
  • ci.yml execution: Attempt 2 succeeded on head c61dd6a162ed0a05cfbc4067c8dcd164ed236309. The handwritten-workflow lint job and final test aggregation succeeded.
  • 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.
  • Outerloop lane: PR-triggered Outerloop run passed as a plumbing smoke test, not full outerloop coverage.
  • Quarantine lane: PR-triggered Quarantined run passed, exercising the affected shared runner.
⚪ Not applicable (5)
  • 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.
Runs and links consulted

@IEvangelist

Copy link
Copy Markdown
Member

PR Testing Report

PR Information

  • PR: microsoft/aspire#20610
  • 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.

Artifact Version Verification

  • Expected commit: c61dd6a162ed0a05cfbc4067c8dcd164ed236309
  • 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.
  • .github\actionlint.yaml - existing custom runner label registration.
  • .github\actions\create-pull-request\action.yml - opt-in existing-PR metadata updates.
  • .github\actions\enumerate-tests\action.yml - unsupported input metadata field removal.
  • .github\workflows\README.md - gate/updater documentation.
  • .github\workflows\ci.yml - blocking lint job and aggregation.
  • .github\workflows\lint-handwritten-workflows.sh - shared handwritten workflow selection.
  • .github\workflows\monitor-scheduled-workflows.config.json - updater watchdog registration.
  • .github\workflows\report-pipeline-failure.js - scheduled-failure wording.
  • .github\workflows\specialized-test-runner.yml - required-input cleanup and unused aggregate-token step/output removal.
  • .github\workflows\update-actionlint.js - release validation and pin update helper.
  • .github\workflows\update-actionlint.yml - weekly updater workflow.
  • docs\ci\ci-failure-issues.md
  • docs\ci\monitor-scheduled-workflows.md
  • docs\ci\pipeline-failure-issues.md
  • docs\ci\specialized-test-failure-issues.md
  • eng\github-ci\ci-skip-entirely-patterns.txt - updater/action paths must receive infrastructure coverage.
  • eng\github-ci\test-trigger-map.yml - infrastructure routing.
  • tests\Infrastructure.Tests\Shared\ProcessRunner.cs - environment overrides.
  • tests\Infrastructure.Tests\Shared\ProcessRunnerTests.cs
  • tests\Infrastructure.Tests\TestTriggerMap\TestTriggerMapTests.cs
  • tests\Infrastructure.Tests\WorkflowScripts\CreatePullRequestActionTests.cs
  • tests\Infrastructure.Tests\WorkflowScripts\ExtensionWorkflowTests.cs
  • tests\Infrastructure.Tests\WorkflowScripts\LintHandwrittenWorkflowsScriptTests.cs
  • tests\Infrastructure.Tests\WorkflowScripts\ReportPipelineFailureIntegrationTests.cs
  • tests\Infrastructure.Tests\WorkflowScripts\ReportPipelineFailureTests.cs
  • tests\Infrastructure.Tests\WorkflowScripts\UpdateActionlintTests.cs
  • tests\Infrastructure.Tests\WorkflowScripts\report-pipeline-failure.integration.harness.js
  • tests\Infrastructure.Tests\WorkflowScripts\update-actionlint.harness.js

Change Categories

  • GitHub Actions / CI infrastructure
  • Infrastructure tests
  • Supporting documentation
  • CLI/product behavior
  • 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.

Evidence: actionlint-ci.log, infrastructure-ci.log, infrastructure-results.trx, relevant-ci-results.json, ci-run-metadata.json.

Scenario 2: Run the real lint gate against source and failure fixtures

Objective: Validate the exact shared script with the actual Actionlint executable.

Coverage type: Happy path, negative, and boundary.

Status: Passed.

Executed bash .github/workflows/lint-handwritten-workflows.sh at the exact PR head: exit 0, 60 workflows linted.

Executed the same repository script from fresh temporary fixture roots:

Fixture Expected outcome Observed
Valid top-level .yml and .yaml; malformed generated and nested files Lint exactly the two handwritten files; ignore generated/nested files Exit 0; Linting 2 handwritten workflow files.
Add a handwritten workflow with an unknown runner label Fail with a runner-label diagnostic Exit 1; diagnostic names unknown-fixture-runner
Only an excluded generated workflow exists Fail rather than treating an empty input set as success Exit 1; No handwritten workflow files found

Evidence: pr20610-fixtures.cjs, local-fixtures.log, local-fixture-results.json.

Scenario 3: Live read-only updater check

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.

Evidence: local-fixtures.log, local-fixture-results.json.

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.

Evidence and Cleanup

Report and evidence are preserved under:

C:\Users\dapine\.copilot\session-state\545ae324-6dc5-4723-8c49-ae26ff9f6adf\files

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>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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.

0 open findings

🧠 Review effort: Balanced

@github-actions

github-actions Bot commented Oct 7, 2026

Copy link
Copy Markdown
Contributor

Retrying the failed CI jobs for this pull request from the CI run attempt. The rerun is being tracked in the rerun attempt.

@radical
Ankit Jain (radical) merged commit d2b0c75 into microsoft:main Oct 7, 2026
758 of 761 checks passed
@microsoft-github-policy-service microsoft-github-policy-service Bot added this to the 17.0 milestone Oct 7, 2026
@github-actions github-actions Bot locked and limited conversation to collaborators Nov 7, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-engineering-systems infrastructure helix infra engineering repo stuff

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants