From 103b9a47073e7f542b8c24ff94826452f1857860 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 6 Aug 2026 02:40:21 +0000 Subject: [PATCH 1/5] Initial plan From 3c161a52c280e8a8a80df2ff2c887db6d44dd17f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 6 Aug 2026 03:02:00 +0000 Subject: [PATCH 2/5] Replace Java PowerShell automation with TypeScript Co-authored-by: XiaofeiCao <92354331+XiaofeiCao@users.noreply.github.com> --- .github/workflows/ci-java.yml | 2 +- eng/pipelines/jobs/spector-coverage.yml | 4 +- packages/typespec-java/Build-Generator.ps1 | 31 -- packages/typespec-java/CONTRIBUTING.md | 18 +- packages/typespec-java/Copy-Sources.ps1 | 76 ----- packages/typespec-java/CoreCommit.ps1 | 151 ---------- packages/typespec-java/Generate.ps1 | 273 ----------------- packages/typespec-java/Spector-Tests.ps1 | 47 --- packages/typespec-java/SyncTests.ps1 | 61 ---- .../typespec-java/emitter-tests/readme.md | 11 +- .../emitter-tests/resolve-spector-specs.js | 4 +- .../emitter-tests/spector.config.yaml | 2 +- .../eng/scripts/build-generator.ts | 36 +++ .../typespec-java/eng/scripts/copy-sources.ts | 65 ++++ .../eng/scripts/core-source.test.ts | 58 ++++ .../typespec-java/eng/scripts/core-source.ts | 98 ++++++ .../typespec-java/eng/scripts/generate.ts | 282 ++++++++++++++++++ .../eng/scripts/spector-tests.ts | 46 +++ .../typespec-java/eng/scripts/sync-tests.ts | 44 +++ packages/typespec-java/package.json | 12 +- packages/typespec-java/vitest.config.ts | 2 +- pnpm-lock.yaml | 6 + 22 files changed, 663 insertions(+), 666 deletions(-) delete mode 100644 packages/typespec-java/Build-Generator.ps1 delete mode 100644 packages/typespec-java/Copy-Sources.ps1 delete mode 100644 packages/typespec-java/CoreCommit.ps1 delete mode 100644 packages/typespec-java/Generate.ps1 delete mode 100644 packages/typespec-java/Spector-Tests.ps1 delete mode 100644 packages/typespec-java/SyncTests.ps1 create mode 100644 packages/typespec-java/eng/scripts/build-generator.ts create mode 100644 packages/typespec-java/eng/scripts/copy-sources.ts create mode 100644 packages/typespec-java/eng/scripts/core-source.test.ts create mode 100644 packages/typespec-java/eng/scripts/core-source.ts create mode 100644 packages/typespec-java/eng/scripts/generate.ts create mode 100644 packages/typespec-java/eng/scripts/spector-tests.ts create mode 100644 packages/typespec-java/eng/scripts/sync-tests.ts diff --git a/.github/workflows/ci-java.yml b/.github/workflows/ci-java.yml index 7e9c627453..99bbbf5aca 100644 --- a/.github/workflows/ci-java.yml +++ b/.github/workflows/ci-java.yml @@ -49,7 +49,7 @@ jobs: run: pnpm install # The emitter compiles against these peer packages; `pnpm run regenerate` - # (Generate.ps1) runs `pnpm build` for the emitter, which needs them already + # (eng/scripts/generate.ts) runs `pnpm build` for the emitter, which needs them already # built. - name: Build dependencies run: pnpm turbo run --filter "@azure-tools/typespec-java^..." build diff --git a/eng/pipelines/jobs/spector-coverage.yml b/eng/pipelines/jobs/spector-coverage.yml index 21fa9eb50f..2b1c695f96 100644 --- a/eng/pipelines/jobs/spector-coverage.yml +++ b/eng/pipelines/jobs/spector-coverage.yml @@ -118,11 +118,11 @@ jobs: # then regenerates the SDK from the spector specs. `pnpm run test:java:e2e` # starts the mock server, runs the JUnit tests and produces # tsp-spector-coverage-java.json. - - pwsh: pnpm run regenerate + - script: pnpm run regenerate displayName: "Regenerate Java test code" workingDirectory: packages/typespec-java - - pwsh: pnpm run test:java:e2e + - script: pnpm run test:java:e2e displayName: "Run Java spector tests" workingDirectory: packages/typespec-java diff --git a/packages/typespec-java/Build-Generator.ps1 b/packages/typespec-java/Build-Generator.ps1 deleted file mode 100644 index 1fa6645f3f..0000000000 --- a/packages/typespec-java/Build-Generator.ps1 +++ /dev/null @@ -1,31 +0,0 @@ -# Build the Java emitter.jar from the generator sources that Copy-Sources.ps1 -# copied out of the core/ submodule into this package's ./generator folder and -# patched with the Azure customization (core.patch). Building the copy means this -# step never touches the core/ submodule. -# -# Run Copy-Sources.ps1 first (the "build:emitter" half of the package build copies -# and patches ./generator); this is the "build:generator" half. -# -# Requires JDK 11+ and Apache Maven on PATH. - -$ErrorActionPreference = "Stop" - -# See Copy-Sources.ps1: silence progress so no cmdlet touches the controlling -# terminal from turbo's background process group (which would SIGTTIN/SIGTTOU-stop -# this process and hang the parallel build). -$ProgressPreference = "SilentlyContinue" - -$packageRoot = $PSScriptRoot -$generatorRoot = Join-Path $packageRoot "generator" -$generatorPom = Join-Path $generatorRoot "pom.xml" -$copySourcesScript = Join-Path $packageRoot "Copy-Sources.ps1" - -if (-not (Test-Path $generatorPom)) { - Write-Error "Copied generator not found at: $generatorPom`nRun $copySourcesScript first (build:emitter) -- it copies and patches ./generator from the 'core' submodule." -} - -Write-Host "Build JAR" -mvn clean install -DskipTests --define spotless.apply.skip=true --define spotless.check.skip=true --no-transfer-progress -T 1C -f $generatorPom -if ($LASTEXITCODE -ne 0) { - exit $LASTEXITCODE -} diff --git a/packages/typespec-java/CONTRIBUTING.md b/packages/typespec-java/CONTRIBUTING.md index 7993cd25ec..44ca95c9dd 100644 --- a/packages/typespec-java/CONTRIBUTING.md +++ b/packages/typespec-java/CONTRIBUTING.md @@ -20,17 +20,17 @@ contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additio Only `src/options.ts` (the Azure-specific emitter options) is committed in this package. The rest of the emitter TypeScript (and tests) is copied from `core/packages/http-client-java/emitter/{src,test}` -at build time by `Copy-Sources.ps1` (excluding `options.ts`). The Java `emitter.jar` is -built by `Build-Generator.ps1` from a patched copy of `core/packages/http-client-java/generator` +at build time by `eng/scripts/copy-sources.ts` (excluding `options.ts`). The Java `emitter.jar` is +built by `eng/scripts/build-generator.ts` from a patched copy of `core/packages/http-client-java/generator` (see below) and staged into `generator/http-client-generator/target/`. ### Azure customization patch -`Copy-Sources.ps1` copies the Java generator sources out of the `core/` submodule into this +`eng/scripts/copy-sources.ts` copies the Java generator sources out of the `core/` submodule into this package's `./generator` folder and applies `core.patch` to that **copy** — never to `core/` itself. The patch swaps the unbranded customization engine in `http-client-generator-core` for Azure's `com.azure.tools:azure-autorest-customization` (resolved from Maven Central), so the -`customization-class` emitter option runs against the Azure customization base. `Build-Generator.ps1` +`customization-class` emitter option runs against the Azure customization base. `eng/scripts/build-generator.ts` then builds `emitter.jar` from the patched `./generator`. Because the patch is only ever applied to the copy, the `core/` submodule working tree stays clean. When the `core/` submodule is bumped, refresh `core.patch` if its context no longer applies. @@ -48,7 +48,7 @@ pnpm run-all --filter "@azure-tools/typespec-java..." build ### Pinning the core commit (`core-commit.json`) -`Copy-Sources.ps1` reads the emitter/generator sources from the `core/` submodule's current checkout. +`eng/scripts/copy-sources.ts` reads the emitter/generator sources from the `core/` submodule's current checkout. The optional `core-commit.json` pins a specific upstream `core` commit to read from instead: ```json @@ -68,7 +68,7 @@ If `pnpm turbo ...` fails with `'turbo' is not recognized as an internal or exte after `pnpm install`, the local install tree is missing Turbo's binary shim. From the repo root, force pnpm to refresh the local install state and rerun the command: -```powershell +```bash pnpm install --force pnpm run-all --filter "@azure-tools/typespec-java..." build ``` @@ -95,7 +95,7 @@ Attach a debugger to port 9229 and set breakpoints in `src/emitter.ts`, TypeScript passes the code model and emitter options to Java through the generated `emitter-tests/tsp-output/code-model.yaml` file. To debug the Java generator directly: -1. Build the package so `Copy-Sources.ps1` creates the patched generator copy under `generator/`. +1. Build the package so `eng/scripts/copy-sources.ts` creates the patched generator copy under `generator/`. 2. Update `DEFAULT_OUTPUT_DIR` in `generator/http-client-generator/src/main/java/com/microsoft/typespec/http/client/generator/Main.java` to the directory containing the `code-model.yaml` to debug. @@ -134,7 +134,7 @@ Pulls the fix in from core; **no version bump**. Example: `sha` in [`core-commit.json`](./core-commit.json) to the target microsoft/typespec commit (e.g. the HEAD of core `main`). Build and sync scripts transiently check this commit out without moving the submodule pointer. -2. **Sync tests from core.** Run `pwsh ./SyncTests.ps1` in [`emitter-tests`](./emitter-tests): it +2. **Sync tests from core.** Run `pnpm sync-tests`: it copies the tests/specs from the pinned core commit and aligns `emitter-tests/package.json`. 3. **Add a `fix` changelog entry** for the fix (`pnpm change add`). @@ -150,7 +150,7 @@ Bumps the version and publishes, after Part A merges. Example: `hotfix-release` skill (creates the `publish/hotfix/-` branch off `release/` and runs `pnpm chronus version --ignore-policies`, consuming the changeset from Part A). The `core` submodule stays unchanged. The bump must also be reflected in `emitter-tests/package.json` (its -`version` and the `*.tgz` dependency) — rerun `pwsh ./SyncTests.ps1` or update it manually. +`version` and the `*.tgz` dependency) — rerun `pnpm sync-tests` or update it manually. Open the PR against `release/`. After it merges: diff --git a/packages/typespec-java/Copy-Sources.ps1 b/packages/typespec-java/Copy-Sources.ps1 deleted file mode 100644 index e8b52633c2..0000000000 --- a/packages/typespec-java/Copy-Sources.ps1 +++ /dev/null @@ -1,76 +0,0 @@ -# Copy the emitter TypeScript sources (and tests) from the upstream -# @typespec/http-client-java checkout in the core/ submodule into this package, -# so `@azure-tools/typespec-java` builds the same emitter while keeping its own -# Azure-flavored `src/options.ts` (excluded from the copy). -# -# The desired core commit is pinned in core-commit.json; CoreCommit.ps1's -# Get-CoreSourceRoot returns a directory to read the emitter/generator sources from -# (extracted from the pinned commit via `git archive` when it is newer than the -# submodule's current checkout), without ever mutating the core/ submodule working -# tree. Remove-CoreSourceRoot cleans up any temporary extraction afterwards. -# -# Mirrors the "Copy TypeScript code" step of autorest.java's Build-TypeSpec.ps1. - -$ErrorActionPreference = "Stop" - -# Disable progress rendering. Under a full parallel `pnpm build`, turbo runs each -# task's process in its own background process group under the build's controlling -# terminal. When a cmdlet here (notably `Copy-Item -Recurse`, which emits a -# Write-Progress) renders progress, PowerShell touches that terminal from a -# background process group and the OS stops the process with SIGTTIN/SIGTTOU -- it -# never resumes, so the emitter build appears to hang forever (intermittently, and -# only for this package since it is the only one that shells out to pwsh). Silencing -# progress removes that terminal interaction entirely. -$ProgressPreference = "SilentlyContinue" - -$packageRoot = $PSScriptRoot -$repoRoot = Resolve-Path (Join-Path $packageRoot ".." "..") -$coreRoot = Join-Path $repoRoot "core" - -. (Join-Path $packageRoot "CoreCommit.ps1") -$core = Get-CoreSourceRoot -CoreRoot $coreRoot -PackageRoot $packageRoot -try { - $emitterRoot = Join-Path $core.Root "emitter" - - Copy-Item -Path (Join-Path $emitterRoot "src") -Destination $packageRoot -Exclude "options.ts" -Recurse -Force - Copy-Item -Path (Join-Path $emitterRoot "test") -Destination $packageRoot -Recurse -Force - - # Copy the Java generator sources out of the submodule and apply the Azure - # customization patch (core.patch) to the copy, so building emitter.jar - # (Build-Generator.ps1) never mutates the core/ submodule. The patch paths are - # relative to this generator folder. - $srcGenerator = Join-Path $core.Root "generator" - $destGenerator = Join-Path $packageRoot "generator" - $patchFile = Join-Path $packageRoot "core.patch" - - # The emitter.jar reactor (generator/pom.xml) only builds http-client-generator, - # http-client-generator-mgmt and http-client-generator-core. The two test modules - # below live in a separate (never-activated) Maven `test` profile and hold ~38MB - # of generated Java test sources that are not part of the emitter build, so skip - # copying them -- it is the bulk of the copy time. (This package's e2e tests - # under emitter-tests/ sync http-client-generator-test straight from core via - # SyncTests.ps1, not from this copy.) - $excludedGeneratorModules = @("http-client-generator-test", "http-client-generator-clientcore-test") - - Write-Host "Copy generator sources from core" - if (Test-Path $destGenerator) { - Remove-Item $destGenerator -Recurse -Force - } - New-Item -ItemType Directory -Path $destGenerator | Out-Null - Get-ChildItem -Path $srcGenerator -Force | - Where-Object { $excludedGeneratorModules -notcontains $_.Name } | - ForEach-Object { Copy-Item -Path $_.FullName -Destination $destGenerator -Recurse -Force } - - Write-Host "Apply Azure customization patch to copied generator" - # Run from the repo root with --directory so git apply resolves the patch's - # (generator-relative) paths correctly; running `git apply` from within a - # subdirectory makes git treat the paths as repo-root-relative and skip them. - $relGenerator = [System.IO.Path]::GetRelativePath($repoRoot, $destGenerator).Replace('\', '/') - git -C $repoRoot apply --directory=$relGenerator -p1 $patchFile --ignore-whitespace - if ($LASTEXITCODE -ne 0) { - Write-Error "Failed to apply core.patch to $destGenerator." - } -} -finally { - Remove-CoreSourceRoot $core -} diff --git a/packages/typespec-java/CoreCommit.ps1 b/packages/typespec-java/CoreCommit.ps1 deleted file mode 100644 index d5c39bddf1..0000000000 --- a/packages/typespec-java/CoreCommit.ps1 +++ /dev/null @@ -1,151 +0,0 @@ -# Shared helper for reading sources from the desired `core/` submodule commit -# WITHOUT mutating the submodule's working tree. -# -# Dot-source this file to import Get-CoreSourceRoot / Remove-CoreSourceRoot, then -# wrap the work in a try/finally so the temporary extraction (if any) is cleaned up: -# -# . (Join-Path $packageRoot "CoreCommit.ps1") -# $core = Get-CoreSourceRoot -CoreRoot $coreRoot -PackageRoot $packageRoot -# try { -# # read sources from under $core.Root (e.g. emitter/, generator/) -# } -# finally { -# Remove-CoreSourceRoot $core -# } -# -# The desired core commit is pinned in /core-commit.json. When that -# file is present and its commit differs from the submodule's current checkout, -# Get-CoreSourceRoot extracts that commit's `packages/http-client-java` subtree into -# a temporary directory via `git archive`. The pin is authoritative: it is used -# whether it is newer or older than the current checkout (no ancestry check, which a -# shallow CI clone cannot reliably evaluate). It never checks out a different SHA or -# otherwise touches the submodule's working tree, so it is safe to run while a -# parallel monorepo build (`pnpm build`, `regen-all-packages-docs`, ...) reads the -# core/ submodule concurrently, and `git status` on core always stays clean. -# -# When core-commit.json is absent, or its commit equals the current checkout, the -# sources are read straight from the submodule's current working tree. - -# Path (relative to the core repo root) of the subtree the Java package reads from. -$script:CoreJavaSubtree = "packages/http-client-java" - -# Resolve where to read the core Java sources from and return a descriptor: -# @{ Root = ; TempDir = } -# Pass the descriptor to Remove-CoreSourceRoot in a finally block to clean up. -function Get-CoreSourceRoot { - param( - # Path to the `core/` submodule. - [Parameter(Mandatory = $true)] - [string] $CoreRoot, - - # Directory that contains the optional core-commit.json pin file. - [Parameter(Mandatory = $true)] - [string] $PackageRoot - ) - - # Never let any git invocation below block on an interactive credential or - # host-key prompt. During a parallel `pnpm build` the emitter build runs - # headless (turbo captures its stdio / attaches a pseudo-terminal), so a git - # prompt has nowhere to read from and would hang the whole build forever -- - # observed as an intermittent, Java-only build hang. Make git fail fast instead. - $env:GIT_TERMINAL_PROMPT = "0" - - # Silence progress: a cmdlet rendering Write-Progress (e.g. the archive - # extraction below) touches the controlling terminal, and under turbo's - # background process group that stops this process with SIGTTIN/SIGTTOU and - # hangs the build. Scoped to this function so callers are unaffected. - $ProgressPreference = "SilentlyContinue" - - $originSha = (git -C $CoreRoot rev-parse HEAD).Trim() - if ($LASTEXITCODE -ne 0) { - Write-Error "Failed to read the current SHA of the 'core' submodule at: $CoreRoot`nMake sure it is checked out (git submodule update --init --recursive)." - } - - $liveRoot = Join-Path $CoreRoot $script:CoreJavaSubtree - - # core-commit.json is optional: when present it pins the core commit to use; - # when absent we just use the submodule's current checkout. - $configPath = Join-Path $PackageRoot "core-commit.json" - if (-not (Test-Path $configPath)) { - Write-Host "core-commit.json not found; using current core checkout $originSha." - return @{ Root = $liveRoot; TempDir = $null } - } - - $targetSha = ((Get-Content -Raw $configPath | ConvertFrom-Json).sha).Trim() - if ([string]::IsNullOrWhiteSpace($targetSha)) { - Write-Error "No 'sha' found in $configPath." - } - - if ($targetSha -eq $originSha) { - return @{ Root = $liveRoot; TempDir = $null } - } - - # Make sure the pinned commit is available locally (the pin may be newer than - # what has been fetched, or may live on a different line of history than the - # submodule's current checkout). Only hit the network when the commit is - # genuinely missing: this code runs on EVERY emitter build and doc regen, and a - # `git fetch` on core/ is the only per-build network operation in the whole repo - # build, so doing it unconditionally turns any transient network/credential - # stall into a Java-only build hang. `git cat-file -e ^{commit}` is a cheap, - # offline existence check. - git -C $CoreRoot cat-file -e "${targetSha}^{commit}" 2>$null - if ($LASTEXITCODE -ne 0) { - git -C $CoreRoot fetch --quiet origin $targetSha 2>$null - git -C $CoreRoot cat-file -e "${targetSha}^{commit}" 2>$null - if ($LASTEXITCODE -ne 0) { - Write-Error "core-commit.json pins commit $targetSha but it could not be found or fetched in the 'core' submodule at $CoreRoot." - } - } - - # The pin in core-commit.json is authoritative: always build from exactly that - # commit, regardless of whether it is an ancestor or descendant of the - # submodule's current checkout. We deliberately do NOT gate on - # `merge-base --is-ancestor`: a shallow submodule clone (as on CI) often lacks the - # connecting history and cannot prove ancestry, which previously caused a silent - # fallback to the submodule checkout and built the emitter from the wrong sources. - # Extract only the packages/http-client-java subtree into a temp directory with - # `git archive`. This never modifies the submodule's working tree, so it stays - # safe under a parallel monorepo build that reads core/. - Write-Host "Reading core sources from pinned commit $targetSha (submodule at $originSha) without checking it out" - $tempDir = Join-Path ([System.IO.Path]::GetTempPath()) ("typespec-java-core-" + [System.Guid]::NewGuid().ToString("N")) - New-Item -ItemType Directory -Path $tempDir | Out-Null - $zipPath = Join-Path $tempDir "core.zip" - $extractDir = Join-Path $tempDir "src" - try { - # Produce a zip (not tar) and expand it with .NET's ZipFile.ExtractToDirectory: - # this keeps the whole flow inside PowerShell/.NET and avoids depending on an - # external `tar`, whose behavior varies by platform (notably Git for Windows' - # GNU tar treats a drive-letter path like C:\...\core.tar as a remote host). - # - # ExtractToDirectory is used instead of Expand-Archive on purpose: the archived - # subtree holds several thousand files, and Expand-Archive emits a per-entry - # Write-Progress that makes it pathologically slow on that many files (worse when - # its output is captured by a parallel `pnpm build`), to the point the emitter - # build looks hung. ZipFile.ExtractToDirectory has no such overhead. - git -C $CoreRoot archive --format=zip -o $zipPath "${targetSha}:$script:CoreJavaSubtree" - if ($LASTEXITCODE -ne 0) { - Write-Error "Failed to archive core commit ${targetSha}:$script:CoreJavaSubtree." - } - Add-Type -AssemblyName System.IO.Compression.FileSystem - [System.IO.Compression.ZipFile]::ExtractToDirectory($zipPath, $extractDir) - } - catch { - Remove-Item $tempDir -Recurse -Force -ErrorAction SilentlyContinue - throw - } - Remove-Item $zipPath -Force - return @{ Root = $extractDir; TempDir = $tempDir } -} - -# Clean up the temporary extraction created by Get-CoreSourceRoot (no-op when the -# submodule's current checkout was read directly). -function Remove-CoreSourceRoot { - param( - [Parameter(Mandatory = $true)] - $CoreSource - ) - - if ($CoreSource.TempDir) { - Remove-Item $CoreSource.TempDir -Recurse -Force -ErrorAction SilentlyContinue - } -} diff --git a/packages/typespec-java/Generate.ps1 b/packages/typespec-java/Generate.ps1 deleted file mode 100644 index 9bb8039ead..0000000000 --- a/packages/typespec-java/Generate.ps1 +++ /dev/null @@ -1,273 +0,0 @@ -# Use case: -# -# The purpose of this script is to compact the steps required to regenerate TypeSpec into a single script. -# -# If 'com.azure.autorest.customization' tests fails, re-install 'customization-base'. -# -# Before running this script the 'tsp' profile must be built, 'mvn install -P local,tsp'. -param ( - [int] $Parallelization = [Environment]::ProcessorCount, - # skip the emitter build; use when the package was already built by a prior step - # (e.g. the repo-wide `pnpm build` in CI) to avoid a redundant second build - [switch] $SkipBuild = $false -) - -$ExitCode = 0 - -if ($Parallelization -lt 1) { - $Parallelization = 1 -} - -Write-Host "Parallelization: $Parallelization" - -$generateScript = { - $tspFile = $_ - - $tspClientFile = $tspFile -replace 'main.tsp', 'client.tsp' - if (($tspClientFile -match 'client.tsp$') -and (Test-Path $tspClientFile)) { - $tspFile = $tspClientFile - } - - # With TypeSpec code generation being parallelized, we need to make sure that the output directory is unique - # for each test run. We do this by appending a random number to the output directory. - # Without this, we could have multiple runs trying to write to the same directory which introduces race conditions. - $tspOptions = "--option ""@azure-tools/typespec-java.emitter-output-dir={project-root}/tsp-output/$(Get-Random)""" - if ($tspFile -match "type[\\/]enum[\\/]extensible[\\/]") { - # override namespace for reserved keyword "enum" - $tspOptions += " --option ""@azure-tools/typespec-java.namespace=type.enums.extensible""" - } elseif ($tspFile -match "type[\\/]enum[\\/]fixed[\\/]") { - # override namespace for reserved keyword "enum" - $tspOptions += " --option ""@azure-tools/typespec-java.namespace=type.enums.fixed""" - } elseif ($tspFile -match "azure[\\/]example[\\/]basic[\\/]") { - # override examples-directory - $tspOptions += " --option ""@azure-tools/typespec-java.examples-dir={project-root}/specs/azure/example/basic/examples""" - } elseif ($tspFile -match "azure[\\/]client-generator-core[\\/]client-initialization[\\/]") { - $tspOptions += " --option ""@azure-tools/typespec-java.enable-subclient=true""" - } elseif ($tspFile -match "resiliency[\\/]srv-driven[\\/]old\.tsp") { - # override namespace for "resiliency/srv-driven/old.tsp" (make it different to that from "main.tsp") - $tspOptions += " --option ""@azure-tools/typespec-java.namespace=resiliency.servicedriven.v1""" - # enable advanced versioning for resiliency test - $tspOptions += " --option ""@azure-tools/typespec-java.advanced-versioning=true""" - } elseif ($tspFile -match "resiliency[\\/]srv-driven[\\/]main\.tsp") { - # enable advanced versioning for resiliency test - $tspOptions += " --option ""@azure-tools/typespec-java.advanced-versioning=true""" - } elseif ($tspFile -match "azure[\\/]resource-manager[\\/].*[\\/]main\.tsp") { - # for mgmt, do not generate tests due to random mock values - $tspOptions += " --option ""@azure-tools/typespec-java.generate-tests=false""" - } elseif ($tspFile -match "azure[\\/]resource-manager[\\/]multi-service-shared-models[\\/]") { - $tspOptions += " --option ""@azure-tools/typespec-java.metadata-suffix=shared-models""" - } elseif ($tspFile -match "tsp[\\/]versioning.tsp") { - # test generating from specific api-version - $tspOptions += " --option ""@azure-tools/typespec-java.api-version=2022-09-01""" - } elseif ($tspFile -match "tsp[\\/]error.tsp") { - # test for default-http-exception-type - $tspOptions += " --option ""@azure-tools/typespec-java.use-default-http-status-code-to-exception-type-mapping=false""" - } elseif ($tspFile -match "type[\\/]array" -or $tspFile -match "type[\\/]dictionary") { - # TODO https://github.com/Azure/autorest.java/issues/2964 - # also serve as a test for "use-object-for-unknown" emitter option - $tspOptions += " --option ""@azure-tools/typespec-java.use-object-for-unknown=true""" - } elseif ($tspFile -match "tsp[\\/]arm.tsp") { - # test service-name - $tspOptions += " --option ""@azure-tools/typespec-java.service-name=Arm Resource Provider""" - # also test generating from specific api-version - $tspOptions += " --option ""@azure-tools/typespec-java.api-version=2023-11-01""" - # enable sync-stack - $tspOptions += " --option ""@azure-tools/typespec-java.enable-sync-stack=true""" - # rename model - $tspOptions += " --option ""@azure-tools/typespec-java.rename-model=TopLevelArmResourceListResult:ResourceListResult,CustomTemplateResourcePropertiesAnonymousEmptyModel:AnonymousEmptyModel""" - # remove inner - $tspOptions += " --option ""@azure-tools/typespec-java.remove-inner=NginxConfigurationResponse""" - # generate async methods - $tspOptions += " --option ""@azure-tools/typespec-java.generate-async-methods=true""" - # backward compatible options - $tspOptions += " --option ""@azure-tools/typespec-java.float32-as-double=false""" - $tspOptions += " --option ""@azure-tools/typespec-java.uuid-as-string=false""" - } elseif ($tspFile -match "tsp[\\/]arm-stream-style-serialization.tsp") { - # test service-name - $tspOptions += " --option ""@azure-tools/typespec-java.service-name=Arm Resource Provider""" - # test property-include-always - $tspOptions += " --option ""@azure-tools/typespec-java.property-include-always=FunctionConfiguration.input""" - # enable client side validations - $tspOptions += " --option ""@azure-tools/typespec-java.client-side-validations=true""" - } elseif ($tspFile -match "tsp[\\/]arm-customization.tsp") { - # add customization code - $tspOptions += " --option ""@azure-tools/typespec-java.customization-class=../../customization/src/main/java/KeyVaultCustomization.java""" - } elseif ($tspFile -match "tsp[\\/]arm-versioned.tsp") { - # enable advanced versioning for resiliency test - $tspOptions += " --option ""@azure-tools/typespec-java.advanced-versioning=true""" - $tspOptions += " --option ""@azure-tools/typespec-java.generate-async-methods=true""" - $tspOptions += " --option ""@azure-tools/typespec-java.enable-sync-stack=false""" - } elseif ($tspFile -match "tsp[\\/]subclient.tsp") { - $tspOptions += " --option ""@azure-tools/typespec-java.enable-subclient=true""" - # test for include-api-view-properties - $tspOptions += " --option ""@azure-tools/typespec-java.include-api-view-properties=false""" - } - - # Test customization for one of the TypeSpec definitions - naming.tsp - if ($tspFile -match "tsp[\\/]naming.tsp$") { - # Test for rename-model - $tspOptions += " --option ""@azure-tools/typespec-java.rename-model=RunObjectLastError1:RunObjectLastErrorRenamed,RunObjectLastErrorCode:RunObjectLastErrorCodeRenamed""" - # Add the customization-class option for Java emitter - $tspOptions += " --option ""@azure-tools/typespec-java.customization-class=../../customization/src/main/java/CustomizationTest.java""" - } - - $tspTrace = "--trace import-resolution --trace projection --trace typespec-java" - # The emitter is resolved by package name from emitter-tests/tspconfig.yaml via Node - # self-reference (this folder has no package.json, so the nearest one is the parent - # typespec-java package). No --emit needed. - $tspCommand = "npx --no-install tsp compile $tspFile $tspOptions $tspTrace" - - # output of "tsp compile" seems trigger powershell error or exit, hence the "2>&1" - $timer = [Diagnostics.Stopwatch]::StartNew() - $generateOutput = Invoke-Expression $tspCommand 2>&1 - $timer.Stop() - - $global:ExitCode = $global:ExitCode -bor $LASTEXITCODE - - if ($LASTEXITCODE -ne 0) { - Write-Host " - ======================== - $tspCommand - ======================== - FAILED (Time elapsed: $($timer.ToString())) - $([String]::Join("`n", $generateOutput)) - " - } else { - Write-Host " - ======================== - $tspCommand - ======================== - SUCCEEDED (Time elapsed: $($timer.ToString())) - " - } - - if ($global:ExitCode -ne 0) { - throw "Failed to generate from tsp $tspFile" - } -} - -# Generate from a shared http-specs/azure-http-specs spec whose selection and -# emitter options come from the opt-in spector.config.yaml (resolved by -# resolve-spector-specs.js). Unlike $generateScript, options are pre-resolved -# (passed in $_.options); only the per-run random output-dir is added here. -$generateSpecScript = { - $spec = $_ - $tspFile = $spec.tspFile - - $tspOptions = "--option ""@azure-tools/typespec-java.emitter-output-dir={project-root}/tsp-output/$(Get-Random)""" - foreach ($opt in $spec.options) { - $tspOptions += " --option ""$opt""" - } - - $tspTrace = "--trace import-resolution --trace projection --trace typespec-java" - $tspCommand = "npx --no-install tsp compile $tspFile $tspOptions $tspTrace" - - # output of "tsp compile" seems trigger powershell error or exit, hence the "2>&1" - $timer = [Diagnostics.Stopwatch]::StartNew() - $generateOutput = Invoke-Expression $tspCommand 2>&1 - $timer.Stop() - - $global:ExitCode = $global:ExitCode -bor $LASTEXITCODE - - if ($LASTEXITCODE -ne 0) { - Write-Host " - ======================== - $tspCommand - ======================== - FAILED (Time elapsed: $($timer.ToString())) - $([String]::Join("`n", $generateOutput)) - " - } else { - Write-Host " - ======================== - $tspCommand - ======================== - SUCCEEDED (Time elapsed: $($timer.ToString())) - " - } - - if ($global:ExitCode -ne 0) { - throw "Failed to generate from tsp $tspFile" - } -} - -# This script lives at the typespec-java package root; the Maven/tsp e2e assets it -# operates on are under ./emitter-tests, so it runs with the working directory pushed -# into that folder (all ./tsp, ./src, ./specs paths are relative to it). -Push-Location (Join-Path $PSScriptRoot "emitter-tests") -try { - if (-not $SkipBuild) { - # Build the emitter (emitter.jar via Build-Generator.ps1 + tsc) from the - # workspace package. The e2e tests consume it directly from the built dist + - # generator, resolved by package name via tspconfig.yaml -- no .tgz. - Push-Location .. - try { - pnpm build - if ($LASTEXITCODE -ne 0) { - throw "Failed to build @azure-tools/typespec-java" - } - } finally { - Pop-Location - } - } - - New-Item -Path ./existingcode/src/main/java/tsptest/ -ItemType Directory -Force | Out-Null - - if (Test-Path ./src/main/java/tsptest/partialupdate) { - Copy-Item -Path ./src/main/java/tsptest/partialupdate -Destination ./existingcode/src/main/java/tsptest/partialupdate -Recurse -Force - } - - if (Test-Path ./src/main) { - Remove-Item ./src/main -Recurse -Force - } - if (Test-Path ./src/samples) { - Remove-Item ./src/samples -Recurse -Force - } - if (Test-Path ./src/test) { - Get-ChildItem -Path ./src/test -Recurse -Directory | Where-Object {$_.Name -match "^generated$"} | Remove-Item -Recurse -Force - } - if (Test-Path ./tsp-output) { - Remove-Item ./tsp-output -Recurse -Force - } - - # generate for other local test sources except partial update - $job = Get-Item ./tsp/* -Filter "*.tsp" -Exclude "*partialupdate*" | ForEach-Object -Parallel $generateScript -ThrottleLimit $Parallelization -AsJob - - $job | Wait-Job -Timeout 600 - $job | Receive-Job - - # partial update test - npx --no-install tsp compile ./tsp/partialupdate.tsp --option="@azure-tools/typespec-java.emitter-output-dir={project-root}/existingcode" - Copy-Item -Path ./existingcode/src/main/java/tsptest/partialupdate -Destination ./src/main/java/tsptest/partialupdate -Recurse -Force - Remove-Item ./existingcode -Recurse -Force - - # generate for http-specs/azure-http-specs test sources (installed under the - # typespec-java package's node_modules by the workspace `pnpm install`). - Copy-Item -Path ../node_modules/@typespec/http-specs/specs -Destination ./ -Recurse -Force - Copy-Item -Path ../node_modules/@azure-tools/azure-http-specs/specs -Destination ./ -Recurse -Force - - # Spec selection + per-spec emitter options live in the opt-in spector.config.yaml - # (see Azure/typespec-azure#4997), resolved by the shared @azure-tools/spector-runner - # package. resolve-spector-specs.js maps each opted-in spec to its tsp entry file - # (preferring client.tsp over main.tsp) and emitter options; adding/removing a spec - # is a one-line config change and disabling one is `false` with a comment. - $specsRoot = Join-Path $PSScriptRoot "emitter-tests/specs" - $specPlan = node ./resolve-spector-specs.js $specsRoot | ConvertFrom-Json - - $job = $specPlan | ForEach-Object -Parallel $generateSpecScript -ThrottleLimit $Parallelization -AsJob - - $job | Wait-Job -Timeout 1200 - $job | Receive-Job - - Remove-Item ./specs -Recurse -Force - - Copy-Item -Path ./tsp-output/*/src -Destination ./ -Recurse -Force -Exclude @("ReadmeSamples.java", "module-info.java") - - Remove-Item ./tsp-output -Recurse -Force - - if ($ExitCode -ne 0) { - throw "Failed to generate from tsp" - } -} finally { - Pop-Location -} diff --git a/packages/typespec-java/Spector-Tests.ps1 b/packages/typespec-java/Spector-Tests.ps1 deleted file mode 100644 index c45beccd62..0000000000 --- a/packages/typespec-java/Spector-Tests.ps1 +++ /dev/null @@ -1,47 +0,0 @@ -#Requires -Version 7.0 - -# Run the Spector tests for @azure-tools/typespec-java. -# -# Prerequisite: the SDK has been regenerated via Generate.ps1 (which builds the -# emitter and regenerates emitter-tests/src/main/java from the spector specs). This -# starts the Spector mock server, compiles and runs the JUnit tests against the -# generated SDK, then stops the server. - -$ErrorActionPreference = 'Stop' -Set-StrictMode -Version 3.0 - -# This script lives at the typespec-java package root, so the pnpm spector-* scripts -# (and node_modules) resolve from here directly. The Maven project (pom.xml, generated -# sources, coverage file) is under ./emitter-tests, entered only for `mvn` below. -Set-Location $PSScriptRoot - -Write-Host "Starting the Spector server" -pnpm run spector-start - -try { - Write-Host "Compile and run the tests" - Push-Location (Join-Path $PSScriptRoot "emitter-tests") - try { - mvn clean test --no-transfer-progress -T 1C - if ($LASTEXITCODE -ne 0) { - throw "Spector tests failed" - } - } - finally { - Pop-Location - } -} -finally { - Write-Host "Stopping the Spector server" - pnpm run spector-stop - - # Stage the coverage report where the ADO upload-spector-coverage.yml template - # expects it: /node_modules/@azure-tools/azure-http-specs/spec-coverage.json. - $coverageSource = "./emitter-tests/tsp-spector-coverage-java.json" - $coverageDest = "./node_modules/@azure-tools/azure-http-specs/spec-coverage.json" - if (Test-Path $coverageSource) { - Copy-Item $coverageSource $coverageDest -Force - } -} - -Write-Host "Finished running the Spector tests" diff --git a/packages/typespec-java/SyncTests.ps1 b/packages/typespec-java/SyncTests.ps1 deleted file mode 100644 index 6cdb485a4b..0000000000 --- a/packages/typespec-java/SyncTests.ps1 +++ /dev/null @@ -1,61 +0,0 @@ -# Sync the hand-written tests (src) and TypeSpec specs (tsp) from the upstream -# http-client-generator-test in the core/ submodule, the source of truth for the -# Java test suite. -# -# NOTE: customization/ is intentionally NOT synced -- those classes are the -# Azure-specific port (com.azure.autorest.customization) maintained in this repo, -# whereas core's customization uses the unbranded base. -# -# After syncing, core.test.patch is applied to the copied test sources to carry the -# Azure-specific edits that must diverge from core (e.g. FileUtils.java resolves the -# http-specs assets under ../node_modules, since here node_modules lives at the -# typespec-java package root rather than in the test project dir). Unlike core.patch -# (applied to a gitignored generator copy), these patched files ARE committed. -# -# Generated output (src/main, **/generated/**) is gitignored; only the -# hand-written tests and specs synced here are committed. -# -# The desired core commit is pinned in core-commit.json; CoreCommit.ps1's -# Get-CoreSourceRoot returns a directory to read the test sources from (extracted -# from the pinned commit via `git archive` when it is newer than the submodule's -# current checkout), without mutating the core/ submodule working tree. -# Remove-CoreSourceRoot cleans up any temporary extraction afterwards. - -$ErrorActionPreference = "Stop" - -# This script lives at the typespec-java package root. The synced tests/specs live -# under ./emitter-tests; CoreCommit.ps1 and core-commit.json are at the package root. -$packageRoot = $PSScriptRoot -$emitterTestsRoot = Join-Path $packageRoot "emitter-tests" -$coreRoot = Resolve-Path (Join-Path $packageRoot ".." ".." "core") - -. (Join-Path $packageRoot "CoreCommit.ps1") -$core = Get-CoreSourceRoot -CoreRoot $coreRoot -PackageRoot $packageRoot -try { - $testRoot = Join-Path $core.Root "generator" "http-client-generator-test" - - $localSrc = Join-Path $emitterTestsRoot "src" - $localTsp = Join-Path $emitterTestsRoot "tsp" - Remove-Item $localSrc -Recurse -Force -ErrorAction SilentlyContinue - Remove-Item $localTsp -Recurse -Force -ErrorAction SilentlyContinue - Copy-Item -Path (Join-Path $testRoot "src") -Destination $localSrc -Recurse -Force - Copy-Item -Path (Join-Path $testRoot "tsp") -Destination $localTsp -Recurse -Force - - Write-Host "Synced src and tsp from $testRoot" - - # Apply the Azure-specific test patch to the freshly synced sources. The patch - # paths are relative to emitter-tests/, so run git apply from the repo root with - # --directory (mirrors Copy-Sources.ps1's core.patch handling). Unlike core.patch, - # the result is committed, so re-running this sync keeps the divergence in place. - $repoRoot = Resolve-Path (Join-Path $packageRoot ".." "..") - $patchFile = Join-Path $packageRoot "core.test.patch" - $relEmitterTests = [System.IO.Path]::GetRelativePath($repoRoot, $emitterTestsRoot).Replace('\', '/') - git -C $repoRoot apply --directory=$relEmitterTests -p1 $patchFile --ignore-whitespace - if ($LASTEXITCODE -ne 0) { - Write-Error "Failed to apply core.test.patch to $emitterTestsRoot." - } - Write-Host "Applied core.test.patch" -} -finally { - Remove-CoreSourceRoot $core -} diff --git a/packages/typespec-java/emitter-tests/readme.md b/packages/typespec-java/emitter-tests/readme.md index 43362feb6b..c78a85d39f 100644 --- a/packages/typespec-java/emitter-tests/readme.md +++ b/packages/typespec-java/emitter-tests/readme.md @@ -28,11 +28,11 @@ http-specs, etc.). Run from the parent (`packages/typespec-java`) folder: -```pwsh +```bash pnpm run regenerate ``` -`regenerate` runs [`Generate.ps1`](../Generate.ps1), which builds the emitter +`regenerate` runs [`eng/scripts/generate.ts`](../eng/scripts/generate.ts), which builds the emitter (`pnpm build`), then regenerates the SDK from the local `tsp/` sources and the [http-specs]/[azure-http-specs] specs into the (gitignored) `src/main/java` folder. This takes a while. @@ -41,11 +41,11 @@ This takes a while. From `packages/typespec-java`: -```pwsh +```bash pnpm run test:java:e2e ``` -This runs [`Spector-Tests.ps1`](../Spector-Tests.ps1): it starts the `tsp-spector` +This runs [`eng/scripts/spector-tests.ts`](../eng/scripts/spector-tests.ts): it starts the `tsp-spector` mock server, runs the JUnit tests (`mvn clean test`) against the generated SDK, then stops the server. It also writes `tsp-spector-coverage-java.json`. @@ -64,7 +64,7 @@ are synced from the unbranded test project in the `core/` submodule (`core/packages/http-client-java/http-client-generator-test`). Refresh them from `packages/typespec-java` with: -```pwsh +```bash pnpm run sync-tests ``` @@ -90,4 +90,3 @@ new versions. [http-specs]: https://github.com/microsoft/typespec/tree/main/packages/http-specs [azure-http-specs]: https://github.com/Azure/typespec-azure/tree/main/packages/azure-http-specs - diff --git a/packages/typespec-java/emitter-tests/resolve-spector-specs.js b/packages/typespec-java/emitter-tests/resolve-spector-specs.js index 53205b8a20..7f3c486559 100644 --- a/packages/typespec-java/emitter-tests/resolve-spector-specs.js +++ b/packages/typespec-java/emitter-tests/resolve-spector-specs.js @@ -1,8 +1,8 @@ /* eslint-disable no-console */ // Resolve the opt-in spector.config.yaml into a JSON list of specs to generate, -// consumed by Generate.ps1. Each entry is { tspFile, options } where options are +// consumed by eng/scripts/generate.ts. Each entry is { tspFile, options } where options are // fully-qualified @azure-tools/typespec-java emitter options (without the random -// output-dir, which Generate.ps1 appends per run). +// output-dir, which generate.ts appends per run). // // Usage: node resolve-spector-specs.js import { existsSync } from "fs"; diff --git a/packages/typespec-java/emitter-tests/spector.config.yaml b/packages/typespec-java/emitter-tests/spector.config.yaml index 950fa75e98..d2ba4d4003 100644 --- a/packages/typespec-java/emitter-tests/spector.config.yaml +++ b/packages/typespec-java/emitter-tests/spector.config.yaml @@ -3,7 +3,7 @@ # Spec selection for @azure-tools/typespec-java (opt-in), for the shared # http-specs / azure-http-specs specs. See Azure/typespec-azure#4997. # Options are @azure-tools/typespec-java emitter options (without the prefix). -# Local ./tsp/*.tsp fixtures are handled separately in Generate.ps1. +# Local ./tsp/*.tsp fixtures are handled separately in eng/scripts/generate.ts. specs: authentication/api-key: true diff --git a/packages/typespec-java/eng/scripts/build-generator.ts b/packages/typespec-java/eng/scripts/build-generator.ts new file mode 100644 index 0000000000..a4172c8000 --- /dev/null +++ b/packages/typespec-java/eng/scripts/build-generator.ts @@ -0,0 +1,36 @@ +/* eslint-disable no-console */ +import { execa } from "execa"; +import { access } from "node:fs/promises"; +import { dirname, join, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; + +const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), "../.."); +const generatorPom = join(packageRoot, "generator", "pom.xml"); + +try { + await access(generatorPom); +} catch { + throw new Error( + `Copied generator not found at: ${generatorPom}\nRun ${join(packageRoot, "eng", "scripts", "copy-sources.ts")} first (build:emitter).`, + ); +} + +console.log("Build JAR"); +await execa( + "mvn", + [ + "clean", + "install", + "-DskipTests", + "--define", + "spotless.apply.skip=true", + "--define", + "spotless.check.skip=true", + "--no-transfer-progress", + "-T", + "1C", + "-f", + generatorPom, + ], + { stdio: "inherit" }, +); diff --git a/packages/typespec-java/eng/scripts/copy-sources.ts b/packages/typespec-java/eng/scripts/copy-sources.ts new file mode 100644 index 0000000000..bb4d3fdba3 --- /dev/null +++ b/packages/typespec-java/eng/scripts/copy-sources.ts @@ -0,0 +1,65 @@ +/* eslint-disable no-console */ +import { execa } from "execa"; +import { cp, mkdir, readdir, rm } from "node:fs/promises"; +import { basename, dirname, join, relative, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; +import { getCoreSourceRoot, removeCoreSourceRoot } from "./core-source.ts"; + +const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), "../.."); +const repoRoot = resolve(packageRoot, "../.."); +const coreRoot = join(repoRoot, "core"); + +async function main() { + const core = await getCoreSourceRoot(coreRoot, packageRoot); + try { + const emitterRoot = join(core.root, "emitter"); + await cp(join(emitterRoot, "src"), join(packageRoot, "src"), { + recursive: true, + force: true, + filter: (source) => basename(source) !== "options.ts", + }); + await cp(join(emitterRoot, "test"), join(packageRoot, "test"), { + recursive: true, + force: true, + }); + + const sourceGenerator = join(core.root, "generator"); + const destinationGenerator = join(packageRoot, "generator"); + const excludedModules = new Set([ + "http-client-generator-test", + "http-client-generator-clientcore-test", + ]); + + console.log("Copy generator sources from core"); + await rm(destinationGenerator, { recursive: true, force: true }); + await mkdir(destinationGenerator); + for (const entry of await readdir(sourceGenerator, { withFileTypes: true })) { + if (!excludedModules.has(entry.name)) { + await cp(join(sourceGenerator, entry.name), join(destinationGenerator, entry.name), { + recursive: true, + force: true, + }); + } + } + + console.log("Apply Azure customization patch to copied generator"); + const relativeGenerator = relative(repoRoot, destinationGenerator).replaceAll("\\", "/"); + await execa( + "git", + [ + "-C", + repoRoot, + "apply", + `--directory=${relativeGenerator}`, + "-p1", + join(packageRoot, "core.patch"), + "--ignore-whitespace", + ], + { stdio: "inherit" }, + ); + } finally { + await removeCoreSourceRoot(core); + } +} + +await main(); diff --git a/packages/typespec-java/eng/scripts/core-source.test.ts b/packages/typespec-java/eng/scripts/core-source.test.ts new file mode 100644 index 0000000000..cb34f353b1 --- /dev/null +++ b/packages/typespec-java/eng/scripts/core-source.test.ts @@ -0,0 +1,58 @@ +import { execa } from "execa"; +import { access, mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { afterEach, describe, expect, it } from "vitest"; +import { getCoreSourceRoot, removeCoreSourceRoot } from "./core-source.ts"; + +describe("core source", () => { + const tempDirectories: string[] = []; + + afterEach(async () => { + await Promise.all( + tempDirectories.splice(0).map((path) => rm(path, { recursive: true, force: true })), + ); + }); + + it("uses the current checkout when the pin is absent", async () => { + const root = await createCoreRepository(); + const packageRoot = join(root, "package"); + await mkdir(packageRoot); + + const source = await getCoreSourceRoot(root, packageRoot); + + expect(source).toEqual({ root: join(root, "packages", "http-client-java") }); + }); + + it("extracts and cleans up a different pinned commit", async () => { + const root = await createCoreRepository(); + const packageRoot = join(root, "package"); + await mkdir(packageRoot); + const originalSha = (await execa("git", ["-C", root, "rev-parse", "HEAD"])).stdout; + const fixture = join(root, "packages", "http-client-java", "emitter", "fixture.txt"); + await writeFile(fixture, "updated"); + await execa("git", ["-C", root, "commit", "-am", "update fixture"]); + await writeFile(join(packageRoot, "core-commit.json"), JSON.stringify({ sha: originalSha })); + + const source = await getCoreSourceRoot(root, packageRoot); + + expect(await readFile(join(source.root, "emitter", "fixture.txt"), "utf8")).toBe("original"); + expect(source.tempDir).toBeDefined(); + await removeCoreSourceRoot(source); + await expect(access(source.tempDir!)).rejects.toThrow(); + }); + + async function createCoreRepository(): Promise { + const root = await mkdtemp(join(tmpdir(), "typespec-java-core-test-")); + tempDirectories.push(root); + const emitter = join(root, "packages", "http-client-java", "emitter"); + await mkdir(emitter, { recursive: true }); + await writeFile(join(emitter, "fixture.txt"), "original"); + await execa("git", ["init", root]); + await execa("git", ["-C", root, "config", "user.name", "TypeSpec Test"]); + await execa("git", ["-C", root, "config", "user.email", "typespec@example.com"]); + await execa("git", ["-C", root, "add", "."]); + await execa("git", ["-C", root, "commit", "-m", "initial fixture"]); + return root; + } +}); diff --git a/packages/typespec-java/eng/scripts/core-source.ts b/packages/typespec-java/eng/scripts/core-source.ts new file mode 100644 index 0000000000..2514ae31ad --- /dev/null +++ b/packages/typespec-java/eng/scripts/core-source.ts @@ -0,0 +1,98 @@ +/* eslint-disable no-console */ +import { execa } from "execa"; +import { mkdir, mkdtemp, readFile, rm } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { extract } from "tar"; + +const coreJavaSubtree = "packages/http-client-java"; +const gitEnvironment = { GIT_TERMINAL_PROMPT: "0" }; + +export interface CoreSource { + root: string; + tempDir?: string; +} + +function isErrnoException(error: unknown): error is NodeJS.ErrnoException { + return error instanceof Error && "code" in error; +} + +async function commitExists(coreRoot: string, sha: string): Promise { + const result = await execa("git", ["-C", coreRoot, "cat-file", "-e", `${sha}^{commit}`], { + env: gitEnvironment, + reject: false, + stderr: "ignore", + }); + return result.exitCode === 0; +} + +export async function getCoreSourceRoot( + coreRoot: string, + packageRoot: string, +): Promise { + const originSha = ( + await execa("git", ["-C", coreRoot, "rev-parse", "HEAD"], { env: gitEnvironment }) + ).stdout.trim(); + const liveRoot = join(coreRoot, coreJavaSubtree); + const configPath = join(packageRoot, "core-commit.json"); + + let targetSha: string; + try { + targetSha = JSON.parse(await readFile(configPath, "utf8")).sha?.trim(); + } catch (error) { + if (isErrnoException(error) && error.code === "ENOENT") { + console.log(`core-commit.json not found; using current core checkout ${originSha}.`); + return { root: liveRoot }; + } + throw error; + } + + if (!targetSha) { + throw new Error(`No 'sha' found in ${configPath}.`); + } + if (targetSha === originSha) { + return { root: liveRoot }; + } + + if (!(await commitExists(coreRoot, targetSha))) { + await execa("git", ["-C", coreRoot, "fetch", "--quiet", "origin", targetSha], { + env: gitEnvironment, + }); + if (!(await commitExists(coreRoot, targetSha))) { + throw new Error( + `core-commit.json pins commit ${targetSha} but it could not be found or fetched in the 'core' submodule at ${coreRoot}.`, + ); + } + } + + console.log( + `Reading core sources from pinned commit ${targetSha} (submodule at ${originSha}) without checking it out`, + ); + const tempDir = await mkdtemp(join(tmpdir(), "typespec-java-core-")); + const archivePath = join(tempDir, "core.tar"); + const extractDir = join(tempDir, "src"); + try { + await mkdir(extractDir); + await execa("git", [ + "-C", + coreRoot, + "archive", + "--format=tar", + "-o", + archivePath, + `${targetSha}:${coreJavaSubtree}`, + ]); + await extract({ cwd: extractDir, file: archivePath }); + await rm(archivePath); + return { root: extractDir, tempDir }; + } catch (error) { + await rm(tempDir, { recursive: true, force: true }); + throw error; + } +} + +export async function removeCoreSourceRoot(coreSource: CoreSource): Promise { + if (coreSource.tempDir) { + await rm(coreSource.tempDir, { recursive: true, force: true }); + } +} diff --git a/packages/typespec-java/eng/scripts/generate.ts b/packages/typespec-java/eng/scripts/generate.ts new file mode 100644 index 0000000000..845285264e --- /dev/null +++ b/packages/typespec-java/eng/scripts/generate.ts @@ -0,0 +1,282 @@ +/* eslint-disable no-console */ +import { execa } from "execa"; +import { randomInt } from "node:crypto"; +import { cp, mkdir, readdir, rm, stat } from "node:fs/promises"; +import { availableParallelism } from "node:os"; +import { basename, dirname, join, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; +import { parseArgs } from "node:util"; + +const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), "../.."); +const emitterTestsRoot = join(packageRoot, "emitter-tests"); +const emitterName = "@azure-tools/typespec-java"; +const traceArguments = [ + "--trace", + "import-resolution", + "--trace", + "projection", + "--trace", + "typespec-java", +]; + +function isErrnoException(error: unknown): error is NodeJS.ErrnoException { + return error instanceof Error && "code" in error; +} + +function getGenerationOptions(typeSpecFile: string): string[] { + const normalized = typeSpecFile.replaceAll("\\", "/"); + const options = [ + `${emitterName}.emitter-output-dir={project-root}/tsp-output/${randomInt(2 ** 31)}`, + ]; + + if (/type\/enum\/extensible\//.test(normalized)) { + options.push(`${emitterName}.namespace=type.enums.extensible`); + } else if (/type\/enum\/fixed\//.test(normalized)) { + options.push(`${emitterName}.namespace=type.enums.fixed`); + } else if (/azure\/example\/basic\//.test(normalized)) { + options.push(`${emitterName}.examples-dir={project-root}/specs/azure/example/basic/examples`); + } else if (/azure\/client-generator-core\/client-initialization\//.test(normalized)) { + options.push(`${emitterName}.enable-subclient=true`); + } else if (/resiliency\/srv-driven\/old\.tsp$/.test(normalized)) { + options.push( + `${emitterName}.namespace=resiliency.servicedriven.v1`, + `${emitterName}.advanced-versioning=true`, + ); + } else if (/resiliency\/srv-driven\/main\.tsp$/.test(normalized)) { + options.push(`${emitterName}.advanced-versioning=true`); + } else if (/azure\/resource-manager\/.*\/main\.tsp$/.test(normalized)) { + options.push(`${emitterName}.generate-tests=false`); + } else if (/azure\/resource-manager\/multi-service-shared-models\//.test(normalized)) { + options.push(`${emitterName}.metadata-suffix=shared-models`); + } else if (/tsp\/versioning\.tsp$/.test(normalized)) { + options.push(`${emitterName}.api-version=2022-09-01`); + } else if (/tsp\/error\.tsp$/.test(normalized)) { + options.push(`${emitterName}.use-default-http-status-code-to-exception-type-mapping=false`); + } else if (/type\/(array|dictionary)/.test(normalized)) { + options.push(`${emitterName}.use-object-for-unknown=true`); + } else if (/tsp\/arm\.tsp$/.test(normalized)) { + options.push( + `${emitterName}.service-name=Arm Resource Provider`, + `${emitterName}.api-version=2023-11-01`, + `${emitterName}.enable-sync-stack=true`, + `${emitterName}.rename-model=TopLevelArmResourceListResult:ResourceListResult,CustomTemplateResourcePropertiesAnonymousEmptyModel:AnonymousEmptyModel`, + `${emitterName}.remove-inner=NginxConfigurationResponse`, + `${emitterName}.generate-async-methods=true`, + `${emitterName}.float32-as-double=false`, + `${emitterName}.uuid-as-string=false`, + ); + } else if (/tsp\/arm-stream-style-serialization\.tsp$/.test(normalized)) { + options.push( + `${emitterName}.service-name=Arm Resource Provider`, + `${emitterName}.property-include-always=FunctionConfiguration.input`, + `${emitterName}.client-side-validations=true`, + ); + } else if (/tsp\/arm-customization\.tsp$/.test(normalized)) { + options.push( + `${emitterName}.customization-class=../../customization/src/main/java/KeyVaultCustomization.java`, + ); + } else if (/tsp\/arm-versioned\.tsp$/.test(normalized)) { + options.push( + `${emitterName}.advanced-versioning=true`, + `${emitterName}.generate-async-methods=true`, + `${emitterName}.enable-sync-stack=false`, + ); + } else if (/tsp\/subclient\.tsp$/.test(normalized)) { + options.push( + `${emitterName}.enable-subclient=true`, + `${emitterName}.include-api-view-properties=false`, + ); + } + + if (/tsp\/naming\.tsp$/.test(normalized)) { + options.push( + `${emitterName}.rename-model=RunObjectLastError1:RunObjectLastErrorRenamed,RunObjectLastErrorCode:RunObjectLastErrorCodeRenamed`, + `${emitterName}.customization-class=../../customization/src/main/java/CustomizationTest.java`, + ); + } + + return options; +} + +async function compile(typeSpecFile: string, options: string[], timeout: number): Promise { + const arguments_ = [ + "exec", + "tsp", + "compile", + typeSpecFile, + ...options.flatMap((option) => ["--option", option]), + ...traceArguments, + ]; + const command = `pnpm ${arguments_.join(" ")}`; + const start = performance.now(); + const result = await execa("pnpm", arguments_, { + cwd: emitterTestsRoot, + reject: false, + timeout, + }); + const elapsed = ((performance.now() - start) / 1000).toFixed(1); + const output = [result.stdout, result.stderr].filter(Boolean).join("\n"); + + console.log(` +======================== +${command} +======================== +${result.exitCode === 0 ? "SUCCEEDED" : "FAILED"} (Time elapsed: ${elapsed}s) +${result.exitCode === 0 ? "" : output} +`); + if (result.exitCode !== 0) { + throw new Error(`Failed to generate from tsp ${typeSpecFile}`); + } +} + +async function runWithConcurrency( + values: T[], + concurrency: number, + action: (value: T) => Promise, +): Promise { + let next = 0; + async function worker() { + while (next < values.length) { + const value = values[next++]; + await action(value); + } + } + await Promise.all(Array.from({ length: Math.min(concurrency, values.length) }, worker)); +} + +async function pathExists(path: string): Promise { + try { + await stat(path); + return true; + } catch (error) { + if (isErrnoException(error) && error.code === "ENOENT") { + return false; + } + throw error; + } +} + +async function removeGeneratedDirectories(root: string): Promise { + if (!(await pathExists(root))) { + return; + } + for (const entry of await readdir(root, { withFileTypes: true })) { + if (entry.isDirectory()) { + const path = join(root, entry.name); + if (entry.name === "generated") { + await rm(path, { recursive: true, force: true }); + } else { + await removeGeneratedDirectories(path); + } + } + } +} + +async function main() { + const { values } = parseArgs({ + args: process.argv.slice(2).filter((argument) => argument !== "--"), + options: { + parallelization: { type: "string", short: "p", default: String(availableParallelism()) }, + "skip-build": { type: "boolean", default: false }, + }, + }); + const parallelization = Math.max(1, Number.parseInt(values.parallelization, 10) || 1); + console.log(`Parallelization: ${parallelization}`); + + if (!values["skip-build"]) { + await execa("pnpm", ["build"], { cwd: packageRoot, stdio: "inherit" }); + } + + const existingPartialUpdate = join( + emitterTestsRoot, + "existingcode", + "src", + "main", + "java", + "tsptest", + "partialupdate", + ); + await mkdir(dirname(existingPartialUpdate), { recursive: true }); + const generatedPartialUpdate = join( + emitterTestsRoot, + "src", + "main", + "java", + "tsptest", + "partialupdate", + ); + if (await pathExists(generatedPartialUpdate)) { + await cp(generatedPartialUpdate, existingPartialUpdate, { recursive: true, force: true }); + } + + await Promise.all([ + rm(join(emitterTestsRoot, "src", "main"), { recursive: true, force: true }), + rm(join(emitterTestsRoot, "src", "samples"), { recursive: true, force: true }), + rm(join(emitterTestsRoot, "tsp-output"), { recursive: true, force: true }), + removeGeneratedDirectories(join(emitterTestsRoot, "src", "test")), + ]); + + const typeSpecRoot = join(emitterTestsRoot, "tsp"); + const localFiles = (await readdir(typeSpecRoot)) + .filter((name) => name.endsWith(".tsp") && !name.includes("partialupdate")) + .map((name) => join(typeSpecRoot, name)); + await runWithConcurrency(localFiles, parallelization, async (originalFile) => { + const clientFile = originalFile.replace(/main\.tsp$/, "client.tsp"); + const typeSpecFile = + clientFile !== originalFile && (await pathExists(clientFile)) ? clientFile : originalFile; + await compile(typeSpecFile, getGenerationOptions(typeSpecFile), 600_000); + }); + + await compile( + join(typeSpecRoot, "partialupdate.tsp"), + [`${emitterName}.emitter-output-dir={project-root}/existingcode`], + 600_000, + ); + await mkdir(dirname(generatedPartialUpdate), { recursive: true }); + await cp(existingPartialUpdate, generatedPartialUpdate, { recursive: true, force: true }); + await rm(join(emitterTestsRoot, "existingcode"), { recursive: true, force: true }); + + const specsRoot = join(emitterTestsRoot, "specs"); + await cp(join(packageRoot, "node_modules", "@typespec", "http-specs", "specs"), specsRoot, { + recursive: true, + force: true, + }); + await cp( + join(packageRoot, "node_modules", "@azure-tools", "azure-http-specs", "specs"), + specsRoot, + { recursive: true, force: true }, + ); + + const planResult = await execa( + "node", + [join(emitterTestsRoot, "resolve-spector-specs.js"), specsRoot], + { cwd: emitterTestsRoot }, + ); + const specPlan: { tspFile: string; options: string[] }[] = JSON.parse(planResult.stdout); + await runWithConcurrency(specPlan, parallelization, (spec) => + compile( + spec.tspFile, + [ + `${emitterName}.emitter-output-dir={project-root}/tsp-output/${randomInt(2 ** 31)}`, + ...spec.options, + ], + 1_200_000, + ), + ); + await rm(specsRoot, { recursive: true, force: true }); + + const outputRoot = join(emitterTestsRoot, "tsp-output"); + for (const output of await readdir(outputRoot)) { + const source = join(outputRoot, output, "src"); + if (await pathExists(source)) { + await cp(source, join(emitterTestsRoot, "src"), { + recursive: true, + force: true, + filter: (path) => !["ReadmeSamples.java", "module-info.java"].includes(basename(path)), + }); + } + } + await rm(outputRoot, { recursive: true, force: true }); +} + +await main(); diff --git a/packages/typespec-java/eng/scripts/spector-tests.ts b/packages/typespec-java/eng/scripts/spector-tests.ts new file mode 100644 index 0000000000..e56a0fd8d7 --- /dev/null +++ b/packages/typespec-java/eng/scripts/spector-tests.ts @@ -0,0 +1,46 @@ +/* eslint-disable no-console */ +import { execa } from "execa"; +import { copyFile } from "node:fs/promises"; +import { dirname, join, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; + +const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), "../.."); +const emitterTestsRoot = join(packageRoot, "emitter-tests"); + +function isErrnoException(error: unknown): error is NodeJS.ErrnoException { + return error instanceof Error && "code" in error; +} + +console.log("Starting the Spector server"); +await execa("pnpm", ["run", "spector-start"], { cwd: packageRoot, stdio: "inherit" }); + +let testError: unknown; +try { + console.log("Compile and run the tests"); + await execa("mvn", ["clean", "test", "--no-transfer-progress", "-T", "1C"], { + cwd: emitterTestsRoot, + stdio: "inherit", + }); +} catch (error) { + testError = error; +} + +console.log("Stopping the Spector server"); +await execa("pnpm", ["run", "spector-stop"], { cwd: packageRoot, stdio: "inherit" }); + +try { + await copyFile( + join(emitterTestsRoot, "tsp-spector-coverage-java.json"), + join(packageRoot, "node_modules", "@azure-tools", "azure-http-specs", "spec-coverage.json"), + ); +} catch (error) { + if (!isErrnoException(error) || error.code !== "ENOENT") { + throw error; + } +} + +if (testError) { + throw testError; +} + +console.log("Finished running the Spector tests"); diff --git a/packages/typespec-java/eng/scripts/sync-tests.ts b/packages/typespec-java/eng/scripts/sync-tests.ts new file mode 100644 index 0000000000..01c0ef60d1 --- /dev/null +++ b/packages/typespec-java/eng/scripts/sync-tests.ts @@ -0,0 +1,44 @@ +/* eslint-disable no-console */ +import { execa } from "execa"; +import { cp, rm } from "node:fs/promises"; +import { dirname, join, relative, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; +import { getCoreSourceRoot, removeCoreSourceRoot } from "./core-source.ts"; + +const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), "../.."); +const repoRoot = resolve(packageRoot, "../.."); +const coreRoot = join(repoRoot, "core"); +const emitterTestsRoot = join(packageRoot, "emitter-tests"); + +const core = await getCoreSourceRoot(coreRoot, packageRoot); +try { + const testRoot = join(core.root, "generator", "http-client-generator-test"); + const localSource = join(emitterTestsRoot, "src"); + const localTypeSpec = join(emitterTestsRoot, "tsp"); + + await Promise.all([ + rm(localSource, { recursive: true, force: true }), + rm(localTypeSpec, { recursive: true, force: true }), + ]); + await cp(join(testRoot, "src"), localSource, { recursive: true, force: true }); + await cp(join(testRoot, "tsp"), localTypeSpec, { recursive: true, force: true }); + console.log(`Synced src and tsp from ${testRoot}`); + + const relativeEmitterTests = relative(repoRoot, emitterTestsRoot).replaceAll("\\", "/"); + await execa( + "git", + [ + "-C", + repoRoot, + "apply", + `--directory=${relativeEmitterTests}`, + "-p1", + join(packageRoot, "core.test.patch"), + "--ignore-whitespace", + ], + { stdio: "inherit" }, + ); + console.log("Applied core.test.patch"); +} finally { + await removeCoreSourceRoot(core); +} diff --git a/packages/typespec-java/package.json b/packages/typespec-java/package.json index 2ee6574e53..7a1dc5c77e 100644 --- a/packages/typespec-java/package.json +++ b/packages/typespec-java/package.json @@ -30,13 +30,13 @@ "scripts": { "clean": "rimraf ./dist ./temp ./generator", "build": "pnpm build:emitter && pnpm build:generator", - "build:emitter": "pwsh -NoProfile -NonInteractive -File Copy-Sources.ps1 && tsc -p .", - "build:generator": "pwsh -NoProfile -NonInteractive -File Build-Generator.ps1", + "build:emitter": "node ./eng/scripts/copy-sources.ts && tsc -p .", + "build:generator": "node ./eng/scripts/build-generator.ts", "watch": "tsc -p . --watch", "test": "vitest run", - "regenerate": "pwsh ./Generate.ps1", - "test:java:e2e": "pwsh ./Spector-Tests.ps1", - "sync-tests": "pwsh ./SyncTests.ps1", + "regenerate": "node ./eng/scripts/generate.ts", + "test:java:e2e": "node ./eng/scripts/spector-tests.ts", + "sync-tests": "node ./eng/scripts/sync-tests.ts", "spector-serve": "tsp-spector serve ./node_modules/@typespec/http-specs/specs ./node_modules/@azure-tools/azure-http-specs/specs --coverageFile ./emitter-tests/tsp-spector-coverage-java.json", "spector-start": "tsp-spector server start ./node_modules/@typespec/http-specs/specs ./node_modules/@azure-tools/azure-http-specs/specs --coverageFile ./emitter-tests/tsp-spector-coverage-java.json", "spector-stop": "tsp-spector server stop", @@ -92,7 +92,9 @@ "@typespec/tspd": "workspace:^", "@typespec/versioning": "workspace:^", "@typespec/xml": "workspace:^", + "execa": "catalog:", "rimraf": "catalog:", + "tar": "catalog:", "typescript": "catalog:", "vitest": "catalog:" } diff --git a/packages/typespec-java/vitest.config.ts b/packages/typespec-java/vitest.config.ts index 9fb08f57fb..4fac907037 100644 --- a/packages/typespec-java/vitest.config.ts +++ b/packages/typespec-java/vitest.config.ts @@ -10,7 +10,7 @@ export default defineConfig({ outputFile: { junit: "./test-results.xml", }, - include: ["test/**/*.test.ts"], + include: ["test/**/*.test.ts", "eng/**/*.test.ts"], exclude: ["node_modules", "dist/test"], }, server: { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 44cb64d0c8..c2a69ae838 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -3853,9 +3853,15 @@ importers: '@typespec/xml': specifier: workspace:^ version: link:../../core/packages/xml + execa: + specifier: 'catalog:' + version: 9.6.1 rimraf: specifier: 'catalog:' version: 6.1.3 + tar: + specifier: 'catalog:' + version: 7.5.20 typescript: specifier: 'catalog:' version: 6.0.3 From 8b2a65dcf17c5a3858d1300ae5c9184b7b01e24a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 6 Aug 2026 03:03:13 +0000 Subject: [PATCH 3/5] Document Java automation migration Co-authored-by: XiaofeiCao <92354331+XiaofeiCao@users.noreply.github.com> --- ...lot-move-away-from-powershell-script-2026-7-6-3-2-25.md | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .chronus/changes/copilot-move-away-from-powershell-script-2026-7-6-3-2-25.md diff --git a/.chronus/changes/copilot-move-away-from-powershell-script-2026-7-6-3-2-25.md b/.chronus/changes/copilot-move-away-from-powershell-script-2026-7-6-3-2-25.md new file mode 100644 index 0000000000..90d062777b --- /dev/null +++ b/.chronus/changes/copilot-move-away-from-powershell-script-2026-7-6-3-2-25.md @@ -0,0 +1,7 @@ +--- +changeKind: internal +packages: + - "@azure-tools/typespec-java" +--- + +Replace the Java emitter's PowerShell build and test automation with native TypeScript scripts. \ No newline at end of file From fbbdf48ef95bd7dc5bfe54f46992b08d5ad56ceb Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 6 Aug 2026 03:46:55 +0000 Subject: [PATCH 4/5] Rename Java core source script Co-authored-by: XiaofeiCao <92354331+XiaofeiCao@users.noreply.github.com> --- packages/typespec-java/eng/scripts/copy-sources.ts | 2 +- .../eng/scripts/{core-source.test.ts => core-commit.test.ts} | 2 +- .../eng/scripts/{core-source.ts => core-commit.ts} | 0 packages/typespec-java/eng/scripts/sync-tests.ts | 2 +- 4 files changed, 3 insertions(+), 3 deletions(-) rename packages/typespec-java/eng/scripts/{core-source.test.ts => core-commit.test.ts} (99%) rename packages/typespec-java/eng/scripts/{core-source.ts => core-commit.ts} (100%) diff --git a/packages/typespec-java/eng/scripts/copy-sources.ts b/packages/typespec-java/eng/scripts/copy-sources.ts index bb4d3fdba3..ac6964cc4f 100644 --- a/packages/typespec-java/eng/scripts/copy-sources.ts +++ b/packages/typespec-java/eng/scripts/copy-sources.ts @@ -3,7 +3,7 @@ import { execa } from "execa"; import { cp, mkdir, readdir, rm } from "node:fs/promises"; import { basename, dirname, join, relative, resolve } from "node:path"; import { fileURLToPath } from "node:url"; -import { getCoreSourceRoot, removeCoreSourceRoot } from "./core-source.ts"; +import { getCoreSourceRoot, removeCoreSourceRoot } from "./core-commit.ts"; const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), "../.."); const repoRoot = resolve(packageRoot, "../.."); diff --git a/packages/typespec-java/eng/scripts/core-source.test.ts b/packages/typespec-java/eng/scripts/core-commit.test.ts similarity index 99% rename from packages/typespec-java/eng/scripts/core-source.test.ts rename to packages/typespec-java/eng/scripts/core-commit.test.ts index cb34f353b1..90fe302eb8 100644 --- a/packages/typespec-java/eng/scripts/core-source.test.ts +++ b/packages/typespec-java/eng/scripts/core-commit.test.ts @@ -3,7 +3,7 @@ import { access, mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promise import { tmpdir } from "node:os"; import { join } from "node:path"; import { afterEach, describe, expect, it } from "vitest"; -import { getCoreSourceRoot, removeCoreSourceRoot } from "./core-source.ts"; +import { getCoreSourceRoot, removeCoreSourceRoot } from "./core-commit.ts"; describe("core source", () => { const tempDirectories: string[] = []; diff --git a/packages/typespec-java/eng/scripts/core-source.ts b/packages/typespec-java/eng/scripts/core-commit.ts similarity index 100% rename from packages/typespec-java/eng/scripts/core-source.ts rename to packages/typespec-java/eng/scripts/core-commit.ts diff --git a/packages/typespec-java/eng/scripts/sync-tests.ts b/packages/typespec-java/eng/scripts/sync-tests.ts index 01c0ef60d1..0c91bfc9d5 100644 --- a/packages/typespec-java/eng/scripts/sync-tests.ts +++ b/packages/typespec-java/eng/scripts/sync-tests.ts @@ -3,7 +3,7 @@ import { execa } from "execa"; import { cp, rm } from "node:fs/promises"; import { dirname, join, relative, resolve } from "node:path"; import { fileURLToPath } from "node:url"; -import { getCoreSourceRoot, removeCoreSourceRoot } from "./core-source.ts"; +import { getCoreSourceRoot, removeCoreSourceRoot } from "./core-commit.ts"; const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), "../.."); const repoRoot = resolve(packageRoot, "../.."); From bd7e909c7605ad103d6ef07a7790b7ef5a5fbe59 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 6 Aug 2026 05:43:00 +0000 Subject: [PATCH 5/5] Address Java automation review feedback Co-authored-by: XiaofeiCao <92354331+XiaofeiCao@users.noreply.github.com> --- packages/typespec-java/CONTRIBUTING.md | 8 ++++---- .../typespec-java/eng/scripts/build-generator.ts | 2 +- .../typespec-java/eng/scripts/core-commit.test.ts | 9 +++++++++ packages/typespec-java/eng/scripts/core-commit.ts | 14 +++++++++++--- 4 files changed, 25 insertions(+), 8 deletions(-) diff --git a/packages/typespec-java/CONTRIBUTING.md b/packages/typespec-java/CONTRIBUTING.md index 44ca95c9dd..ebd9edbe31 100644 --- a/packages/typespec-java/CONTRIBUTING.md +++ b/packages/typespec-java/CONTRIBUTING.md @@ -134,8 +134,8 @@ Pulls the fix in from core; **no version bump**. Example: `sha` in [`core-commit.json`](./core-commit.json) to the target microsoft/typespec commit (e.g. the HEAD of core `main`). Build and sync scripts transiently check this commit out without moving the submodule pointer. -2. **Sync tests from core.** Run `pnpm sync-tests`: it - copies the tests/specs from the pinned core commit and aligns `emitter-tests/package.json`. +2. **Sync tests from core.** Run `pnpm sync-tests`: it copies the tests/specs from the pinned core + commit. 3. **Add a `fix` changelog entry** for the fix (`pnpm change add`). Open the PR against `release/` (not `main`) and merge it. _(Optional)_ Before merging, @@ -149,8 +149,8 @@ Bumps the version and publishes, after Part A merges. Example: [#4990](https://github.com/Azure/typespec-azure/pull/4990). Prepare the version bump per the `hotfix-release` skill (creates the `publish/hotfix/-` branch off `release/` and runs `pnpm chronus version --ignore-policies`, consuming the changeset from Part A). The `core` -submodule stays unchanged. The bump must also be reflected in `emitter-tests/package.json` (its -`version` and the `*.tgz` dependency) — rerun `pnpm sync-tests` or update it manually. +submodule stays unchanged. The `emitter-tests/` folder is part of the parent workspace package, so +it requires no separate version update. Open the PR against `release/`. After it merges: diff --git a/packages/typespec-java/eng/scripts/build-generator.ts b/packages/typespec-java/eng/scripts/build-generator.ts index a4172c8000..a43332b47e 100644 --- a/packages/typespec-java/eng/scripts/build-generator.ts +++ b/packages/typespec-java/eng/scripts/build-generator.ts @@ -11,7 +11,7 @@ try { await access(generatorPom); } catch { throw new Error( - `Copied generator not found at: ${generatorPom}\nRun ${join(packageRoot, "eng", "scripts", "copy-sources.ts")} first (build:emitter).`, + `Copied generator not found at: ${generatorPom}\nRun 'pnpm run build:emitter' first.`, ); } diff --git a/packages/typespec-java/eng/scripts/core-commit.test.ts b/packages/typespec-java/eng/scripts/core-commit.test.ts index 90fe302eb8..f27e8fd778 100644 --- a/packages/typespec-java/eng/scripts/core-commit.test.ts +++ b/packages/typespec-java/eng/scripts/core-commit.test.ts @@ -24,6 +24,15 @@ describe("core source", () => { expect(source).toEqual({ root: join(root, "packages", "http-client-java") }); }); + it("reports how to initialize a missing core submodule", async () => { + const root = await mkdtemp(join(tmpdir(), "typespec-java-core-test-")); + tempDirectories.push(root); + + await expect(getCoreSourceRoot(root, join(root, "package"))).rejects.toThrow( + "Run 'git submodule update --init core' from the repository root.", + ); + }); + it("extracts and cleans up a different pinned commit", async () => { const root = await createCoreRepository(); const packageRoot = join(root, "package"); diff --git a/packages/typespec-java/eng/scripts/core-commit.ts b/packages/typespec-java/eng/scripts/core-commit.ts index 2514ae31ad..063c5d7ad0 100644 --- a/packages/typespec-java/eng/scripts/core-commit.ts +++ b/packages/typespec-java/eng/scripts/core-commit.ts @@ -30,9 +30,17 @@ export async function getCoreSourceRoot( coreRoot: string, packageRoot: string, ): Promise { - const originSha = ( - await execa("git", ["-C", coreRoot, "rev-parse", "HEAD"], { env: gitEnvironment }) - ).stdout.trim(); + let originSha: string; + try { + originSha = ( + await execa("git", ["-C", coreRoot, "rev-parse", "HEAD"], { env: gitEnvironment }) + ).stdout.trim(); + } catch (error) { + throw new Error( + `The 'core' submodule at ${coreRoot} is not initialized. Run 'git submodule update --init core' from the repository root.`, + { cause: error }, + ); + } const liveRoot = join(coreRoot, coreJavaSubtree); const configPath = join(packageRoot, "core-commit.json");