Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .github/actionlint-version.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"version": "1.7.12",
"linuxAmd64Sha256": "8aca8db96f1b94770f1b0d72b6dddcb1ebb8123cb3712530b08cc387b349a3d8"
}
4 changes: 4 additions & 0 deletions .github/actionlint.yaml
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
self-hosted-runner:
labels:
- 8-core-ubuntu-latest

paths:
.github/workflows/{extension-changelog,pr-docs-check,release-notes-generate,release-update-support-mdx}.lock.yml:
ignore:
Expand Down
22 changes: 16 additions & 6 deletions .github/actions/create-pull-request/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@ inputs:
description: 'Create the pull request as a draft'
required: false
default: 'false'
update-existing-pr-metadata:
description: 'Update the title and body when the pull request already exists'
required: false
default: 'false'
outputs:
pull-request-number:
description: 'The pull request number'
Expand Down Expand Up @@ -96,6 +100,7 @@ runs:
PR_BODY: ${{ inputs.body }}
LABELS: ${{ inputs.labels }}
DRAFT: ${{ inputs.draft }}
UPDATE_EXISTING_PR_METADATA: ${{ inputs.update-existing-pr-metadata }}
run: |
# Check if a PR already exists for this branch
EXISTING_PR=$(gh pr list --head "$BRANCH" --base "$BASE" --json number,url --jq '.[0] // empty')
Expand All @@ -104,9 +109,14 @@ runs:
PR_NUMBER=$(echo "$EXISTING_PR" | jq -r '.number')
PR_URL=$(echo "$EXISTING_PR" | jq -r '.url')
echo "Pull request #$PR_NUMBER already exists: $PR_URL"
echo "pull-request-number=$PR_NUMBER" >> $GITHUB_OUTPUT
echo "pull-request-url=$PR_URL" >> $GITHUB_OUTPUT
echo "pull-request-operation=none" >> $GITHUB_OUTPUT
echo "pull-request-number=$PR_NUMBER" >> "$GITHUB_OUTPUT"
echo "pull-request-url=$PR_URL" >> "$GITHUB_OUTPUT"
if [ "$UPDATE_EXISTING_PR_METADATA" = "true" ]; then
gh pr edit "$PR_NUMBER" --title "$PR_TITLE" --body-file - <<< "$PR_BODY"
echo "pull-request-operation=updated" >> "$GITHUB_OUTPUT"
else
echo "pull-request-operation=none" >> "$GITHUB_OUTPUT"
fi
else
# Build label arguments as a bash array (avoids eval/injection).
# Filter out any labels that don't exist in the repo — `gh pr create`
Expand Down Expand Up @@ -157,7 +167,7 @@ runs:

PR_NUMBER=$(gh pr view "$BRANCH" --json number --jq '.number')
echo "Created pull request #$PR_NUMBER: $PR_URL"
echo "pull-request-number=$PR_NUMBER" >> $GITHUB_OUTPUT
echo "pull-request-url=$PR_URL" >> $GITHUB_OUTPUT
echo "pull-request-operation=created" >> $GITHUB_OUTPUT
echo "pull-request-number=$PR_NUMBER" >> "$GITHUB_OUTPUT"
echo "pull-request-url=$PR_URL" >> "$GITHUB_OUTPUT"
echo "pull-request-operation=created" >> "$GITHUB_OUTPUT"
fi
1 change: 0 additions & 1 deletion .github/actions/enumerate-tests/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ description: 'Enumerate list of test projects'
inputs:
buildArgs:
required: false
type: string
default: ''
description: 'Additional MSBuild arguments passed to the test matrix generation step (e.g., /p:IncludeTemplateTests=true /p:OnlyDeploymentTests=true)'
checkout:
Expand Down
18 changes: 16 additions & 2 deletions .github/workflows/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,22 @@ workflow sources still trigger it even before their generated locks exist.

Compile-time lint diagnostics alone are not a blocking gate. The lint command
owns the actionlint image and compatibility exceptions; `.github/actionlint.yaml`
only scopes the known stale-check output workaround to the affected generated
workflows.
is shared by both lint paths: it configures the runner label for handwritten
workflow linting and scopes the known stale-check output workaround to the
affected generated workflows. Main CI separately runs pinned core actionlint
over handwritten workflows and their local actions through
`lint-handwritten-workflows.sh`, which selects the files and excludes gh-aw output.
The version and linux_amd64 archive SHA-256 are pinned in `.github/actionlint-version.json`,
outside `.github/workflows`, so the Aspire bot can update them without workflow-write access.
The weekly `update-actionlint.yml` workflow opens a draft PR when a newer release exists. It
verifies the downloaded archive against the release API `sha256:` digest and lints the
handwritten workflows with the candidate before proposing the bump. That digest comes from the
same upstream release, so review the release notes; the committed hash then makes CI detect any
later substitution of the archive. Manual dispatch defaults to `validate`, which does not change
the updater branch or open a PR and is safe to run from a fork; `propose` runs only from the
upstream repository's default branch and follows the scheduled PR-update path when a newer release
is available. Scheduled and `propose` runs are serialized end to end by a shared concurrency
group, so each run picks its release only after the previous run has pushed.

Explicit action versions in Markdown survive recompilation, so update deprecated
inputs and action runtimes in the sources, not just the generated YAML. The
Expand Down
57 changes: 45 additions & 12 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,37 @@ concurrency:

jobs:

actionlint:
name: Lint handwritten workflows
runs-on: ubuntu-latest
if: ${{ github.repository_owner == 'microsoft' }}
steps:
- name: Checkout code
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false

# The pin lives outside .github/workflows so the scheduled updater
# (update-actionlint.yml) can propose bumps without workflow-write access.
- name: Install actionlint
run: |
ACTIONLINT_VERSION=$(jq -er '.version' .github/actionlint-version.json)
ACTIONLINT_LINUX_AMD64_SHA256=$(jq -er '.linuxAmd64Sha256' .github/actionlint-version.json)
if [[ ! "${ACTIONLINT_VERSION}" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "::error::Invalid actionlint version '${ACTIONLINT_VERSION}' in .github/actionlint-version.json"
exit 1
fi
archive="actionlint_${ACTIONLINT_VERSION}_linux_amd64.tar.gz"
curl --fail --location --retry 3 --retry-all-errors \
--output "${archive}" \
"https://github.com/rhysd/actionlint/releases/download/v${ACTIONLINT_VERSION}/${archive}"
echo "${ACTIONLINT_LINUX_AMD64_SHA256} ${archive}" | sha256sum --check -
tar -xzf "${archive}" -C "${RUNNER_TEMP}" actionlint
echo "${RUNNER_TEMP}" >> "${GITHUB_PATH}"

- name: Lint handwritten workflow files
run: bash .github/workflows/lint-handwritten-workflows.sh

prepare_for_ci:
runs-on: ubuntu-latest
name: Prepare for CI
Expand Down Expand Up @@ -177,20 +208,21 @@ jobs:
if: ${{ always() && github.repository_owner == 'microsoft' }}
runs-on: ubuntu-latest
name: Final Results
needs: [prepare_for_ci, tests, stabilization_check]
needs: [actionlint, prepare_for_ci, tests, stabilization_check]

steps:
- name: Fail if any of the dependent jobs failed
# Don't fail if the workflow is being skipped.
# Build and test jobs may be skipped, but handwritten workflow lint remains required.
if: >-
${{ always() &&
needs.prepare_for_ci.outputs.skip_workflow != 'true' &&
(contains(needs.*.result, 'failure') ||
contains(needs.*.result, 'cancelled') ||
needs.tests.result != 'success' ||
(needs.prepare_for_ci.outputs.STABILIZATION_ENABLED == 'true' &&
github.event_name == 'pull_request' &&
needs.stabilization_check.result != 'success')) }}
(needs.actionlint.result != 'success' ||
(needs.prepare_for_ci.outputs.skip_workflow != 'true' &&
(contains(needs.*.result, 'failure') ||
contains(needs.*.result, 'cancelled') ||
needs.tests.result != 'success' ||
(needs.prepare_for_ci.outputs.STABILIZATION_ENABLED == 'true' &&
github.event_name == 'pull_request' &&
needs.stabilization_check.result != 'success')))) }}
run: |
echo "One or more dependent jobs failed."
exit 1
Expand All @@ -205,13 +237,13 @@ jobs:
# CI_RED / CI_GREEN are derived from explicit needs.*.result checks rather than
# failure()/success() because tests are *skipped* on the
# no-relevant-changes path (skip_workflow): a skipped run is neither red nor green,
# so the script leaves any open issue untouched (no CI signal). Requiring both
# so the script leaves any open issue untouched (no CI signal). Requiring actionlint,
# prepare_for_ci and tests to have *succeeded* for CI_GREEN means a docs-only push
# never closes a still-red issue without actually re-running CI. Stabilization is
# PR-only and cannot contribute a success result to this push-only tracker.
ci_failure_tracker:
name: Track CI Failure
needs: [prepare_for_ci, tests, stabilization_check]
needs: [actionlint, prepare_for_ci, tests, stabilization_check]
if: ${{ always() && github.event_name == 'push' && github.repository_owner == 'microsoft' }}
runs-on: ubuntu-latest
permissions:
Expand All @@ -228,7 +260,8 @@ jobs:
REF: ${{ github.ref_name }}
CI_RED: ${{ contains(needs.*.result, 'failure') }}
CI_GREEN: >-
${{ needs.prepare_for_ci.result == 'success' &&
${{ needs.actionlint.result == 'success' &&
needs.prepare_for_ci.result == 'success' &&
needs.tests.result == 'success' }}
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
Expand Down
38 changes: 38 additions & 0 deletions .github/workflows/lint-handwritten-workflows.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
#!/usr/bin/env bash
# Lints the handwritten GitHub Actions workflows with the `actionlint` on PATH.
#
# Both ci.yml ("Lint handwritten workflows") and update-actionlint.yml (which
# proves a candidate release before proposing a bump) call this script, so the
# blocking gate and the updater always lint the same file set.
#
# Run from the repository root. Lints top-level .github/workflows/*.yml and
# *.yaml files and excludes gh-aw generated output:
# - *.lock.yml compiled agentic workflows
# - agentics-maintenance*.yml gh-aw side-repo maintenance workflows
# Generated workflows are validated by `gh aw compile` instead.
#
# The shellcheck and pyflakes integrations are disabled so that results do not
# depend on whichever versions the runner image happens to ship.
#
# Exits non-zero when actionlint reports errors or when no files match, so a
# moved or renamed workflow directory cannot silently pass the gate.
set -euo pipefail

files=()
while IFS= read -r file; do
files+=("$file")
done < <(
find .github/workflows -maxdepth 1 -type f \
\( -name '*.yml' -o -name '*.yaml' \) \
! -name '*.lock.yml' \
! -name 'agentics-maintenance*.yml' |
LC_ALL=C sort
)

if [[ ${#files[@]} -eq 0 ]]; then
echo "::error::No handwritten workflow files found under .github/workflows." >&2
exit 1
fi

echo "Linting ${#files[@]} handwritten workflow files."
actionlint -shellcheck= -pyflakes= "${files[@]}"
3 changes: 2 additions & 1 deletion .github/workflows/monitor-scheduled-workflows.config.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
{ "file": "tests-outerloop.yml", "name": "Outerloop Tests", "selfReports": true },
{ "file": "tests-quarantine.yml", "name": "Quarantined Tests", "selfReports": true },
{ "file": "tests-daily-smoke.yml", "name": "Daily CLI Smoke Tests", "selfReports": true, "labels": ["area-cli"] },
{ "file": "deployment-tests.yml", "name": "Deployment E2E Tests", "selfReports": true, "labels": ["area-testing", "deployment-e2e"] }
{ "file": "deployment-tests.yml", "name": "Deployment E2E Tests", "selfReports": true, "labels": ["area-testing", "deployment-e2e"] },
{ "file": "update-actionlint.yml", "name": "Update actionlint", "selfReports": true, "labels": ["area-engineering-systems"] }
]
}
8 changes: 4 additions & 4 deletions .github/workflows/report-pipeline-failure.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// Nightly-pipeline failure reporter, used by deployment-tests.yml and
// tests-daily-smoke.yml. Invoked from an actions/github-script step via report().
// Scheduled-pipeline failure reporter, used by deployment-tests.yml,
// tests-daily-smoke.yml, and the weekly update-actionlint.yml. Invoked from an actions/github-script step via report().
//
// These scheduled pipelines otherwise fail silently — GitHub only emails whoever
// last edited the workflow file. This reporter files a single deduplicated issue
Expand Down Expand Up @@ -42,13 +42,13 @@ function buildMarker(workflowFile) {
}

function buildIssueTitle(displayName) {
return `Nightly run failing: ${displayName}`;
return `Scheduled run failing: ${displayName}`;
}

// Builds the static issue body. Each failed run is recorded as a comment (see
// report()), so the body is a fixed description written once at filing.
function buildIssueBody({ marker, displayName, workflowFile, cc = '' }) {
const lead = `The scheduled workflow [\`${workflowFile}\`](../../actions/workflows/${workflowFile}) (**${displayName}**) is failing on its nightly run.`;
const lead = `The scheduled workflow [\`${workflowFile}\`](../../actions/workflows/${workflowFile}) (**${displayName}**) is failing on its scheduled run.`;

return tracking.buildBody({
marker,
Expand Down
12 changes: 0 additions & 12 deletions .github/workflows/specialized-test-runner.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ on:
enablePlaywrightInstall:
required: true
type: boolean
default: false
# When true, ignores test failures and checks if .trx files have nonzero test count
ignoreTestFailures:
required: false
Expand All @@ -39,7 +38,6 @@ jobs:
runsheet: ${{ steps.inject_properties.outputs.runsheet }}
requiresNugets: ${{ steps.check_nugets.outputs.requiresNugets }}
requiresCliArchive: ${{ steps.check_cli_archive.outputs.requiresCliArchive }}
requiresGitHubToken: ${{ steps.check_github_token.outputs.requiresGitHubToken }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1

Expand Down Expand Up @@ -106,16 +104,6 @@ jobs:
echo "requiresCliArchive=false" >> $GITHUB_OUTPUT
fi

- name: Check if any test requires GitHub token
id: check_github_token
run: |
RUNSHEET='${{ steps.generate_tests_matrix.outputs.runsheet }}'
if echo "$RUNSHEET" | jq -e 'any(.[]; .requiresGitHubToken == true)' > /dev/null 2>&1; then
echo "requiresGitHubToken=true" >> $GITHUB_OUTPUT
else
echo "requiresGitHubToken=false" >> $GITHUB_OUTPUT
fi

- name: Upload logs, and test results
if: ${{ always() }}
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
Expand Down
Loading
Loading