From 7f00808b98979c9a7c4bba556568bc190912d889 Mon Sep 17 00:00:00 2001 From: Miller Wang Date: Tue, 11 Aug 2026 16:52:54 +0800 Subject: [PATCH 01/16] docs: plan stable release network isolation Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- ...-08-11-stable-release-network-isolation.md | 582 ++++++++++++++++++ 1 file changed, 582 insertions(+) create mode 100644 docs/superpowers/plans/2026-08-11-stable-release-network-isolation.md diff --git a/docs/superpowers/plans/2026-08-11-stable-release-network-isolation.md b/docs/superpowers/plans/2026-08-11-stable-release-network-isolation.md new file mode 100644 index 00000000000..ed0aa3a0d95 --- /dev/null +++ b/docs/superpowers/plans/2026-08-11-stable-release-network-isolation.md @@ -0,0 +1,582 @@ +# Stable Release Network Isolation Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Enable 1ES Network Isolation for the stable release pipeline and force ordinary Maven and Gradle package restores through the `vscjava` Central Feed Service. + +**Architecture:** Add pipeline-only Maven and Gradle policy files under `.azure-pipelines`. Maven mirrors `central` to CFS; a Gradle init script redirects general-purpose public repositories to CFS while preserving Maven Local and specialized vendor repositories. The release pipeline imports the non-secret feed URL, maps `System.AccessToken` only into the build step, and removes the network-isolation opt-out. + +**Tech Stack:** Azure Pipelines YAML, Maven settings XML, Gradle 9.1 Groovy init scripts, PowerShell, 1ES Pipeline Templates + +--- + +## File Structure + +- Create `.azure-pipelines/cfs-variables.yml`: non-secret CFS endpoint shared by Maven and Gradle. +- Create `.azure-pipelines/cfs-settings.xml`: pipeline-only Maven mirror and server configuration. +- Create `.azure-pipelines/cfs-init.gradle`: pipeline-only Gradle repository redirection, validation, and fail-closed behavior. +- Modify `.azure-pipelines/sign-for-stable-release.yml`: enable network isolation and apply the CFS policies to the build step. +- Keep `docs/superpowers/specs/2026-08-11-stable-release-network-isolation-design.md` as the approved design record. + +### Task 1: Add Shared CFS and Maven Configuration + +**Files:** +- Create: `.azure-pipelines/cfs-variables.yml` +- Create: `.azure-pipelines/cfs-settings.xml` + +- [ ] **Step 1: Run the precondition check** + +Run: + +```powershell +$paths = @( + '.azure-pipelines\cfs-variables.yml', + '.azure-pipelines\cfs-settings.xml' +) +$existing = $paths | Where-Object { Test-Path $_ } +if ($existing) { + throw "Expected new files, but these already exist: $($existing -join ', ')" +} +Write-Host 'PASS: CFS configuration files do not exist yet' +``` + +Expected: `PASS: CFS configuration files do not exist yet`. + +- [ ] **Step 2: Create the non-secret CFS variable template** + +Create `.azure-pipelines/cfs-variables.yml`: + +```yaml +# Non-secret Central Feed Service configuration shared by the Maven and Gradle +# dependency restores in sign-for-stable-release.yml. System.AccessToken is a +# secret and must be mapped directly on the build step instead of being declared +# here. +variables: + - name: CFS_MAVEN_URL + value: https://pkgs.dev.azure.com/mseng/VSJava/_packaging/vscjava/maven/v1 +``` + +- [ ] **Step 3: Create the pipeline-only Maven settings** + +Create `.azure-pipelines/cfs-settings.xml`: + +```xml + + + + + + vscjava + Central Feed Service + ${env.CFS_MAVEN_URL} + central + + + + + vscjava + AzureDevOps + ${env.SYSTEM_ACCESSTOKEN} + + + +``` + +- [ ] **Step 4: Validate the files** + +Run: + +```powershell +[xml]$settings = Get-Content '.azure-pipelines\cfs-settings.xml' -Raw +$ns = New-Object System.Xml.XmlNamespaceManager($settings.NameTable) +$ns.AddNamespace('m', 'http://maven.apache.org/SETTINGS/1.0.0') + +$mirror = $settings.SelectSingleNode('/m:settings/m:mirrors/m:mirror', $ns) +$server = $settings.SelectSingleNode('/m:settings/m:servers/m:server', $ns) +if ($mirror.id -ne 'vscjava' -or $server.id -ne 'vscjava') { + throw 'Maven mirror and server IDs must both be vscjava' +} +if ($mirror.mirrorOf -ne 'central') { + throw 'Maven mirror must be scoped to central' +} +if ($mirror.url -ne '${env.CFS_MAVEN_URL}') { + throw 'Maven mirror must read CFS_MAVEN_URL from the environment' +} +if ($server.password -ne '${env.SYSTEM_ACCESSTOKEN}') { + throw 'Maven server must read SYSTEM_ACCESSTOKEN from the environment' +} + +$variables = Get-Content '.azure-pipelines\cfs-variables.yml' -Raw +if ($variables -notmatch '(?m)^\s*-\s+name:\s+CFS_MAVEN_URL\s*$' -or + $variables -notmatch 'https://pkgs\.dev\.azure\.com/mseng/VSJava/_packaging/vscjava/maven/v1') { + throw 'CFS_MAVEN_URL is missing or incorrect' +} +Write-Host 'PASS: Maven CFS configuration is valid' +``` + +Expected: `PASS: Maven CFS configuration is valid`. + +- [ ] **Step 5: Commit the Maven policy** + +Run: + +```powershell +git add -- .azure-pipelines/cfs-variables.yml .azure-pipelines/cfs-settings.xml +git commit -m "build: add Maven CFS configuration" -m "Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>" +``` + +Expected: one commit containing the two new files. + +### Task 2: Add the Gradle Repository Policy + +**Files:** +- Create: `.azure-pipelines/cfs-init.gradle` + +- [ ] **Step 1: Run the precondition check** + +Run: + +```powershell +if (Test-Path '.azure-pipelines\cfs-init.gradle') { + throw '.azure-pipelines\cfs-init.gradle already exists' +} +Write-Host 'PASS: Gradle CFS policy does not exist yet' +``` + +Expected: `PASS: Gradle CFS policy does not exist yet`. + +- [ ] **Step 2: Create the Gradle init script** + +Create `.azure-pipelines/cfs-init.gradle`: + +```groovy +import org.gradle.api.artifacts.repositories.MavenArtifactRepository + +/* + * Pipeline-only repository policy for 1ES Network Isolation. + * + * General-purpose Maven repositories are redirected to CFS. Maven Local and + * repositories that serve CFS-incompatible vendor artifacts remain unchanged. + * The script is applied only by sign-for-stable-release.yml, so local developer + * builds keep their existing repository configuration. + */ + +def cfsUrl = System.getenv('CFS_MAVEN_URL') +def cfsToken = System.getenv('SYSTEM_ACCESSTOKEN') + +if (!cfsUrl?.trim() || !cfsToken?.trim()) { + throw new GradleException( + 'CFS_MAVEN_URL and SYSTEM_ACCESSTOKEN must both be set when cfs-init.gradle is applied. ' + + 'Failing instead of restoring packages from a public repository.') +} + +def forbiddenHosts = [ + 'repo.maven.apache.org', + 'repo1.maven.org', + 'plugins.gradle.org', + 'plugins-artifacts.gradle.org', + 'oss.sonatype.org', + 's01.oss.sonatype.org', + 'maven-central.storage-download.googleapis.com', +] as Set + +def isForbiddenRepository = { URI uri -> + def host = uri?.host?.toLowerCase(Locale.ROOT) + def path = uri?.path?.toLowerCase(Locale.ROOT) ?: '' + host in forbiddenHosts || + (host == 'cache-redirector.jetbrains.com' && + (path.startsWith('/repo1.maven.org/') || path.startsWith('/plugins.gradle.org/'))) +} + +def routeToCfs = { MavenArtifactRepository repository -> + if (isForbiddenRepository(repository.url)) { + repository.setUrl(cfsUrl) + repository.credentials { credentials -> + credentials.username = 'AzureDevOps' + credentials.password = cfsToken + } + } +} + +def watchRepositories = { handler -> + handler.withType(MavenArtifactRepository).configureEach { repository -> + routeToCfs(repository) + } +} + +def addCfsRepository = { handler -> + handler.maven { repository -> + repository.name = 'vscjava' + repository.setUrl(cfsUrl) + repository.credentials { credentials -> + credentials.username = 'AzureDevOps' + credentials.password = cfsToken + } + } +} + +def assertNoForbiddenRepositories = { String owner, handler -> + def forbidden = handler.withType(MavenArtifactRepository).findAll { repository -> + isForbiddenRepository(repository.url) + } + if (!forbidden.isEmpty()) { + def descriptions = forbidden.collect { repository -> + "${repository.name} (${repository.url})" + }.join(', ') + throw new GradleException( + "Forbidden public repositories remain in ${owner}: ${descriptions}") + } +} + +beforeSettings { settings -> + settings.pluginManagement.repositories { repositories -> + repositories.clear() + watchRepositories(repositories) + addCfsRepository(repositories) + } + + watchRepositories(settings.dependencyResolutionManagement.repositories) +} + +allprojects { project -> + watchRepositories(project.repositories) + watchRepositories(project.buildscript.repositories) +} + +settingsEvaluated { settings -> + assertNoForbiddenRepositories( + 'plugin management', + settings.pluginManagement.repositories) + assertNoForbiddenRepositories( + 'dependency resolution management', + settings.dependencyResolutionManagement.repositories) +} + +projectsEvaluated { + gradle.rootProject.allprojects { project -> + assertNoForbiddenRepositories( + "${project.path} project repositories", + project.repositories) + assertNoForbiddenRepositories( + "${project.path} buildscript repositories", + project.buildscript.repositories) + } +} +``` + +- [ ] **Step 3: Verify missing credentials fail before project evaluation** + +Run: + +```powershell +$tempProject = Join-Path $env:TEMP 'azure-tools-cfs-init-test' +if (Test-Path $tempProject) { + Remove-Item $tempProject -Recurse -Force +} +New-Item -ItemType Directory -Path $tempProject | Out-Null +Set-Content -Path (Join-Path $tempProject 'settings.gradle') -Value "rootProject.name = 'cfs-init-test'" +Set-Content -Path (Join-Path $tempProject 'build.gradle') -Value '' + +Remove-Item Env:CFS_MAVEN_URL -ErrorAction SilentlyContinue +Remove-Item Env:SYSTEM_ACCESSTOKEN -ErrorAction SilentlyContinue + +Push-Location 'PluginsAndFeatures\azure-toolkit-for-intellij' +try { + $output = .\gradlew.bat -p $tempProject help ` + --init-script '..\..\.azure-pipelines\cfs-init.gradle' ` + --no-daemon --no-configuration-cache 2>&1 + $exitCode = $LASTEXITCODE +} finally { + Pop-Location +} + +if ($exitCode -eq 0) { + throw 'Expected Gradle to reject missing CFS configuration' +} +if (($output -join "`n") -notmatch 'CFS_MAVEN_URL and SYSTEM_ACCESSTOKEN must both be set') { + throw "Gradle failed for an unexpected reason:`n$($output -join "`n")" +} +Write-Host 'PASS: Gradle policy fails closed when credentials are missing' +``` + +Expected: `PASS: Gradle policy fails closed when credentials are missing`. + +- [ ] **Step 4: Verify public repositories are redirected and vendor/local repositories remain** + +Run: + +```powershell +$tempProject = Join-Path $env:TEMP 'azure-tools-cfs-init-test' +@' +repositories { + mavenCentral() + mavenLocal() + maven { url = uri('https://cache-redirector.jetbrains.com/repo1.maven.org/maven2') } + maven { url = uri('https://cache-redirector.jetbrains.com/intellij-dependencies') } + maven { url = uri('https://maven.atlassian.com/repository/public') } +} + +tasks.register('printRepositories') { + doLast { + repositories.each { repository -> + def location = repository.hasProperty('url') ? repository.url : repository.name + println("REPOSITORY=${repository.name}|${location}") + } + } +} +'@ | Set-Content -Path (Join-Path $tempProject 'build.gradle') + +$env:CFS_MAVEN_URL = 'https://example.invalid/vscjava/maven/v1' +$env:SYSTEM_ACCESSTOKEN = 'test-token' + +Push-Location 'PluginsAndFeatures\azure-toolkit-for-intellij' +try { + $output = .\gradlew.bat -p $tempProject printRepositories ` + --init-script '..\..\.azure-pipelines\cfs-init.gradle' ` + --no-daemon --no-configuration-cache 2>&1 + $exitCode = $LASTEXITCODE +} finally { + Pop-Location + Remove-Item Env:CFS_MAVEN_URL -ErrorAction SilentlyContinue + Remove-Item Env:SYSTEM_ACCESSTOKEN -ErrorAction SilentlyContinue + Remove-Item $tempProject -Recurse -Force +} + +if ($exitCode -ne 0) { + throw "Gradle repository test failed:`n$($output -join "`n")" +} +$text = $output -join "`n" +if ($text -match 'repo\.maven\.apache\.org|repo1\.maven\.org') { + throw "A forbidden repository remains:`n$text" +} +if ($text -notmatch 'https://example\.invalid/vscjava/maven/v1') { + throw "CFS replacement was not present:`n$text" +} +if ($text -notmatch 'cache-redirector\.jetbrains\.com/intellij-dependencies') { + throw "The IntelliJ dependency repository was not preserved:`n$text" +} +if ($text -notmatch 'maven\.atlassian\.com/repository/public') { + throw "The Atlassian repository was not preserved:`n$text" +} +if ($text -notmatch 'MavenLocal') { + throw "Maven Local was not preserved:`n$text" +} +Write-Host 'PASS: Gradle repositories follow the approved policy' +``` + +Expected: `PASS: Gradle repositories follow the approved policy`. + +- [ ] **Step 5: Commit the Gradle policy** + +Run: + +```powershell +git add -- .azure-pipelines/cfs-init.gradle +git commit -m "build: route Gradle packages through CFS" -m "Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>" +``` + +Expected: one commit containing the Gradle init script. + +### Task 3: Wire CFS into the Stable Release Pipeline + +**Files:** +- Modify: `.azure-pipelines/sign-for-stable-release.yml:2-4` +- Modify: `.azure-pipelines/sign-for-stable-release.yml:68-75` +- Modify: `.azure-pipelines/sign-for-stable-release.yml:125-137` + +- [ ] **Step 1: Verify the pipeline is not isolated yet** + +Run: + +```powershell +$pipeline = Get-Content '.azure-pipelines\sign-for-stable-release.yml' -Raw +if ($pipeline -notmatch 'disableNetworkIsolation:\s*true') { + throw 'Expected the existing network-isolation opt-out' +} +if ($pipeline -match 'cfs-variables\.yml|cfs-settings\.xml|cfs-init\.gradle') { + throw 'Expected CFS wiring to be absent before this task' +} +Write-Host 'PASS: pipeline still has the expected pre-change behavior' +``` + +Expected: `PASS: pipeline still has the expected pre-change behavior`. + +- [ ] **Step 2: Import the shared CFS variables** + +Add this entry after the `Codeql.Enabled` variable in +`.azure-pipelines/sign-for-stable-release.yml`: + +```yaml + - template: /.azure-pipelines/cfs-variables.yml@self +``` + +- [ ] **Step 3: Enable 1ES Network Isolation** + +Remove this block from the `extends.parameters` section: + +```yaml + featureFlags: + disableNetworkIsolation: true +``` + +Leave the existing `pool` and all stages unchanged. + +- [ ] **Step 4: Apply the Maven and Gradle policies to the build step** + +Replace the `Build Plugin` script and environment block with: + +```yaml + - task: PowerShell@2 + displayName: Build Plugin + inputs: + targetType: inline + script: | + mvn -v + # ./gradlew buildUtils || exit -1 + mvn clean install --settings "$(Build.SourcesDirectory)\.azure-pipelines\cfs-settings.xml" -f ./Utils/pom.xml -T 1C "-Dcheckstyle.skip=true" "-Dmaven.test.skip=true" "-Dmaven.javadoc.skip=true" + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + cd PluginsAndFeatures/azure-toolkit-for-intellij + ./gradlew clean buildPlugin --init-script "$(Build.SourcesDirectory)\.azure-pipelines\cfs-init.gradle" -s "-Papplicationinsights.key=$(INTELLIJ_KEY)" "-PneedPatchVersion=false" "-Psources=false" "-Porg.gradle.configureondemand=false" "-Porg.gradle.daemon=false" "-Porg.gradle.unsafe.configuration-cache=false" "-Porg.gradle.caching=false" + env: + USE_STABLE_VERSION: $(IsStableBuild) + CFS_MAVEN_URL: $(CFS_MAVEN_URL) + SYSTEM_ACCESSTOKEN: $(System.AccessToken) +``` + +- [ ] **Step 5: Run static pipeline assertions** + +Run: + +```powershell +$pipeline = Get-Content '.azure-pipelines\sign-for-stable-release.yml' -Raw +$required = @( + '/.azure-pipelines/cfs-variables.yml@self', + '--settings "$(Build.SourcesDirectory)\.azure-pipelines\cfs-settings.xml"', + '--init-script "$(Build.SourcesDirectory)\.azure-pipelines\cfs-init.gradle"', + 'CFS_MAVEN_URL: $(CFS_MAVEN_URL)', + 'SYSTEM_ACCESSTOKEN: $(System.AccessToken)' +) +foreach ($value in $required) { + if (-not $pipeline.Contains($value)) { + throw "Missing pipeline wiring: $value" + } +} +if ($pipeline -match 'disableNetworkIsolation') { + throw 'Network isolation is still disabled' +} +if (($pipeline | Select-String -Pattern 'SYSTEM_ACCESSTOKEN:' -AllMatches).Matches.Count -ne 1) { + throw 'System.AccessToken must be scoped to exactly one build step' +} +Write-Host 'PASS: stable release pipeline CFS wiring is complete' +``` + +Expected: `PASS: stable release pipeline CFS wiring is complete`. + +- [ ] **Step 6: Commit the pipeline wiring** + +Run: + +```powershell +git add -- .azure-pipelines/sign-for-stable-release.yml +git commit -m "build: enable stable release network isolation" -m "Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>" +``` + +Expected: one commit containing only the stable release pipeline update. + +### Task 4: Verify Behavior and Compliance + +**Files:** +- Verify: `.azure-pipelines/cfs-variables.yml` +- Verify: `.azure-pipelines/cfs-settings.xml` +- Verify: `.azure-pipelines/cfs-init.gradle` +- Verify: `.azure-pipelines/sign-for-stable-release.yml` + +- [ ] **Step 1: Check formatting and the final change set** + +Run: + +```powershell +git --no-pager diff --check HEAD~3..HEAD +git --no-pager status --short +git --no-pager log -5 --oneline +``` + +Expected: + +- `git diff --check` prints no errors. +- `git status --short` is empty. +- The log shows the design and plan commits followed by the three implementation + commits. + +- [ ] **Step 2: Verify local Gradle configuration is unaffected** + +Run: + +```powershell +Push-Location 'PluginsAndFeatures\azure-toolkit-for-intellij' +try { + .\gradlew.bat help --no-daemon --no-configuration-cache + if ($LASTEXITCODE -ne 0) { + exit $LASTEXITCODE + } +} finally { + Pop-Location +} +``` + +Expected: `BUILD SUCCESSFUL`. The command does not reference `cfs-init.gradle` and +does not require `CFS_MAVEN_URL` or `SYSTEM_ACCESSTOKEN`. + +- [ ] **Step 3: Re-run the policy tests** + +Repeat Task 2 Steps 3 and 4. + +Expected: + +- Missing credentials fail with the explicit CFS configuration error. +- General-purpose public repositories are replaced with CFS. +- Maven Local, IntelliJ dependencies, and Atlassian remain available. + +- [ ] **Step 4: Preview or run the Azure Pipeline** + +Use the Azure Pipelines definition backed by +`.azure-pipelines/sign-for-stable-release.yml` to compile the updated YAML and run a +non-publishing validation build from the implementation branch. Set +`ForceRealSign=false` and `ForceTestSignRelease=false`. + +Expected: + +- YAML compilation succeeds. +- `Build_Plugin.Build_and_Sign` completes. +- `Release_Plugin` is skipped. +- Maven logs show the `vscjava` mirror. +- Gradle resolves general-purpose packages from `pkgs.dev.azure.com`. +- JDK, Gradle distribution, and JetBrains-specific endpoints remain the only direct + public artifact endpoints. + +If Azure Pipelines access is unavailable, record this as the only unverified item; +do not claim CFS feed coverage or policy compliance from local checks alone. + +- [ ] **Step 5: Inspect 1ES network policy telemetry** + +Inspect the validation run's 1ES policy results. + +Expected: + +- `CFSClean`: zero findings. +- `CFSClean2`: zero findings. +- `CFSClean3`: zero findings. +- Maven Central, Gradle Plugin Portal, Sonatype general-purpose endpoints, and + Maven Central public mirrors: zero requests. +- Remaining `DefaultDeny` entries correspond only to JDK 25 acquisition, the Gradle + distribution, JetBrains/IntelliJ artifacts, signing, or Marketplace publishing. + +If a general-purpose package source remains, add it to the Gradle script's forbidden +source classification and repeat Tasks 2 through 4. Do not add a broad direct-access +exception. From 90c74159f42021d8b9dab3807b4fc36ed03f8531 Mon Sep 17 00:00:00 2001 From: Miller Wang Date: Tue, 11 Aug 2026 16:57:45 +0800 Subject: [PATCH 02/16] build: add Maven CFS configuration Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .azure-pipelines/cfs-settings.xml | 25 +++++++++++++++++++++++++ .azure-pipelines/cfs-variables.yml | 7 +++++++ 2 files changed, 32 insertions(+) create mode 100644 .azure-pipelines/cfs-settings.xml create mode 100644 .azure-pipelines/cfs-variables.yml diff --git a/.azure-pipelines/cfs-settings.xml b/.azure-pipelines/cfs-settings.xml new file mode 100644 index 00000000000..03c433b931a --- /dev/null +++ b/.azure-pipelines/cfs-settings.xml @@ -0,0 +1,25 @@ + + + + + + vscjava + Central Feed Service + ${env.CFS_MAVEN_URL} + central + + + + + vscjava + AzureDevOps + ${env.SYSTEM_ACCESSTOKEN} + + + diff --git a/.azure-pipelines/cfs-variables.yml b/.azure-pipelines/cfs-variables.yml new file mode 100644 index 00000000000..8da433918f8 --- /dev/null +++ b/.azure-pipelines/cfs-variables.yml @@ -0,0 +1,7 @@ +# Non-secret Central Feed Service configuration shared by the Maven and Gradle +# dependency restores in sign-for-stable-release.yml. System.AccessToken is a +# secret and must be mapped directly on the build step instead of being declared +# here. +variables: + - name: CFS_MAVEN_URL + value: https://pkgs.dev.azure.com/mseng/VSJava/_packaging/vscjava/maven/v1 From a5fb09fae7c10fc4d749091e51e5e41b5b57972c Mon Sep 17 00:00:00 2001 From: Miller Wang Date: Tue, 11 Aug 2026 17:06:48 +0800 Subject: [PATCH 03/16] build: route Gradle packages through CFS Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .azure-pipelines/cfs-init.gradle | 128 +++++++++++++++++++++++++++++++ 1 file changed, 128 insertions(+) create mode 100644 .azure-pipelines/cfs-init.gradle diff --git a/.azure-pipelines/cfs-init.gradle b/.azure-pipelines/cfs-init.gradle new file mode 100644 index 00000000000..878bf512bcc --- /dev/null +++ b/.azure-pipelines/cfs-init.gradle @@ -0,0 +1,128 @@ +import org.gradle.api.GradleException +import org.gradle.api.artifacts.repositories.MavenArtifactRepository + +import java.util.Locale + +def cfsUrl = System.getenv('CFS_MAVEN_URL') +def cfsToken = System.getenv('SYSTEM_ACCESSTOKEN') + +if (!cfsUrl?.trim() || !cfsToken?.trim()) { + throw new GradleException( + 'CFS_MAVEN_URL and SYSTEM_ACCESSTOKEN must both be set when cfs-init.gradle is applied. ' + + 'Failing instead of restoring packages from a public repository.') +} + +def forbiddenHosts = [ + 'repo.maven.apache.org', + 'repo1.maven.org', + 'plugins.gradle.org', + 'plugins-artifacts.gradle.org', + 'oss.sonatype.org', + 's01.oss.sonatype.org', + 'maven-central.storage-download.googleapis.com', +] as Set + +def isForbiddenRepository = { MavenArtifactRepository repository -> + def repositoryUrl = repository.url + def host = repositoryUrl?.host?.toLowerCase(Locale.ROOT) + def path = (repositoryUrl?.path ?: '').toLowerCase(Locale.ROOT) + + if (!host) { + return false + } + + if (forbiddenHosts.contains(host)) { + return true + } + + // Preserve JetBrains-specialized paths such as /intellij-dependencies. + if (host == 'cache-redirector.jetbrains.com') { + return path == '/repo1.maven.org' || + path.startsWith('/repo1.maven.org/') || + path == '/plugins.gradle.org' || + path.startsWith('/plugins.gradle.org/') + } + + return false +} + +def routeToCfs = { MavenArtifactRepository repository -> + if (!isForbiddenRepository(repository)) { + return + } + + repository.url = uri(cfsUrl) + repository.credentials { + username = 'AzureDevOps' + password = cfsToken + } +} + +def watchRepositories +watchRepositories = { repositories -> + repositories.all { repository -> + if (repository instanceof MavenArtifactRepository) { + routeToCfs(repository) + } + } +} + +def addCfsRepository = { repositories -> + repositories.maven { repository -> + repository.name = 'vscjava' + repository.url = uri(cfsUrl) + repository.credentials { + username = 'AzureDevOps' + password = cfsToken + } + } +} + +def describeRepository = { MavenArtifactRepository repository -> + "${repository.name ?: repository.displayName} -> ${repository.url}" +} + +def assertNoForbiddenRepositories = { String scope, repositories -> + def offendingRepositories = repositories.findAll { repository -> + repository instanceof MavenArtifactRepository && + isForbiddenRepository(repository as MavenArtifactRepository) + } as List + + if (!offendingRepositories.isEmpty()) { + throw new GradleException( + "Forbidden public Maven repository remained in ${scope}: " + + offendingRepositories.collect(describeRepository).join(', ')) + } +} + +beforeSettings { settings -> + // Pipeline-only policy: route general-purpose public Maven traffic through authenticated CFS. + settings.pluginManagement.repositories { repositories -> + repositories.clear() + watchRepositories(repositories) + addCfsRepository(repositories) + } + + // Preserve local and vendor-specific settings repositories while rewriting forbidden Maven sources. + watchRepositories(settings.dependencyResolutionManagement.repositories) +} + +allprojects { project -> + // Keep watching project and buildscript repositories because plugins can add them later. + watchRepositories(project.repositories) + watchRepositories(project.buildscript.repositories) +} + +settingsEvaluated { settings -> + assertNoForbiddenRepositories('plugin management repositories', settings.pluginManagement.repositories) + assertNoForbiddenRepositories( + 'settings dependency resolution repositories', + settings.dependencyResolutionManagement.repositories) +} + +projectsEvaluated { + gradle.rootProject.allprojects { project -> + assertNoForbiddenRepositories("project ${project.path} repositories", project.repositories) + assertNoForbiddenRepositories("project ${project.path} buildscript repositories", project.buildscript.repositories) + } +} From dc8bdd3b34d904175080fbdefd25cf93489b6cb3 Mon Sep 17 00:00:00 2001 From: Miller Wang Date: Tue, 11 Aug 2026 17:26:15 +0800 Subject: [PATCH 04/16] build: cover Maven Central cache redirects Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .azure-pipelines/cfs-init.gradle | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/.azure-pipelines/cfs-init.gradle b/.azure-pipelines/cfs-init.gradle index 878bf512bcc..3ad7c306c85 100644 --- a/.azure-pipelines/cfs-init.gradle +++ b/.azure-pipelines/cfs-init.gradle @@ -37,10 +37,8 @@ def isForbiddenRepository = { MavenArtifactRepository repository -> // Preserve JetBrains-specialized paths such as /intellij-dependencies. if (host == 'cache-redirector.jetbrains.com') { - return path == '/repo1.maven.org' || - path.startsWith('/repo1.maven.org/') || - path == '/plugins.gradle.org' || - path.startsWith('/plugins.gradle.org/') + def upstreamHost = path.tokenize('/').find() + return upstreamHost != null && forbiddenHosts.contains(upstreamHost) } return false From 71b3f9247d3e2bf492fb6d109a7e906d52a55075 Mon Sep 17 00:00:00 2001 From: Miller Wang Date: Tue, 11 Aug 2026 17:31:36 +0800 Subject: [PATCH 05/16] build: enable stable release network isolation Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .azure-pipelines/sign-for-stable-release.yml | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/.azure-pipelines/sign-for-stable-release.yml b/.azure-pipelines/sign-for-stable-release.yml index 7ea9e095ad7..203138fc8e7 100644 --- a/.azure-pipelines/sign-for-stable-release.yml +++ b/.azure-pipelines/sign-for-stable-release.yml @@ -2,6 +2,7 @@ name: $(Date:yyyyMMdd).$(Rev:r) variables: - name: Codeql.Enabled value: true + - template: /.azure-pipelines/cfs-variables.yml@self - name: IsStableBuild value: ${{ or(eq(variables['Build.SourceBranch'], 'refs/heads/main'), startsWith(variables['Build.SourceBranch'], 'refs/heads/release-')) }} - name: IsNightlyBuild @@ -68,8 +69,6 @@ schedules: extends: template: v1/1ES.Official.PipelineTemplate.yml@1esPipelines parameters: - featureFlags: - disableNetworkIsolation: true pool: name: VSEngSS-MicroBuild2022-1ES stages: @@ -130,12 +129,14 @@ extends: script: | mvn -v # ./gradlew buildUtils || exit -1 - mvn clean install -f ./Utils/pom.xml -T 1C "-Dcheckstyle.skip=true" "-Dmaven.test.skip=true" "-Dmaven.javadoc.skip=true" + mvn -s "$(Build.SourcesDirectory)/.azure-pipelines/cfs-settings.xml" clean install -f ./Utils/pom.xml -T 1C "-Dcheckstyle.skip=true" "-Dmaven.test.skip=true" "-Dmaven.javadoc.skip=true" mvn clean -f ./Utils/pom.xml cd PluginsAndFeatures/azure-toolkit-for-intellij - ./gradlew clean buildPlugin -s "-Papplicationinsights.key=$(INTELLIJ_KEY)" "-PneedPatchVersion=false" "-Psources=false" "-Porg.gradle.configureondemand=false" "-Porg.gradle.daemon=false" "-Porg.gradle.unsafe.configuration-cache=false" "-Porg.gradle.caching=false" + ./gradlew -I "$(Build.SourcesDirectory)/.azure-pipelines/cfs-init.gradle" clean buildPlugin -s "-Papplicationinsights.key=$(INTELLIJ_KEY)" "-PneedPatchVersion=false" "-Psources=false" "-Porg.gradle.configureondemand=false" "-Porg.gradle.daemon=false" "-Porg.gradle.unsafe.configuration-cache=false" "-Porg.gradle.caching=false" env: USE_STABLE_VERSION: $(IsStableBuild) + CFS_MAVEN_URL: $(CFS_MAVEN_URL) + SYSTEM_ACCESSTOKEN: $(System.AccessToken) - task: PowerShell@2 displayName: Unpackage inputs: From 8e5ccc5270a00f5305632a7746364fe7ecd38b6a Mon Sep 17 00:00:00 2001 From: Miller Wang Date: Tue, 11 Aug 2026 17:57:43 +0800 Subject: [PATCH 06/16] build: scope Atlassian repository exception Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .azure-pipelines/cfs-init.gradle | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/.azure-pipelines/cfs-init.gradle b/.azure-pipelines/cfs-init.gradle index 3ad7c306c85..693d8ab6a9e 100644 --- a/.azure-pipelines/cfs-init.gradle +++ b/.azure-pipelines/cfs-init.gradle @@ -22,10 +22,22 @@ def forbiddenHosts = [ 'maven-central.storage-download.googleapis.com', ] as Set +def normalizeRepositoryPath = { repositoryUrl -> + def path = (repositoryUrl?.path ?: '').toLowerCase(Locale.ROOT).replaceAll('/+$', '') + path ?: '/' +} + +def isAtlassianPublicRepository = { MavenArtifactRepository repository -> + def repositoryUrl = repository.url + def host = repositoryUrl?.host?.toLowerCase(Locale.ROOT) + def path = normalizeRepositoryPath(repositoryUrl) + host == 'maven.atlassian.com' && path == '/repository/public' +} + def isForbiddenRepository = { MavenArtifactRepository repository -> def repositoryUrl = repository.url def host = repositoryUrl?.host?.toLowerCase(Locale.ROOT) - def path = (repositoryUrl?.path ?: '').toLowerCase(Locale.ROOT) + def path = normalizeRepositoryPath(repositoryUrl) if (!host) { return false @@ -44,6 +56,17 @@ def isForbiddenRepository = { MavenArtifactRepository repository -> return false } +def restrictAtlassianRepository = { MavenArtifactRepository repository -> + if (!isAtlassianPublicRepository(repository)) { + return + } + + // Microba is only available here; keep Atlassian only for that vendor group. + repository.content { + includeGroup 'com.michaelbaranov' + } +} + def routeToCfs = { MavenArtifactRepository repository -> if (!isForbiddenRepository(repository)) { return @@ -60,6 +83,7 @@ def watchRepositories watchRepositories = { repositories -> repositories.all { repository -> if (repository instanceof MavenArtifactRepository) { + restrictAtlassianRepository(repository) routeToCfs(repository) } } From d2ae9948c80a262f9beb925d6c8ab5c0c9505c76 Mon Sep 17 00:00:00 2001 From: Miller Wang Date: Tue, 11 Aug 2026 18:57:57 +0800 Subject: [PATCH 07/16] docs: update network isolation verification plan Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- ...2026-08-11-stable-release-network-isolation.md | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/docs/superpowers/plans/2026-08-11-stable-release-network-isolation.md b/docs/superpowers/plans/2026-08-11-stable-release-network-isolation.md index ed0aa3a0d95..1a30244d292 100644 --- a/docs/superpowers/plans/2026-08-11-stable-release-network-isolation.md +++ b/docs/superpowers/plans/2026-08-11-stable-release-network-isolation.md @@ -497,22 +497,25 @@ Expected: one commit containing only the stable release pipeline update. - Verify: `.azure-pipelines/cfs-init.gradle` - Verify: `.azure-pipelines/sign-for-stable-release.yml` -- [ ] **Step 1: Check formatting and the final change set** +- [ ] **Step 1: Check formatting, source cleanliness, and the final change set** Run: ```powershell -git --no-pager diff --check HEAD~3..HEAD +git --no-pager diff --check d116c373d..HEAD git --no-pager status --short -git --no-pager log -5 --oneline +git --no-pager status --short --ignored +git --no-pager log --oneline --decorate --reverse d116c373d..HEAD ``` Expected: - `git diff --check` prints no errors. -- `git status --short` is empty. -- The log shows the design and plan commits followed by the three implementation - commits. +- `git status --short` is empty, confirming tracked/untracked source cleanliness. +- `git status --short --ignored` is reported separately and may still list ignored + build/cache outputs; those do not count as source changes. +- The log shows the design commit, plan commit, three primary implementation + commits, and two reviewer-requested Gradle policy correction commits. - [ ] **Step 2: Verify local Gradle configuration is unaffected** From baf38fe37efdedd2e9d129e845df3082f2fe528e Mon Sep 17 00:00:00 2001 From: Miller Wang Date: Tue, 11 Aug 2026 19:00:44 +0800 Subject: [PATCH 08/16] docs: fix network isolation verification range Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../2026-08-11-stable-release-network-isolation.md | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/docs/superpowers/plans/2026-08-11-stable-release-network-isolation.md b/docs/superpowers/plans/2026-08-11-stable-release-network-isolation.md index 1a30244d292..cd075c637e5 100644 --- a/docs/superpowers/plans/2026-08-11-stable-release-network-isolation.md +++ b/docs/superpowers/plans/2026-08-11-stable-release-network-isolation.md @@ -502,10 +502,10 @@ Expected: one commit containing only the stable release pipeline update. Run: ```powershell -git --no-pager diff --check d116c373d..HEAD +git --no-pager diff --check d116c373b^..HEAD git --no-pager status --short git --no-pager status --short --ignored -git --no-pager log --oneline --decorate --reverse d116c373d..HEAD +git --no-pager log --oneline --decorate --reverse d116c373b^..HEAD ``` Expected: @@ -514,8 +514,10 @@ Expected: - `git status --short` is empty, confirming tracked/untracked source cleanliness. - `git status --short --ignored` is reported separately and may still list ignored build/cache outputs; those do not count as source changes. -- The log shows the design commit, plan commit, three primary implementation - commits, and two reviewer-requested Gradle policy correction commits. +- The log includes the named milestones in order: design `d116c373b`, plan + `afcae1c19`, primary implementation `849056f7f`, `fcc8d0cc9`, `946b7378f`, + reviewer corrections `fc6f8293f`, `04c5fbedb`, and verification-plan + correction `75fc90433`; later review/documentation commits may follow. - [ ] **Step 2: Verify local Gradle configuration is unaffected** From d70e0ae8dfec729931d6c45cf94694cafb85f82b Mon Sep 17 00:00:00 2001 From: Miller Wang Date: Tue, 11 Aug 2026 19:04:06 +0800 Subject: [PATCH 09/16] docs: correct network isolation commit order Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../plans/2026-08-11-stable-release-network-isolation.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/docs/superpowers/plans/2026-08-11-stable-release-network-isolation.md b/docs/superpowers/plans/2026-08-11-stable-release-network-isolation.md index cd075c637e5..ef1214e91c4 100644 --- a/docs/superpowers/plans/2026-08-11-stable-release-network-isolation.md +++ b/docs/superpowers/plans/2026-08-11-stable-release-network-isolation.md @@ -515,9 +515,11 @@ Expected: - `git status --short --ignored` is reported separately and may still list ignored build/cache outputs; those do not count as source changes. - The log includes the named milestones in order: design `d116c373b`, plan - `afcae1c19`, primary implementation `849056f7f`, `fcc8d0cc9`, `946b7378f`, - reviewer corrections `fc6f8293f`, `04c5fbedb`, and verification-plan - correction `75fc90433`; later review/documentation commits may follow. + `afcae1c19`, Maven primary `849056f7f`, Gradle primary `fcc8d0cc9`, Gradle + reviewer cache fix `fc6f8293f`, pipeline primary `946b7378f`, Atlassian + reviewer fix `04c5fbedb`, verification-plan correction `75fc90433`, and + verification-range correction `96300b929`; later review/documentation commits + may follow. - [ ] **Step 2: Verify local Gradle configuration is unaffected** From 12e4bf511a6589f500152e1da1614b96077b1596 Mon Sep 17 00:00:00 2001 From: Miller Wang Date: Tue, 11 Aug 2026 19:37:06 +0800 Subject: [PATCH 10/16] build: isolate Maven local handoff Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .azure-pipelines/cfs-init.gradle | 72 ++++++++++++++++++++ .azure-pipelines/sign-for-stable-release.yml | 7 +- 2 files changed, 76 insertions(+), 3 deletions(-) diff --git a/.azure-pipelines/cfs-init.gradle b/.azure-pipelines/cfs-init.gradle index 693d8ab6a9e..2b06954ca80 100644 --- a/.azure-pipelines/cfs-init.gradle +++ b/.azure-pipelines/cfs-init.gradle @@ -1,6 +1,7 @@ import org.gradle.api.GradleException import org.gradle.api.artifacts.repositories.MavenArtifactRepository +import java.io.File import java.util.Locale def cfsUrl = System.getenv('CFS_MAVEN_URL') @@ -12,6 +13,44 @@ if (!cfsUrl?.trim() || !cfsToken?.trim()) { 'Failing instead of restoring packages from a public repository.') } +def mavenLocalRepositoryPath = System.getProperty('maven.repo.local') +if (!mavenLocalRepositoryPath?.trim()) { + throw new GradleException( + 'maven.repo.local must be explicitly set when cfs-init.gradle is applied. ' + + 'This pipeline-only init script requires a scoped Maven local repository.') +} + +def utilsReactorModules = [ + ['com.microsoft.azuretools', 'utils'], + ['com.microsoft.azure', 'azure-toolkit-ide-libs'], + ['com.microsoft.azure', 'azure-toolkit-ide-applicationinsights-lib'], + ['com.microsoft.azure', 'azure-toolkit-ide-appservice-lib'], + ['com.microsoft.azure', 'azure-toolkit-ide-arm-lib'], + ['com.microsoft.azure', 'azure-toolkit-ide-cognitiveservices-lib'], + ['com.microsoft.azure', 'azure-toolkit-ide-common-lib'], + ['com.microsoft.azure', 'azure-toolkit-ide-containerapps-lib'], + ['com.microsoft.azure', 'azure-toolkit-ide-containerregistry-lib'], + ['com.microsoft.azure', 'azure-toolkit-ide-containerservice-lib'], + ['com.microsoft.azure', 'azure-toolkit-ide-cosmos-lib'], + ['com.microsoft.azure', 'azure-toolkit-ide-database-lib'], + ['com.microsoft.azure', 'azure-toolkit-ide-eventhubs-lib'], + ['com.microsoft.azure', 'azure-toolkit-ide-keyvault-lib'], + ['com.microsoft.azure', 'azure-toolkit-ide-redis-lib'], + ['com.microsoft.azure', 'azure-toolkit-ide-servicebus-lib'], + ['com.microsoft.azure', 'azure-toolkit-ide-springcloud-lib'], + ['com.microsoft.azure', 'azure-toolkit-ide-storage-lib'], + ['com.microsoft.azure', 'azure-toolkit-ide-vm-lib'], + ['com.microsoft.hdinsight', 'azure-toolkit-ide-hdinsight-libs'], + ['com.microsoft.hdinsight', 'azure-explorer-common'], + ['com.microsoft.hdinsight', 'azure-toolkit-ide-cosmos-spark-lib'], + ['com.microsoft.hdinsight', 'azure-toolkit-ide-hdinsight-spark-lib'], + ['com.microsoft.hdinsight', 'azure-toolkit-ide-sqlserver-spark-lib'], + ['com.microsoft.hdinsight', 'azure-toolkit-ide-synapse-spark-lib'], + ['com.microsoft.hdinsight', 'azuretools-core'], + ['com.microsoft.hdinsight', 'hdinsight-node-common'], + ['com.microsoft.azuretools', 'spark-localrun-mock'], +] as List> + def forbiddenHosts = [ 'repo.maven.apache.org', 'repo1.maven.org', @@ -27,6 +66,22 @@ def normalizeRepositoryPath = { repositoryUrl -> path ?: '/' } +def normalizeRepositoryUri = { repositoryUrl -> + def normalizedUri = (repositoryUrl instanceof URI ? repositoryUrl : uri(repositoryUrl)).normalize() + + if (!normalizedUri?.scheme || normalizedUri.scheme.equalsIgnoreCase('file')) { + return new File(normalizedUri).canonicalFile.toURI().normalize().toString().toLowerCase(Locale.ROOT).replaceAll('/+$', '') + } + + def scheme = normalizedUri.scheme?.toLowerCase(Locale.ROOT) ?: '' + def host = normalizedUri.host?.toLowerCase(Locale.ROOT) ?: '' + def port = normalizedUri.port >= 0 ? ":${normalizedUri.port}" : '' + def path = normalizeRepositoryPath(normalizedUri) + "${scheme}://${host}${port}${path}" +} + +def scopedMavenLocalRepositoryUri = normalizeRepositoryUri(new File(mavenLocalRepositoryPath).canonicalFile.toURI()) + def isAtlassianPublicRepository = { MavenArtifactRepository repository -> def repositoryUrl = repository.url def host = repositoryUrl?.host?.toLowerCase(Locale.ROOT) @@ -34,6 +89,10 @@ def isAtlassianPublicRepository = { MavenArtifactRepository repository -> host == 'maven.atlassian.com' && path == '/repository/public' } +def isScopedMavenLocalRepository = { MavenArtifactRepository repository -> + normalizeRepositoryUri(repository.url) == scopedMavenLocalRepositoryUri +} + def isForbiddenRepository = { MavenArtifactRepository repository -> def repositoryUrl = repository.url def host = repositoryUrl?.host?.toLowerCase(Locale.ROOT) @@ -67,6 +126,18 @@ def restrictAtlassianRepository = { MavenArtifactRepository repository -> } } +def restrictScopedMavenLocalRepository = { MavenArtifactRepository repository -> + if (!isScopedMavenLocalRepository(repository)) { + return + } + + repository.content { + utilsReactorModules.each { group, artifact -> + includeModule group, artifact + } + } +} + def routeToCfs = { MavenArtifactRepository repository -> if (!isForbiddenRepository(repository)) { return @@ -83,6 +154,7 @@ def watchRepositories watchRepositories = { repositories -> repositories.all { repository -> if (repository instanceof MavenArtifactRepository) { + restrictScopedMavenLocalRepository(repository) restrictAtlassianRepository(repository) routeToCfs(repository) } diff --git a/.azure-pipelines/sign-for-stable-release.yml b/.azure-pipelines/sign-for-stable-release.yml index 203138fc8e7..ebe59e63da3 100644 --- a/.azure-pipelines/sign-for-stable-release.yml +++ b/.azure-pipelines/sign-for-stable-release.yml @@ -129,12 +129,13 @@ extends: script: | mvn -v # ./gradlew buildUtils || exit -1 - mvn -s "$(Build.SourcesDirectory)/.azure-pipelines/cfs-settings.xml" clean install -f ./Utils/pom.xml -T 1C "-Dcheckstyle.skip=true" "-Dmaven.test.skip=true" "-Dmaven.javadoc.skip=true" - mvn clean -f ./Utils/pom.xml + mvn -Dmaven.repo.local="$env:CFS_MAVEN_LOCAL_REPOSITORY" -s "$(Build.SourcesDirectory)\.azure-pipelines\cfs-settings.xml" clean install -f "$(Build.SourcesDirectory)\Utils\pom.xml" -T 1C "-Dcheckstyle.skip=true" "-Dmaven.test.skip=true" "-Dmaven.javadoc.skip=true" + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } cd PluginsAndFeatures/azure-toolkit-for-intellij - ./gradlew -I "$(Build.SourcesDirectory)/.azure-pipelines/cfs-init.gradle" clean buildPlugin -s "-Papplicationinsights.key=$(INTELLIJ_KEY)" "-PneedPatchVersion=false" "-Psources=false" "-Porg.gradle.configureondemand=false" "-Porg.gradle.daemon=false" "-Porg.gradle.unsafe.configuration-cache=false" "-Porg.gradle.caching=false" + ./gradlew -Dmaven.repo.local="$env:CFS_MAVEN_LOCAL_REPOSITORY" -I "$(Build.SourcesDirectory)\.azure-pipelines\cfs-init.gradle" clean buildPlugin -s "-Papplicationinsights.key=$(INTELLIJ_KEY)" "-PneedPatchVersion=false" "-Psources=false" "-Porg.gradle.configureondemand=false" "-Porg.gradle.daemon=false" "-Porg.gradle.unsafe.configuration-cache=false" "-Porg.gradle.caching=false" env: USE_STABLE_VERSION: $(IsStableBuild) + CFS_MAVEN_LOCAL_REPOSITORY: $(Agent.TempDirectory)\azure-tools-maven-repository CFS_MAVEN_URL: $(CFS_MAVEN_URL) SYSTEM_ACCESSTOKEN: $(System.AccessToken) - task: PowerShell@2 From bf650d3e71e4f77beec6d0b99dc8116a60665849 Mon Sep 17 00:00:00 2001 From: Miller Wang Date: Tue, 11 Aug 2026 20:32:10 +0800 Subject: [PATCH 11/16] build: make Utils handoff repository exclusive Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .azure-pipelines/cfs-init.gradle | 28 ++++++++++++++++++++++++---- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/.azure-pipelines/cfs-init.gradle b/.azure-pipelines/cfs-init.gradle index 2b06954ca80..bddad2555ed 100644 --- a/.azure-pipelines/cfs-init.gradle +++ b/.azure-pipelines/cfs-init.gradle @@ -20,6 +20,7 @@ if (!mavenLocalRepositoryPath?.trim()) { 'This pipeline-only init script requires a scoped Maven local repository.') } +// Keep this exact allowlist in sync with the Utils reactor outputs built into the scoped Maven local handoff. def utilsReactorModules = [ ['com.microsoft.azuretools', 'utils'], ['com.microsoft.azure', 'azure-toolkit-ide-libs'], @@ -82,6 +83,14 @@ def normalizeRepositoryUri = { repositoryUrl -> def scopedMavenLocalRepositoryUri = normalizeRepositoryUri(new File(mavenLocalRepositoryPath).canonicalFile.toURI()) +def includeUtilsReactorModules = { contentFilter -> + utilsReactorModules.each { group, artifact -> + contentFilter.includeModule(group, artifact) + } +} + +def scopedMavenLocalExclusiveRegistered = Collections.newSetFromMap(new IdentityHashMap<>()) + def isAtlassianPublicRepository = { MavenArtifactRepository repository -> def repositoryUrl = repository.url def host = repositoryUrl?.host?.toLowerCase(Locale.ROOT) @@ -126,14 +135,23 @@ def restrictAtlassianRepository = { MavenArtifactRepository repository -> } } -def restrictScopedMavenLocalRepository = { MavenArtifactRepository repository -> +def restrictScopedMavenLocalRepository = { repositories, MavenArtifactRepository repository -> if (!isScopedMavenLocalRepository(repository)) { return } repository.content { - utilsReactorModules.each { group, artifact -> - includeModule group, artifact + includeUtilsReactorModules(delegate) + } + + if (!scopedMavenLocalExclusiveRegistered.add(repositories)) { + return + } + + repositories.exclusiveContent { spec -> + spec.forRepositories(repository) + spec.filter { contentFilter -> + includeUtilsReactorModules(contentFilter) } } } @@ -154,7 +172,7 @@ def watchRepositories watchRepositories = { repositories -> repositories.all { repository -> if (repository instanceof MavenArtifactRepository) { - restrictScopedMavenLocalRepository(repository) + restrictScopedMavenLocalRepository(repositories, repository) restrictAtlassianRepository(repository) routeToCfs(repository) } @@ -198,6 +216,7 @@ beforeSettings { settings -> } // Preserve local and vendor-specific settings repositories while rewriting forbidden Maven sources. + watchRepositories(settings.buildscript.repositories) watchRepositories(settings.dependencyResolutionManagement.repositories) } @@ -209,6 +228,7 @@ allprojects { project -> settingsEvaluated { settings -> assertNoForbiddenRepositories('plugin management repositories', settings.pluginManagement.repositories) + assertNoForbiddenRepositories('settings buildscript repositories', settings.buildscript.repositories) assertNoForbiddenRepositories( 'settings dependency resolution repositories', settings.dependencyResolutionManagement.repositories) From 391ff6dea5e916ef2fc2d959b853c79c857338ef Mon Sep 17 00:00:00 2001 From: Miller Wang Date: Tue, 11 Aug 2026 21:01:31 +0800 Subject: [PATCH 12/16] docs: document exclusive Utils handoff Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- ...-08-11-stable-release-network-isolation.md | 570 ++++++++++++++---- 1 file changed, 450 insertions(+), 120 deletions(-) diff --git a/docs/superpowers/plans/2026-08-11-stable-release-network-isolation.md b/docs/superpowers/plans/2026-08-11-stable-release-network-isolation.md index ef1214e91c4..aff3b266993 100644 --- a/docs/superpowers/plans/2026-08-11-stable-release-network-isolation.md +++ b/docs/superpowers/plans/2026-08-11-stable-release-network-isolation.md @@ -4,7 +4,7 @@ **Goal:** Enable 1ES Network Isolation for the stable release pipeline and force ordinary Maven and Gradle package restores through the `vscjava` Central Feed Service. -**Architecture:** Add pipeline-only Maven and Gradle policy files under `.azure-pipelines`. Maven mirrors `central` to CFS; a Gradle init script redirects general-purpose public repositories to CFS while preserving Maven Local and specialized vendor repositories. The release pipeline imports the non-secret feed URL, maps `System.AccessToken` only into the build step, and removes the network-isolation opt-out. +**Architecture:** Add pipeline-only Maven and Gradle policy files under `.azure-pipelines`. The Build Plugin step sets one pipeline-scoped Maven local repository under `$(Agent.TempDirectory)\azure-tools-maven-repository`, passes that path via `maven.repo.local` to both Maven and Gradle, and uses the Gradle init script to make the exact 28 Utils reactor coordinates exclusive to that local handoff. All ordinary packages still route through CFS, while the Atlassian Microba exception and purpose-specific vendor repositories remain narrowly scoped direct endpoints. **Tech Stack:** Azure Pipelines YAML, Maven settings XML, Gradle 9.1 Groovy init scripts, PowerShell, 1ES Pipeline Templates @@ -14,8 +14,8 @@ - Create `.azure-pipelines/cfs-variables.yml`: non-secret CFS endpoint shared by Maven and Gradle. - Create `.azure-pipelines/cfs-settings.xml`: pipeline-only Maven mirror and server configuration. -- Create `.azure-pipelines/cfs-init.gradle`: pipeline-only Gradle repository redirection, validation, and fail-closed behavior. -- Modify `.azure-pipelines/sign-for-stable-release.yml`: enable network isolation and apply the CFS policies to the build step. +- Create `.azure-pipelines/cfs-init.gradle`: pipeline-only Gradle repository redirection, explicit `maven.repo.local` validation, exact Utils allowlisting, exclusive local handoff, and fail-closed behavior. +- Modify `.azure-pipelines/sign-for-stable-release.yml`: enable network isolation, set `CFS_MAVEN_LOCAL_REPOSITORY`, and apply the Maven/Gradle CFS policies plus both `maven.repo.local` CLI properties to the build step. - Keep `docs/superpowers/specs/2026-08-11-stable-release-network-isolation-design.md` as the approved design record. ### Task 1: Add Shared CFS and Maven Configuration @@ -133,7 +133,7 @@ git commit -m "build: add Maven CFS configuration" -m "Co-authored-by: Copilot < Expected: one commit containing the two new files. -### Task 2: Add the Gradle Repository Policy +### Task 2: Add the Gradle Repository Policy and Scoped Utils Handoff **Files:** - Create: `.azure-pipelines/cfs-init.gradle` @@ -156,16 +156,11 @@ Expected: `PASS: Gradle CFS policy does not exist yet`. Create `.azure-pipelines/cfs-init.gradle`: ```groovy +import org.gradle.api.GradleException import org.gradle.api.artifacts.repositories.MavenArtifactRepository -/* - * Pipeline-only repository policy for 1ES Network Isolation. - * - * General-purpose Maven repositories are redirected to CFS. Maven Local and - * repositories that serve CFS-incompatible vendor artifacts remain unchanged. - * The script is applied only by sign-for-stable-release.yml, so local developer - * builds keep their existing repository configuration. - */ +import java.io.File +import java.util.Locale def cfsUrl = System.getenv('CFS_MAVEN_URL') def cfsToken = System.getenv('SYSTEM_ACCESSTOKEN') @@ -173,9 +168,48 @@ def cfsToken = System.getenv('SYSTEM_ACCESSTOKEN') if (!cfsUrl?.trim() || !cfsToken?.trim()) { throw new GradleException( 'CFS_MAVEN_URL and SYSTEM_ACCESSTOKEN must both be set when cfs-init.gradle is applied. ' + - 'Failing instead of restoring packages from a public repository.') + 'Failing instead of restoring packages from a public repository.') } +def mavenLocalRepositoryPath = System.getProperty('maven.repo.local') +if (!mavenLocalRepositoryPath?.trim()) { + throw new GradleException( + 'maven.repo.local must be explicitly set when cfs-init.gradle is applied. ' + + 'This pipeline-only init script requires a scoped Maven local repository.') +} + +// Keep this exact allowlist in sync with the Utils reactor outputs built into the scoped Maven local handoff. +def utilsReactorModules = [ + ['com.microsoft.azuretools', 'utils'], + ['com.microsoft.azure', 'azure-toolkit-ide-libs'], + ['com.microsoft.azure', 'azure-toolkit-ide-applicationinsights-lib'], + ['com.microsoft.azure', 'azure-toolkit-ide-appservice-lib'], + ['com.microsoft.azure', 'azure-toolkit-ide-arm-lib'], + ['com.microsoft.azure', 'azure-toolkit-ide-cognitiveservices-lib'], + ['com.microsoft.azure', 'azure-toolkit-ide-common-lib'], + ['com.microsoft.azure', 'azure-toolkit-ide-containerapps-lib'], + ['com.microsoft.azure', 'azure-toolkit-ide-containerregistry-lib'], + ['com.microsoft.azure', 'azure-toolkit-ide-containerservice-lib'], + ['com.microsoft.azure', 'azure-toolkit-ide-cosmos-lib'], + ['com.microsoft.azure', 'azure-toolkit-ide-database-lib'], + ['com.microsoft.azure', 'azure-toolkit-ide-eventhubs-lib'], + ['com.microsoft.azure', 'azure-toolkit-ide-keyvault-lib'], + ['com.microsoft.azure', 'azure-toolkit-ide-redis-lib'], + ['com.microsoft.azure', 'azure-toolkit-ide-servicebus-lib'], + ['com.microsoft.azure', 'azure-toolkit-ide-springcloud-lib'], + ['com.microsoft.azure', 'azure-toolkit-ide-storage-lib'], + ['com.microsoft.azure', 'azure-toolkit-ide-vm-lib'], + ['com.microsoft.hdinsight', 'azure-toolkit-ide-hdinsight-libs'], + ['com.microsoft.hdinsight', 'azure-explorer-common'], + ['com.microsoft.hdinsight', 'azure-toolkit-ide-cosmos-spark-lib'], + ['com.microsoft.hdinsight', 'azure-toolkit-ide-hdinsight-spark-lib'], + ['com.microsoft.hdinsight', 'azure-toolkit-ide-sqlserver-spark-lib'], + ['com.microsoft.hdinsight', 'azure-toolkit-ide-synapse-spark-lib'], + ['com.microsoft.hdinsight', 'azuretools-core'], + ['com.microsoft.hdinsight', 'hdinsight-node-common'], + ['com.microsoft.azuretools', 'spark-localrun-mock'], +] as List> + def forbiddenHosts = [ 'repo.maven.apache.org', 'repo1.maven.org', @@ -186,140 +220,365 @@ def forbiddenHosts = [ 'maven-central.storage-download.googleapis.com', ] as Set -def isForbiddenRepository = { URI uri -> - def host = uri?.host?.toLowerCase(Locale.ROOT) - def path = uri?.path?.toLowerCase(Locale.ROOT) ?: '' - host in forbiddenHosts || - (host == 'cache-redirector.jetbrains.com' && - (path.startsWith('/repo1.maven.org/') || path.startsWith('/plugins.gradle.org/'))) +def normalizeRepositoryPath = { repositoryUrl -> + def path = (repositoryUrl?.path ?: '').toLowerCase(Locale.ROOT).replaceAll('/+$', '') + path ?: '/' } -def routeToCfs = { MavenArtifactRepository repository -> - if (isForbiddenRepository(repository.url)) { - repository.setUrl(cfsUrl) - repository.credentials { credentials -> - credentials.username = 'AzureDevOps' - credentials.password = cfsToken +def normalizeRepositoryUri = { repositoryUrl -> + def normalizedUri = (repositoryUrl instanceof URI ? repositoryUrl : uri(repositoryUrl)).normalize() + + if (!normalizedUri?.scheme || normalizedUri.scheme.equalsIgnoreCase('file')) { + return new File(normalizedUri).canonicalFile.toURI().normalize().toString().toLowerCase(Locale.ROOT).replaceAll('/+$', '') + } + + def scheme = normalizedUri.scheme?.toLowerCase(Locale.ROOT) ?: '' + def host = normalizedUri.host?.toLowerCase(Locale.ROOT) ?: '' + def port = normalizedUri.port >= 0 ? ":${normalizedUri.port}" : '' + def path = normalizeRepositoryPath(normalizedUri) + "${scheme}://${host}${port}${path}" +} + +def scopedMavenLocalRepositoryUri = normalizeRepositoryUri(new File(mavenLocalRepositoryPath).canonicalFile.toURI()) + +def includeUtilsReactorModules = { contentFilter -> + utilsReactorModules.each { group, artifact -> + contentFilter.includeModule(group, artifact) + } +} + +def scopedMavenLocalExclusiveRegistered = Collections.newSetFromMap(new IdentityHashMap<>()) + +def isAtlassianPublicRepository = { MavenArtifactRepository repository -> + def repositoryUrl = repository.url + def host = repositoryUrl?.host?.toLowerCase(Locale.ROOT) + def path = normalizeRepositoryPath(repositoryUrl) + host == 'maven.atlassian.com' && path == '/repository/public' +} + +def isScopedMavenLocalRepository = { MavenArtifactRepository repository -> + normalizeRepositoryUri(repository.url) == scopedMavenLocalRepositoryUri +} + +def isForbiddenRepository = { MavenArtifactRepository repository -> + def repositoryUrl = repository.url + def host = repositoryUrl?.host?.toLowerCase(Locale.ROOT) + def path = normalizeRepositoryPath(repositoryUrl) + + if (!host) { + return false + } + + if (forbiddenHosts.contains(host)) { + return true + } + + // Preserve JetBrains-specialized paths such as /intellij-dependencies. + if (host == 'cache-redirector.jetbrains.com') { + def upstreamHost = path.tokenize('/').find() + return upstreamHost != null && forbiddenHosts.contains(upstreamHost) + } + + return false +} + +def restrictAtlassianRepository = { MavenArtifactRepository repository -> + if (!isAtlassianPublicRepository(repository)) { + return + } + + // Microba is only available here; keep Atlassian only for that vendor group. + repository.content { + includeGroup 'com.michaelbaranov' + } +} + +def restrictScopedMavenLocalRepository = { repositories, MavenArtifactRepository repository -> + if (!isScopedMavenLocalRepository(repository)) { + return + } + + repository.content { + includeUtilsReactorModules(delegate) + } + + if (!scopedMavenLocalExclusiveRegistered.add(repositories)) { + return + } + + repositories.exclusiveContent { spec -> + spec.forRepositories(repository) + spec.filter { contentFilter -> + includeUtilsReactorModules(contentFilter) } } } -def watchRepositories = { handler -> - handler.withType(MavenArtifactRepository).configureEach { repository -> - routeToCfs(repository) +def routeToCfs = { MavenArtifactRepository repository -> + if (!isForbiddenRepository(repository)) { + return + } + + repository.url = uri(cfsUrl) + repository.credentials { + username = 'AzureDevOps' + password = cfsToken } } -def addCfsRepository = { handler -> - handler.maven { repository -> - repository.name = 'vscjava' - repository.setUrl(cfsUrl) - repository.credentials { credentials -> - credentials.username = 'AzureDevOps' - credentials.password = cfsToken +def watchRepositories +watchRepositories = { repositories -> + repositories.all { repository -> + if (repository instanceof MavenArtifactRepository) { + restrictScopedMavenLocalRepository(repositories, repository) + restrictAtlassianRepository(repository) + routeToCfs(repository) } } } -def assertNoForbiddenRepositories = { String owner, handler -> - def forbidden = handler.withType(MavenArtifactRepository).findAll { repository -> - isForbiddenRepository(repository.url) +def addCfsRepository = { repositories -> + repositories.maven { repository -> + repository.name = 'vscjava' + repository.url = uri(cfsUrl) + repository.credentials { + username = 'AzureDevOps' + password = cfsToken + } } - if (!forbidden.isEmpty()) { - def descriptions = forbidden.collect { repository -> - "${repository.name} (${repository.url})" - }.join(', ') +} + +def describeRepository = { MavenArtifactRepository repository -> + "${repository.name ?: repository.displayName} -> ${repository.url}" +} + +def assertNoForbiddenRepositories = { String scope, repositories -> + def offendingRepositories = repositories.findAll { repository -> + repository instanceof MavenArtifactRepository && + isForbiddenRepository(repository as MavenArtifactRepository) + } as List + + if (!offendingRepositories.isEmpty()) { throw new GradleException( - "Forbidden public repositories remain in ${owner}: ${descriptions}") + "Forbidden public Maven repository remained in ${scope}: " + + offendingRepositories.collect(describeRepository).join(', ')) } } beforeSettings { settings -> + // Pipeline-only policy: route general-purpose public Maven traffic through authenticated CFS. settings.pluginManagement.repositories { repositories -> repositories.clear() watchRepositories(repositories) addCfsRepository(repositories) } + // Preserve local and vendor-specific settings repositories while rewriting forbidden Maven sources. + watchRepositories(settings.buildscript.repositories) watchRepositories(settings.dependencyResolutionManagement.repositories) } allprojects { project -> + // Keep watching project and buildscript repositories because plugins can add them later. watchRepositories(project.repositories) watchRepositories(project.buildscript.repositories) } settingsEvaluated { settings -> + assertNoForbiddenRepositories('plugin management repositories', settings.pluginManagement.repositories) + assertNoForbiddenRepositories('settings buildscript repositories', settings.buildscript.repositories) assertNoForbiddenRepositories( - 'plugin management', - settings.pluginManagement.repositories) - assertNoForbiddenRepositories( - 'dependency resolution management', + 'settings dependency resolution repositories', settings.dependencyResolutionManagement.repositories) } projectsEvaluated { gradle.rootProject.allprojects { project -> - assertNoForbiddenRepositories( - "${project.path} project repositories", - project.repositories) - assertNoForbiddenRepositories( - "${project.path} buildscript repositories", - project.buildscript.repositories) + assertNoForbiddenRepositories("project ${project.path} repositories", project.repositories) + assertNoForbiddenRepositories("project ${project.path} buildscript repositories", project.buildscript.repositories) } } ``` -- [ ] **Step 3: Verify missing credentials fail before project evaluation** +- [ ] **Step 3: Verify fail-fast validation for both CFS credentials and the scoped local handoff** Run: ```powershell -$tempProject = Join-Path $env:TEMP 'azure-tools-cfs-init-test' -if (Test-Path $tempProject) { - Remove-Item $tempProject -Recurse -Force -} -New-Item -ItemType Directory -Path $tempProject | Out-Null -Set-Content -Path (Join-Path $tempProject 'settings.gradle') -Value "rootProject.name = 'cfs-init-test'" -Set-Content -Path (Join-Path $tempProject 'build.gradle') -Value '' - -Remove-Item Env:CFS_MAVEN_URL -ErrorAction SilentlyContinue -Remove-Item Env:SYSTEM_ACCESSTOKEN -ErrorAction SilentlyContinue +$testRoot = Join-Path (Get-Location) '.scratch\cfs-init-validation' +$projectDir = Join-Path $testRoot 'project' +$dummyRepo = Join-Path $testRoot 'scoped-m2' +Remove-Item $testRoot -Recurse -Force -ErrorAction SilentlyContinue +New-Item -ItemType Directory -Path $projectDir, $dummyRepo -Force | Out-Null +Set-Content -Path (Join-Path $projectDir 'settings.gradle') -Value "rootProject.name = 'cfs-init-validation'" +Set-Content -Path (Join-Path $projectDir 'build.gradle') -Value '' Push-Location 'PluginsAndFeatures\azure-toolkit-for-intellij' try { - $output = .\gradlew.bat -p $tempProject help ` + $env:CFS_MAVEN_URL = 'https://example.invalid/vscjava/maven/v1' + $env:SYSTEM_ACCESSTOKEN = 'test-token' + $missingLocalOutput = .\gradlew.bat -p $projectDir help ` --init-script '..\..\.azure-pipelines\cfs-init.gradle' ` --no-daemon --no-configuration-cache 2>&1 - $exitCode = $LASTEXITCODE + $missingLocalExitCode = $LASTEXITCODE + + Remove-Item Env:CFS_MAVEN_URL -ErrorAction SilentlyContinue + Remove-Item Env:SYSTEM_ACCESSTOKEN -ErrorAction SilentlyContinue + $missingCredentialsOutput = .\gradlew.bat -p $projectDir help ` + --init-script '..\..\.azure-pipelines\cfs-init.gradle' ` + --no-daemon --no-configuration-cache "-Dmaven.repo.local=$dummyRepo" 2>&1 + $missingCredentialsExitCode = $LASTEXITCODE } finally { Pop-Location + Remove-Item Env:CFS_MAVEN_URL -ErrorAction SilentlyContinue + Remove-Item Env:SYSTEM_ACCESSTOKEN -ErrorAction SilentlyContinue + Remove-Item $testRoot -Recurse -Force -ErrorAction SilentlyContinue } -if ($exitCode -eq 0) { - throw 'Expected Gradle to reject missing CFS configuration' +if ($missingLocalExitCode -eq 0 -or ($missingLocalOutput -join "`n") -notmatch 'maven\.repo\.local must be explicitly set') { + throw "Expected the scoped local handoff validation error:`n$($missingLocalOutput -join "`n")" } -if (($output -join "`n") -notmatch 'CFS_MAVEN_URL and SYSTEM_ACCESSTOKEN must both be set') { - throw "Gradle failed for an unexpected reason:`n$($output -join "`n")" +if ($missingCredentialsExitCode -eq 0 -or ($missingCredentialsOutput -join "`n") -notmatch 'CFS_MAVEN_URL and SYSTEM_ACCESSTOKEN must both be set') { + throw "Expected the CFS credential validation error:`n$($missingCredentialsOutput -join "`n")" } -Write-Host 'PASS: Gradle policy fails closed when credentials are missing' +Write-Host 'PASS: Gradle rejects missing scoped local handoff and missing CFS credentials' +``` + +Expected: `PASS: Gradle rejects missing scoped local handoff and missing CFS credentials`. + +- [ ] **Step 4: Verify the exact local allowlist stays synchronized with the Utils reactor** + +Run: + +```powershell +@' +import re +import xml.etree.ElementTree as ET +from pathlib import Path + +root = Path.cwd() +utils_root = root / 'Utils' +ns = {'m': 'http://maven.apache.org/POM/4.0.0'} + + +def pom_coords(pom_path: Path): + tree = ET.parse(pom_path) + group = tree.findtext('m:groupId', namespaces=ns) + if group is None: + group = tree.findtext('m:parent/m:groupId', namespaces=ns) + artifact = tree.findtext('m:artifactId', namespaces=ns) + return group, artifact + + +reactor = [] +for pom_path in [ + utils_root / 'pom.xml', + utils_root / 'azure-toolkit-ide-libs' / 'pom.xml', + utils_root / 'azure-toolkit-ide-hdinsight-libs' / 'pom.xml', +]: + tree = ET.parse(pom_path) + reactor.append(pom_coords(pom_path)) + for module in tree.findall('m:modules/m:module', ns): + reactor.append(pom_coords(pom_path.parent / module.text / 'pom.xml')) + +reactor = list(dict.fromkeys(reactor)) +allowlist = re.findall(r"\['([^']+)', '([^']+)'\]", (root / '.azure-pipelines' / 'cfs-init.gradle').read_text(encoding='utf-8')) +missing = sorted(set(reactor) - set(allowlist)) +extra = sorted(set(allowlist) - set(reactor)) + +if len(reactor) != 28: + raise SystemExit(f'Expected 28 Utils reactor coordinates, found {len(reactor)}: {reactor}') +if missing or extra: + raise SystemExit(f'Allowlist drift detected. Missing={missing} Extra={extra}') + +print('PASS: Gradle allowlist exactly matches all 28 Utils reactor coordinates') +'@ | python - ``` -Expected: `PASS: Gradle policy fails closed when credentials are missing`. +Expected: `PASS: Gradle allowlist exactly matches all 28 Utils reactor coordinates`. -- [ ] **Step 4: Verify public repositories are redirected and vendor/local repositories remain** +- [ ] **Step 5: Verify normalized scoped-local matching, future handler coverage, exclusive provenance, and no fallback** Run: ```powershell -$tempProject = Join-Path $env:TEMP 'azure-tools-cfs-init-test' +$testRoot = Join-Path (Get-Location) '.scratch\cfs-init-provenance' +$projectDir = Join-Path $testRoot 'project' +$scopedRepo = Join-Path $testRoot 'scoped-m2' +$cfsRepo = Join-Path $testRoot 'cfs-m2' +Remove-Item $testRoot -Recurse -Force -ErrorAction SilentlyContinue +New-Item -ItemType Directory -Path $projectDir, $scopedRepo, $cfsRepo -Force | Out-Null + +function New-MavenStubArtifact { + param( + [string]$RepoRoot, + [string]$GroupId, + [string]$ArtifactId, + [string]$Version + ) + + $groupPath = $GroupId -replace '\.', '\\' + $artifactDir = Join-Path $RepoRoot "$groupPath\$ArtifactId\$Version" + New-Item -ItemType Directory -Path $artifactDir -Force | Out-Null + @" + + 4.0.0 + $GroupId + $ArtifactId + $Version + +"@ | Set-Content -Path (Join-Path $artifactDir "$ArtifactId-$Version.pom") + [System.IO.File]::WriteAllBytes((Join-Path $artifactDir "$ArtifactId-$Version.jar"), [byte[]]@()) +} + +New-MavenStubArtifact $scopedRepo 'com.microsoft.azure' 'azure-toolkit-ide-common-lib' '1.0.0-test' +New-MavenStubArtifact $scopedRepo 'org.example' 'warmed-cache-only' '1.0.0-test' +New-MavenStubArtifact $scopedRepo 'com.microsoft.azure' 'azure-toolkit-common-lib' '1.0.0-test' + +New-MavenStubArtifact $cfsRepo 'org.example' 'warmed-cache-only' '1.0.0-test' +New-MavenStubArtifact $cfsRepo 'com.microsoft.azure' 'azure-toolkit-common-lib' '1.0.0-test' +New-MavenStubArtifact $cfsRepo 'com.microsoft.azure' 'azure-toolkit-ide-appservice-lib' '1.0.0-test' + +Set-Content -Path (Join-Path $projectDir 'settings.gradle') -Value "rootProject.name = 'cfs-init-provenance'" @' +def scopedRepo = new File(System.getProperty('maven.repo.local')).canonicalFile + repositories { + maven { + name = 'manualScopedVariant' + url = uri(scopedRepo.toURI().toString() + '../' + scopedRepo.name + '/./') + } mavenCentral() - mavenLocal() - maven { url = uri('https://cache-redirector.jetbrains.com/repo1.maven.org/maven2') } - maven { url = uri('https://cache-redirector.jetbrains.com/intellij-dependencies') } - maven { url = uri('https://maven.atlassian.com/repository/public') } + maven { + name = 'intellijVendor' + url = uri('https://cache-redirector.jetbrains.com/intellij-dependencies') + } + maven { + name = 'atlassianPublic' + url = uri('https://maven.atlassian.com/repository/public') + } +} + +afterEvaluate { + repositories.mavenLocal() + repositories.maven { + name = 'lateForbidden' + url = uri('https://cache-redirector.jetbrains.com/repo1.maven.org/maven2') + } +} + +configurations { + localProbe + thirdPartyProbe + microsoftProbe + missingLocalProbe +} + +dependencies { + localProbe 'com.microsoft.azure:azure-toolkit-ide-common-lib:1.0.0-test' + thirdPartyProbe 'org.example:warmed-cache-only:1.0.0-test' + microsoftProbe 'com.microsoft.azure:azure-toolkit-common-lib:1.0.0-test' + missingLocalProbe 'com.microsoft.azure:azure-toolkit-ide-appservice-lib:1.0.0-test' } tasks.register('printRepositories') { @@ -330,49 +589,95 @@ tasks.register('printRepositories') { } } } -'@ | Set-Content -Path (Join-Path $tempProject 'build.gradle') -$env:CFS_MAVEN_URL = 'https://example.invalid/vscjava/maven/v1' +tasks.register('printProvenance') { + doLast { + [ + localProbe: configurations.localProbe.singleFile, + thirdPartyProbe: configurations.thirdPartyProbe.singleFile, + microsoftProbe: configurations.microsoftProbe.singleFile, + ].each { name, file -> + println("PROVENANCE=${name}|${file}") + } + } +} + +tasks.register('resolveMissingLocal') { + doLast { + configurations.missingLocalProbe.resolve() + } +} +'@ | Set-Content -Path (Join-Path $projectDir 'build.gradle') + +$cfsRepoUri = 'file:///' + ((Resolve-Path $cfsRepo).Path -replace '\\', '/') +if (-not $cfsRepoUri.EndsWith('/')) { + $cfsRepoUri += '/' +} +$env:CFS_MAVEN_URL = $cfsRepoUri $env:SYSTEM_ACCESSTOKEN = 'test-token' Push-Location 'PluginsAndFeatures\azure-toolkit-for-intellij' try { - $output = .\gradlew.bat -p $tempProject printRepositories ` + $successOutput = .\gradlew.bat -p $projectDir printRepositories printProvenance ` --init-script '..\..\.azure-pipelines\cfs-init.gradle' ` - --no-daemon --no-configuration-cache 2>&1 - $exitCode = $LASTEXITCODE + --no-daemon --no-configuration-cache "-Dmaven.repo.local=$scopedRepo" 2>&1 + $successExitCode = $LASTEXITCODE + + $failureOutput = .\gradlew.bat -p $projectDir resolveMissingLocal ` + --init-script '..\..\.azure-pipelines\cfs-init.gradle' ` + --no-daemon --no-configuration-cache "-Dmaven.repo.local=$scopedRepo" 2>&1 + $failureExitCode = $LASTEXITCODE } finally { Pop-Location Remove-Item Env:CFS_MAVEN_URL -ErrorAction SilentlyContinue Remove-Item Env:SYSTEM_ACCESSTOKEN -ErrorAction SilentlyContinue - Remove-Item $tempProject -Recurse -Force + Remove-Item $testRoot -Recurse -Force -ErrorAction SilentlyContinue +} + +if ($successExitCode -ne 0) { + throw "Gradle provenance test failed:`n$($successOutput -join "`n")" } +$successText = $successOutput -join "`n" +$localJar = Join-Path $scopedRepo 'com\microsoft\azure\azure-toolkit-ide-common-lib\1.0.0-test\azure-toolkit-ide-common-lib-1.0.0-test.jar' +$thirdPartyCfsJar = Join-Path $cfsRepo 'org\example\warmed-cache-only\1.0.0-test\warmed-cache-only-1.0.0-test.jar' +$msCfsJar = Join-Path $cfsRepo 'com\microsoft\azure\azure-toolkit-common-lib\1.0.0-test\azure-toolkit-common-lib-1.0.0-test.jar' -if ($exitCode -ne 0) { - throw "Gradle repository test failed:`n$($output -join "`n")" +if ($successText -notmatch [regex]::Escape("REPOSITORY=lateForbidden|$cfsRepoUri")) { + throw "Late-added forbidden repository was not rewritten to CFS:`n$successText" } -$text = $output -join "`n" -if ($text -match 'repo\.maven\.apache\.org|repo1\.maven\.org') { - throw "A forbidden repository remains:`n$text" +if ($successText -notmatch 'REPOSITORY=atlassianPublic\|https://maven\.atlassian\.com/repository/public') { + throw "Atlassian exception was not preserved:`n$successText" } -if ($text -notmatch 'https://example\.invalid/vscjava/maven/v1') { - throw "CFS replacement was not present:`n$text" +if ($successText -notmatch 'REPOSITORY=intellijVendor\|https://cache-redirector\.jetbrains\.com/intellij-dependencies') { + throw "JetBrains vendor repository was not preserved:`n$successText" } -if ($text -notmatch 'cache-redirector\.jetbrains\.com/intellij-dependencies') { - throw "The IntelliJ dependency repository was not preserved:`n$text" +if ($successText -notmatch [regex]::Escape("PROVENANCE=localProbe|$localJar")) { + throw "Allowlisted Utils module did not resolve from the scoped local handoff:`n$successText" } -if ($text -notmatch 'maven\.atlassian\.com/repository/public') { - throw "The Atlassian repository was not preserved:`n$text" +if ($successText -notmatch [regex]::Escape("PROVENANCE=thirdPartyProbe|$thirdPartyCfsJar")) { + throw "Third-party dependency did not resolve from CFS:`n$successText" } -if ($text -notmatch 'MavenLocal') { - throw "Maven Local was not preserved:`n$text" +if ($successText -notmatch [regex]::Escape("PROVENANCE=microsoftProbe|$msCfsJar")) { + throw "Non-reactor Microsoft dependency did not resolve from CFS:`n$successText" } -Write-Host 'PASS: Gradle repositories follow the approved policy' +if ($successText -match [regex]::Escape((Join-Path $scopedRepo 'org\example\warmed-cache-only'))) { + throw "Third-party warmed-cache content leaked through the scoped local handoff:`n$successText" +} +if ($successText -match [regex]::Escape((Join-Path $scopedRepo 'com\microsoft\azure\azure-toolkit-common-lib'))) { + throw "Non-reactor Microsoft warmed-cache content leaked through the scoped local handoff:`n$successText" +} +if ($failureExitCode -eq 0) { + throw 'Expected an allowlisted module that is absent locally to fail' +} +if (($failureOutput -join "`n") -notmatch 'Could not find com\.microsoft\.azure:azure-toolkit-ide-appservice-lib:1\.0\.0-test') { + throw "Gradle failed for an unexpected reason:`n$($failureOutput -join "`n")" +} +Write-Host 'PASS: Gradle keeps the scoped local handoff exclusive to allowlisted Utils modules' ``` -Expected: `PASS: Gradle repositories follow the approved policy`. +Expected: `PASS: Gradle keeps the scoped local handoff exclusive to allowlisted Utils modules`. -- [ ] **Step 5: Commit the Gradle policy** +- [ ] **Step 6: Commit the Gradle policy** Run: @@ -439,12 +744,13 @@ Replace the `Build Plugin` script and environment block with: script: | mvn -v # ./gradlew buildUtils || exit -1 - mvn clean install --settings "$(Build.SourcesDirectory)\.azure-pipelines\cfs-settings.xml" -f ./Utils/pom.xml -T 1C "-Dcheckstyle.skip=true" "-Dmaven.test.skip=true" "-Dmaven.javadoc.skip=true" + mvn -Dmaven.repo.local="$env:CFS_MAVEN_LOCAL_REPOSITORY" -s "$(Build.SourcesDirectory)\.azure-pipelines\cfs-settings.xml" clean install -f "$(Build.SourcesDirectory)\Utils\pom.xml" -T 1C "-Dcheckstyle.skip=true" "-Dmaven.test.skip=true" "-Dmaven.javadoc.skip=true" if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } cd PluginsAndFeatures/azure-toolkit-for-intellij - ./gradlew clean buildPlugin --init-script "$(Build.SourcesDirectory)\.azure-pipelines\cfs-init.gradle" -s "-Papplicationinsights.key=$(INTELLIJ_KEY)" "-PneedPatchVersion=false" "-Psources=false" "-Porg.gradle.configureondemand=false" "-Porg.gradle.daemon=false" "-Porg.gradle.unsafe.configuration-cache=false" "-Porg.gradle.caching=false" + ./gradlew -Dmaven.repo.local="$env:CFS_MAVEN_LOCAL_REPOSITORY" -I "$(Build.SourcesDirectory)\.azure-pipelines\cfs-init.gradle" clean buildPlugin -s "-Papplicationinsights.key=$(INTELLIJ_KEY)" "-PneedPatchVersion=false" "-Psources=false" "-Porg.gradle.configureondemand=false" "-Porg.gradle.daemon=false" "-Porg.gradle.unsafe.configuration-cache=false" "-Porg.gradle.caching=false" env: USE_STABLE_VERSION: $(IsStableBuild) + CFS_MAVEN_LOCAL_REPOSITORY: $(Agent.TempDirectory)\azure-tools-maven-repository CFS_MAVEN_URL: $(CFS_MAVEN_URL) SYSTEM_ACCESSTOKEN: $(System.AccessToken) ``` @@ -457,8 +763,9 @@ Run: $pipeline = Get-Content '.azure-pipelines\sign-for-stable-release.yml' -Raw $required = @( '/.azure-pipelines/cfs-variables.yml@self', - '--settings "$(Build.SourcesDirectory)\.azure-pipelines\cfs-settings.xml"', - '--init-script "$(Build.SourcesDirectory)\.azure-pipelines\cfs-init.gradle"', + '-Dmaven.repo.local="$env:CFS_MAVEN_LOCAL_REPOSITORY" -s "$(Build.SourcesDirectory)\.azure-pipelines\cfs-settings.xml"', + '-Dmaven.repo.local="$env:CFS_MAVEN_LOCAL_REPOSITORY" -I "$(Build.SourcesDirectory)\.azure-pipelines\cfs-init.gradle"', + 'CFS_MAVEN_LOCAL_REPOSITORY: $(Agent.TempDirectory)\azure-tools-maven-repository', 'CFS_MAVEN_URL: $(CFS_MAVEN_URL)', 'SYSTEM_ACCESSTOKEN: $(System.AccessToken)' ) @@ -473,10 +780,16 @@ if ($pipeline -match 'disableNetworkIsolation') { if (($pipeline | Select-String -Pattern 'SYSTEM_ACCESSTOKEN:' -AllMatches).Matches.Count -ne 1) { throw 'System.AccessToken must be scoped to exactly one build step' } -Write-Host 'PASS: stable release pipeline CFS wiring is complete' +if (($pipeline | Select-String -Pattern 'CFS_MAVEN_LOCAL_REPOSITORY:' -AllMatches).Matches.Count -ne 1) { + throw 'CFS_MAVEN_LOCAL_REPOSITORY must be scoped to exactly one build step' +} +if (($pipeline | Select-String -Pattern '-Dmaven\.repo\.local=' -AllMatches).Matches.Count -ne 2) { + throw 'Expected exactly two maven.repo.local command-line properties' +} +Write-Host 'PASS: stable release pipeline CFS wiring and scoped local handoff are complete' ``` -Expected: `PASS: stable release pipeline CFS wiring is complete`. +Expected: `PASS: stable release pipeline CFS wiring and scoped local handoff are complete`. - [ ] **Step 6: Commit the pipeline wiring** @@ -514,12 +827,15 @@ Expected: - `git status --short` is empty, confirming tracked/untracked source cleanliness. - `git status --short --ignored` is reported separately and may still list ignored build/cache outputs; those do not count as source changes. -- The log includes the named milestones in order: design `d116c373b`, plan +- The log includes these milestones in order: design `d116c373b`, plan `afcae1c19`, Maven primary `849056f7f`, Gradle primary `fcc8d0cc9`, Gradle reviewer cache fix `fc6f8293f`, pipeline primary `946b7378f`, Atlassian - reviewer fix `04c5fbedb`, verification-plan correction `75fc90433`, and - verification-range correction `96300b929`; later review/documentation commits - may follow. + reviewer fix `04c5fbedb`, verification-plan correction `75fc90433`, + verification-range correction `96300b929`, scoped local handoff `2464ba5ef`, + and exclusive handoff `73c9f421c3`. +- Later documentation commits, including `98d5c48ea0` and the current docs + sync commit, may follow after those implementation milestones. Do not rely on + the total number of log entries. - [ ] **Step 2: Verify local Gradle configuration is unaffected** @@ -542,13 +858,22 @@ does not require `CFS_MAVEN_URL` or `SYSTEM_ACCESSTOKEN`. - [ ] **Step 3: Re-run the policy tests** -Repeat Task 2 Steps 3 and 4. +Repeat Task 2 Steps 3 through 5. Expected: -- Missing credentials fail with the explicit CFS configuration error. -- General-purpose public repositories are replaced with CFS. -- Maven Local, IntelliJ dependencies, and Atlassian remain available. +- Missing `maven.repo.local` and missing CFS credentials both fail fast with the + explicit configuration errors. +- The extracted allowlist exactly matches all 28 Utils reactor coordinates, + including the parent/aggregator POMs. +- Allowlisted Utils modules resolve from the scoped + `$(Agent.TempDirectory)\azure-tools-maven-repository` handoff. +- Warmed third-party and non-reactor Microsoft coordinates resolve from CFS even + when matching artifacts exist in the scoped local repository. +- An allowlisted module that is absent locally fails rather than falling back to + CFS. +- JetBrains vendor repositories and the Atlassian Microba exception remain + scoped direct exceptions. - [ ] **Step 4: Preview or run the Azure Pipeline** @@ -562,10 +887,14 @@ Expected: - YAML compilation succeeds. - `Build_Plugin.Build_and_Sign` completes. - `Release_Plugin` is skipped. -- Maven logs show the `vscjava` mirror. -- Gradle resolves general-purpose packages from `pkgs.dev.azure.com`. -- JDK, Gradle distribution, and JetBrains-specific endpoints remain the only direct - public artifact endpoints. +- Maven logs show the `vscjava` mirror and `-Dmaven.repo.local` pointing at + `$(Agent.TempDirectory)\azure-tools-maven-repository`. +- Gradle resolves allowlisted Utils reactor coordinates from + `azure-tools-maven-repository`. +- Gradle resolves ordinary third-party and non-reactor Microsoft coordinates + from `pkgs.dev.azure.com` rather than Maven Local. +- JDK, Gradle distribution, JetBrains-specific endpoints, and the Atlassian + Microba exception remain the only direct package-source exceptions. If Azure Pipelines access is unavailable, record this as the only unverified item; do not claim CFS feed coverage or policy compliance from local checks alone. @@ -581,8 +910,9 @@ Expected: - `CFSClean3`: zero findings. - Maven Central, Gradle Plugin Portal, Sonatype general-purpose endpoints, and Maven Central public mirrors: zero requests. -- Remaining `DefaultDeny` entries correspond only to JDK 25 acquisition, the Gradle - distribution, JetBrains/IntelliJ artifacts, signing, or Marketplace publishing. +- Remaining `DefaultDeny` entries correspond only to JDK 25 acquisition, the + Gradle distribution, JetBrains/IntelliJ artifacts, the Atlassian Microba + exception when exercised, signing, or Marketplace publishing. If a general-purpose package source remains, add it to the Gradle script's forbidden source classification and repeat Tasks 2 through 4. Do not add a broad direct-access From 34047e92e890fbf3b7fc07846c2d7f966c98d392 Mon Sep 17 00:00:00 2001 From: Miller Wang Date: Tue, 11 Aug 2026 21:11:00 +0800 Subject: [PATCH 13/16] docs: fix network isolation plan references Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- ...-08-11-stable-release-network-isolation.md | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/docs/superpowers/plans/2026-08-11-stable-release-network-isolation.md b/docs/superpowers/plans/2026-08-11-stable-release-network-isolation.md index aff3b266993..6c2a284e363 100644 --- a/docs/superpowers/plans/2026-08-11-stable-release-network-isolation.md +++ b/docs/superpowers/plans/2026-08-11-stable-release-network-isolation.md @@ -691,9 +691,9 @@ Expected: one commit containing the Gradle init script. ### Task 3: Wire CFS into the Stable Release Pipeline **Files:** -- Modify: `.azure-pipelines/sign-for-stable-release.yml:2-4` -- Modify: `.azure-pipelines/sign-for-stable-release.yml:68-75` -- Modify: `.azure-pipelines/sign-for-stable-release.yml:125-137` +- Modify: `.azure-pipelines/sign-for-stable-release.yml` variables section (template import at line 5, after `Codeql.Enabled`) +- Modify: `.azure-pipelines/sign-for-stable-release.yml` `extends.parameters` section (remove the network-isolation opt-out) +- Modify: `.azure-pipelines/sign-for-stable-release.yml` Build Plugin block (lines 124-139) - [ ] **Step 1: Verify the pipeline is not isolated yet** @@ -827,15 +827,15 @@ Expected: - `git status --short` is empty, confirming tracked/untracked source cleanliness. - `git status --short --ignored` is reported separately and may still list ignored build/cache outputs; those do not count as source changes. -- The log includes these milestones in order: design `d116c373b`, plan - `afcae1c19`, Maven primary `849056f7f`, Gradle primary `fcc8d0cc9`, Gradle - reviewer cache fix `fc6f8293f`, pipeline primary `946b7378f`, Atlassian - reviewer fix `04c5fbedb`, verification-plan correction `75fc90433`, - verification-range correction `96300b929`, scoped local handoff `2464ba5ef`, - and exclusive handoff `73c9f421c3`. -- Later documentation commits, including `98d5c48ea0` and the current docs - sync commit, may follow after those implementation milestones. Do not rely on - the total number of log entries. +- The log includes these milestones in order: design `d116c373bf`, plan + `afcae1c195`, Maven primary `849056f7f0`, Gradle primary `fcc8d0cc9c`, + Gradle reviewer cache fix `fc6f8293f5`, pipeline primary `946b7378fa`, + Atlassian reviewer fix `04c5fbedb5`, verification-plan correction + `75fc904339`, verification-range correction `96300b929e`, docs correction + `98d5c48ea0`, scoped local handoff `2464ba5ef2`, exclusive handoff + `73c9f421c3`, and docs handoff document `38345b10cd`. +- A later documentation correction commit follows these milestones; do not + rely on the total number of log entries. - [ ] **Step 2: Verify local Gradle configuration is unaffected** From 66b5bd5f49578dd5ddb9115f57ecc55d69878dc8 Mon Sep 17 00:00:00 2001 From: Miller Wang Date: Tue, 11 Aug 2026 21:24:38 +0800 Subject: [PATCH 14/16] docs: harden network isolation verification Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- ...-08-11-stable-release-network-isolation.md | 220 ++++++++++++++++-- 1 file changed, 197 insertions(+), 23 deletions(-) diff --git a/docs/superpowers/plans/2026-08-11-stable-release-network-isolation.md b/docs/superpowers/plans/2026-08-11-stable-release-network-isolation.md index 6c2a284e363..e3acdd0feac 100644 --- a/docs/superpowers/plans/2026-08-11-stable-release-network-isolation.md +++ b/docs/superpowers/plans/2026-08-11-stable-release-network-isolation.md @@ -4,7 +4,7 @@ **Goal:** Enable 1ES Network Isolation for the stable release pipeline and force ordinary Maven and Gradle package restores through the `vscjava` Central Feed Service. -**Architecture:** Add pipeline-only Maven and Gradle policy files under `.azure-pipelines`. The Build Plugin step sets one pipeline-scoped Maven local repository under `$(Agent.TempDirectory)\azure-tools-maven-repository`, passes that path via `maven.repo.local` to both Maven and Gradle, and uses the Gradle init script to make the exact 28 Utils reactor coordinates exclusive to that local handoff. All ordinary packages still route through CFS, while the Atlassian Microba exception and purpose-specific vendor repositories remain narrowly scoped direct endpoints. +**Architecture:** Add pipeline-only Maven and Gradle policy files under `.azure-pipelines`. The Build Plugin step sets one pipeline-scoped Maven local repository under `$(Agent.TempDirectory)\azure-tools-maven-repository`, passes that path via `maven.repo.local` to both Maven and Gradle, and uses the Gradle init script to make the current Utils reactor coordinates exclusive to that local handoff. All ordinary packages still route through CFS, while the Atlassian Microba exception and purpose-specific vendor repositories remain narrowly scoped direct endpoints. **Tech Stack:** Azure Pipelines YAML, Maven settings XML, Gradle 9.1 Groovy init scripts, PowerShell, 1ES Pipeline Templates @@ -486,16 +486,22 @@ allowlist = re.findall(r"\['([^']+)', '([^']+)'\]", (root / '.azure-pipelines' / missing = sorted(set(reactor) - set(allowlist)) extra = sorted(set(allowlist) - set(reactor)) -if len(reactor) != 28: - raise SystemExit(f'Expected 28 Utils reactor coordinates, found {len(reactor)}: {reactor}') +required = { + ('com.microsoft.azuretools', 'utils'), + ('com.microsoft.azure', 'azure-toolkit-ide-libs'), + ('com.microsoft.hdinsight', 'azure-toolkit-ide-hdinsight-libs'), +} + +if not required.issubset(set(reactor)): + raise SystemExit(f'Reactor probe missed required parent/aggregator coordinates: {sorted(required - set(reactor))}') if missing or extra: raise SystemExit(f'Allowlist drift detected. Missing={missing} Extra={extra}') -print('PASS: Gradle allowlist exactly matches all 28 Utils reactor coordinates') +print('PASS: Gradle allowlist matches the current Utils reactor coordinates, including the parent and aggregator POMs') '@ | python - ``` -Expected: `PASS: Gradle allowlist exactly matches all 28 Utils reactor coordinates`. +Expected: `PASS: Gradle allowlist matches the current Utils reactor coordinates, including the parent and aggregator POMs`. - [ ] **Step 5: Verify normalized scoped-local matching, future handler coverage, exclusive provenance, and no fallback** @@ -677,7 +683,150 @@ Write-Host 'PASS: Gradle keeps the scoped local handoff exclusive to allowlisted Expected: `PASS: Gradle keeps the scoped local handoff exclusive to allowlisted Utils modules`. -- [ ] **Step 6: Commit the Gradle policy** +- [ ] **Step 6: Run a focused Gradle 9.1 negative test for unreachable CFS** + +Run: + +```powershell +$testRoot = Join-Path (Get-Location) '.scratch\cfs-init-unreachable-cfs' +$projectDir = Join-Path $testRoot 'project' +$scopedRepo = Join-Path $testRoot 'scoped-m2' +$gradleUserHome = Join-Path $testRoot 'gradle-user-home' +$sourceGradleUserHome = Join-Path $HOME '.gradle' +$wrapperDistRoot = Join-Path $sourceGradleUserHome 'wrapper\dists\gradle-9.1.0-bin' +Remove-Item $testRoot -Recurse -Force -ErrorAction SilentlyContinue +New-Item -ItemType Directory -Path $projectDir, $scopedRepo, $gradleUserHome -Force | Out-Null + +function New-MavenStubArtifact { + param( + [string]$RepoRoot, + [string]$GroupId, + [string]$ArtifactId, + [string]$Version + ) + + $groupPath = $GroupId -replace '\.', '\\' + $artifactDir = Join-Path $RepoRoot "$groupPath\$ArtifactId\$Version" + New-Item -ItemType Directory -Path $artifactDir -Force | Out-Null + @" + + 4.0.0 + $GroupId + $ArtifactId + $Version + +"@ | Set-Content -Path (Join-Path $artifactDir "$ArtifactId-$Version.pom") + [System.IO.File]::WriteAllBytes((Join-Path $artifactDir "$ArtifactId-$Version.jar"), [byte[]]@()) +} + +New-MavenStubArtifact $scopedRepo 'junit' 'junit' '4.13.2' + +Set-Content -Path (Join-Path $projectDir 'settings.gradle') -Value "rootProject.name = 'cfs-init-unreachable-cfs'" +@' +repositories { + mavenLocal() + mavenCentral() + maven { + name = 'pluginPortalMirror' + url = uri('https://plugins.gradle.org/m2') + } + maven { + name = 'sonatypeSnapshots' + url = uri('https://s01.oss.sonatype.org/content/repositories/snapshots/') + } +} + +configurations { + blockedLocalProbe +} + +dependencies { + blockedLocalProbe 'junit:junit:4.13.2' +} + +tasks.register('printRepositories') { + doLast { + repositories.each { repository -> + def location = repository.hasProperty('url') ? repository.url : repository.name + println("REPOSITORY=${repository.name}|${location}") + } + } +} + +tasks.register('resolveBlockedLocal') { + doLast { + configurations.blockedLocalProbe.resolve().each { file -> + println("RESOLVED=${file}") + } + } +} +'@ | Set-Content -Path (Join-Path $projectDir 'build.gradle') + +$wrapperDist = Get-ChildItem $wrapperDistRoot -Directory -ErrorAction SilentlyContinue | + Sort-Object LastWriteTime -Descending | + Select-Object -First 1 +if ($null -eq $wrapperDist) { + throw "Expected the Gradle 9.1 wrapper distribution under $wrapperDistRoot. Run one of the earlier Gradle validation steps first so the wrapper is already cached." +} +New-Item -ItemType Directory -Path (Join-Path $gradleUserHome 'wrapper\dists\gradle-9.1.0-bin') -Force | Out-Null +Copy-Item $wrapperDist.FullName -Destination (Join-Path $gradleUserHome 'wrapper\dists\gradle-9.1.0-bin') -Recurse -Force + +Push-Location 'PluginsAndFeatures\azure-toolkit-for-intellij' +try { + $env:CFS_MAVEN_URL = 'https://127.0.0.1:1/repository' + $env:SYSTEM_ACCESSTOKEN = 'test-token' + + $repoOutput = .\gradlew.bat -p $projectDir printRepositories ` + --init-script '..\..\.azure-pipelines\cfs-init.gradle' ` + --no-daemon --no-configuration-cache ` + --gradle-user-home $gradleUserHome ` + "--Dmaven.repo.local=$scopedRepo" 2>&1 + $repoExitCode = $LASTEXITCODE + + $failureOutput = .\gradlew.bat -p $projectDir resolveBlockedLocal ` + --init-script '..\..\.azure-pipelines\cfs-init.gradle' ` + --no-daemon --no-configuration-cache ` + --gradle-user-home $gradleUserHome ` + "--Dmaven.repo.local=$scopedRepo" 2>&1 + $failureExitCode = $LASTEXITCODE +} finally { + Pop-Location + Remove-Item Env:CFS_MAVEN_URL -ErrorAction SilentlyContinue + Remove-Item Env:SYSTEM_ACCESSTOKEN -ErrorAction SilentlyContinue + Remove-Item $testRoot -Recurse -Force -ErrorAction SilentlyContinue +} + +if ($repoExitCode -ne 0) { + throw "Gradle 9.1 repository rewrite probe failed:`n$($repoOutput -join "`n")" +} +$repoText = $repoOutput -join "`n" +$failureText = $failureOutput -join "`n" +$combinedText = $repoText + "`n" + $failureText +$localJunitJar = Join-Path $scopedRepo 'junit\junit\4.13.2\junit-4.13.2.jar' + +if (($repoText | Select-String -Pattern [regex]::Escape('https://127.0.0.1:1/repository') -AllMatches).Matches.Count -lt 3) { + throw "Expected Maven Central, Plugin Portal, and Sonatype repositories to be rewritten to the loopback CFS endpoint:`n$repoText" +} +if ($failureExitCode -eq 0) { + throw "Expected ordinary coordinate resolution to fail when CFS is unreachable, but Gradle 9.1 succeeded:`n$failureText" +} +if ($failureText -notmatch '127\.0\.0\.1:1/repository' -or + $failureText -notmatch 'junit:junit:4\.13\.2' -or + $failureText -notmatch 'Connection refused|ConnectException|actively refused|Failed to connect') { + throw "Expected the failing resolution to target the unreachable loopback CFS endpoint for junit:junit:4.13.2:`n$failureText" +} +if ($combinedText -match 'repo\.maven\.apache\.org|repo1\.maven\.org|plugins\.gradle\.org|plugins-artifacts\.gradle\.org|oss\.sonatype\.org|s01\.oss\.sonatype\.org') { + throw "A public package source URL leaked through the Gradle CFS policy:`n$combinedText" +} +if ($combinedText -match [regex]::Escape($localJunitJar) -or $combinedText -match 'RESOLVED=') { + throw "Ordinary coordinate unexpectedly resolved from the scoped local repository instead of failing closed at CFS:`n$combinedText" +} +Write-Host 'PASS: Gradle 9.1 fails closed against unreachable CFS and does not fall back to public or local ordinary coordinates' +``` + +Expected: `PASS: Gradle 9.1 fails closed against unreachable CFS and does not fall back to public or local ordinary coordinates`. + +- [ ] **Step 7: Commit the Gradle policy** Run: @@ -691,9 +840,9 @@ Expected: one commit containing the Gradle init script. ### Task 3: Wire CFS into the Stable Release Pipeline **Files:** -- Modify: `.azure-pipelines/sign-for-stable-release.yml` variables section (template import at line 5, after `Codeql.Enabled`) +- Modify: `.azure-pipelines/sign-for-stable-release.yml` variables section (add the shared CFS template after `Codeql.Enabled`) - Modify: `.azure-pipelines/sign-for-stable-release.yml` `extends.parameters` section (remove the network-isolation opt-out) -- Modify: `.azure-pipelines/sign-for-stable-release.yml` Build Plugin block (lines 124-139) +- Modify: `.azure-pipelines/sign-for-stable-release.yml` `Build Plugin` step script and env block - [ ] **Step 1: Verify the pipeline is not isolated yet** @@ -815,10 +964,33 @@ Expected: one commit containing only the stable release pipeline update. Run: ```powershell -git --no-pager diff --check d116c373b^..HEAD -git --no-pager status --short -git --no-pager status --short --ignored -git --no-pager log --oneline --decorate --reverse d116c373b^..HEAD +$relevantFiles = @( + '.azure-pipelines/cfs-variables.yml', + '.azure-pipelines/cfs-settings.xml', + '.azure-pipelines/cfs-init.gradle', + '.azure-pipelines/sign-for-stable-release.yml' +) + +git --no-pager diff --check +$statusOutput = git --no-pager status --short +$ignoredOutput = git --no-pager status --short --ignored +$logOutput = git --no-pager log --oneline -- $relevantFiles + +$statusOutput +$ignoredOutput +$logOutput + +$logText = $logOutput -join "`n" +foreach ($subject in @( + 'build: add Maven CFS configuration', + 'build: route Gradle packages through CFS', + 'build: enable stable release network isolation' +)) { + if ($logText -notmatch [regex]::Escape($subject)) { + throw "Missing expected commit subject in the relevant file history: $subject" + } +} +Write-Host 'PASS: final change set is clean and the relevant commit subjects are present' ``` Expected: @@ -827,15 +999,11 @@ Expected: - `git status --short` is empty, confirming tracked/untracked source cleanliness. - `git status --short --ignored` is reported separately and may still list ignored build/cache outputs; those do not count as source changes. -- The log includes these milestones in order: design `d116c373bf`, plan - `afcae1c195`, Maven primary `849056f7f0`, Gradle primary `fcc8d0cc9c`, - Gradle reviewer cache fix `fc6f8293f5`, pipeline primary `946b7378fa`, - Atlassian reviewer fix `04c5fbedb5`, verification-plan correction - `75fc904339`, verification-range correction `96300b929e`, docs correction - `98d5c48ea0`, scoped local handoff `2464ba5ef2`, exclusive handoff - `73c9f421c3`, and docs handoff document `38345b10cd`. -- A later documentation correction commit follows these milestones; do not - rely on the total number of log entries. +- `git log --oneline -- ` includes the required build subjects + for Maven CFS configuration, Gradle CFS routing, and stable release network + isolation. Do not rely on commit hashes, exact counts, or a fixed order. +- The script prints `PASS: final change set is clean and the relevant commit + subjects are present`. - [ ] **Step 2: Verify local Gradle configuration is unaffected** @@ -858,13 +1026,15 @@ does not require `CFS_MAVEN_URL` or `SYSTEM_ACCESSTOKEN`. - [ ] **Step 3: Re-run the policy tests** -Repeat Task 2 Steps 3 through 5. +Repeat Task 2's fail-fast validation, allowlist-sync check, scoped-local +provenance/no-fallback coverage, and the focused Gradle 9.1 unreachable-CFS +negative test. Expected: - Missing `maven.repo.local` and missing CFS credentials both fail fast with the explicit configuration errors. -- The extracted allowlist exactly matches all 28 Utils reactor coordinates, +- The extracted allowlist matches the current Utils reactor coordinates, including the parent/aggregator POMs. - Allowlisted Utils modules resolve from the scoped `$(Agent.TempDirectory)\azure-tools-maven-repository` handoff. @@ -872,6 +1042,10 @@ Expected: when matching artifacts exist in the scoped local repository. - An allowlisted module that is absent locally fails rather than falling back to CFS. +- In a minimal Gradle 9.1 project, an ordinary coordinate warmed into the + scoped Maven local repository still fails against the unreachable loopback CFS + endpoint, emits no Maven Central/Plugin Portal/Sonatype public URL, and never + falls back to the scoped local repository. - JetBrains vendor repositories and the Atlassian Microba exception remain scoped direct exceptions. From 20bebafc6a16676487a4f4f7a4f5d8accd07ebe5 Mon Sep 17 00:00:00 2001 From: Miller Wang Date: Tue, 11 Aug 2026 22:01:18 +0800 Subject: [PATCH 15/16] docs: fix network isolation test commands Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- ...-08-11-stable-release-network-isolation.md | 415 +++++++++++++++--- 1 file changed, 350 insertions(+), 65 deletions(-) diff --git a/docs/superpowers/plans/2026-08-11-stable-release-network-isolation.md b/docs/superpowers/plans/2026-08-11-stable-release-network-isolation.md index e3acdd0feac..5ba74cdfd4b 100644 --- a/docs/superpowers/plans/2026-08-11-stable-release-network-isolation.md +++ b/docs/superpowers/plans/2026-08-11-stable-release-network-isolation.md @@ -408,28 +408,41 @@ Run: $testRoot = Join-Path (Get-Location) '.scratch\cfs-init-validation' $projectDir = Join-Path $testRoot 'project' $dummyRepo = Join-Path $testRoot 'scoped-m2' +$gradleWrapper = (Resolve-Path 'PluginsAndFeatures\azure-toolkit-for-intellij\gradlew.bat').Path +$initScript = (Resolve-Path '.azure-pipelines\cfs-init.gradle').Path Remove-Item $testRoot -Recurse -Force -ErrorAction SilentlyContinue New-Item -ItemType Directory -Path $projectDir, $dummyRepo -Force | Out-Null Set-Content -Path (Join-Path $projectDir 'settings.gradle') -Value "rootProject.name = 'cfs-init-validation'" Set-Content -Path (Join-Path $projectDir 'build.gradle') -Value '' -Push-Location 'PluginsAndFeatures\azure-toolkit-for-intellij' +$missingLocalArgs = @( + '-p', $projectDir, + '--init-script', $initScript, + '--no-daemon', + '--no-configuration-cache', + 'help' +) +$missingCredentialsArgs = @( + '-p', $projectDir, + '--init-script', $initScript, + '--no-daemon', + '--no-configuration-cache', + "-Dmaven.repo.local=$dummyRepo", + 'help' +) + try { $env:CFS_MAVEN_URL = 'https://example.invalid/vscjava/maven/v1' $env:SYSTEM_ACCESSTOKEN = 'test-token' - $missingLocalOutput = .\gradlew.bat -p $projectDir help ` - --init-script '..\..\.azure-pipelines\cfs-init.gradle' ` - --no-daemon --no-configuration-cache 2>&1 + $missingLocalOutput = & $gradleWrapper @missingLocalArgs 2>&1 $missingLocalExitCode = $LASTEXITCODE Remove-Item Env:CFS_MAVEN_URL -ErrorAction SilentlyContinue Remove-Item Env:SYSTEM_ACCESSTOKEN -ErrorAction SilentlyContinue - $missingCredentialsOutput = .\gradlew.bat -p $projectDir help ` - --init-script '..\..\.azure-pipelines\cfs-init.gradle' ` - --no-daemon --no-configuration-cache "-Dmaven.repo.local=$dummyRepo" 2>&1 + + $missingCredentialsOutput = & $gradleWrapper @missingCredentialsArgs 2>&1 $missingCredentialsExitCode = $LASTEXITCODE } finally { - Pop-Location Remove-Item Env:CFS_MAVEN_URL -ErrorAction SilentlyContinue Remove-Item Env:SYSTEM_ACCESSTOKEN -ErrorAction SilentlyContinue Remove-Item $testRoot -Recurse -Force -ErrorAction SilentlyContinue @@ -503,7 +516,7 @@ print('PASS: Gradle allowlist matches the current Utils reactor coordinates, inc Expected: `PASS: Gradle allowlist matches the current Utils reactor coordinates, including the parent and aggregator POMs`. -- [ ] **Step 5: Verify normalized scoped-local matching, future handler coverage, exclusive provenance, and no fallback** +- [ ] **Step 5: Verify normalized scoped-local matching, future handler coverage, exclusive provenance, and no fallback via an authenticated loopback CFS fixture** Run: @@ -512,15 +525,26 @@ $testRoot = Join-Path (Get-Location) '.scratch\cfs-init-provenance' $projectDir = Join-Path $testRoot 'project' $scopedRepo = Join-Path $testRoot 'scoped-m2' $cfsRepo = Join-Path $testRoot 'cfs-m2' +$gradleUserHome = Join-Path $testRoot 'gradle-user-home' +$sourceGradleUserHome = Join-Path $HOME '.gradle' +$wrapperDistRoot = Join-Path $sourceGradleUserHome 'wrapper\dists\gradle-9.1.0-bin' +$gradleWrapper = (Resolve-Path 'PluginsAndFeatures\azure-toolkit-for-intellij\gradlew.bat').Path +$initScript = (Resolve-Path '.azure-pipelines\cfs-init.gradle').Path +$serverHost = if (Get-Command powershell -ErrorAction SilentlyContinue) { + (Get-Command powershell).Source +} else { + (Get-Command pwsh -ErrorAction Stop).Source +} Remove-Item $testRoot -Recurse -Force -ErrorAction SilentlyContinue -New-Item -ItemType Directory -Path $projectDir, $scopedRepo, $cfsRepo -Force | Out-Null +New-Item -ItemType Directory -Path $projectDir, $scopedRepo, $cfsRepo, $gradleUserHome -Force | Out-Null function New-MavenStubArtifact { param( [string]$RepoRoot, [string]$GroupId, [string]$ArtifactId, - [string]$Version + [string]$Version, + [string]$Marker ) $groupPath = $GroupId -replace '\.', '\\' @@ -532,18 +556,29 @@ function New-MavenStubArtifact { $GroupId $ArtifactId $Version + jar "@ | Set-Content -Path (Join-Path $artifactDir "$ArtifactId-$Version.pom") - [System.IO.File]::WriteAllBytes((Join-Path $artifactDir "$ArtifactId-$Version.jar"), [byte[]]@()) + Set-Content -Path (Join-Path $artifactDir "$ArtifactId-$Version.jar") -Value $Marker -NoNewline +} + +function Get-FreeTcpPort { + $listener = [System.Net.Sockets.TcpListener]::new([System.Net.IPAddress]::Parse('127.0.0.1'), 0) + $listener.Start() + try { + return ([System.Net.IPEndPoint]$listener.LocalEndpoint).Port + } finally { + $listener.Stop() + } } -New-MavenStubArtifact $scopedRepo 'com.microsoft.azure' 'azure-toolkit-ide-common-lib' '1.0.0-test' -New-MavenStubArtifact $scopedRepo 'org.example' 'warmed-cache-only' '1.0.0-test' -New-MavenStubArtifact $scopedRepo 'com.microsoft.azure' 'azure-toolkit-common-lib' '1.0.0-test' +New-MavenStubArtifact $scopedRepo 'com.microsoft.azure' 'azure-toolkit-ide-common-lib' '1.0.0-test' 'SCOPED-LOCAL-ONLY' +New-MavenStubArtifact $scopedRepo 'org.example' 'warmed-cache-only' '1.0.0-test' 'SCOPED-LEAK-THIRD-PARTY' +New-MavenStubArtifact $scopedRepo 'com.microsoft.azure' 'azure-toolkit-common-lib' '1.0.0-test' 'SCOPED-LEAK-MICROSOFT' -New-MavenStubArtifact $cfsRepo 'org.example' 'warmed-cache-only' '1.0.0-test' -New-MavenStubArtifact $cfsRepo 'com.microsoft.azure' 'azure-toolkit-common-lib' '1.0.0-test' -New-MavenStubArtifact $cfsRepo 'com.microsoft.azure' 'azure-toolkit-ide-appservice-lib' '1.0.0-test' +New-MavenStubArtifact $cfsRepo 'org.example' 'warmed-cache-only' '1.0.0-test' 'CFS-THIRD-PARTY' +New-MavenStubArtifact $cfsRepo 'com.microsoft.azure' 'azure-toolkit-common-lib' '1.0.0-test' 'CFS-MICROSOFT' +New-MavenStubArtifact $cfsRepo 'com.microsoft.azure' 'azure-toolkit-ide-appservice-lib' '1.0.0-test' 'CFS-ALLOWLISTED-BUT-SHOULD-NOT-RESOLVE' Set-Content -Path (Join-Path $projectDir 'settings.gradle') -Value "rootProject.name = 'cfs-init-provenance'" @' @@ -603,7 +638,7 @@ tasks.register('printProvenance') { thirdPartyProbe: configurations.thirdPartyProbe.singleFile, microsoftProbe: configurations.microsoftProbe.singleFile, ].each { name, file -> - println("PROVENANCE=${name}|${file}") + println("PROVENANCE=${name}|${file}|${file.getText('UTF-8')}") } } } @@ -615,38 +650,212 @@ tasks.register('resolveMissingLocal') { } '@ | Set-Content -Path (Join-Path $projectDir 'build.gradle') -$cfsRepoUri = 'file:///' + ((Resolve-Path $cfsRepo).Path -replace '\\', '/') -if (-not $cfsRepoUri.EndsWith('/')) { - $cfsRepoUri += '/' +$serverScript = Join-Path $testRoot 'cfs-fixture-server.ps1' +@' +param( + [Parameter(Mandatory = $true)] + [string]$RepoRoot, + [Parameter(Mandatory = $true)] + [int]$Port, + [Parameter(Mandatory = $true)] + [string]$RequestLog, + [Parameter(Mandatory = $true)] + [string]$ReadyFile +) + +$rootPath = [System.IO.Path]::GetFullPath($RepoRoot) +$listener = [System.Net.Sockets.TcpListener]::new([System.Net.IPAddress]::Parse('127.0.0.1'), $Port) +$listener.Start() +Set-Content -Path $ReadyFile -Value $PID + +function Send-HttpResponse { + param( + [System.Net.Sockets.TcpClient]$Client, + [int]$StatusCode, + [string]$StatusText, + [string]$ContentType, + [byte[]]$Body, + [hashtable]$Headers, + [switch]$SkipBody + ) + + if ($null -eq $Body) { + $Body = [byte[]]@() + } + if ($null -eq $Headers) { + $Headers = @{} + } + + $headerLines = [System.Collections.Generic.List[string]]::new() + $headerLines.Add("HTTP/1.1 $StatusCode $StatusText") + $headerLines.Add("Content-Length: $($Body.Length)") + $headerLines.Add("Content-Type: $ContentType") + $headerLines.Add('Connection: close') + foreach ($entry in $Headers.GetEnumerator()) { + $headerLines.Add("$($entry.Key): $($entry.Value)") + } + $headerText = ($headerLines + '', '') -join "`r`n" + $headerBytes = [System.Text.Encoding]::ASCII.GetBytes($headerText) + + $stream = $Client.GetStream() + $stream.Write($headerBytes, 0, $headerBytes.Length) + if (-not $SkipBody -and $Body.Length -gt 0) { + $stream.Write($Body, 0, $Body.Length) + } + $stream.Flush() } -$env:CFS_MAVEN_URL = $cfsRepoUri -$env:SYSTEM_ACCESSTOKEN = 'test-token' -Push-Location 'PluginsAndFeatures\azure-toolkit-for-intellij' try { - $successOutput = .\gradlew.bat -p $projectDir printRepositories printProvenance ` - --init-script '..\..\.azure-pipelines\cfs-init.gradle' ` - --no-daemon --no-configuration-cache "-Dmaven.repo.local=$scopedRepo" 2>&1 + while ($true) { + $client = $listener.AcceptTcpClient() + try { + $stream = $client.GetStream() + $reader = New-Object System.IO.StreamReader($stream, [System.Text.Encoding]::ASCII, $false, 1024, $true) + $requestLine = $reader.ReadLine() + if ([string]::IsNullOrWhiteSpace($requestLine)) { + continue + } + + $headers = @{} + while ($true) { + $line = $reader.ReadLine() + if ($null -eq $line -or $line -eq '') { + break + } + $parts = $line -split ':\s*', 2 + if ($parts.Count -eq 2) { + $headers[$parts[0]] = $parts[1] + } + } + + $parts = $requestLine -split ' ' + $method = $parts[0] + $rawPath = if ($parts.Count -ge 2) { $parts[1] } else { '/' } + $pathOnly = ($rawPath -split '\?', 2)[0] + $authorization = if ($headers.ContainsKey('Authorization')) { $headers['Authorization'] } else { '' } + + $statusCode = 500 + if ($pathOnly -eq '/__health') { + $statusCode = 200 + $body = [System.Text.Encoding]::UTF8.GetBytes('ready') + Send-HttpResponse -Client $client -StatusCode 200 -StatusText 'OK' -ContentType 'text/plain' -Body $body -Headers @{} + } elseif (-not $authorization) { + $statusCode = 401 + $body = [System.Text.Encoding]::UTF8.GetBytes('auth required') + Send-HttpResponse -Client $client -StatusCode 401 -StatusText 'Unauthorized' -ContentType 'text/plain' -Body $body -Headers @{ 'WWW-Authenticate' = 'Basic realm="cfs"' } + } else { + $relativePath = $pathOnly -replace '^/repository/?', '' + $relativePath = [System.Uri]::UnescapeDataString($relativePath).Replace('/', '\') + $candidatePath = [System.IO.Path]::GetFullPath((Join-Path $rootPath $relativePath)) + if (-not $candidatePath.StartsWith($rootPath, [System.StringComparison]::OrdinalIgnoreCase) -or -not (Test-Path $candidatePath -PathType Leaf)) { + $statusCode = 404 + $body = [System.Text.Encoding]::UTF8.GetBytes('not found') + Send-HttpResponse -Client $client -StatusCode 404 -StatusText 'Not Found' -ContentType 'text/plain' -Body $body -Headers @{} + } else { + $statusCode = 200 + $body = [System.IO.File]::ReadAllBytes($candidatePath) + $contentType = if ($candidatePath.EndsWith('.pom')) { 'application/xml' } else { 'application/java-archive' } + Send-HttpResponse -Client $client -StatusCode 200 -StatusText 'OK' -ContentType $contentType -Body $body -Headers @{} -SkipBody:($method -eq 'HEAD') + } + } + + Add-Content -Path $RequestLog -Value "$method`t$pathOnly`t$statusCode`t$authorization" + } finally { + $client.Dispose() + } + } +} finally { + $listener.Stop() +} +'@ | Set-Content -Path $serverScript + +$wrapperDist = Get-ChildItem $wrapperDistRoot -Directory -ErrorAction SilentlyContinue | + Sort-Object LastWriteTime -Descending | + Select-Object -First 1 +if ($null -eq $wrapperDist) { + throw "Expected the Gradle 9.1 wrapper distribution under $wrapperDistRoot. Run one of the earlier Gradle validation steps first so the wrapper is already cached." +} +New-Item -ItemType Directory -Path (Join-Path $gradleUserHome 'wrapper\dists\gradle-9.1.0-bin') -Force | Out-Null +Copy-Item $wrapperDist.FullName -Destination (Join-Path $gradleUserHome 'wrapper\dists\gradle-9.1.0-bin') -Recurse -Force + +$port = Get-FreeTcpPort +$cfsRepoUri = "http://127.0.0.1:$port/repository" +$requestLog = Join-Path $testRoot 'requests.log' +$readyFile = Join-Path $testRoot 'server.ready' +$serverStdOut = Join-Path $testRoot 'server.stdout.log' +$serverStdErr = Join-Path $testRoot 'server.stderr.log' +$serverProcess = $null +$requestLogText = '' +$serverStdOutText = '' +$serverStdErrText = '' + +$commonGradleArgs = @( + '-p', $projectDir, + '--init-script', $initScript, + '--no-daemon', + '--no-configuration-cache', + '--gradle-user-home', $gradleUserHome, + "-Dmaven.repo.local=$scopedRepo" +) +$successArgs = $commonGradleArgs + @('printRepositories', 'printProvenance') +$failureArgs = $commonGradleArgs + @('resolveMissingLocal') + +try { + $serverProcess = Start-Process -FilePath $serverHost -ArgumentList @( + '-NoLogo', + '-NoProfile', + '-ExecutionPolicy', 'Bypass', + '-File', $serverScript, + '-RepoRoot', $cfsRepo, + '-Port', $port, + '-RequestLog', $requestLog, + '-ReadyFile', $readyFile + ) -PassThru -RedirectStandardOutput $serverStdOut -RedirectStandardError $serverStdErr + + $deadline = (Get-Date).AddSeconds(15) + while (-not (Test-Path $readyFile)) { + if ($serverProcess.HasExited) { + $startupErr = if (Test-Path $serverStdErr) { Get-Content $serverStdErr -Raw } else { '' } + $startupOut = if (Test-Path $serverStdOut) { Get-Content $serverStdOut -Raw } else { '' } + throw "Loopback CFS fixture exited before becoming ready.`nSTDOUT:`n$startupOut`nSTDERR:`n$startupErr" + } + if ((Get-Date) -ge $deadline) { + throw 'Timed out waiting for the loopback CFS fixture to start.' + } + Start-Sleep -Milliseconds 100 + } + + $env:CFS_MAVEN_URL = $cfsRepoUri + $env:SYSTEM_ACCESSTOKEN = 'test-token' + + $successOutput = & $gradleWrapper @successArgs 2>&1 $successExitCode = $LASTEXITCODE - $failureOutput = .\gradlew.bat -p $projectDir resolveMissingLocal ` - --init-script '..\..\.azure-pipelines\cfs-init.gradle' ` - --no-daemon --no-configuration-cache "-Dmaven.repo.local=$scopedRepo" 2>&1 + $failureOutput = & $gradleWrapper @failureArgs 2>&1 $failureExitCode = $LASTEXITCODE + + $requestLogText = if (Test-Path $requestLog) { Get-Content $requestLog -Raw } else { '' } + $serverStdOutText = if (Test-Path $serverStdOut) { Get-Content $serverStdOut -Raw } else { '' } + $serverStdErrText = if (Test-Path $serverStdErr) { Get-Content $serverStdErr -Raw } else { '' } } finally { - Pop-Location Remove-Item Env:CFS_MAVEN_URL -ErrorAction SilentlyContinue Remove-Item Env:SYSTEM_ACCESSTOKEN -ErrorAction SilentlyContinue + if ($serverProcess) { + $serverProcess.Refresh() + if (-not $serverProcess.HasExited) { + $serverProcess.Kill() + } + $serverProcess.WaitForExit() + } Remove-Item $testRoot -Recurse -Force -ErrorAction SilentlyContinue } if ($successExitCode -ne 0) { - throw "Gradle provenance test failed:`n$($successOutput -join "`n")" + throw "Gradle provenance test failed:`n$($successOutput -join "`n")`nREQUEST LOG:`n$requestLogText`nSERVER STDOUT:`n$serverStdOutText`nSERVER STDERR:`n$serverStdErrText" } $successText = $successOutput -join "`n" +$failureText = $failureOutput -join "`n" $localJar = Join-Path $scopedRepo 'com\microsoft\azure\azure-toolkit-ide-common-lib\1.0.0-test\azure-toolkit-ide-common-lib-1.0.0-test.jar' -$thirdPartyCfsJar = Join-Path $cfsRepo 'org\example\warmed-cache-only\1.0.0-test\warmed-cache-only-1.0.0-test.jar' -$msCfsJar = Join-Path $cfsRepo 'com\microsoft\azure\azure-toolkit-common-lib\1.0.0-test\azure-toolkit-common-lib-1.0.0-test.jar' if ($successText -notmatch [regex]::Escape("REPOSITORY=lateForbidden|$cfsRepoUri")) { throw "Late-added forbidden repository was not rewritten to CFS:`n$successText" @@ -657,31 +866,37 @@ if ($successText -notmatch 'REPOSITORY=atlassianPublic\|https://maven\.atlassian if ($successText -notmatch 'REPOSITORY=intellijVendor\|https://cache-redirector\.jetbrains\.com/intellij-dependencies') { throw "JetBrains vendor repository was not preserved:`n$successText" } -if ($successText -notmatch [regex]::Escape("PROVENANCE=localProbe|$localJar")) { +if ($successText -notmatch [regex]::Escape("PROVENANCE=localProbe|$localJar|SCOPED-LOCAL-ONLY")) { throw "Allowlisted Utils module did not resolve from the scoped local handoff:`n$successText" } -if ($successText -notmatch [regex]::Escape("PROVENANCE=thirdPartyProbe|$thirdPartyCfsJar")) { - throw "Third-party dependency did not resolve from CFS:`n$successText" +if ($successText -notmatch '(?m)^PROVENANCE=thirdPartyProbe\|.*\|CFS-THIRD-PARTY$') { + throw "Third-party dependency did not resolve from the loopback CFS fixture:`n$successText" +} +if ($successText -notmatch '(?m)^PROVENANCE=microsoftProbe\|.*\|CFS-MICROSOFT$') { + throw "Non-reactor Microsoft dependency did not resolve from the loopback CFS fixture:`n$successText" } -if ($successText -notmatch [regex]::Escape("PROVENANCE=microsoftProbe|$msCfsJar")) { - throw "Non-reactor Microsoft dependency did not resolve from CFS:`n$successText" +if ($successText -match 'SCOPED-LEAK-THIRD-PARTY|SCOPED-LEAK-MICROSOFT') { + throw "Scoped local warmed-cache content leaked through the exclusive allowlist:`n$successText" } -if ($successText -match [regex]::Escape((Join-Path $scopedRepo 'org\example\warmed-cache-only'))) { - throw "Third-party warmed-cache content leaked through the scoped local handoff:`n$successText" +if ($requestLogText -notmatch '(?m)^.*org/example/warmed-cache-only/1\.0\.0-test/.*\t200\tBasic .*$') { + throw "Expected authenticated loopback CFS requests for the third-party dependency:`n$requestLogText" } -if ($successText -match [regex]::Escape((Join-Path $scopedRepo 'com\microsoft\azure\azure-toolkit-common-lib'))) { - throw "Non-reactor Microsoft warmed-cache content leaked through the scoped local handoff:`n$successText" +if ($requestLogText -notmatch '(?m)^.*com/microsoft/azure/azure-toolkit-common-lib/1\.0\.0-test/.*\t200\tBasic .*$') { + throw "Expected authenticated loopback CFS requests for the non-reactor Microsoft dependency:`n$requestLogText" +} +if ($requestLogText -match 'azure-toolkit-ide-common-lib/1\.0\.0-test|azure-toolkit-ide-appservice-lib/1\.0\.0-test') { + throw "Allowlisted Utils coordinates unexpectedly hit the loopback CFS fixture:`n$requestLogText" } if ($failureExitCode -eq 0) { - throw 'Expected an allowlisted module that is absent locally to fail' + throw "Expected an allowlisted module that is absent locally to fail instead of falling back to CFS:`n$failureText" } -if (($failureOutput -join "`n") -notmatch 'Could not find com\.microsoft\.azure:azure-toolkit-ide-appservice-lib:1\.0\.0-test') { - throw "Gradle failed for an unexpected reason:`n$($failureOutput -join "`n")" +if ($failureText -notmatch 'Could not find com\.microsoft\.azure:azure-toolkit-ide-appservice-lib:1\.0\.0-test') { + throw "Gradle failed for an unexpected reason:`n$failureText" } -Write-Host 'PASS: Gradle keeps the scoped local handoff exclusive to allowlisted Utils modules' +Write-Host 'PASS: Gradle keeps the scoped local handoff exclusive to allowlisted Utils modules and routes ordinary dependencies through authenticated loopback CFS' ``` -Expected: `PASS: Gradle keeps the scoped local handoff exclusive to allowlisted Utils modules`. +Expected: `PASS: Gradle keeps the scoped local handoff exclusive to allowlisted Utils modules and routes ordinary dependencies through authenticated loopback CFS`. - [ ] **Step 6: Run a focused Gradle 9.1 negative test for unreachable CFS** @@ -694,6 +909,8 @@ $scopedRepo = Join-Path $testRoot 'scoped-m2' $gradleUserHome = Join-Path $testRoot 'gradle-user-home' $sourceGradleUserHome = Join-Path $HOME '.gradle' $wrapperDistRoot = Join-Path $sourceGradleUserHome 'wrapper\dists\gradle-9.1.0-bin' +$gradleWrapper = (Resolve-Path 'PluginsAndFeatures\azure-toolkit-for-intellij\gradlew.bat').Path +$initScript = (Resolve-Path '.azure-pipelines\cfs-init.gradle').Path Remove-Item $testRoot -Recurse -Force -ErrorAction SilentlyContinue New-Item -ItemType Directory -Path $projectDir, $scopedRepo, $gradleUserHome -Force | Out-Null @@ -771,26 +988,27 @@ if ($null -eq $wrapperDist) { New-Item -ItemType Directory -Path (Join-Path $gradleUserHome 'wrapper\dists\gradle-9.1.0-bin') -Force | Out-Null Copy-Item $wrapperDist.FullName -Destination (Join-Path $gradleUserHome 'wrapper\dists\gradle-9.1.0-bin') -Recurse -Force -Push-Location 'PluginsAndFeatures\azure-toolkit-for-intellij' +$commonGradleArgs = @( + '-p', $projectDir, + '--init-script', $initScript, + '--no-daemon', + '--no-configuration-cache', + '--gradle-user-home', $gradleUserHome, + "-Dmaven.repo.local=$scopedRepo" +) +$repoArgs = $commonGradleArgs + @('printRepositories') +$failureArgs = $commonGradleArgs + @('resolveBlockedLocal') + try { $env:CFS_MAVEN_URL = 'https://127.0.0.1:1/repository' $env:SYSTEM_ACCESSTOKEN = 'test-token' - $repoOutput = .\gradlew.bat -p $projectDir printRepositories ` - --init-script '..\..\.azure-pipelines\cfs-init.gradle' ` - --no-daemon --no-configuration-cache ` - --gradle-user-home $gradleUserHome ` - "--Dmaven.repo.local=$scopedRepo" 2>&1 + $repoOutput = & $gradleWrapper @repoArgs 2>&1 $repoExitCode = $LASTEXITCODE - $failureOutput = .\gradlew.bat -p $projectDir resolveBlockedLocal ` - --init-script '..\..\.azure-pipelines\cfs-init.gradle' ` - --no-daemon --no-configuration-cache ` - --gradle-user-home $gradleUserHome ` - "--Dmaven.repo.local=$scopedRepo" 2>&1 + $failureOutput = & $gradleWrapper @failureArgs 2>&1 $failureExitCode = $LASTEXITCODE } finally { - Pop-Location Remove-Item Env:CFS_MAVEN_URL -ErrorAction SilentlyContinue Remove-Item Env:SYSTEM_ACCESSTOKEN -ErrorAction SilentlyContinue Remove-Item $testRoot -Recurse -Force -ErrorAction SilentlyContinue @@ -804,7 +1022,7 @@ $failureText = $failureOutput -join "`n" $combinedText = $repoText + "`n" + $failureText $localJunitJar = Join-Path $scopedRepo 'junit\junit\4.13.2\junit-4.13.2.jar' -if (($repoText | Select-String -Pattern [regex]::Escape('https://127.0.0.1:1/repository') -AllMatches).Matches.Count -lt 3) { +if ([regex]::Matches($repoText, [regex]::Escape('https://127.0.0.1:1/repository')).Count -lt 3) { throw "Expected Maven Central, Plugin Portal, and Sonatype repositories to be rewritten to the loopback CFS endpoint:`n$repoText" } if ($failureExitCode -eq 0) { @@ -1026,9 +1244,73 @@ does not require `CFS_MAVEN_URL` or `SYSTEM_ACCESSTOKEN`. - [ ] **Step 3: Re-run the policy tests** -Repeat Task 2's fail-fast validation, allowlist-sync check, scoped-local -provenance/no-fallback coverage, and the focused Gradle 9.1 unreachable-CFS -negative test. +Run: + +```powershell +$planPath = (Resolve-Path 'docs\superpowers\plans\2026-08-11-stable-release-network-isolation.md').Path +$planText = Get-Content $planPath -Raw +$scratchRoot = Join-Path (Get-Location) '.scratch\reverify-network-isolation-plan' +$shellHost = if (Get-Command powershell -ErrorAction SilentlyContinue) { + (Get-Command powershell).Source +} else { + (Get-Command pwsh -ErrorAction Stop).Source +} +Remove-Item $scratchRoot -Recurse -Force -ErrorAction SilentlyContinue +New-Item -ItemType Directory -Path $scratchRoot -Force | Out-Null + +function Get-PlanStepScript { + param( + [string]$Heading + ) + + $pattern = [regex]::Escape($Heading) + + "\r?\n\r?\nRun:\r?\n\r?\n" + + [regex]::Escape('```powershell') + + "\r?\n(.*?)\r?\n" + + [regex]::Escape('```') + $match = [regex]::Match( + $planText, + $pattern, + [System.Text.RegularExpressions.RegexOptions]::Singleline + ) + if (-not $match.Success) { + throw "Could not extract the command block for: $Heading" + } + $match.Groups[1].Value +} + +$stepDefinitions = @( + @{ + Heading = '- [ ] **Step 3: Verify fail-fast validation for both CFS credentials and the scoped local handoff**' + ScriptName = 'task2-step3.ps1' + }, + @{ + Heading = '- [ ] **Step 4: Verify the exact local allowlist stays synchronized with the Utils reactor**' + ScriptName = 'task2-step4.ps1' + }, + @{ + Heading = '- [ ] **Step 5: Verify normalized scoped-local matching, future handler coverage, exclusive provenance, and no fallback via an authenticated loopback CFS fixture**' + ScriptName = 'task2-step5.ps1' + }, + @{ + Heading = '- [ ] **Step 6: Run a focused Gradle 9.1 negative test for unreachable CFS**' + ScriptName = 'task2-step6.ps1' + } +) + +try { + foreach ($definition in $stepDefinitions) { + $scriptPath = Join-Path $scratchRoot $definition.ScriptName + Get-PlanStepScript -Heading $definition.Heading | Set-Content -Path $scriptPath + & $shellHost -NoLogo -NoProfile -ExecutionPolicy Bypass -File $scriptPath + if ($LASTEXITCODE -ne 0) { + throw "Re-verification failed for $($definition.Heading)" + } + } +} finally { + Remove-Item $scratchRoot -Recurse -Force -ErrorAction SilentlyContinue +} +``` Expected: @@ -1040,6 +1322,9 @@ Expected: `$(Agent.TempDirectory)\azure-tools-maven-repository` handoff. - Warmed third-party and non-reactor Microsoft coordinates resolve from CFS even when matching artifacts exist in the scoped local repository. +- The loopback CFS fixture records authenticated requests for third-party and + non-reactor Microsoft coordinates and records no requests for allowlisted Utils + coordinates. - An allowlisted module that is absent locally fails rather than falling back to CFS. - In a minimal Gradle 9.1 project, an ordinary coordinate warmed into the From e08cd469c7da4fa71cb2eba0196a9925010bb3c5 Mon Sep 17 00:00:00 2001 From: Miller Wang Date: Tue, 11 Aug 2026 22:32:51 +0800 Subject: [PATCH 16/16] docs: complete network isolation negative checks Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- ...-08-11-stable-release-network-isolation.md | 70 ++++++++++++------- 1 file changed, 43 insertions(+), 27 deletions(-) diff --git a/docs/superpowers/plans/2026-08-11-stable-release-network-isolation.md b/docs/superpowers/plans/2026-08-11-stable-release-network-isolation.md index 5ba74cdfd4b..e63a3b059d2 100644 --- a/docs/superpowers/plans/2026-08-11-stable-release-network-isolation.md +++ b/docs/superpowers/plans/2026-08-11-stable-release-network-isolation.md @@ -422,7 +422,15 @@ $missingLocalArgs = @( '--no-configuration-cache', 'help' ) -$missingCredentialsArgs = @( +$missingUrlArgs = @( + '-p', $projectDir, + '--init-script', $initScript, + '--no-daemon', + '--no-configuration-cache', + "-Dmaven.repo.local=$dummyRepo", + 'help' +) +$missingTokenArgs = @( '-p', $projectDir, '--init-script', $initScript, '--no-daemon', @@ -440,8 +448,18 @@ try { Remove-Item Env:CFS_MAVEN_URL -ErrorAction SilentlyContinue Remove-Item Env:SYSTEM_ACCESSTOKEN -ErrorAction SilentlyContinue - $missingCredentialsOutput = & $gradleWrapper @missingCredentialsArgs 2>&1 - $missingCredentialsExitCode = $LASTEXITCODE + $env:CFS_MAVEN_URL = '' + $env:SYSTEM_ACCESSTOKEN = 'test-token' + $missingUrlOutput = & $gradleWrapper @missingUrlArgs 2>&1 + $missingUrlExitCode = $LASTEXITCODE + + Remove-Item Env:CFS_MAVEN_URL -ErrorAction SilentlyContinue + Remove-Item Env:SYSTEM_ACCESSTOKEN -ErrorAction SilentlyContinue + + $env:CFS_MAVEN_URL = 'https://example.invalid/vscjava/maven/v1' + $env:SYSTEM_ACCESSTOKEN = '' + $missingTokenOutput = & $gradleWrapper @missingTokenArgs 2>&1 + $missingTokenExitCode = $LASTEXITCODE } finally { Remove-Item Env:CFS_MAVEN_URL -ErrorAction SilentlyContinue Remove-Item Env:SYSTEM_ACCESSTOKEN -ErrorAction SilentlyContinue @@ -451,13 +469,16 @@ try { if ($missingLocalExitCode -eq 0 -or ($missingLocalOutput -join "`n") -notmatch 'maven\.repo\.local must be explicitly set') { throw "Expected the scoped local handoff validation error:`n$($missingLocalOutput -join "`n")" } -if ($missingCredentialsExitCode -eq 0 -or ($missingCredentialsOutput -join "`n") -notmatch 'CFS_MAVEN_URL and SYSTEM_ACCESSTOKEN must both be set') { - throw "Expected the CFS credential validation error:`n$($missingCredentialsOutput -join "`n")" +if ($missingUrlExitCode -eq 0 -or ($missingUrlOutput -join "`n") -notmatch 'CFS_MAVEN_URL and SYSTEM_ACCESSTOKEN must both be set') { + throw "Expected the missing CFS_MAVEN_URL validation error:`n$($missingUrlOutput -join "`n")" +} +if ($missingTokenExitCode -eq 0 -or ($missingTokenOutput -join "`n") -notmatch 'CFS_MAVEN_URL and SYSTEM_ACCESSTOKEN must both be set') { + throw "Expected the missing SYSTEM_ACCESSTOKEN validation error:`n$($missingTokenOutput -join "`n")" } -Write-Host 'PASS: Gradle rejects missing scoped local handoff and missing CFS credentials' +Write-Host 'PASS: Gradle rejects missing scoped local handoff, missing CFS_MAVEN_URL, and missing SYSTEM_ACCESSTOKEN' ``` -Expected: `PASS: Gradle rejects missing scoped local handoff and missing CFS credentials`. +Expected: `PASS: Gradle rejects missing scoped local handoff, missing CFS_MAVEN_URL, and missing SYSTEM_ACCESSTOKEN`. - [ ] **Step 4: Verify the exact local allowlist stays synchronized with the Utils reactor** @@ -1260,40 +1281,35 @@ New-Item -ItemType Directory -Path $scratchRoot -Force | Out-Null function Get-PlanStepScript { param( - [string]$Heading + [string]$StepTitle ) - $pattern = [regex]::Escape($Heading) + - "\r?\n\r?\nRun:\r?\n\r?\n" + - [regex]::Escape('```powershell') + - "\r?\n(.*?)\r?\n" + - [regex]::Escape('```') - $match = [regex]::Match( + $pattern = '(?ms)^- \[(?: |x|X)\] \*\*' + [regex]::Escape($StepTitle) + '\*\*\r?\n\r?\nRun:\r?\n\r?\n```powershell\r?\n(.*?)\r?\n```' + $matches = [regex]::Matches( $planText, - $pattern, - [System.Text.RegularExpressions.RegexOptions]::Singleline + $pattern ) - if (-not $match.Success) { - throw "Could not extract the command block for: $Heading" + if ($matches.Count -ne 1) { + throw "Expected exactly one command block for: $StepTitle, but found $($matches.Count)" } - $match.Groups[1].Value + $matches[0].Groups[1].Value } $stepDefinitions = @( @{ - Heading = '- [ ] **Step 3: Verify fail-fast validation for both CFS credentials and the scoped local handoff**' + StepTitle = 'Step 3: Verify fail-fast validation for both CFS credentials and the scoped local handoff' ScriptName = 'task2-step3.ps1' }, @{ - Heading = '- [ ] **Step 4: Verify the exact local allowlist stays synchronized with the Utils reactor**' + StepTitle = 'Step 4: Verify the exact local allowlist stays synchronized with the Utils reactor' ScriptName = 'task2-step4.ps1' }, @{ - Heading = '- [ ] **Step 5: Verify normalized scoped-local matching, future handler coverage, exclusive provenance, and no fallback via an authenticated loopback CFS fixture**' + StepTitle = 'Step 5: Verify normalized scoped-local matching, future handler coverage, exclusive provenance, and no fallback via an authenticated loopback CFS fixture' ScriptName = 'task2-step5.ps1' }, @{ - Heading = '- [ ] **Step 6: Run a focused Gradle 9.1 negative test for unreachable CFS**' + StepTitle = 'Step 6: Run a focused Gradle 9.1 negative test for unreachable CFS' ScriptName = 'task2-step6.ps1' } ) @@ -1301,10 +1317,10 @@ $stepDefinitions = @( try { foreach ($definition in $stepDefinitions) { $scriptPath = Join-Path $scratchRoot $definition.ScriptName - Get-PlanStepScript -Heading $definition.Heading | Set-Content -Path $scriptPath + Get-PlanStepScript -StepTitle $definition.StepTitle | Set-Content -Path $scriptPath & $shellHost -NoLogo -NoProfile -ExecutionPolicy Bypass -File $scriptPath if ($LASTEXITCODE -ne 0) { - throw "Re-verification failed for $($definition.Heading)" + throw "Re-verification failed for $($definition.StepTitle)" } } } finally { @@ -1314,8 +1330,8 @@ try { Expected: -- Missing `maven.repo.local` and missing CFS credentials both fail fast with the - explicit configuration errors. +- Missing `maven.repo.local`, missing `CFS_MAVEN_URL`, and missing + `SYSTEM_ACCESSTOKEN` each fail fast with the explicit configuration errors. - The extracted allowlist matches the current Utils reactor coordinates, including the parent/aggregator POMs. - Allowlisted Utils modules resolve from the scoped