Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
6 changes: 6 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -320,6 +320,10 @@ jobs:
name: TypeScript SDK Unit Tests
uses: ./.github/workflows/typescript-sdk-tests.yml

typescript_api_compat:
name: TypeScript API Compatibility
uses: ./.github/workflows/typescript-api-compat.yml

results:
if: ${{ always() && github.repository_owner == 'microsoft' }}
runs-on: ubuntu-latest
Expand All @@ -336,6 +340,7 @@ jobs:
extension_tests_win,
cli_starter_validation_windows,
typescript_sdk_tests,
typescript_api_compat,
tests_no_nugets,
tests_no_nugets_overflow,
tests_requires_nugets_linux,
Expand Down Expand Up @@ -420,6 +425,7 @@ jobs:
(needs.extension_tests_win.result == 'skipped' ||
needs.cli_starter_validation_windows.result == 'skipped' ||
needs.typescript_sdk_tests.result == 'skipped' ||
needs.typescript_api_compat.result == 'skipped' ||
needs.tests_no_nugets.result == 'skipped' ||
needs.tests_requires_nugets_linux.result == 'skipped' ||
needs.tests_requires_nugets_windows.result == 'skipped' ||
Expand Down
122 changes: 122 additions & 0 deletions .github/workflows/typescript-api-compat.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
name: TypeScript API Compatibility

on:
workflow_call:

permissions:
contents: read

jobs:
typescript_api_compat:
name: TypeScript API Compatibility
if: ${{ github.event_name == 'pull_request' }}
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0

- name: Restore
run: ./restore.sh

- name: Build CLI and compatibility tool
run: |
./dotnet.sh build src/Aspire.Cli/Aspire.Cli.csproj --configuration Release /p:SkipNativeBuild=true
./dotnet.sh build tools/TypeScriptApiCompat/TypeScriptApiCompat.csproj --configuration Release

- name: Extract base ATS baseline
shell: bash
env:
BASE_BRANCH: ${{ github.base_ref }}
run: |
set -euo pipefail

BASE_REF="origin/${BASE_BRANCH}"
BASELINE_DIR="${RUNNER_TEMP}/ats-baseline"
mkdir -p "$BASELINE_DIR"

git fetch --no-tags --depth=1 origin "+refs/heads/${BASE_BRANCH}:refs/remotes/origin/${BASE_BRANCH}"
git ls-tree -r --name-only "$BASE_REF" -- src | grep '\.ats\.txt$' > "${RUNNER_TEMP}/ats-baseline-files.txt"

while IFS= read -r file; do
mkdir -p "${BASELINE_DIR}/$(dirname "$file")"
git show "${BASE_REF}:${file}" > "${BASELINE_DIR}/${file}"
done < "${RUNNER_TEMP}/ats-baseline-files.txt"

echo "BASELINE_DIR=$BASELINE_DIR" >> "$GITHUB_ENV"

- name: Generate current ATS surface
shell: bash
run: |
set -euo pipefail

CURRENT_DIR="${RUNNER_TEMP}/ats-current"
PROJECTS_FILE="${RUNNER_TEMP}/ats-projects.txt"
SORTED_PROJECTS_FILE="${RUNNER_TEMP}/ats-projects-sorted.txt"
ASPIRE_CLI="./dotnet.sh run --no-build --project src/Aspire.Cli/Aspire.Cli.csproj --configuration Release -- --nologo"

mkdir -p "$CURRENT_DIR/src/Aspire.Hosting/api"
: > "$PROJECTS_FILE"

echo "::group::Aspire.Hosting (core)"
$ASPIRE_CLI sdk dump --format ci -o "$CURRENT_DIR/src/Aspire.Hosting/api/Aspire.Hosting.ats.txt"
echo "::endgroup::"

while IFS= read -r file; do
case "$file" in
*/obj/*|*/bin/*|*Aspire.Hosting.Analyzers*|*Aspire.Hosting.CodeGeneration*|*Aspire.Hosting.RemoteHost*)
continue
;;
esac

dir=$(dirname "$file")
while [ "$dir" != "." ] && [ "$dir" != "/" ]; do
if compgen -G "$dir/*.csproj" > /dev/null; then
echo "$dir" >> "$PROJECTS_FILE"
break
fi

dir=$(dirname "$dir")
done
done < <(grep -rl '\[AspireExport(' src/Aspire.Hosting.*/ --include='*.cs' || true)

sort -u "$PROJECTS_FILE" > "$SORTED_PROJECTS_FILE"

while IFS= read -r proj; do
[ -n "$proj" ] || continue
proj_name=$(basename "$proj")
csproj="$proj/$proj_name.csproj"
if [ -f "$csproj" ]; then
echo "::group::$proj_name"
mkdir -p "$CURRENT_DIR/$proj/api"
$ASPIRE_CLI sdk dump --format ci "$csproj" -o "$CURRENT_DIR/$proj/api/$proj_name.ats.txt"
echo "::endgroup::"
fi
done < "$SORTED_PROJECTS_FILE"

echo "CURRENT_DIR=$CURRENT_DIR" >> "$GITHUB_ENV"

- name: Check TypeScript API compatibility
shell: bash
run: |
set -euo pipefail

./dotnet.sh run \
--no-build \
--project tools/TypeScriptApiCompat/TypeScriptApiCompat.csproj \
--configuration Release \
-- \
--baseline "$BASELINE_DIR" \
--current "$CURRENT_DIR" \
--suppressions-root "${{ github.workspace }}" \
--report "${RUNNER_TEMP}/typescript-api-compat-report.md" \
--github-annotations

- name: Upload TypeScript API compatibility report
if: failure()
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: typescript-api-compat-report
path: ${{ runner.temp }}/typescript-api-compat-report.md
if-no-files-found: ignore
1 change: 1 addition & 0 deletions Aspire.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -537,6 +537,7 @@
<Project Path="tools/CreateFailingTestIssue/CreateFailingTestIssue.csproj" />
<Project Path="tools/CreateLayout/CreateLayout.csproj" />
<Project Path="tools/GenerateTestSummary/GenerateTestSummary.csproj" />
<Project Path="tools/TypeScriptApiCompat/TypeScriptApiCompat.csproj" />
</Folder>
<Folder Name="/Tools/QuarantineTools/">
<Project Path="tools/QuarantineTools/QuarantineTools.csproj" />
Expand Down
85 changes: 85 additions & 0 deletions docs/ci/typescript-api-compat.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
# TypeScript API compatibility

The TypeScript API compatibility check prevents pull requests from introducing undeclared breaking changes in the ATS surface used to generate TypeScript polyglot AppHost SDKs.

## Baseline

The checked-in `src/Aspire.Hosting*/api/*.ats.txt` files are the compatibility baseline. In pull request CI, `.github/workflows/typescript-api-compat.yml` reads those files from the pull request target branch and compares them with fresh `aspire sdk dump --format ci` output generated from the pull request.

This intentionally differs from a plain git diff. A pull request cannot hide a breaking change by editing the baseline file in the same PR; the baseline is always loaded from the merge target branch.

The scheduled `.github/workflows/generate-ats-diffs.yml` workflow remains the review and release mechanism for updating the checked-in ATS files after API changes are accepted.

After a new version ships, reset the compatibility baseline by updating the checked-in ATS files to the shipped surface. Suppressions for breaks that are now part of that new baseline should be deleted in the same change; keep only suppressions that still describe intentional breaks relative to the target branch baseline. The compatibility checker fails on unused suppressions, which helps catch declarations that should have been removed during the reset.

## Breaking changes

The checker treats these ATS changes as breaking:

- Removed packages, handle types, DTO types, enum types, exported values, or capabilities.
- Removed handle flags such as `ExposeProperties` or `ExposeMethods`.
- Removed DTO properties, optional-to-required DTO property changes, DTO property type changes, or newly added required DTO properties.
- Removed enum values.
- Exported value type or literal value changes.
- Removed capability parameters, optional-to-required parameter changes, required parameter additions, parameter type changes, parameter order changes, or return type changes.

Additive changes such as new capabilities, new optional parameters, new optional DTO properties, new enum values, and new exported values do not require suppressions.

## Suppressions

Intentional breaks must be declared in a suppression file. Per-package suppressions should live next to the ATS file:

```text
src/Aspire.Hosting.Redis/api/Aspire.Hosting.Redis.tscompat.suppression.txt
```

Cross-cutting suppressions may use:

```text
eng/TypeScriptApiCompat/global.suppression.txt
```

The format is:

```text
BREAK <kind> <package> <symbol> -- <issue-or-pr-url> -- <reason>
```

The `<issue-or-pr-url>` field is required so every intentional breaking change is traceable to the issue or pull request where the API break was reviewed and approved. The `<reason>` should briefly explain why the break is intentional.

Supported `<kind>` values are:

- `package-removed`
- `handle-removed`
- `handle-flag-removed`
- `dto-removed`
- `dto-property-removed`
- `dto-property-type-changed`
- `dto-property-required`
- `dto-property-added-required`
- `enum-removed`
- `enum-value-removed`
- `exported-value-removed`
- `exported-value-type-changed`
- `exported-value-changed`
- `capability-removed`
- `capability-return-type-changed`
- `capability-parameter-removed`
- `capability-parameter-type-changed`
- `capability-parameter-required`
- `capability-parameter-added-required`
- `capability-parameter-order-changed`

Example:

```text
BREAK capability-removed Aspire.Hosting.Redis Aspire.Hosting.Redis/withRedisCommander -- https://github.com/microsoft/aspire/issues/16961 -- Removed unsupported API before GA
```

Suppression matching is exact. Unused suppressions fail the check so stale entries are removed when the API surface changes again.

## Generated TypeScript declarations

This check currently compares the ATS source surface that feeds TypeScript generation. Generator implementation changes are still covered by TypeScript code generation tests and `tests/PolyglotAppHosts/*/TypeScript` validation, but they are not yet classified against a generated `.d.ts` baseline.

If generator-only API shape changes need the same breaking-change treatment, extend `tools/TypeScriptApiCompat` to generate declaration-only output from the checked-in TypeScript validation AppHosts and compare those declarations with the same suppression format.
1 change: 1 addition & 0 deletions tests/Infrastructure.Tests/Infrastructure.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@

<ItemGroup>
<ProjectReference Include="..\Aspire.TestUtilities\Aspire.TestUtilities.csproj" />
<ProjectReference Include="..\..\tools\TypeScriptApiCompat\TypeScriptApiCompat.csproj" />
</ItemGroup>

<ItemGroup>
Expand Down
Loading
Loading