From 54ef32bd9fb829f2d94afc2e9dbec3fe86270968 Mon Sep 17 00:00:00 2001 From: Ian Hou <45278651+iankhou@users.noreply.github.com> Date: Wed, 29 Jul 2026 15:56:52 -0400 Subject: [PATCH 01/26] feat(cli-integ): also run integ test suites on Windows Adds windowsTestRunsOn to the integ workflow. When set, every integ suite (integ_cli, integ_toolkit-lib, integ_telemetry, integ_init-templates, integ_tool-integrations) is instantiated a second time on a Windows runner, suffixed with _windows, to catch platform-specific regressions in path handling and subprocess spawning. The prepare/build job stays on Linux, so there is no repo checkout on the Windows runners; they only download the built artifacts and run the suites. Steps run under Git Bash so the shared bash step scripts work unchanged. --- .github/workflows/integ.yml | 622 +++++++++++++++++++++++++++++++- .projenrc.ts | 4 + projenrc/cdk-cli-integ-tests.ts | 77 +++- 3 files changed, 687 insertions(+), 16 deletions(-) diff --git a/.github/workflows/integ.yml b/.github/workflows/integ.yml index 6b77cd5d3..e33ada73e 100644 --- a/.github/workflows/integ.yml +++ b/.github/workflows/integ.yml @@ -91,6 +91,9 @@ jobs: MAVEN_ARGS: --no-transfer-progress IS_CANARY: "true" CI: "true" + defaults: + run: + shell: bash if: github.event_name != 'merge_group' && !contains(github.event.pull_request.labels.*.name, 'pr/exempt-integ-test') steps: - name: Download artifact @@ -213,6 +216,9 @@ jobs: MAVEN_ARGS: --no-transfer-progress IS_CANARY: "true" CI: "true" + defaults: + run: + shell: bash if: github.event_name != 'merge_group' && !contains(github.event.pull_request.labels.*.name, 'pr/exempt-integ-test') steps: - name: Download artifact @@ -325,6 +331,9 @@ jobs: MAVEN_ARGS: --no-transfer-progress IS_CANARY: "true" CI: "true" + defaults: + run: + shell: bash if: github.event_name != 'merge_group' && !contains(github.event.pull_request.labels.*.name, 'pr/exempt-integ-test') steps: - name: Download artifact @@ -434,6 +443,9 @@ jobs: MAVEN_ARGS: --no-transfer-progress IS_CANARY: "true" CI: "true" + defaults: + run: + shell: bash if: github.event_name != 'merge_group' && !contains(github.event.pull_request.labels.*.name, 'pr/exempt-integ-test') steps: - name: Download artifact @@ -557,6 +569,9 @@ jobs: MAVEN_ARGS: --no-transfer-progress IS_CANARY: "true" CI: "true" + defaults: + run: + shell: bash if: github.event_name != 'merge_group' && !contains(github.event.pull_request.labels.*.name, 'pr/exempt-integ-test') steps: - name: Download artifact @@ -654,6 +669,596 @@ jobs: - tool-integrations node: - "20" + integ_cli_windows: + needs: prepare + runs-on: windows-latest + permissions: + contents: read + id-token: write + environment: run-tests + env: + NODE_NO_WARNINGS: "1" + MAVEN_ARGS: --no-transfer-progress + IS_CANARY: "true" + CI: "true" + defaults: + run: + shell: bash + if: github.event_name != 'merge_group' && !contains(github.event.pull_request.labels.*.name, 'pr/exempt-integ-test') + steps: + - name: Download artifact + uses: actions/download-artifact@v8 + with: + artifact-ids: ${{needs.prepare.outputs.packagesArtifact}} + path: packages + - name: Download artifact + uses: actions/download-artifact@v8 + with: + artifact-ids: ${{needs.prepare.outputs.scriptsArtifact}} + path: .projen + - name: Setup Node.js + uses: actions/setup-node@v6 + with: + node-version: ${{ matrix.node }} + package-manager-cache: false + - name: Set up JDK 18 + if: matrix.suite == 'init-java' || matrix.suite == 'cli-integ-tests' + uses: actions/setup-java@v5 + with: + java-version: "18" + distribution: corretto + - name: Set git identity + run: |- + git config --global user.name "aws-cdk-cli-integ" + git config --global user.email "noreply@example.com" + - name: Prepare Verdaccio + run: chmod +x .projen/prepare-verdaccio.sh && .projen/prepare-verdaccio.sh + - name: Download and install the test artifact + run: npm install @aws-cdk-testing/cli-integ + - name: Determine latest package versions + id: versions + run: |- + CLI_VERSION=$(cd ${TMPDIR:-/tmp} && npm view aws-cdk version) + echo "CLI version: ${CLI_VERSION}" + echo "cli_version=${CLI_VERSION}" >> $GITHUB_OUTPUT + LIB_VERSION=$(cd ${TMPDIR:-/tmp} && npm view aws-cdk-lib version) + echo "lib version: ${LIB_VERSION}" + echo "lib_version=${LIB_VERSION}" >> $GITHUB_OUTPUT + - name: Authenticate Via OIDC Role + id: creds + uses: aws-actions/configure-aws-credentials@v6 + with: + aws-region: us-east-1 + role-duration-seconds: 3600 + role-to-assume: ${{ vars.CDK_ATMOSPHERE_PROD_OIDC_ROLE }} + role-session-name: run-tests@aws-cdk-cli-integ + output-credentials: true + - name: "Run the test suite: ${{ matrix.suite }}" + env: + JSII_SILENCE_WARNING_DEPRECATED_NODE_VERSION: "true" + JSII_SILENCE_WARNING_UNTESTED_NODE_VERSION: "true" + JSII_SILENCE_WARNING_KNOWN_BROKEN_NODE_VERSION: "true" + DOCKERHUB_DISABLED: "true" + CDK_INTEG_ATMOSPHERE_ENABLED: "true" + CDK_INTEG_ATMOSPHERE_ENDPOINT: ${{ vars.CDK_ATMOSPHERE_PROD_ENDPOINT }} + CDK_INTEG_ATMOSPHERE_POOL: ${{ vars.CDK_INTEG_ATMOSPHERE_POOL }} + CDK_MAJOR_VERSION: "2" + RELEASE_TAG: latest + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + INTEG_LOGS: logs + run: npx run-suite --shard="${{ matrix.shard }}/12" --use-cli-release=${{ steps.versions.outputs.cli_version }} --framework-version=${{ steps.versions.outputs.lib_version }} ${{ matrix.suite }} + - name: Set workflow summary + if: always() + run: |- + if compgen -G "logs/md/*.md" > /dev/null; then + cat logs/md/*.md >> $GITHUB_STEP_SUMMARY; + fi + - name: Slugify artifact id + id: artifactid + if: always() + env: + INPUT: logs_windows-${{ matrix.suite }}-${{ matrix.node }}-${{ matrix.shard }} + run: |- + slug=$(node -p 'process.env.INPUT.replace(/[^a-z0-9._-]/gi, "-")') + echo "slug=$slug" >> "$GITHUB_OUTPUT" + - name: Upload logs + id: logupload + if: always() + uses: actions/upload-artifact@v7 + with: + name: ${{ steps.artifactid.outputs.slug }} + path: logs/ + overwrite: true + - name: Append artifact URL + if: always() + run: |- + echo "" >> $GITHUB_STEP_SUMMARY + echo "[Logs](${{ steps.logupload.outputs.artifact-url }})" >> $GITHUB_STEP_SUMMARY + strategy: + fail-fast: false + matrix: + suite: + - cli-integ-tests + node: + - lts/* + shard: + - 1 + - 2 + - 3 + - 4 + - 5 + - 6 + - 7 + - 8 + - 9 + - 10 + - 11 + - 12 + integ_toolkit-lib_windows: + needs: prepare + runs-on: windows-latest + permissions: + contents: read + id-token: write + environment: run-tests + env: + NODE_NO_WARNINGS: "1" + MAVEN_ARGS: --no-transfer-progress + IS_CANARY: "true" + CI: "true" + defaults: + run: + shell: bash + if: github.event_name != 'merge_group' && !contains(github.event.pull_request.labels.*.name, 'pr/exempt-integ-test') + steps: + - name: Download artifact + uses: actions/download-artifact@v8 + with: + artifact-ids: ${{needs.prepare.outputs.packagesArtifact}} + path: packages + - name: Download artifact + uses: actions/download-artifact@v8 + with: + artifact-ids: ${{needs.prepare.outputs.scriptsArtifact}} + path: .projen + - name: Setup Node.js + uses: actions/setup-node@v6 + with: + node-version: ${{ matrix.node }} + package-manager-cache: false + - name: Set up JDK 18 + if: matrix.suite == 'init-java' || matrix.suite == 'cli-integ-tests' + uses: actions/setup-java@v5 + with: + java-version: "18" + distribution: corretto + - name: Set git identity + run: |- + git config --global user.name "aws-cdk-cli-integ" + git config --global user.email "noreply@example.com" + - name: Prepare Verdaccio + run: chmod +x .projen/prepare-verdaccio.sh && .projen/prepare-verdaccio.sh + - name: Download and install the test artifact + run: npm install @aws-cdk-testing/cli-integ + - name: Determine latest package versions + id: versions + run: |- + CLI_VERSION=$(cd ${TMPDIR:-/tmp} && npm view aws-cdk version) + echo "CLI version: ${CLI_VERSION}" + echo "cli_version=${CLI_VERSION}" >> $GITHUB_OUTPUT + LIB_VERSION=$(cd ${TMPDIR:-/tmp} && npm view aws-cdk-lib version) + echo "lib version: ${LIB_VERSION}" + echo "lib_version=${LIB_VERSION}" >> $GITHUB_OUTPUT + - name: Authenticate Via OIDC Role + id: creds + uses: aws-actions/configure-aws-credentials@v6 + with: + aws-region: us-east-1 + role-duration-seconds: 3600 + role-to-assume: ${{ vars.CDK_ATMOSPHERE_PROD_OIDC_ROLE }} + role-session-name: run-tests@aws-cdk-cli-integ + output-credentials: true + - name: "Run the test suite: ${{ matrix.suite }}" + env: + JSII_SILENCE_WARNING_DEPRECATED_NODE_VERSION: "true" + JSII_SILENCE_WARNING_UNTESTED_NODE_VERSION: "true" + JSII_SILENCE_WARNING_KNOWN_BROKEN_NODE_VERSION: "true" + DOCKERHUB_DISABLED: "true" + CDK_INTEG_ATMOSPHERE_ENABLED: "true" + CDK_INTEG_ATMOSPHERE_ENDPOINT: ${{ vars.CDK_ATMOSPHERE_PROD_ENDPOINT }} + CDK_INTEG_ATMOSPHERE_POOL: ${{ vars.CDK_INTEG_ATMOSPHERE_POOL }} + CDK_MAJOR_VERSION: "2" + RELEASE_TAG: latest + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + INTEG_LOGS: logs + run: npx run-suite --use-cli-release=${{ steps.versions.outputs.cli_version }} --framework-version=${{ steps.versions.outputs.lib_version }} ${{ matrix.suite }} + - name: Set workflow summary + if: always() + run: |- + if compgen -G "logs/md/*.md" > /dev/null; then + cat logs/md/*.md >> $GITHUB_STEP_SUMMARY; + fi + - name: Slugify artifact id + id: artifactid + if: always() + env: + INPUT: logs_windows-${{ matrix.suite }}-${{ matrix.node }} + run: |- + slug=$(node -p 'process.env.INPUT.replace(/[^a-z0-9._-]/gi, "-")') + echo "slug=$slug" >> "$GITHUB_OUTPUT" + - name: Upload logs + id: logupload + if: always() + uses: actions/upload-artifact@v7 + with: + name: ${{ steps.artifactid.outputs.slug }} + path: logs/ + overwrite: true + - name: Append artifact URL + if: always() + run: |- + echo "" >> $GITHUB_STEP_SUMMARY + echo "[Logs](${{ steps.logupload.outputs.artifact-url }})" >> $GITHUB_STEP_SUMMARY + strategy: + fail-fast: false + matrix: + suite: + - toolkit-lib-integ-tests + node: + - lts/* + - "20" + - "22" + - "24" + integ_telemetry_windows: + needs: prepare + runs-on: windows-latest + permissions: + contents: read + id-token: write + environment: run-tests + env: + NODE_NO_WARNINGS: "1" + MAVEN_ARGS: --no-transfer-progress + IS_CANARY: "true" + CI: "true" + defaults: + run: + shell: bash + if: github.event_name != 'merge_group' && !contains(github.event.pull_request.labels.*.name, 'pr/exempt-integ-test') + steps: + - name: Download artifact + uses: actions/download-artifact@v8 + with: + artifact-ids: ${{needs.prepare.outputs.packagesArtifact}} + path: packages + - name: Download artifact + uses: actions/download-artifact@v8 + with: + artifact-ids: ${{needs.prepare.outputs.scriptsArtifact}} + path: .projen + - name: Setup Node.js + uses: actions/setup-node@v6 + with: + node-version: ${{ matrix.node }} + package-manager-cache: false + - name: Set up JDK 18 + if: matrix.suite == 'init-java' || matrix.suite == 'cli-integ-tests' + uses: actions/setup-java@v5 + with: + java-version: "18" + distribution: corretto + - name: Set git identity + run: |- + git config --global user.name "aws-cdk-cli-integ" + git config --global user.email "noreply@example.com" + - name: Prepare Verdaccio + run: chmod +x .projen/prepare-verdaccio.sh && .projen/prepare-verdaccio.sh + - name: Download and install the test artifact + run: npm install @aws-cdk-testing/cli-integ + - name: Determine latest package versions + id: versions + run: |- + CLI_VERSION=$(cd ${TMPDIR:-/tmp} && npm view aws-cdk version) + echo "CLI version: ${CLI_VERSION}" + echo "cli_version=${CLI_VERSION}" >> $GITHUB_OUTPUT + LIB_VERSION=$(cd ${TMPDIR:-/tmp} && npm view aws-cdk-lib version) + echo "lib version: ${LIB_VERSION}" + echo "lib_version=${LIB_VERSION}" >> $GITHUB_OUTPUT + - name: Authenticate Via OIDC Role + id: creds + uses: aws-actions/configure-aws-credentials@v6 + with: + aws-region: us-east-1 + role-duration-seconds: 3600 + role-to-assume: ${{ vars.CDK_ATMOSPHERE_PROD_OIDC_ROLE }} + role-session-name: run-tests@aws-cdk-cli-integ + output-credentials: true + - name: "Run the test suite: ${{ matrix.suite }}" + env: + JSII_SILENCE_WARNING_DEPRECATED_NODE_VERSION: "true" + JSII_SILENCE_WARNING_UNTESTED_NODE_VERSION: "true" + JSII_SILENCE_WARNING_KNOWN_BROKEN_NODE_VERSION: "true" + DOCKERHUB_DISABLED: "true" + CDK_INTEG_ATMOSPHERE_ENABLED: "true" + CDK_INTEG_ATMOSPHERE_ENDPOINT: ${{ vars.CDK_ATMOSPHERE_PROD_ENDPOINT }} + CDK_INTEG_ATMOSPHERE_POOL: ${{ vars.CDK_INTEG_ATMOSPHERE_POOL }} + CDK_MAJOR_VERSION: "2" + RELEASE_TAG: latest + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + INTEG_LOGS: logs + run: npx run-suite --use-cli-release=${{ steps.versions.outputs.cli_version }} --framework-version=${{ steps.versions.outputs.lib_version }} ${{ matrix.suite }} + - name: Set workflow summary + if: always() + run: |- + if compgen -G "logs/md/*.md" > /dev/null; then + cat logs/md/*.md >> $GITHUB_STEP_SUMMARY; + fi + - name: Slugify artifact id + id: artifactid + if: always() + env: + INPUT: logs_windows-${{ matrix.suite }}-${{ matrix.node }} + run: |- + slug=$(node -p 'process.env.INPUT.replace(/[^a-z0-9._-]/gi, "-")') + echo "slug=$slug" >> "$GITHUB_OUTPUT" + - name: Upload logs + id: logupload + if: always() + uses: actions/upload-artifact@v7 + with: + name: ${{ steps.artifactid.outputs.slug }} + path: logs/ + overwrite: true + - name: Append artifact URL + if: always() + run: |- + echo "" >> $GITHUB_STEP_SUMMARY + echo "[Logs](${{ steps.logupload.outputs.artifact-url }})" >> $GITHUB_STEP_SUMMARY + strategy: + fail-fast: false + matrix: + suite: + - telemetry-integ-tests + node: + - lts/* + integ_init-templates_windows: + needs: prepare + runs-on: windows-latest + permissions: + contents: read + id-token: write + environment: run-tests + env: + NODE_NO_WARNINGS: "1" + MAVEN_ARGS: --no-transfer-progress + IS_CANARY: "true" + CI: "true" + defaults: + run: + shell: bash + if: github.event_name != 'merge_group' && !contains(github.event.pull_request.labels.*.name, 'pr/exempt-integ-test') + steps: + - name: Download artifact + uses: actions/download-artifact@v8 + with: + artifact-ids: ${{needs.prepare.outputs.packagesArtifact}} + path: packages + - name: Download artifact + uses: actions/download-artifact@v8 + with: + artifact-ids: ${{needs.prepare.outputs.scriptsArtifact}} + path: .projen + - name: Setup Node.js + uses: actions/setup-node@v6 + with: + node-version: ${{ matrix.node }} + package-manager-cache: false + - name: Set up JDK 18 + if: matrix.suite == 'init-java' || matrix.suite == 'cli-integ-tests' + uses: actions/setup-java@v5 + with: + java-version: "18" + distribution: corretto + - name: Set git identity + run: |- + git config --global user.name "aws-cdk-cli-integ" + git config --global user.email "noreply@example.com" + - name: Prepare Verdaccio + run: chmod +x .projen/prepare-verdaccio.sh && .projen/prepare-verdaccio.sh + - name: Download and install the test artifact + run: npm install @aws-cdk-testing/cli-integ + - name: Determine latest package versions + id: versions + run: |- + CLI_VERSION=$(cd ${TMPDIR:-/tmp} && npm view aws-cdk version) + echo "CLI version: ${CLI_VERSION}" + echo "cli_version=${CLI_VERSION}" >> $GITHUB_OUTPUT + LIB_VERSION=$(cd ${TMPDIR:-/tmp} && npm view aws-cdk-lib version) + echo "lib version: ${LIB_VERSION}" + echo "lib_version=${LIB_VERSION}" >> $GITHUB_OUTPUT + - name: Authenticate Via OIDC Role + id: creds + uses: aws-actions/configure-aws-credentials@v6 + with: + aws-region: us-east-1 + role-duration-seconds: 3600 + role-to-assume: ${{ vars.CDK_ATMOSPHERE_PROD_OIDC_ROLE }} + role-session-name: run-tests@aws-cdk-cli-integ + output-credentials: true + - name: "Run the test suite: ${{ matrix.suite }}" + env: + JSII_SILENCE_WARNING_DEPRECATED_NODE_VERSION: "true" + JSII_SILENCE_WARNING_UNTESTED_NODE_VERSION: "true" + JSII_SILENCE_WARNING_KNOWN_BROKEN_NODE_VERSION: "true" + DOCKERHUB_DISABLED: "true" + CDK_INTEG_ATMOSPHERE_ENABLED: "true" + CDK_INTEG_ATMOSPHERE_ENDPOINT: ${{ vars.CDK_ATMOSPHERE_PROD_ENDPOINT }} + CDK_INTEG_ATMOSPHERE_POOL: ${{ vars.CDK_INTEG_ATMOSPHERE_POOL }} + CDK_MAJOR_VERSION: "2" + RELEASE_TAG: latest + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + INTEG_LOGS: logs + run: npx run-suite --use-cli-release=${{ steps.versions.outputs.cli_version }} --framework-version=${{ steps.versions.outputs.lib_version }} ${{ matrix.suite }} + - name: Set workflow summary + if: always() + run: |- + if compgen -G "logs/md/*.md" > /dev/null; then + cat logs/md/*.md >> $GITHUB_STEP_SUMMARY; + fi + - name: Slugify artifact id + id: artifactid + if: always() + env: + INPUT: logs_windows-${{ matrix.suite }}-${{ matrix.node }} + run: |- + slug=$(node -p 'process.env.INPUT.replace(/[^a-z0-9._-]/gi, "-")') + echo "slug=$slug" >> "$GITHUB_OUTPUT" + - name: Upload logs + id: logupload + if: always() + uses: actions/upload-artifact@v7 + with: + name: ${{ steps.artifactid.outputs.slug }} + path: logs/ + overwrite: true + - name: Append artifact URL + if: always() + run: |- + echo "" >> $GITHUB_STEP_SUMMARY + echo "[Logs](${{ steps.logupload.outputs.artifact-url }})" >> $GITHUB_STEP_SUMMARY + strategy: + fail-fast: false + matrix: + include: + - suite: init-typescript-app + node: "20" + - suite: init-typescript-app + node: "22" + - suite: init-typescript-app + node: "24" + suite: + - init-csharp + - init-fsharp + - init-go + - init-java + - init-javascript + - init-python + - init-typescript-app + - init-typescript-lib + node: + - lts/* + integ_tool-integrations_windows: + needs: prepare + runs-on: windows-latest + permissions: + contents: read + id-token: write + environment: run-tests + env: + NODE_NO_WARNINGS: "1" + MAVEN_ARGS: --no-transfer-progress + IS_CANARY: "true" + CI: "true" + defaults: + run: + shell: bash + if: github.event_name != 'merge_group' && !contains(github.event.pull_request.labels.*.name, 'pr/exempt-integ-test') + steps: + - name: Download artifact + uses: actions/download-artifact@v8 + with: + artifact-ids: ${{needs.prepare.outputs.packagesArtifact}} + path: packages + - name: Download artifact + uses: actions/download-artifact@v8 + with: + artifact-ids: ${{needs.prepare.outputs.scriptsArtifact}} + path: .projen + - name: Setup Node.js + uses: actions/setup-node@v6 + with: + node-version: ${{ matrix.node }} + package-manager-cache: false + - name: Set up JDK 18 + if: matrix.suite == 'init-java' || matrix.suite == 'cli-integ-tests' + uses: actions/setup-java@v5 + with: + java-version: "18" + distribution: corretto + - name: Set git identity + run: |- + git config --global user.name "aws-cdk-cli-integ" + git config --global user.email "noreply@example.com" + - name: Prepare Verdaccio + run: chmod +x .projen/prepare-verdaccio.sh && .projen/prepare-verdaccio.sh + - name: Download and install the test artifact + run: npm install @aws-cdk-testing/cli-integ + - name: Determine latest package versions + id: versions + run: |- + CLI_VERSION=$(cd ${TMPDIR:-/tmp} && npm view aws-cdk version) + echo "CLI version: ${CLI_VERSION}" + echo "cli_version=${CLI_VERSION}" >> $GITHUB_OUTPUT + LIB_VERSION=$(cd ${TMPDIR:-/tmp} && npm view aws-cdk-lib version) + echo "lib version: ${LIB_VERSION}" + echo "lib_version=${LIB_VERSION}" >> $GITHUB_OUTPUT + - name: Authenticate Via OIDC Role + id: creds + uses: aws-actions/configure-aws-credentials@v6 + with: + aws-region: us-east-1 + role-duration-seconds: 3600 + role-to-assume: ${{ vars.CDK_ATMOSPHERE_PROD_OIDC_ROLE }} + role-session-name: run-tests@aws-cdk-cli-integ + output-credentials: true + - name: "Run the test suite: ${{ matrix.suite }}" + env: + JSII_SILENCE_WARNING_DEPRECATED_NODE_VERSION: "true" + JSII_SILENCE_WARNING_UNTESTED_NODE_VERSION: "true" + JSII_SILENCE_WARNING_KNOWN_BROKEN_NODE_VERSION: "true" + DOCKERHUB_DISABLED: "true" + CDK_INTEG_ATMOSPHERE_ENABLED: "true" + CDK_INTEG_ATMOSPHERE_ENDPOINT: ${{ vars.CDK_ATMOSPHERE_PROD_ENDPOINT }} + CDK_INTEG_ATMOSPHERE_POOL: ${{ vars.CDK_INTEG_ATMOSPHERE_POOL }} + CDK_MAJOR_VERSION: "2" + RELEASE_TAG: latest + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + INTEG_LOGS: logs + run: npx run-suite --use-cli-release=${{ steps.versions.outputs.cli_version }} --framework-version=${{ steps.versions.outputs.lib_version }} ${{ matrix.suite }} + - name: Set workflow summary + if: always() + run: |- + if compgen -G "logs/md/*.md" > /dev/null; then + cat logs/md/*.md >> $GITHUB_STEP_SUMMARY; + fi + - name: Slugify artifact id + id: artifactid + if: always() + env: + INPUT: logs_windows-${{ matrix.suite }}-${{ matrix.node }} + run: |- + slug=$(node -p 'process.env.INPUT.replace(/[^a-z0-9._-]/gi, "-")') + echo "slug=$slug" >> "$GITHUB_OUTPUT" + - name: Upload logs + id: logupload + if: always() + uses: actions/upload-artifact@v7 + with: + name: ${{ steps.artifactid.outputs.slug }} + path: logs/ + overwrite: true + - name: Append artifact URL + if: always() + run: |- + echo "" >> $GITHUB_STEP_SUMMARY + echo "[Logs](${{ steps.logupload.outputs.artifact-url }})" >> $GITHUB_STEP_SUMMARY + strategy: + fail-fast: false + matrix: + suite: + - tool-integrations + node: + - "20" integ: needs: - integ_cli @@ -661,6 +1266,11 @@ jobs: - integ_telemetry - integ_init-templates - integ_tool-integrations + - integ_cli_windows + - integ_toolkit-lib_windows + - integ_telemetry_windows + - integ_init-templates_windows + - integ_tool-integrations_windows runs-on: ubuntu-latest permissions: {} if: always() @@ -675,6 +1285,16 @@ jobs: run: echo ${{ needs.integ_init-templates.result }} - name: integ_tool-integrations result run: echo ${{ needs.integ_tool-integrations.result }} + - name: integ_cli_windows result + run: echo ${{ needs.integ_cli_windows.result }} + - name: integ_toolkit-lib_windows result + run: echo ${{ needs.integ_toolkit-lib_windows.result }} + - name: integ_telemetry_windows result + run: echo ${{ needs.integ_telemetry_windows.result }} + - name: integ_init-templates_windows result + run: echo ${{ needs.integ_init-templates_windows.result }} + - name: integ_tool-integrations_windows result + run: echo ${{ needs.integ_tool-integrations_windows.result }} - name: Set status based on test results - if: ${{ !(contains(fromJSON('["success", "skipped"]'), needs.integ_cli.result) && contains(fromJSON('["success", "skipped"]'), needs.integ_toolkit-lib.result) && contains(fromJSON('["success", "skipped"]'), needs.integ_telemetry.result) && contains(fromJSON('["success", "skipped"]'), needs.integ_init-templates.result) && contains(fromJSON('["success", "skipped"]'), needs.integ_tool-integrations.result)) }} + if: ${{ !(contains(fromJSON('["success", "skipped"]'), needs.integ_cli.result) && contains(fromJSON('["success", "skipped"]'), needs.integ_toolkit-lib.result) && contains(fromJSON('["success", "skipped"]'), needs.integ_telemetry.result) && contains(fromJSON('["success", "skipped"]'), needs.integ_init-templates.result) && contains(fromJSON('["success", "skipped"]'), needs.integ_tool-integrations.result) && contains(fromJSON('["success", "skipped"]'), needs.integ_cli_windows.result) && contains(fromJSON('["success", "skipped"]'), needs.integ_toolkit-lib_windows.result) && contains(fromJSON('["success", "skipped"]'), needs.integ_telemetry_windows.result) && contains(fromJSON('["success", "skipped"]'), needs.integ_init-templates_windows.result) && contains(fromJSON('["success", "skipped"]'), needs.integ_tool-integrations_windows.result)) }} run: exit 1 diff --git a/.projenrc.ts b/.projenrc.ts index 87f0b8b86..e18f01b0e 100644 --- a/.projenrc.ts +++ b/.projenrc.ts @@ -1764,6 +1764,10 @@ new CdkCliIntegTestsWorkflow(repo, { testEnvironment: TEST_ENVIRONMENT, buildRunsOn: POWERFUL_RUNNER, testRunsOn: POWERFUL_RUNNER, + // Also run the integ suites on Windows to catch platform-specific + // regressions (paths, subprocess spawning). Uses the free standard runner + // for now; switch to a larger runner label once one is provisioned. + windowsTestRunsOn: 'windows-latest', allowUpstreamVersions: [ // cloud-assembly-schema gets referenced under multiple versions diff --git a/projenrc/cdk-cli-integ-tests.ts b/projenrc/cdk-cli-integ-tests.ts index 3a48de7a6..d8b00f81a 100644 --- a/projenrc/cdk-cli-integ-tests.ts +++ b/projenrc/cdk-cli-integ-tests.ts @@ -130,6 +130,17 @@ export interface CdkCliIntegTestsWorkflowProps { */ readonly testRunsOn: string; + /** + * If given, additionally run every integ test matrix job on this Windows + * runner (in addition to the `testRunsOn` runner). + * + * The Windows jobs are suffixed with `_windows` and run all steps under Git + * Bash so the shared bash step scripts keep working. + * + * @default - integ tests only run on `testRunsOn` + */ + readonly windowsTestRunsOn?: string; + /** * GitHub environment name for approvals * @@ -445,36 +456,37 @@ export class CdkCliIntegTestsWorkflow extends Component { // Ensure this is an array const additionalNodeVersionsToTest = this.props.additionalNodeVersionsToTest ?? []; - const testJobs = [ + // The integ test suites, defined once and instantiated per platform. + const suites: Array<[string, MatrixIntegTestProps]> = [ // cli-integ-tests - this.addMatrixJob('cli', { + ['cli', { domain: { suite: ['cli-integ-tests'], shards: 12, }, - }), + }], // toolkit-lib - this.addMatrixJob('toolkit-lib', { + ['toolkit-lib', { domain: { suite: [ 'toolkit-lib-integ-tests', ], node: ['lts/*', ...additionalNodeVersionsToTest], }, - }), + }], // telemetry - this.addMatrixJob('telemetry', { + ['telemetry', { domain: { suite: [ 'telemetry-integ-tests', ], }, - }), + }], // init-templates - this.addMatrixJob('init-templates', { + ['init-templates', { domain: { suite: [ 'init-csharp', @@ -493,15 +505,27 @@ export class CdkCliIntegTestsWorkflow extends Component { suite: 'init-typescript-app', node, })), - }), + }], // We are finding that Amplify works on Node 20, but fails on Node >=22.10. Remove the 'lts/*' test and use a Node 20 for now. - this.addMatrixJob('tool-integrations', { + ['tool-integrations', { domain: { suite: ['tool-integrations'], node: ['20'], }, - }), + }], + ]; + + const testJobs = [ + ...suites.map(([name, jobProps]) => this.addMatrixJob(name, jobProps, { + runsOn: this.props.testRunsOn, + })), + ...(this.props.windowsTestRunsOn + ? suites.map(([name, jobProps]) => this.addMatrixJob(name, jobProps, { + runsOn: this.props.windowsTestRunsOn!, + suffix: '_windows', + })) + : []), ]; // Add a job that collates all matrix jobs into a single status @@ -528,12 +552,13 @@ export class CdkCliIntegTestsWorkflow extends Component { }); } - private addMatrixJob(testName: string, props: MatrixIntegTestProps): string { - const jobName = `integ_${testName}`; + private addMatrixJob(testName: string, props: MatrixIntegTestProps, platform: PlatformOptions): string { + const suffix = platform.suffix ?? ''; + const jobName = `integ_${testName}${suffix}`; let shard: any; let shardArg = ''; - let logName = 'logs-${{ matrix.suite }}-${{ matrix.node }}'; + let logName = `logs${suffix}-\${{ matrix.suite }}-\${{ matrix.node }}`; if (props.domain.shards) { shard = Array(props.domain.shards).fill(0).map((_, i) => i + 1); shardArg = ` --shard="\${{ matrix.shard }}/${props.domain.shards}"`; @@ -542,12 +567,19 @@ export class CdkCliIntegTestsWorkflow extends Component { this.workflow.addJob(jobName, { environment: this.props.testEnvironment, - runsOn: [this.props.testRunsOn], + runsOn: [platform.runsOn], needs: [this.JOB_PREPARE], permissions: { contents: github.workflows.JobPermission.READ, idToken: github.workflows.JobPermission.WRITE, }, + // The step scripts are written for bash; on Windows runners use Git Bash + // (preinstalled) so they run unchanged while still exercising Windows. + defaults: { + run: { + shell: 'bash', + }, + }, env: { // Integ tests heavily rely on processing stdout, node warnings (mostly deprecations) are muddying this. // We can disable any warnings here, there's plenty of other places we will see them. @@ -685,3 +717,18 @@ interface MatrixIntegTestProps { readonly exclude?: github.workflows.JobMatrix['exclude']; readonly extraEnv?: Record; } + +interface PlatformOptions { + /** + * The runner label to run this instance of the job on. + */ + readonly runsOn: string; + + /** + * Suffix appended to the job name and log artifact names, to disambiguate + * multiple platform instances of the same suite. + * + * @default - no suffix + */ + readonly suffix?: string; +} From f2185ad73b722358cf63bca3d3963542405e8c8d Mon Sep 17 00:00:00 2001 From: Ian Hou <45278651+iankhou@users.noreply.github.com> Date: Wed, 29 Jul 2026 17:13:22 -0400 Subject: [PATCH 02/26] fix(cli-integ): start Verdaccio via node interpreter for Windows pm2's fork mode cannot launch the global `verdaccio` bin on Windows because it is a `.cmd` shim, not a Node script, so the registry never came up and every integ job failed at the publish step. Resolve the JS entrypoint and start it with an explicit `--interpreter node`, which works on all platforms. --- projenrc/cdk-cli-integ-tests.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/projenrc/cdk-cli-integ-tests.ts b/projenrc/cdk-cli-integ-tests.ts index d8b00f81a..9119a1eca 100644 --- a/projenrc/cdk-cli-integ-tests.ts +++ b/projenrc/cdk-cli-integ-tests.ts @@ -303,7 +303,12 @@ export class CdkCliIntegTestsWorkflow extends Component { 'npm install -g verdaccio pm2', 'mkdir -p $HOME/.config/verdaccio', `echo '${JSON.stringify(verdaccioConfig)}' > $HOME/.config/verdaccio/config.yaml`, - 'pm2 start verdaccio -- --config $HOME/.config/verdaccio/config.yaml', + // Start Verdaccio through pm2 by pointing at its JS entrypoint with an + // explicit Node interpreter. On Windows the global `verdaccio` bin is a + // `.cmd` shim, which pm2's fork mode cannot execute; the resolved JS + // file works on every platform. + 'VERDACCIO_BIN="$(npm root -g)/verdaccio/bin/verdaccio"', + 'pm2 start "$VERDACCIO_BIN" --interpreter node -- --config $HOME/.config/verdaccio/config.yaml', 'sleep 5', // Wait for Verdaccio to start // Configure NPM to use local registry 'echo \'//localhost:4873/:_authToken="MWRjNDU3OTE1NTljYWUyOTFkMWJkOGUyYTIwZWMwNTI6YTgwZjkyNDE0NzgwYWQzNQ=="\' > ~/.npmrc', From 993965e67c34a2d7e71037f7fe1efa30fdc114c3 Mon Sep 17 00:00:00 2001 From: Ian Hou <45278651+iankhou@users.noreply.github.com> Date: Thu, 30 Jul 2026 09:55:39 -0400 Subject: [PATCH 03/26] fix(cli-integ): make test harness work on Windows Three POSIX-isms in the test harness broke every Windows integ run: - cloneDirectory() shelled out to `rm -rf`/`mkdir -p`/`cp -R`, which do not exist under cmd.exe. Replaced with fs.promises equivalents. - addToShellPath() split and joined PATH with ':'; on Windows the delimiter is ';', so prepending the CLI bin dir corrupted the PATH and `cdk` was never found. Use path.delimiter. - A few tests shelled out to `rm`, `cat` (with globs), and `diff`; replaced with portable fs-based implementations. --- .../@aws-cdk-testing/cli-integ/lib/shell.ts | 4 +-- .../cli-integ/lib/with-cdk-app.ts | 9 +++--- ...nerating-and-loading-assembly.integtest.ts | 29 +++++++++++++++++-- ...isk-contain-metadata-resource.integtest.ts | 23 +++++++++++---- .../init-typescript-app.integtest.ts | 2 +- 5 files changed, 53 insertions(+), 14 deletions(-) diff --git a/packages/@aws-cdk-testing/cli-integ/lib/shell.ts b/packages/@aws-cdk-testing/cli-integ/lib/shell.ts index 436ee7633..1be3c53f4 100644 --- a/packages/@aws-cdk-testing/cli-integ/lib/shell.ts +++ b/packages/@aws-cdk-testing/cli-integ/lib/shell.ts @@ -310,13 +310,13 @@ export function rimraf(fsPath: string): boolean { } export function addToShellPath(x: string) { - const parts = process.env.PATH?.split(':') ?? []; + const parts = process.env.PATH?.split(path.delimiter) ?? []; if (!parts.includes(x)) { parts.unshift(x); } - process.env.PATH = parts.join(':'); + process.env.PATH = parts.join(path.delimiter); } /** diff --git a/packages/@aws-cdk-testing/cli-integ/lib/with-cdk-app.ts b/packages/@aws-cdk-testing/cli-integ/lib/with-cdk-app.ts index 5923a445e..44c925ab1 100644 --- a/packages/@aws-cdk-testing/cli-integ/lib/with-cdk-app.ts +++ b/packages/@aws-cdk-testing/cli-integ/lib/with-cdk-app.ts @@ -13,7 +13,7 @@ import type { ITestCliSource, ITestLibrarySource } from './package-sources/sourc import { testSource } from './package-sources/subprocess'; import { RESOURCES_DIR } from './resources'; import type { ShellOptions } from './shell'; -import { shell, ShellHelper, rimraf } from './shell'; +import { ShellHelper, rimraf } from './shell'; import type { AwsContext, AwsContextOptions } from './with-aws'; import { atmosphereEnabled, withAws } from './with-aws'; import { withTimeout } from './with-timeout'; @@ -279,9 +279,10 @@ export interface CdkDestroyCliOptions extends CdkCliOptions { * Prepare a target dir byreplicating a source directory */ export async function cloneDirectory(source: string, target: string, output?: NodeJS.WritableStream) { - await shell(['rm', '-rf', target], { outputs: output ? [output] : [] }); - await shell(['mkdir', '-p', target], { outputs: output ? [output] : [] }); - await shell(['cp', '-R', source + '/*', target], { outputs: output ? [output] : [] }); + output?.write(`Cloning ${source} into ${target}\n`); + await fs.promises.rm(target, { recursive: true, force: true }); + await fs.promises.mkdir(target, { recursive: true }); + await fs.promises.cp(source, target, { recursive: true }); } interface CommonCdkBootstrapCommandOptions { diff --git a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/deploy/cdk-generating-and-loading-assembly.integtest.ts b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/deploy/cdk-generating-and-loading-assembly.integtest.ts index b4106c500..893afa16f 100644 --- a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/deploy/cdk-generating-and-loading-assembly.integtest.ts +++ b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/deploy/cdk-generating-and-loading-assembly.integtest.ts @@ -7,14 +7,14 @@ integTest( 'generating and loading assembly', withDefaultFixture(async (fixture) => { const asmOutputDir = `${fixture.integTestDir}-cdk-integ-asm`; - await fixture.shell(['rm', '-rf', asmOutputDir]); + await fs.rm(asmOutputDir, { recursive: true, force: true }); // Synthesize a Cloud Assembly tothe default directory (cdk.out) and a specific directory. await fixture.cdk(['synth']); await fixture.cdk(['synth', '--output', asmOutputDir]); // cdk.out in the current directory and the indicated --output should be the same - await fixture.shell(['diff', 'cdk.out', asmOutputDir]); + await assertDirsEqual(path.join(fixture.integTestDir, 'cdk.out'), asmOutputDir); // Check that we can 'ls' the synthesized asm. // Change to some random directory to make sure we're not accidentally loading cdk.json @@ -48,3 +48,28 @@ integTest( }), ); +/** + * Assert that two directories have the same files with the same contents (like `diff -r`) + */ +async function assertDirsEqual(dirA: string, dirB: string) { + const filesA = await relativeFiles(dirA); + const filesB = await relativeFiles(dirB); + expect(filesB).toEqual(filesA); + + for (const file of filesA) { + const contentsA = await fs.readFile(path.join(dirA, file), 'utf-8'); + const contentsB = await fs.readFile(path.join(dirB, file), 'utf-8'); + if (contentsA !== contentsB) { + throw new Error(`File ${file} differs between ${dirA} and ${dirB}`); + } + } +} + +async function relativeFiles(root: string): Promise { + const entries = await fs.readdir(root, { recursive: true, withFileTypes: true }); + return entries + .filter((e) => e.isFile()) + .map((e) => path.join(path.relative(root, e.parentPath), e.name)) + .sort(); +} + diff --git a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/synth/cdk-templates-on-disk-contain-metadata-resource.integtest.ts b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/synth/cdk-templates-on-disk-contain-metadata-resource.integtest.ts index 8587a51ab..448eb30ff 100644 --- a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/synth/cdk-templates-on-disk-contain-metadata-resource.integtest.ts +++ b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/synth/cdk-templates-on-disk-contain-metadata-resource.integtest.ts @@ -1,3 +1,5 @@ +import { promises as fs } from 'fs'; +import * as path from 'path'; import { integTest, withDefaultFixture } from '../../../lib'; integTest( @@ -7,17 +9,28 @@ integTest( await fixture.cdk(['synth', '--version-reporting=true']); // Load template from disk from root assembly - const templateContents = await fixture.shell(['cat', 'cdk.out/*-lambda.template.json']); + const templateContents = await readMatchingFile(path.join(fixture.integTestDir, 'cdk.out'), /-lambda\.template\.json$/); expect(JSON.parse(templateContents).Resources.CDKMetadata).toBeTruthy(); // Load template from nested assembly - const nestedTemplateContents = await fixture.shell([ - 'cat', - 'cdk.out/assembly-*-stage/*StackInStage*.template.json', - ]); + const assemblyDir = await findMatchingFile(path.join(fixture.integTestDir, 'cdk.out'), /^assembly-.*-stage$/); + const nestedTemplateContents = await readMatchingFile(assemblyDir, /StackInStage.*\.template\.json$/); expect(JSON.parse(nestedTemplateContents).Resources.CDKMetadata).toBeTruthy(); }), ); +async function findMatchingFile(dir: string, pattern: RegExp): Promise { + const entries = await fs.readdir(dir); + const match = entries.find((e) => pattern.test(e)); + if (!match) { + throw new Error(`No file matching ${pattern} found in ${dir}`); + } + return path.join(dir, match); +} + +async function readMatchingFile(dir: string, pattern: RegExp): Promise { + return fs.readFile(await findMatchingFile(dir, pattern), 'utf-8'); +} + diff --git a/packages/@aws-cdk-testing/cli-integ/tests/init-typescript-app/init-typescript-app.integtest.ts b/packages/@aws-cdk-testing/cli-integ/tests/init-typescript-app/init-typescript-app.integtest.ts index 83025e56f..f87e8b76f 100644 --- a/packages/@aws-cdk-testing/cli-integ/tests/init-typescript-app/init-typescript-app.integtest.ts +++ b/packages/@aws-cdk-testing/cli-integ/tests/init-typescript-app/init-typescript-app.integtest.ts @@ -55,7 +55,7 @@ TYPESCRIPT_VERSIONS.forEach(tsVersion => { await shell.shell(['npm', 'ls']); // this will fail if we have unmet peer dependencies // We just removed the 'jest' dependency so remove the tests as well because they won't compile - await shell.shell(['rm', '-rf', 'test/']); + await fs.rm(path.join(context.integTestDir, 'test'), { recursive: true, force: true }); await shell.shell(['npm', 'run', 'build']); await shell.shell(['cdk', 'synth']); From f40aaa7b3f538d631ad3e6699cb4ec414165dbc8 Mon Sep 17 00:00:00 2001 From: Ian Hou <45278651+iankhou@users.noreply.github.com> Date: Thu, 30 Jul 2026 10:24:08 -0400 Subject: [PATCH 04/26] fix(cli-integ): spawn npm through node in typescript version lookups spawnSync('npm', ...) fails on Windows because npm is a .cmd shim, not an executable; JSON.parse then chokes on the undefined stdout. Resolve the npm JS entrypoint and invoke it through process.execPath, matching how the rest of the harness already calls npm. --- packages/@aws-cdk-testing/cli-integ/lib/npm.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/@aws-cdk-testing/cli-integ/lib/npm.ts b/packages/@aws-cdk-testing/cli-integ/lib/npm.ts index 82c96a5f8..a2a20251a 100644 --- a/packages/@aws-cdk-testing/cli-integ/lib/npm.ts +++ b/packages/@aws-cdk-testing/cli-integ/lib/npm.ts @@ -40,7 +40,8 @@ export async function npmQueryInstalledVersion(packageName: string, dir: string) * Use NPM preinstalled on the machine to look up a list of TypeScript versions */ export function typescriptVersionsSync(): string[] { - const { stdout } = spawnSync('npm', ['--silent', 'view', `typescript@>=${MINIMUM_VERSION}`, 'version', '--json'], { encoding: 'utf-8' }); + // Invoke npm through Node: on Windows `npm` is a `.cmd` file, which spawnSync cannot execute directly + const { stdout } = spawnSync(process.execPath, [require.resolve('npm'), '--silent', 'view', `typescript@>=${MINIMUM_VERSION}`, 'version', '--json'], { encoding: 'utf-8' }); const versions: string[] = JSON.parse(stdout); return Array.from(new Set(versions.map(v => v.split('.').slice(0, 2).join('.')))); @@ -50,7 +51,7 @@ export function typescriptVersionsSync(): string[] { * Use NPM preinstalled on the machine to query publish times of versions */ export function typescriptVersionsYoungerThanDaysSync(days: number, versions: string[]): string[] { - const { stdout } = spawnSync('npm', ['--silent', 'view', 'typescript', 'time', '--json'], { encoding: 'utf-8' }); + const { stdout } = spawnSync(process.execPath, [require.resolve('npm'), '--silent', 'view', 'typescript', 'time', '--json'], { encoding: 'utf-8' }); const versionTsMap: Record = JSON.parse(stdout); const cutoffDate = new Date(Date.now() - (days * 24 * 3600 * 1000)); From 6f8285c5998a080ba98ba7630f99455dbbd0d748 Mon Sep 17 00:00:00 2001 From: Ian Hou <45278651+iankhou@users.noreply.github.com> Date: Thu, 30 Jul 2026 10:31:00 -0400 Subject: [PATCH 05/26] fix(cli-integ): search all stage assemblies for nested template The portable replacement for `cat cdk.out/assembly-*-stage/*StackInStage*` picked the first directory matching assembly-*-stage, which can be the bundling stage that contains no StackInStage template. Search recursively for the full relative path instead, mirroring the original glob semantics. --- ...isk-contain-metadata-resource.integtest.ts | 28 +++++++++++-------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/synth/cdk-templates-on-disk-contain-metadata-resource.integtest.ts b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/synth/cdk-templates-on-disk-contain-metadata-resource.integtest.ts index 448eb30ff..082d1db5f 100644 --- a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/synth/cdk-templates-on-disk-contain-metadata-resource.integtest.ts +++ b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/synth/cdk-templates-on-disk-contain-metadata-resource.integtest.ts @@ -9,28 +9,34 @@ integTest( await fixture.cdk(['synth', '--version-reporting=true']); // Load template from disk from root assembly - const templateContents = await readMatchingFile(path.join(fixture.integTestDir, 'cdk.out'), /-lambda\.template\.json$/); + const templateContents = await readMatchingFile(path.join(fixture.integTestDir, 'cdk.out'), /^[^\\/]*-lambda\.template\.json$/); expect(JSON.parse(templateContents).Resources.CDKMetadata).toBeTruthy(); - // Load template from nested assembly - const assemblyDir = await findMatchingFile(path.join(fixture.integTestDir, 'cdk.out'), /^assembly-.*-stage$/); - const nestedTemplateContents = await readMatchingFile(assemblyDir, /StackInStage.*\.template\.json$/); + // Load template from nested assembly (multiple stage assemblies exist; find the one holding StackInStage) + const nestedTemplate = await findMatchingFile( + path.join(fixture.integTestDir, 'cdk.out'), + /^assembly-.*-stage[\\/].*StackInStage.*\.template\.json$/, + ); + const nestedTemplateContents = await fs.readFile(nestedTemplate, 'utf-8'); expect(JSON.parse(nestedTemplateContents).Resources.CDKMetadata).toBeTruthy(); }), ); -async function findMatchingFile(dir: string, pattern: RegExp): Promise { - const entries = await fs.readdir(dir); - const match = entries.find((e) => pattern.test(e)); +/** + * Find a file whose path relative to `root` matches `pattern`, searching recursively (like a shell glob) + */ +async function findMatchingFile(root: string, pattern: RegExp): Promise { + const entries = await fs.readdir(root, { recursive: true, withFileTypes: true }); + const match = entries.find((e) => e.isFile() && pattern.test(path.join(path.relative(root, e.parentPath), e.name))); if (!match) { - throw new Error(`No file matching ${pattern} found in ${dir}`); + throw new Error(`No file matching ${pattern} found in ${root}`); } - return path.join(dir, match); + return path.join(match.parentPath, match.name); } -async function readMatchingFile(dir: string, pattern: RegExp): Promise { - return fs.readFile(await findMatchingFile(dir, pattern), 'utf-8'); +async function readMatchingFile(root: string, pattern: RegExp): Promise { + return fs.readFile(await findMatchingFile(root, pattern), 'utf-8'); } From 5ef1c467fdd37aba399a6607d70e3f5adf5dea6e Mon Sep 17 00:00:00 2001 From: Ian Hou <45278651+iankhou@users.noreply.github.com> Date: Thu, 30 Jul 2026 10:53:08 -0400 Subject: [PATCH 06/26] fix(cli-integ): Windows fixes for docker login env expansion and python venv layout - ecrPublicLogin passed the ECR password as a literal `${ECR_PASSWORD}`, which cmd.exe does not expand, so `docker login` sent the literal string and got a 400. Use `%ECR_PASSWORD%` on Windows. This failed every test using the default fixture (~200 tests) since the fixture logs in at setup. - init-python looked for the virtualenv binaries in `.venv/bin`; on Windows virtualenv creates `.venv/Scripts`. Also use path.delimiter for PATH. --- .../@aws-cdk-testing/cli-integ/lib/with-cdk-app.ts | 7 ++++++- .../tests/init-python/init-python.integtest.ts | 10 ++++++---- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/packages/@aws-cdk-testing/cli-integ/lib/with-cdk-app.ts b/packages/@aws-cdk-testing/cli-integ/lib/with-cdk-app.ts index 44c925ab1..62825da75 100644 --- a/packages/@aws-cdk-testing/cli-integ/lib/with-cdk-app.ts +++ b/packages/@aws-cdk-testing/cli-integ/lib/with-cdk-app.ts @@ -515,9 +515,14 @@ export class TestFixture extends ShellHelper { const decoded = Buffer.from(authData, 'base64').toString('utf-8'); const [username, password] = decoded.split(':'); + // Reference the password via an environment variable so it doesn't leak into + // process listings. The expansion syntax depends on the shell interpreting it + // (cmd.exe on Windows, /bin/sh elsewhere). + const passwordRef = process.platform === 'win32' ? '%ECR_PASSWORD%' : '${ECR_PASSWORD}'; + await this.shell([docker, 'login', '--username', username, - '--password', '${ECR_PASSWORD}', + '--password', passwordRef, 'public.ecr.aws'], { shell: true, modEnv: { diff --git a/packages/@aws-cdk-testing/cli-integ/tests/init-python/init-python.integtest.ts b/packages/@aws-cdk-testing/cli-integ/tests/init-python/init-python.integtest.ts index 4e4a89b22..4e2f8461b 100644 --- a/packages/@aws-cdk-testing/cli-integ/tests/init-python/init-python.integtest.ts +++ b/packages/@aws-cdk-testing/cli-integ/tests/init-python/init-python.integtest.ts @@ -10,11 +10,13 @@ import { integTest, withTemporaryDirectory, ShellHelper, withPackages } from '.. await shell.shell(['cdk', 'init', '--lib-version', context.library.requestedVersion(), '-l', 'python', template]); const venvPath = path.resolve(context.integTestDir, '.venv'); - const venv = { PATH: `${venvPath}/bin:${process.env.PATH}`, VIRTUAL_ENV: venvPath }; + // Virtualenvs put binaries in 'Scripts' on Windows and 'bin' elsewhere + const venvBin = path.join(venvPath, process.platform === 'win32' ? 'Scripts' : 'bin'); + const venv = { PATH: `${venvBin}${path.delimiter}${process.env.PATH}`, VIRTUAL_ENV: venvPath }; - await shell.shell([`${venvPath}/bin/pip`, 'install', '-r', 'requirements.txt'], { modEnv: venv }); - await shell.shell([`${venvPath}/bin/pip`, 'install', '-r', 'requirements-dev.txt'], { modEnv: venv }); - await shell.shell([`${venvPath}/bin/pytest`], { modEnv: venv }); + await shell.shell([path.join(venvBin, 'pip'), 'install', '-r', 'requirements.txt'], { modEnv: venv }); + await shell.shell([path.join(venvBin, 'pip'), 'install', '-r', 'requirements-dev.txt'], { modEnv: venv }); + await shell.shell([path.join(venvBin, 'pytest')], { modEnv: venv }); await shell.shell(['cdk', 'synth'], { modEnv: venv }); }))); }); From 5400df17a5bee7f86018f262c1c219a5e2c7855d Mon Sep 17 00:00:00 2001 From: Ian Hou <45278651+iankhou@users.noreply.github.com> Date: Thu, 30 Jul 2026 11:24:24 -0400 Subject: [PATCH 07/26] fix(cli-integ): disable wincred credential helper for docker login on Windows The %ECR_PASSWORD% expansion fix got docker login past authentication, but storing the credential then failed with 'error storing credentials - err: exit status 1, out: The stub received bad data' from the wincred helper, which does not handle ~40 parallel logins from concurrent test workers. Write {"credsStore": ""} into the per-test DOCKER_CONFIG dir so credentials go to the config file, matching Linux runner behavior. --- packages/@aws-cdk-testing/cli-integ/lib/with-cdk-app.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/packages/@aws-cdk-testing/cli-integ/lib/with-cdk-app.ts b/packages/@aws-cdk-testing/cli-integ/lib/with-cdk-app.ts index 62825da75..45f36045d 100644 --- a/packages/@aws-cdk-testing/cli-integ/lib/with-cdk-app.ts +++ b/packages/@aws-cdk-testing/cli-integ/lib/with-cdk-app.ts @@ -520,6 +520,15 @@ export class TestFixture extends ShellHelper { // (cmd.exe on Windows, /bin/sh elsewhere). const passwordRef = process.platform === 'win32' ? '%ECR_PASSWORD%' : '${ECR_PASSWORD}'; + // On Windows, Docker defaults to the 'wincred' credential helper, which fails + // under concurrent logins from parallel tests ('The stub received bad data'). + // Disable it so credentials are stored in the per-test config file, matching + // the behavior on Linux runners (which have no credential helper installed). + if (process.platform === 'win32') { + await fs.promises.mkdir(this.dockerConfigDir, { recursive: true }); + await fs.promises.writeFile(path.join(this.dockerConfigDir, 'config.json'), JSON.stringify({ credsStore: '' })); + } + await this.shell([docker, 'login', '--username', username, '--password', passwordRef, From 7043b5c0d5691734d6b2acd54393807df1b783d9 Mon Sep 17 00:00:00 2001 From: Ian Hou <45278651+iankhou@users.noreply.github.com> Date: Thu, 30 Jul 2026 11:55:58 -0400 Subject: [PATCH 08/26] fix(cli-integ): raise init test timeouts to 5 minutes The init suites (java, go, python, csharp, fsharp, javascript, typescript-lib) ran with jest's suite default of 60s. On Windows runners a `cdk init` plus toolchain build (mvn package, go test, pip install) takes 3-5 minutes, so every test timed out. Worse, jest keeps the timed-out test body running while it starts the retry, and the zombie's subsequent commands fail confusingly. init-typescript-app already used 300s; apply the same to the rest. --- .../cli-integ/tests/init-csharp/init-csharp.integtest.ts | 2 +- .../cli-integ/tests/init-fsharp/init-fsharp.integtest.ts | 2 +- .../cli-integ/tests/init-go/init-go.integtest.ts | 2 +- .../cli-integ/tests/init-java/init-java.integtest.ts | 2 +- .../tests/init-javascript/init-javascript.integtest.ts | 4 ++-- .../cli-integ/tests/init-python/init-python.integtest.ts | 2 +- .../init-typescript-lib/init-typescript-lib.integtest.ts | 2 +- .../use-lib-as-bundled-dependency.integtest.ts | 2 +- 8 files changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/@aws-cdk-testing/cli-integ/tests/init-csharp/init-csharp.integtest.ts b/packages/@aws-cdk-testing/cli-integ/tests/init-csharp/init-csharp.integtest.ts index 98fc4da23..3af10939d 100644 --- a/packages/@aws-cdk-testing/cli-integ/tests/init-csharp/init-csharp.integtest.ts +++ b/packages/@aws-cdk-testing/cli-integ/tests/init-csharp/init-csharp.integtest.ts @@ -10,6 +10,6 @@ import { integTest, withTemporaryDirectory, ShellHelper, withPackages } from '.. await shell.shell(['cdk', 'init', '--lib-version', context.library.requestedVersion(), '-l', 'csharp', template]); await context.library.initializeDotnetPackages(context.integTestDir); await shell.shell(['cdk', 'synth']); - }))); + })), 300_000); }); diff --git a/packages/@aws-cdk-testing/cli-integ/tests/init-fsharp/init-fsharp.integtest.ts b/packages/@aws-cdk-testing/cli-integ/tests/init-fsharp/init-fsharp.integtest.ts index b53b28a91..d7d96e032 100644 --- a/packages/@aws-cdk-testing/cli-integ/tests/init-fsharp/init-fsharp.integtest.ts +++ b/packages/@aws-cdk-testing/cli-integ/tests/init-fsharp/init-fsharp.integtest.ts @@ -10,6 +10,6 @@ import { integTest, withTemporaryDirectory, ShellHelper, withPackages } from '.. await shell.shell(['cdk', 'init', '--lib-version', context.library.requestedVersion(), '-l', 'fsharp', template]); await context.library.initializeDotnetPackages(context.integTestDir); await shell.shell(['cdk', 'synth']); - }))); + })), 300_000); }); diff --git a/packages/@aws-cdk-testing/cli-integ/tests/init-go/init-go.integtest.ts b/packages/@aws-cdk-testing/cli-integ/tests/init-go/init-go.integtest.ts index cd256f723..8f501d1c4 100644 --- a/packages/@aws-cdk-testing/cli-integ/tests/init-go/init-go.integtest.ts +++ b/packages/@aws-cdk-testing/cli-integ/tests/init-go/init-go.integtest.ts @@ -25,5 +25,5 @@ import { integTest, withTemporaryDirectory, ShellHelper, withPackages } from '.. await shell.shell(['go', 'test']); await shell.shell(['cdk', 'synth']); - }))); + })), 300_000); }); diff --git a/packages/@aws-cdk-testing/cli-integ/tests/init-java/init-java.integtest.ts b/packages/@aws-cdk-testing/cli-integ/tests/init-java/init-java.integtest.ts index dbeedda4e..45d5dead0 100644 --- a/packages/@aws-cdk-testing/cli-integ/tests/init-java/init-java.integtest.ts +++ b/packages/@aws-cdk-testing/cli-integ/tests/init-java/init-java.integtest.ts @@ -10,5 +10,5 @@ import { integTest, withTemporaryDirectory, ShellHelper, withPackages } from '.. await shell.shell(['cdk', 'init', '--lib-version', context.library.requestedVersion(), '-l', 'java', template]); await shell.shell(['mvn', 'package']); await shell.shell(['cdk', 'synth']); - }))); + })), 300_000); }); diff --git a/packages/@aws-cdk-testing/cli-integ/tests/init-javascript/init-javascript.integtest.ts b/packages/@aws-cdk-testing/cli-integ/tests/init-javascript/init-javascript.integtest.ts index 1e01e9767..38b9e2014 100644 --- a/packages/@aws-cdk-testing/cli-integ/tests/init-javascript/init-javascript.integtest.ts +++ b/packages/@aws-cdk-testing/cli-integ/tests/init-javascript/init-javascript.integtest.ts @@ -13,7 +13,7 @@ import { integTest, withTemporaryDirectory, ShellHelper, withPackages } from '.. await shell.shell(['npm', 'run', 'test']); await shell.shell(['cdk', 'synth']); - }))); + })), 300_000); }); integTest('Test importing CDK from ESM', withTemporaryDirectory(withPackages(async (context) => { @@ -55,4 +55,4 @@ new TestjsStack(app, 'TestjsStack'); await fs.writeJson(path.join(context.integTestDir, 'cdk.json'), cdkJson); await shell.shell(['cdk', 'synth']); -}))); +})), 300_000); diff --git a/packages/@aws-cdk-testing/cli-integ/tests/init-python/init-python.integtest.ts b/packages/@aws-cdk-testing/cli-integ/tests/init-python/init-python.integtest.ts index 4e2f8461b..075671f78 100644 --- a/packages/@aws-cdk-testing/cli-integ/tests/init-python/init-python.integtest.ts +++ b/packages/@aws-cdk-testing/cli-integ/tests/init-python/init-python.integtest.ts @@ -18,5 +18,5 @@ import { integTest, withTemporaryDirectory, ShellHelper, withPackages } from '.. await shell.shell([path.join(venvBin, 'pip'), 'install', '-r', 'requirements-dev.txt'], { modEnv: venv }); await shell.shell([path.join(venvBin, 'pytest')], { modEnv: venv }); await shell.shell(['cdk', 'synth'], { modEnv: venv }); - }))); + })), 300_000); }); diff --git a/packages/@aws-cdk-testing/cli-integ/tests/init-typescript-lib/init-typescript-lib.integtest.ts b/packages/@aws-cdk-testing/cli-integ/tests/init-typescript-lib/init-typescript-lib.integtest.ts index 57d7adfdf..2f73b06ed 100644 --- a/packages/@aws-cdk-testing/cli-integ/tests/init-typescript-lib/init-typescript-lib.integtest.ts +++ b/packages/@aws-cdk-testing/cli-integ/tests/init-typescript-lib/init-typescript-lib.integtest.ts @@ -10,4 +10,4 @@ integTest('typescript init lib', withTemporaryDirectory(withPackages(async (cont await shell.shell(['npm', 'ls']); // this will fail if we have unmet peer dependencies await shell.shell(['npm', 'run', 'build']); await shell.shell(['npm', 'run', 'test']); -}))); +})), 300_000); diff --git a/packages/@aws-cdk-testing/cli-integ/tests/init-typescript-lib/use-lib-as-bundled-dependency.integtest.ts b/packages/@aws-cdk-testing/cli-integ/tests/init-typescript-lib/use-lib-as-bundled-dependency.integtest.ts index c757fa7e7..9b22aab91 100644 --- a/packages/@aws-cdk-testing/cli-integ/tests/init-typescript-lib/use-lib-as-bundled-dependency.integtest.ts +++ b/packages/@aws-cdk-testing/cli-integ/tests/init-typescript-lib/use-lib-as-bundled-dependency.integtest.ts @@ -22,4 +22,4 @@ integTest('using aws-cdk-lib as a bundled dependency', withTemporaryDirectory(wi await fs.writeFile(packageJsonPath, JSON.stringify(packageJson, undefined, 2), 'utf-8'); await shell.shell(['npm', 'install']); -}))); +})), 300_000); From c2ab2389d3481f8a5f4752c3b494fb1e53c823b6 Mon Sep 17 00:00:00 2001 From: Ian Hou <45278651+iankhou@users.noreply.github.com> Date: Thu, 30 Jul 2026 12:27:43 -0400 Subject: [PATCH 09/26] fix(cli-integ): write ECR auth directly to docker config on Windows Disabling credsStore in the config file was not enough: docker on Windows auto-detects the wincred helper and still fails to store the ~1.5KB ECR token ('The stub received bad data'). Skip `docker login` entirely on Windows and write the auths entry into the per-test DOCKER_CONFIG config.json ourselves - the same end state docker login produces on the Linux runners. --- .../cli-integ/lib/with-cdk-app.ts | 36 ++++++++++--------- 1 file changed, 20 insertions(+), 16 deletions(-) diff --git a/packages/@aws-cdk-testing/cli-integ/lib/with-cdk-app.ts b/packages/@aws-cdk-testing/cli-integ/lib/with-cdk-app.ts index 45f36045d..bddfc3650 100644 --- a/packages/@aws-cdk-testing/cli-integ/lib/with-cdk-app.ts +++ b/packages/@aws-cdk-testing/cli-integ/lib/with-cdk-app.ts @@ -506,32 +506,36 @@ export class TestFixture extends ShellHelper { const tokenResponse = await this.aws.ecrPublic.send(new GetAuthorizationTokenCommand({})); const authData = tokenResponse.authorizationData?.authorizationToken; - const docker = process.env.CDK_DOCKER ?? 'docker'; - if (!authData) { throw new Error('Could not retrieve ECR public auth token.'); } - const decoded = Buffer.from(authData, 'base64').toString('utf-8'); - const [username, password] = decoded.split(':'); - - // Reference the password via an environment variable so it doesn't leak into - // process listings. The expansion syntax depends on the shell interpreting it - // (cmd.exe on Windows, /bin/sh elsewhere). - const passwordRef = process.platform === 'win32' ? '%ECR_PASSWORD%' : '${ECR_PASSWORD}'; - - // On Windows, Docker defaults to the 'wincred' credential helper, which fails - // under concurrent logins from parallel tests ('The stub received bad data'). - // Disable it so credentials are stored in the per-test config file, matching - // the behavior on Linux runners (which have no credential helper installed). if (process.platform === 'win32') { + // `docker login` on Windows stores credentials through the wincred credential + // helper (auto-detected even if `credsStore` is empty in the config file), and + // wincred cannot store ECR tokens: they exceed Windows Credential Manager's + // 2560-byte limit ('The stub received bad data'). Write the auth directly into + // the per-test Docker config file instead, which is exactly what `docker login` + // produces on the Linux runners, where no credential helper is installed. + // The plaintext `auths` entry takes precedence over any credential helper. await fs.promises.mkdir(this.dockerConfigDir, { recursive: true }); - await fs.promises.writeFile(path.join(this.dockerConfigDir, 'config.json'), JSON.stringify({ credsStore: '' })); + await fs.promises.writeFile( + path.join(this.dockerConfigDir, 'config.json'), + JSON.stringify({ auths: { 'public.ecr.aws': { auth: authData } } }), + ); + return; } + const docker = process.env.CDK_DOCKER ?? 'docker'; + + const decoded = Buffer.from(authData, 'base64').toString('utf-8'); + const [username, password] = decoded.split(':'); + + // Reference the password via an environment variable so it doesn't leak into + // process listings; the shell expands it. await this.shell([docker, 'login', '--username', username, - '--password', passwordRef, + '--password', '${ECR_PASSWORD}', 'public.ecr.aws'], { shell: true, modEnv: { From b065ccf641b46eb843529271236f9b1457318e88 Mon Sep 17 00:00:00 2001 From: Ian Hou <45278651+iankhou@users.noreply.github.com> Date: Thu, 30 Jul 2026 13:07:06 -0400 Subject: [PATCH 10/26] feat(cli-integ): exclude work directories from Defender on Windows runners Defender real-time scanning hooks every file write. The integ tests are dominated by npm installs and toolchain builds that create tens of thousands of small files, making Windows jobs 3-10x slower than Linux (e.g. tool-integrations: 19-29 min vs 2.5 min). Exclude the workspace, temp, npm and toolcache directories from scanning; the runner VM is ephemeral and job-isolated so this carries no persistent risk. --- .github/workflows/integ.yml | 15 +++++++++++++++ projenrc/cdk-cli-integ-tests.ts | 19 +++++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/.github/workflows/integ.yml b/.github/workflows/integ.yml index e33ada73e..2669137f8 100644 --- a/.github/workflows/integ.yml +++ b/.github/workflows/integ.yml @@ -686,6 +686,9 @@ jobs: shell: bash if: github.event_name != 'merge_group' && !contains(github.event.pull_request.labels.*.name, 'pr/exempt-integ-test') steps: + - name: Exclude work directories from Windows Defender + run: Add-MpPreference -ExclusionPath "$env:GITHUB_WORKSPACE", "$env:TEMP", "$env:USERPROFILE", "C:\npm", "C:\hostedtoolcache" + shell: powershell - name: Download artifact uses: actions/download-artifact@v8 with: @@ -811,6 +814,9 @@ jobs: shell: bash if: github.event_name != 'merge_group' && !contains(github.event.pull_request.labels.*.name, 'pr/exempt-integ-test') steps: + - name: Exclude work directories from Windows Defender + run: Add-MpPreference -ExclusionPath "$env:GITHUB_WORKSPACE", "$env:TEMP", "$env:USERPROFILE", "C:\npm", "C:\hostedtoolcache" + shell: powershell - name: Download artifact uses: actions/download-artifact@v8 with: @@ -926,6 +932,9 @@ jobs: shell: bash if: github.event_name != 'merge_group' && !contains(github.event.pull_request.labels.*.name, 'pr/exempt-integ-test') steps: + - name: Exclude work directories from Windows Defender + run: Add-MpPreference -ExclusionPath "$env:GITHUB_WORKSPACE", "$env:TEMP", "$env:USERPROFILE", "C:\npm", "C:\hostedtoolcache" + shell: powershell - name: Download artifact uses: actions/download-artifact@v8 with: @@ -1038,6 +1047,9 @@ jobs: shell: bash if: github.event_name != 'merge_group' && !contains(github.event.pull_request.labels.*.name, 'pr/exempt-integ-test') steps: + - name: Exclude work directories from Windows Defender + run: Add-MpPreference -ExclusionPath "$env:GITHUB_WORKSPACE", "$env:TEMP", "$env:USERPROFILE", "C:\npm", "C:\hostedtoolcache" + shell: powershell - name: Download artifact uses: actions/download-artifact@v8 with: @@ -1164,6 +1176,9 @@ jobs: shell: bash if: github.event_name != 'merge_group' && !contains(github.event.pull_request.labels.*.name, 'pr/exempt-integ-test') steps: + - name: Exclude work directories from Windows Defender + run: Add-MpPreference -ExclusionPath "$env:GITHUB_WORKSPACE", "$env:TEMP", "$env:USERPROFILE", "C:\npm", "C:\hostedtoolcache" + shell: powershell - name: Download artifact uses: actions/download-artifact@v8 with: diff --git a/projenrc/cdk-cli-integ-tests.ts b/projenrc/cdk-cli-integ-tests.ts index 9119a1eca..7a90492cf 100644 --- a/projenrc/cdk-cli-integ-tests.ts +++ b/projenrc/cdk-cli-integ-tests.ts @@ -529,6 +529,7 @@ export class CdkCliIntegTestsWorkflow extends Component { ? suites.map(([name, jobProps]) => this.addMatrixJob(name, jobProps, { runsOn: this.props.windowsTestRunsOn!, suffix: '_windows', + windows: true, })) : []), ]; @@ -614,6 +615,15 @@ export class CdkCliIntegTestsWorkflow extends Component { }, }, steps: [ + ...platform.windows ? [{ + // Defender's real-time scanning hooks every file write; npm-heavy tests + // create tens of thousands of small files, and scanning slows them down + // 3-10x. The runner VM is ephemeral and job-isolated, so exclude the + // work, tool and temp directories from scanning. + name: 'Exclude work directories from Windows Defender', + shell: 'powershell', + run: 'Add-MpPreference -ExclusionPath "$env:GITHUB_WORKSPACE", "$env:TEMP", "$env:USERPROFILE", "C:\\npm", "C:\\hostedtoolcache"', + }] : [], github.WorkflowSteps.downloadArtifact({ with: { artifactIds: [`\${{needs.${this.JOB_PREPARE}.outputs.packagesArtifact}}`], @@ -736,4 +746,13 @@ interface PlatformOptions { * @default - no suffix */ readonly suffix?: string; + + /** + * Whether this job runs on a Windows runner. + * + * Adds Windows-specific setup steps. + * + * @default false + */ + readonly windows?: boolean; } From 609a653da3be03d4d324d04681989b385842bbb7 Mon Sep 17 00:00:00 2001 From: Ian Hou <45278651+iankhou@users.noreply.github.com> Date: Thu, 30 Jul 2026 14:16:02 -0400 Subject: [PATCH 11/26] fix(cli-integ): skip Linux docker tests on Windows, raise typescript-app timeouts - GitHub Windows runners run Docker in Windows-containers mode and cannot pull or build Linux images ('no matching manifest for windows/amd64', or no daemon pipe at all). Skip the 13 tests that build/run Linux images on Windows via CDK_INTEG_SKIP_TESTS; they retain full coverage on Linux. - The typescript-version matrix tests in init-typescript-app still used the 60s suite default and timed out on Windows; the templated init tests hit 300s when several npm installs run concurrently. Raise to 300s/600s. --- .github/workflows/integ.yml | 5 ++++ .../init-typescript-app.integtest.ts | 4 +-- projenrc/cdk-cli-integ-tests.ts | 26 +++++++++++++++++++ 3 files changed, 33 insertions(+), 2 deletions(-) diff --git a/.github/workflows/integ.yml b/.github/workflows/integ.yml index 2669137f8..56433acd6 100644 --- a/.github/workflows/integ.yml +++ b/.github/workflows/integ.yml @@ -681,6 +681,7 @@ jobs: MAVEN_ARGS: --no-transfer-progress IS_CANARY: "true" CI: "true" + CDK_INTEG_SKIP_TESTS: deploy same docker asset to multiple regions,deploy same docker asset to multiple stacks,deploy stack with multiple docker assets,deploy stack with docker asset,cdk-assets smoke test,deploy new style synthesis to new style bootstrap (with docker image),Garbage Collection untags in-use ecr images,Garbage Collection keeps in use ecr images,Garbage Collection deletes unused ecr images,Garbage Collection tags unused ecr images,all calls from isolated container go through proxy,docker-credential-cdk-assets can assume role and fetch ECR credentials,toolkit deploy stack with multiple docker assets defaults: run: shell: bash @@ -809,6 +810,7 @@ jobs: MAVEN_ARGS: --no-transfer-progress IS_CANARY: "true" CI: "true" + CDK_INTEG_SKIP_TESTS: deploy same docker asset to multiple regions,deploy same docker asset to multiple stacks,deploy stack with multiple docker assets,deploy stack with docker asset,cdk-assets smoke test,deploy new style synthesis to new style bootstrap (with docker image),Garbage Collection untags in-use ecr images,Garbage Collection keeps in use ecr images,Garbage Collection deletes unused ecr images,Garbage Collection tags unused ecr images,all calls from isolated container go through proxy,docker-credential-cdk-assets can assume role and fetch ECR credentials,toolkit deploy stack with multiple docker assets defaults: run: shell: bash @@ -927,6 +929,7 @@ jobs: MAVEN_ARGS: --no-transfer-progress IS_CANARY: "true" CI: "true" + CDK_INTEG_SKIP_TESTS: deploy same docker asset to multiple regions,deploy same docker asset to multiple stacks,deploy stack with multiple docker assets,deploy stack with docker asset,cdk-assets smoke test,deploy new style synthesis to new style bootstrap (with docker image),Garbage Collection untags in-use ecr images,Garbage Collection keeps in use ecr images,Garbage Collection deletes unused ecr images,Garbage Collection tags unused ecr images,all calls from isolated container go through proxy,docker-credential-cdk-assets can assume role and fetch ECR credentials,toolkit deploy stack with multiple docker assets defaults: run: shell: bash @@ -1042,6 +1045,7 @@ jobs: MAVEN_ARGS: --no-transfer-progress IS_CANARY: "true" CI: "true" + CDK_INTEG_SKIP_TESTS: deploy same docker asset to multiple regions,deploy same docker asset to multiple stacks,deploy stack with multiple docker assets,deploy stack with docker asset,cdk-assets smoke test,deploy new style synthesis to new style bootstrap (with docker image),Garbage Collection untags in-use ecr images,Garbage Collection keeps in use ecr images,Garbage Collection deletes unused ecr images,Garbage Collection tags unused ecr images,all calls from isolated container go through proxy,docker-credential-cdk-assets can assume role and fetch ECR credentials,toolkit deploy stack with multiple docker assets defaults: run: shell: bash @@ -1171,6 +1175,7 @@ jobs: MAVEN_ARGS: --no-transfer-progress IS_CANARY: "true" CI: "true" + CDK_INTEG_SKIP_TESTS: deploy same docker asset to multiple regions,deploy same docker asset to multiple stacks,deploy stack with multiple docker assets,deploy stack with docker asset,cdk-assets smoke test,deploy new style synthesis to new style bootstrap (with docker image),Garbage Collection untags in-use ecr images,Garbage Collection keeps in use ecr images,Garbage Collection deletes unused ecr images,Garbage Collection tags unused ecr images,all calls from isolated container go through proxy,docker-credential-cdk-assets can assume role and fetch ECR credentials,toolkit deploy stack with multiple docker assets defaults: run: shell: bash diff --git a/packages/@aws-cdk-testing/cli-integ/tests/init-typescript-app/init-typescript-app.integtest.ts b/packages/@aws-cdk-testing/cli-integ/tests/init-typescript-app/init-typescript-app.integtest.ts index f87e8b76f..5f8796336 100644 --- a/packages/@aws-cdk-testing/cli-integ/tests/init-typescript-app/init-typescript-app.integtest.ts +++ b/packages/@aws-cdk-testing/cli-integ/tests/init-typescript-app/init-typescript-app.integtest.ts @@ -19,7 +19,7 @@ import { typescriptVersionsSync, typescriptVersionsYoungerThanDaysSync } from '. await shell.shell(['npm', 'run', 'test']); await shell.shell(['cdk', 'synth']); - })), 300_000); + })), 600_000); }); // Same as https://github.com/DefinitelyTyped/DefinitelyTyped?tab=readme-ov-file#support-window @@ -59,7 +59,7 @@ TYPESCRIPT_VERSIONS.forEach(tsVersion => { await shell.shell(['npm', 'run', 'build']); await shell.shell(['cdk', 'synth']); - }))); + })), 300_000); }); async function removeDevDependencies(context: TemporaryDirectoryContext) { diff --git a/projenrc/cdk-cli-integ-tests.ts b/projenrc/cdk-cli-integ-tests.ts index 7a90492cf..208d39770 100644 --- a/projenrc/cdk-cli-integ-tests.ts +++ b/projenrc/cdk-cli-integ-tests.ts @@ -12,6 +12,29 @@ export function fixupTestTask(project: Project, taskName = 'test'): void { const NOT_FLAGGED_EXPR = "!contains(github.event.pull_request.labels.*.name, 'pr/exempt-integ-test')"; +/** + * Tests that build or run Linux Docker images. + * + * GitHub-hosted Windows runners run Docker in Windows-containers mode and + * cannot pull or build Linux images ('no matching manifest for windows/amd64'), + * so these tests are skipped on Windows. + */ +const DOCKER_TESTS_SKIPPED_ON_WINDOWS = [ + 'deploy same docker asset to multiple regions', + 'deploy same docker asset to multiple stacks', + 'deploy stack with multiple docker assets', + 'deploy stack with docker asset', + 'cdk-assets smoke test', + 'deploy new style synthesis to new style bootstrap (with docker image)', + 'Garbage Collection untags in-use ecr images', + 'Garbage Collection keeps in use ecr images', + 'Garbage Collection deletes unused ecr images', + 'Garbage Collection tags unused ecr images', + 'all calls from isolated container go through proxy', + 'docker-credential-cdk-assets can assume role and fetch ECR credentials', + 'toolkit deploy stack with multiple docker assets', +]; + function setupNodeStep(nodeVersion: string): github.workflows.JobStep { return { name: 'Setup Node.js', @@ -596,6 +619,9 @@ export class CdkCliIntegTestsWorkflow extends Component { // assumptions about the availability of source packages. IS_CANARY: 'true', CI: 'true', + ...platform.windows ? { + CDK_INTEG_SKIP_TESTS: DOCKER_TESTS_SKIPPED_ON_WINDOWS.join(','), + } : {}, // add extra env at end so it can override ...props.extraEnv, }, From 30725f51c8052e8e9861e7dfdc0fe429908e159e Mon Sep 17 00:00:00 2001 From: Ian Hou <45278651+iankhou@users.noreply.github.com> Date: Thu, 30 Jul 2026 14:44:22 -0400 Subject: [PATCH 12/26] feat(cli-integ): replace Defender exclusion with a Dev Drive for TEMP A/B timing between runs with and without the Defender exclusion showed no measurable difference - runner images evidently already handle it. Replace it with a Dev Drive (ReFS VHDX): all test fixtures live under os.tmpdir(), so pointing TEMP/TMP at the Dev Drive moves the npm-install-heavy file churn onto the faster filesystem. windows-latest is Server 2025 (build 26100), which supports Format-Volume -DevDrive natively. --- .github/workflows/integ.yml | 45 +++++++++++++++++++++++++-------- projenrc/cdk-cli-integ-tests.ts | 19 +++++++++----- 2 files changed, 48 insertions(+), 16 deletions(-) diff --git a/.github/workflows/integ.yml b/.github/workflows/integ.yml index 56433acd6..8356602c1 100644 --- a/.github/workflows/integ.yml +++ b/.github/workflows/integ.yml @@ -687,8 +687,13 @@ jobs: shell: bash if: github.event_name != 'merge_group' && !contains(github.event.pull_request.labels.*.name, 'pr/exempt-integ-test') steps: - - name: Exclude work directories from Windows Defender - run: Add-MpPreference -ExclusionPath "$env:GITHUB_WORKSPACE", "$env:TEMP", "$env:USERPROFILE", "C:\npm", "C:\hostedtoolcache" + - name: Set up Dev Drive for TEMP + run: |- + $vhd = "C:\devdrive.vhdx" + $drive = (New-VHD -Path $vhd -SizeBytes 20GB -Dynamic | Mount-VHD -PassThru | Initialize-Disk -PassThru | New-Partition -AssignDriveLetter -UseMaximumSize | Format-Volume -DevDrive -Confirm:$false).DriveLetter + New-Item -ItemType Directory -Path "${drive}:\temp" | Out-Null + echo "TEMP=${drive}:\temp" >> $env:GITHUB_ENV + echo "TMP=${drive}:\temp" >> $env:GITHUB_ENV shell: powershell - name: Download artifact uses: actions/download-artifact@v8 @@ -816,8 +821,13 @@ jobs: shell: bash if: github.event_name != 'merge_group' && !contains(github.event.pull_request.labels.*.name, 'pr/exempt-integ-test') steps: - - name: Exclude work directories from Windows Defender - run: Add-MpPreference -ExclusionPath "$env:GITHUB_WORKSPACE", "$env:TEMP", "$env:USERPROFILE", "C:\npm", "C:\hostedtoolcache" + - name: Set up Dev Drive for TEMP + run: |- + $vhd = "C:\devdrive.vhdx" + $drive = (New-VHD -Path $vhd -SizeBytes 20GB -Dynamic | Mount-VHD -PassThru | Initialize-Disk -PassThru | New-Partition -AssignDriveLetter -UseMaximumSize | Format-Volume -DevDrive -Confirm:$false).DriveLetter + New-Item -ItemType Directory -Path "${drive}:\temp" | Out-Null + echo "TEMP=${drive}:\temp" >> $env:GITHUB_ENV + echo "TMP=${drive}:\temp" >> $env:GITHUB_ENV shell: powershell - name: Download artifact uses: actions/download-artifact@v8 @@ -935,8 +945,13 @@ jobs: shell: bash if: github.event_name != 'merge_group' && !contains(github.event.pull_request.labels.*.name, 'pr/exempt-integ-test') steps: - - name: Exclude work directories from Windows Defender - run: Add-MpPreference -ExclusionPath "$env:GITHUB_WORKSPACE", "$env:TEMP", "$env:USERPROFILE", "C:\npm", "C:\hostedtoolcache" + - name: Set up Dev Drive for TEMP + run: |- + $vhd = "C:\devdrive.vhdx" + $drive = (New-VHD -Path $vhd -SizeBytes 20GB -Dynamic | Mount-VHD -PassThru | Initialize-Disk -PassThru | New-Partition -AssignDriveLetter -UseMaximumSize | Format-Volume -DevDrive -Confirm:$false).DriveLetter + New-Item -ItemType Directory -Path "${drive}:\temp" | Out-Null + echo "TEMP=${drive}:\temp" >> $env:GITHUB_ENV + echo "TMP=${drive}:\temp" >> $env:GITHUB_ENV shell: powershell - name: Download artifact uses: actions/download-artifact@v8 @@ -1051,8 +1066,13 @@ jobs: shell: bash if: github.event_name != 'merge_group' && !contains(github.event.pull_request.labels.*.name, 'pr/exempt-integ-test') steps: - - name: Exclude work directories from Windows Defender - run: Add-MpPreference -ExclusionPath "$env:GITHUB_WORKSPACE", "$env:TEMP", "$env:USERPROFILE", "C:\npm", "C:\hostedtoolcache" + - name: Set up Dev Drive for TEMP + run: |- + $vhd = "C:\devdrive.vhdx" + $drive = (New-VHD -Path $vhd -SizeBytes 20GB -Dynamic | Mount-VHD -PassThru | Initialize-Disk -PassThru | New-Partition -AssignDriveLetter -UseMaximumSize | Format-Volume -DevDrive -Confirm:$false).DriveLetter + New-Item -ItemType Directory -Path "${drive}:\temp" | Out-Null + echo "TEMP=${drive}:\temp" >> $env:GITHUB_ENV + echo "TMP=${drive}:\temp" >> $env:GITHUB_ENV shell: powershell - name: Download artifact uses: actions/download-artifact@v8 @@ -1181,8 +1201,13 @@ jobs: shell: bash if: github.event_name != 'merge_group' && !contains(github.event.pull_request.labels.*.name, 'pr/exempt-integ-test') steps: - - name: Exclude work directories from Windows Defender - run: Add-MpPreference -ExclusionPath "$env:GITHUB_WORKSPACE", "$env:TEMP", "$env:USERPROFILE", "C:\npm", "C:\hostedtoolcache" + - name: Set up Dev Drive for TEMP + run: |- + $vhd = "C:\devdrive.vhdx" + $drive = (New-VHD -Path $vhd -SizeBytes 20GB -Dynamic | Mount-VHD -PassThru | Initialize-Disk -PassThru | New-Partition -AssignDriveLetter -UseMaximumSize | Format-Volume -DevDrive -Confirm:$false).DriveLetter + New-Item -ItemType Directory -Path "${drive}:\temp" | Out-Null + echo "TEMP=${drive}:\temp" >> $env:GITHUB_ENV + echo "TMP=${drive}:\temp" >> $env:GITHUB_ENV shell: powershell - name: Download artifact uses: actions/download-artifact@v8 diff --git a/projenrc/cdk-cli-integ-tests.ts b/projenrc/cdk-cli-integ-tests.ts index 208d39770..6b0fdb145 100644 --- a/projenrc/cdk-cli-integ-tests.ts +++ b/projenrc/cdk-cli-integ-tests.ts @@ -642,13 +642,20 @@ export class CdkCliIntegTestsWorkflow extends Component { }, steps: [ ...platform.windows ? [{ - // Defender's real-time scanning hooks every file write; npm-heavy tests - // create tens of thousands of small files, and scanning slows them down - // 3-10x. The runner VM is ephemeral and job-isolated, so exclude the - // work, tool and temp directories from scanning. - name: 'Exclude work directories from Windows Defender', + // The integ tests are dominated by npm installs and toolchain builds: + // many small file writes, which are slow on the runner's NTFS OS disk. + // A Dev Drive (ReFS VHDX) is much faster for this pattern. Create one + // and point TEMP at it, which is where all test fixtures live + // (the harness creates its working directories under os.tmpdir()). + name: 'Set up Dev Drive for TEMP', shell: 'powershell', - run: 'Add-MpPreference -ExclusionPath "$env:GITHUB_WORKSPACE", "$env:TEMP", "$env:USERPROFILE", "C:\\npm", "C:\\hostedtoolcache"', + run: [ + '$vhd = "C:\\devdrive.vhdx"', + '$drive = (New-VHD -Path $vhd -SizeBytes 20GB -Dynamic | Mount-VHD -PassThru | Initialize-Disk -PassThru | New-Partition -AssignDriveLetter -UseMaximumSize | Format-Volume -DevDrive -Confirm:$false).DriveLetter', + 'New-Item -ItemType Directory -Path "${drive}:\\temp" | Out-Null', + 'echo "TEMP=${drive}:\\temp" >> $env:GITHUB_ENV', + 'echo "TMP=${drive}:\\temp" >> $env:GITHUB_ENV', + ].join('\n'), }] : [], github.WorkflowSteps.downloadArtifact({ with: { From e5a5c43354103d7c9ee0af0df4fd3728663aef02 Mon Sep 17 00:00:00 2001 From: Ian Hou <45278651+iankhou@users.noreply.github.com> Date: Thu, 30 Jul 2026 16:29:54 -0400 Subject: [PATCH 13/26] feat(cli-integ): extend Dev Drive to npm cache, grow VHDX to 40GB TEMP-only Dev Drive placement showed 25-35% job speedups on npm-heavy suites but left the npm cache on the OS disk, where every npm invocation in the job (global verdaccio install, per-test installs) still pays NTFS tax. Point npm_config_cache at the Dev Drive as well, and grow the dynamically-allocated VHDX to 40GB to fit cache plus concurrent fixtures. --- .github/workflows/integ.yml | 30 ++++++++++++++++++++---------- projenrc/cdk-cli-integ-tests.ts | 8 ++++++-- 2 files changed, 26 insertions(+), 12 deletions(-) diff --git a/.github/workflows/integ.yml b/.github/workflows/integ.yml index 8356602c1..f7878edfd 100644 --- a/.github/workflows/integ.yml +++ b/.github/workflows/integ.yml @@ -687,13 +687,15 @@ jobs: shell: bash if: github.event_name != 'merge_group' && !contains(github.event.pull_request.labels.*.name, 'pr/exempt-integ-test') steps: - - name: Set up Dev Drive for TEMP + - name: Set up Dev Drive for TEMP and npm cache run: |- $vhd = "C:\devdrive.vhdx" - $drive = (New-VHD -Path $vhd -SizeBytes 20GB -Dynamic | Mount-VHD -PassThru | Initialize-Disk -PassThru | New-Partition -AssignDriveLetter -UseMaximumSize | Format-Volume -DevDrive -Confirm:$false).DriveLetter + $drive = (New-VHD -Path $vhd -SizeBytes 40GB -Dynamic | Mount-VHD -PassThru | Initialize-Disk -PassThru | New-Partition -AssignDriveLetter -UseMaximumSize | Format-Volume -DevDrive -Confirm:$false).DriveLetter New-Item -ItemType Directory -Path "${drive}:\temp" | Out-Null + New-Item -ItemType Directory -Path "${drive}:\npm-cache" | Out-Null echo "TEMP=${drive}:\temp" >> $env:GITHUB_ENV echo "TMP=${drive}:\temp" >> $env:GITHUB_ENV + echo "npm_config_cache=${drive}:\npm-cache" >> $env:GITHUB_ENV shell: powershell - name: Download artifact uses: actions/download-artifact@v8 @@ -821,13 +823,15 @@ jobs: shell: bash if: github.event_name != 'merge_group' && !contains(github.event.pull_request.labels.*.name, 'pr/exempt-integ-test') steps: - - name: Set up Dev Drive for TEMP + - name: Set up Dev Drive for TEMP and npm cache run: |- $vhd = "C:\devdrive.vhdx" - $drive = (New-VHD -Path $vhd -SizeBytes 20GB -Dynamic | Mount-VHD -PassThru | Initialize-Disk -PassThru | New-Partition -AssignDriveLetter -UseMaximumSize | Format-Volume -DevDrive -Confirm:$false).DriveLetter + $drive = (New-VHD -Path $vhd -SizeBytes 40GB -Dynamic | Mount-VHD -PassThru | Initialize-Disk -PassThru | New-Partition -AssignDriveLetter -UseMaximumSize | Format-Volume -DevDrive -Confirm:$false).DriveLetter New-Item -ItemType Directory -Path "${drive}:\temp" | Out-Null + New-Item -ItemType Directory -Path "${drive}:\npm-cache" | Out-Null echo "TEMP=${drive}:\temp" >> $env:GITHUB_ENV echo "TMP=${drive}:\temp" >> $env:GITHUB_ENV + echo "npm_config_cache=${drive}:\npm-cache" >> $env:GITHUB_ENV shell: powershell - name: Download artifact uses: actions/download-artifact@v8 @@ -945,13 +949,15 @@ jobs: shell: bash if: github.event_name != 'merge_group' && !contains(github.event.pull_request.labels.*.name, 'pr/exempt-integ-test') steps: - - name: Set up Dev Drive for TEMP + - name: Set up Dev Drive for TEMP and npm cache run: |- $vhd = "C:\devdrive.vhdx" - $drive = (New-VHD -Path $vhd -SizeBytes 20GB -Dynamic | Mount-VHD -PassThru | Initialize-Disk -PassThru | New-Partition -AssignDriveLetter -UseMaximumSize | Format-Volume -DevDrive -Confirm:$false).DriveLetter + $drive = (New-VHD -Path $vhd -SizeBytes 40GB -Dynamic | Mount-VHD -PassThru | Initialize-Disk -PassThru | New-Partition -AssignDriveLetter -UseMaximumSize | Format-Volume -DevDrive -Confirm:$false).DriveLetter New-Item -ItemType Directory -Path "${drive}:\temp" | Out-Null + New-Item -ItemType Directory -Path "${drive}:\npm-cache" | Out-Null echo "TEMP=${drive}:\temp" >> $env:GITHUB_ENV echo "TMP=${drive}:\temp" >> $env:GITHUB_ENV + echo "npm_config_cache=${drive}:\npm-cache" >> $env:GITHUB_ENV shell: powershell - name: Download artifact uses: actions/download-artifact@v8 @@ -1066,13 +1072,15 @@ jobs: shell: bash if: github.event_name != 'merge_group' && !contains(github.event.pull_request.labels.*.name, 'pr/exempt-integ-test') steps: - - name: Set up Dev Drive for TEMP + - name: Set up Dev Drive for TEMP and npm cache run: |- $vhd = "C:\devdrive.vhdx" - $drive = (New-VHD -Path $vhd -SizeBytes 20GB -Dynamic | Mount-VHD -PassThru | Initialize-Disk -PassThru | New-Partition -AssignDriveLetter -UseMaximumSize | Format-Volume -DevDrive -Confirm:$false).DriveLetter + $drive = (New-VHD -Path $vhd -SizeBytes 40GB -Dynamic | Mount-VHD -PassThru | Initialize-Disk -PassThru | New-Partition -AssignDriveLetter -UseMaximumSize | Format-Volume -DevDrive -Confirm:$false).DriveLetter New-Item -ItemType Directory -Path "${drive}:\temp" | Out-Null + New-Item -ItemType Directory -Path "${drive}:\npm-cache" | Out-Null echo "TEMP=${drive}:\temp" >> $env:GITHUB_ENV echo "TMP=${drive}:\temp" >> $env:GITHUB_ENV + echo "npm_config_cache=${drive}:\npm-cache" >> $env:GITHUB_ENV shell: powershell - name: Download artifact uses: actions/download-artifact@v8 @@ -1201,13 +1209,15 @@ jobs: shell: bash if: github.event_name != 'merge_group' && !contains(github.event.pull_request.labels.*.name, 'pr/exempt-integ-test') steps: - - name: Set up Dev Drive for TEMP + - name: Set up Dev Drive for TEMP and npm cache run: |- $vhd = "C:\devdrive.vhdx" - $drive = (New-VHD -Path $vhd -SizeBytes 20GB -Dynamic | Mount-VHD -PassThru | Initialize-Disk -PassThru | New-Partition -AssignDriveLetter -UseMaximumSize | Format-Volume -DevDrive -Confirm:$false).DriveLetter + $drive = (New-VHD -Path $vhd -SizeBytes 40GB -Dynamic | Mount-VHD -PassThru | Initialize-Disk -PassThru | New-Partition -AssignDriveLetter -UseMaximumSize | Format-Volume -DevDrive -Confirm:$false).DriveLetter New-Item -ItemType Directory -Path "${drive}:\temp" | Out-Null + New-Item -ItemType Directory -Path "${drive}:\npm-cache" | Out-Null echo "TEMP=${drive}:\temp" >> $env:GITHUB_ENV echo "TMP=${drive}:\temp" >> $env:GITHUB_ENV + echo "npm_config_cache=${drive}:\npm-cache" >> $env:GITHUB_ENV shell: powershell - name: Download artifact uses: actions/download-artifact@v8 diff --git a/projenrc/cdk-cli-integ-tests.ts b/projenrc/cdk-cli-integ-tests.ts index 6b0fdb145..586950d72 100644 --- a/projenrc/cdk-cli-integ-tests.ts +++ b/projenrc/cdk-cli-integ-tests.ts @@ -647,14 +647,18 @@ export class CdkCliIntegTestsWorkflow extends Component { // A Dev Drive (ReFS VHDX) is much faster for this pattern. Create one // and point TEMP at it, which is where all test fixtures live // (the harness creates its working directories under os.tmpdir()). - name: 'Set up Dev Drive for TEMP', + name: 'Set up Dev Drive for TEMP and npm cache', shell: 'powershell', run: [ '$vhd = "C:\\devdrive.vhdx"', - '$drive = (New-VHD -Path $vhd -SizeBytes 20GB -Dynamic | Mount-VHD -PassThru | Initialize-Disk -PassThru | New-Partition -AssignDriveLetter -UseMaximumSize | Format-Volume -DevDrive -Confirm:$false).DriveLetter', + '$drive = (New-VHD -Path $vhd -SizeBytes 40GB -Dynamic | Mount-VHD -PassThru | Initialize-Disk -PassThru | New-Partition -AssignDriveLetter -UseMaximumSize | Format-Volume -DevDrive -Confirm:$false).DriveLetter', 'New-Item -ItemType Directory -Path "${drive}:\\temp" | Out-Null', + 'New-Item -ItemType Directory -Path "${drive}:\\npm-cache" | Out-Null', 'echo "TEMP=${drive}:\\temp" >> $env:GITHUB_ENV', 'echo "TMP=${drive}:\\temp" >> $env:GITHUB_ENV', + // Every npm invocation in the job (global installs, per-test installs) + // reads and writes the cache, so move it onto the Dev Drive too + 'echo "npm_config_cache=${drive}:\\npm-cache" >> $env:GITHUB_ENV', ].join('\n'), }] : [], github.WorkflowSteps.downloadArtifact({ From 299e6bf166357f359b14d504c169c434309c22d2 Mon Sep 17 00:00:00 2001 From: Ian Hou <45278651+iankhou@users.noreply.github.com> Date: Thu, 30 Jul 2026 17:10:58 -0400 Subject: [PATCH 14/26] fix(cli-integ): skip four more Linux-image tests on Windows These tests do not have docker in the name but build Linux images as a side effect and fail on Windows runners the same way as the named docker tests: python lambda bundling (sam/build-python3.12 image), SAM metadata asset bundling, a DockerImageAsset in the session-tags fixture stack, and a docker-app deploy from a copied cloud assembly. Their hung docker builds were also occupying jest workers and pushing unrelated tests in the same shard past the 1200s harness timeout. --- .github/workflows/integ.yml | 10 +++++----- projenrc/cdk-cli-integ-tests.ts | 7 +++++++ 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/.github/workflows/integ.yml b/.github/workflows/integ.yml index f7878edfd..8b6fa1c92 100644 --- a/.github/workflows/integ.yml +++ b/.github/workflows/integ.yml @@ -681,7 +681,7 @@ jobs: MAVEN_ARGS: --no-transfer-progress IS_CANARY: "true" CI: "true" - CDK_INTEG_SKIP_TESTS: deploy same docker asset to multiple regions,deploy same docker asset to multiple stacks,deploy stack with multiple docker assets,deploy stack with docker asset,cdk-assets smoke test,deploy new style synthesis to new style bootstrap (with docker image),Garbage Collection untags in-use ecr images,Garbage Collection keeps in use ecr images,Garbage Collection deletes unused ecr images,Garbage Collection tags unused ecr images,all calls from isolated container go through proxy,docker-credential-cdk-assets can assume role and fetch ECR credentials,toolkit deploy stack with multiple docker assets + CDK_INTEG_SKIP_TESTS: deploy same docker asset to multiple regions,deploy same docker asset to multiple stacks,deploy stack with multiple docker assets,deploy stack with docker asset,cdk-assets smoke test,deploy new style synthesis to new style bootstrap (with docker image),Garbage Collection untags in-use ecr images,Garbage Collection keeps in use ecr images,Garbage Collection deletes unused ecr images,Garbage Collection tags unused ecr images,all calls from isolated container go through proxy,docker-credential-cdk-assets can assume role and fetch ECR credentials,toolkit deploy stack with multiple docker assets,CDK synth bundled functions as expected,CDK synth add the metadata properties expected by sam,can deploy with session tags on the deploy, lookup, file asset, and image asset publishing roles,generating and loading assembly defaults: run: shell: bash @@ -817,7 +817,7 @@ jobs: MAVEN_ARGS: --no-transfer-progress IS_CANARY: "true" CI: "true" - CDK_INTEG_SKIP_TESTS: deploy same docker asset to multiple regions,deploy same docker asset to multiple stacks,deploy stack with multiple docker assets,deploy stack with docker asset,cdk-assets smoke test,deploy new style synthesis to new style bootstrap (with docker image),Garbage Collection untags in-use ecr images,Garbage Collection keeps in use ecr images,Garbage Collection deletes unused ecr images,Garbage Collection tags unused ecr images,all calls from isolated container go through proxy,docker-credential-cdk-assets can assume role and fetch ECR credentials,toolkit deploy stack with multiple docker assets + CDK_INTEG_SKIP_TESTS: deploy same docker asset to multiple regions,deploy same docker asset to multiple stacks,deploy stack with multiple docker assets,deploy stack with docker asset,cdk-assets smoke test,deploy new style synthesis to new style bootstrap (with docker image),Garbage Collection untags in-use ecr images,Garbage Collection keeps in use ecr images,Garbage Collection deletes unused ecr images,Garbage Collection tags unused ecr images,all calls from isolated container go through proxy,docker-credential-cdk-assets can assume role and fetch ECR credentials,toolkit deploy stack with multiple docker assets,CDK synth bundled functions as expected,CDK synth add the metadata properties expected by sam,can deploy with session tags on the deploy, lookup, file asset, and image asset publishing roles,generating and loading assembly defaults: run: shell: bash @@ -943,7 +943,7 @@ jobs: MAVEN_ARGS: --no-transfer-progress IS_CANARY: "true" CI: "true" - CDK_INTEG_SKIP_TESTS: deploy same docker asset to multiple regions,deploy same docker asset to multiple stacks,deploy stack with multiple docker assets,deploy stack with docker asset,cdk-assets smoke test,deploy new style synthesis to new style bootstrap (with docker image),Garbage Collection untags in-use ecr images,Garbage Collection keeps in use ecr images,Garbage Collection deletes unused ecr images,Garbage Collection tags unused ecr images,all calls from isolated container go through proxy,docker-credential-cdk-assets can assume role and fetch ECR credentials,toolkit deploy stack with multiple docker assets + CDK_INTEG_SKIP_TESTS: deploy same docker asset to multiple regions,deploy same docker asset to multiple stacks,deploy stack with multiple docker assets,deploy stack with docker asset,cdk-assets smoke test,deploy new style synthesis to new style bootstrap (with docker image),Garbage Collection untags in-use ecr images,Garbage Collection keeps in use ecr images,Garbage Collection deletes unused ecr images,Garbage Collection tags unused ecr images,all calls from isolated container go through proxy,docker-credential-cdk-assets can assume role and fetch ECR credentials,toolkit deploy stack with multiple docker assets,CDK synth bundled functions as expected,CDK synth add the metadata properties expected by sam,can deploy with session tags on the deploy, lookup, file asset, and image asset publishing roles,generating and loading assembly defaults: run: shell: bash @@ -1066,7 +1066,7 @@ jobs: MAVEN_ARGS: --no-transfer-progress IS_CANARY: "true" CI: "true" - CDK_INTEG_SKIP_TESTS: deploy same docker asset to multiple regions,deploy same docker asset to multiple stacks,deploy stack with multiple docker assets,deploy stack with docker asset,cdk-assets smoke test,deploy new style synthesis to new style bootstrap (with docker image),Garbage Collection untags in-use ecr images,Garbage Collection keeps in use ecr images,Garbage Collection deletes unused ecr images,Garbage Collection tags unused ecr images,all calls from isolated container go through proxy,docker-credential-cdk-assets can assume role and fetch ECR credentials,toolkit deploy stack with multiple docker assets + CDK_INTEG_SKIP_TESTS: deploy same docker asset to multiple regions,deploy same docker asset to multiple stacks,deploy stack with multiple docker assets,deploy stack with docker asset,cdk-assets smoke test,deploy new style synthesis to new style bootstrap (with docker image),Garbage Collection untags in-use ecr images,Garbage Collection keeps in use ecr images,Garbage Collection deletes unused ecr images,Garbage Collection tags unused ecr images,all calls from isolated container go through proxy,docker-credential-cdk-assets can assume role and fetch ECR credentials,toolkit deploy stack with multiple docker assets,CDK synth bundled functions as expected,CDK synth add the metadata properties expected by sam,can deploy with session tags on the deploy, lookup, file asset, and image asset publishing roles,generating and loading assembly defaults: run: shell: bash @@ -1203,7 +1203,7 @@ jobs: MAVEN_ARGS: --no-transfer-progress IS_CANARY: "true" CI: "true" - CDK_INTEG_SKIP_TESTS: deploy same docker asset to multiple regions,deploy same docker asset to multiple stacks,deploy stack with multiple docker assets,deploy stack with docker asset,cdk-assets smoke test,deploy new style synthesis to new style bootstrap (with docker image),Garbage Collection untags in-use ecr images,Garbage Collection keeps in use ecr images,Garbage Collection deletes unused ecr images,Garbage Collection tags unused ecr images,all calls from isolated container go through proxy,docker-credential-cdk-assets can assume role and fetch ECR credentials,toolkit deploy stack with multiple docker assets + CDK_INTEG_SKIP_TESTS: deploy same docker asset to multiple regions,deploy same docker asset to multiple stacks,deploy stack with multiple docker assets,deploy stack with docker asset,cdk-assets smoke test,deploy new style synthesis to new style bootstrap (with docker image),Garbage Collection untags in-use ecr images,Garbage Collection keeps in use ecr images,Garbage Collection deletes unused ecr images,Garbage Collection tags unused ecr images,all calls from isolated container go through proxy,docker-credential-cdk-assets can assume role and fetch ECR credentials,toolkit deploy stack with multiple docker assets,CDK synth bundled functions as expected,CDK synth add the metadata properties expected by sam,can deploy with session tags on the deploy, lookup, file asset, and image asset publishing roles,generating and loading assembly defaults: run: shell: bash diff --git a/projenrc/cdk-cli-integ-tests.ts b/projenrc/cdk-cli-integ-tests.ts index 586950d72..94eb8dcdf 100644 --- a/projenrc/cdk-cli-integ-tests.ts +++ b/projenrc/cdk-cli-integ-tests.ts @@ -33,6 +33,13 @@ const DOCKER_TESTS_SKIPPED_ON_WINDOWS = [ 'all calls from isolated container go through proxy', 'docker-credential-cdk-assets can assume role and fetch ECR credentials', 'toolkit deploy stack with multiple docker assets', + // These do not have 'docker' in the name, but build Linux images as a side + // effect: python lambda bundling, SAM asset bundling, a DockerImageAsset in + // the fixture stack, and a docker-app deploy from a copied assembly. + 'CDK synth bundled functions as expected', + 'CDK synth add the metadata properties expected by sam', + 'can deploy with session tags on the deploy, lookup, file asset, and image asset publishing roles', + 'generating and loading assembly', ]; function setupNodeStep(nodeVersion: string): github.workflows.JobStep { From d220bb8753840a5c7b630c40f3705846b814b052 Mon Sep 17 00:00:00 2001 From: Ian Hou <45278651+iankhou@users.noreply.github.com> Date: Thu, 30 Jul 2026 17:29:22 -0400 Subject: [PATCH 15/26] fix(cli-integ): request 4-hour OIDC session for Windows integ jobs Windows cli shards run 30-80 minutes; tests still executing past the 1-hour default session expiry fail with '403 the security token included in the request is expired', including Atmosphere release calls. Request a 4-hour session on Windows jobs, matching the non-Atmosphere path. Takes effect only if the OIDC role's maximum session duration allows it, which this change also verifies. --- .github/workflows/integ.yml | 10 +++++----- projenrc/cdk-cli-integ-tests.ts | 9 ++++++--- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/.github/workflows/integ.yml b/.github/workflows/integ.yml index 8b6fa1c92..33f44426f 100644 --- a/.github/workflows/integ.yml +++ b/.github/workflows/integ.yml @@ -740,7 +740,7 @@ jobs: uses: aws-actions/configure-aws-credentials@v6 with: aws-region: us-east-1 - role-duration-seconds: 3600 + role-duration-seconds: 14400 role-to-assume: ${{ vars.CDK_ATMOSPHERE_PROD_OIDC_ROLE }} role-session-name: run-tests@aws-cdk-cli-integ output-credentials: true @@ -876,7 +876,7 @@ jobs: uses: aws-actions/configure-aws-credentials@v6 with: aws-region: us-east-1 - role-duration-seconds: 3600 + role-duration-seconds: 14400 role-to-assume: ${{ vars.CDK_ATMOSPHERE_PROD_OIDC_ROLE }} role-session-name: run-tests@aws-cdk-cli-integ output-credentials: true @@ -1002,7 +1002,7 @@ jobs: uses: aws-actions/configure-aws-credentials@v6 with: aws-region: us-east-1 - role-duration-seconds: 3600 + role-duration-seconds: 14400 role-to-assume: ${{ vars.CDK_ATMOSPHERE_PROD_OIDC_ROLE }} role-session-name: run-tests@aws-cdk-cli-integ output-credentials: true @@ -1125,7 +1125,7 @@ jobs: uses: aws-actions/configure-aws-credentials@v6 with: aws-region: us-east-1 - role-duration-seconds: 3600 + role-duration-seconds: 14400 role-to-assume: ${{ vars.CDK_ATMOSPHERE_PROD_OIDC_ROLE }} role-session-name: run-tests@aws-cdk-cli-integ output-credentials: true @@ -1262,7 +1262,7 @@ jobs: uses: aws-actions/configure-aws-credentials@v6 with: aws-region: us-east-1 - role-duration-seconds: 3600 + role-duration-seconds: 14400 role-to-assume: ${{ vars.CDK_ATMOSPHERE_PROD_OIDC_ROLE }} role-session-name: run-tests@aws-cdk-cli-integ output-credentials: true diff --git a/projenrc/cdk-cli-integ-tests.ts b/projenrc/cdk-cli-integ-tests.ts index 94eb8dcdf..4046db50b 100644 --- a/projenrc/cdk-cli-integ-tests.ts +++ b/projenrc/cdk-cli-integ-tests.ts @@ -53,14 +53,14 @@ function setupNodeStep(nodeVersion: string): github.workflows.JobStep { }; } -function awsAuthStep(props: CdkCliIntegTestsWorkflowProps, sessionName: string): github.workflows.JobStep { +function awsAuthStep(props: CdkCliIntegTestsWorkflowProps, sessionName: string, durationSeconds?: number): github.workflows.JobStep { return { name: 'Authenticate Via OIDC Role', id: 'creds', uses: 'aws-actions/configure-aws-credentials@v6', with: { 'aws-region': 'us-east-1', - 'role-duration-seconds': props.enableAtmosphere ? 60 * 60 : 4 * 60 * 60, + 'role-duration-seconds': durationSeconds ?? (props.enableAtmosphere ? 60 * 60 : 4 * 60 * 60), // Expect this in Environment Variables 'role-to-assume': props.enableAtmosphere ? props.enableAtmosphere.oidcRoleArn : '${{ vars.AWS_ROLE_TO_ASSUME_FOR_TESTING }}', 'role-session-name': sessionName, @@ -696,7 +696,10 @@ export class CdkCliIntegTestsWorkflow extends Component { // Run AWS OIDC auth after everything else, so creds are only easily accessible then. // This is defense in depth. Since the workflow's ambient identify is trusted, any script at any point can assume the OIDC role. // The OIDC role is designed to not be able to do anything else but vending Atmosphere creds. - awsAuthStep(this.props, 'run-tests@aws-cdk-cli-integ'), + // Windows jobs run considerably longer than the 1-hour default session; + // tests still running past expiry fail with 403s. Request a longer + // session there (subject to the role's maximum session duration). + awsAuthStep(this.props, 'run-tests@aws-cdk-cli-integ', platform.windows ? 4 * 60 * 60 : undefined), { name: 'Run the test suite: ${{ matrix.suite }}', run: `npx run-suite${this.maxWorkersArg}${shardArg} --use-cli-release=\${{ steps.versions.outputs.cli_version }} --framework-version=\${{ steps.versions.outputs.lib_version }} \${{ matrix.suite }}`, From 8d28ef19871b017304a9a71c87c0bbdbc326a0c0 Mon Sep 17 00:00:00 2001 From: Ian Hou <45278651+iankhou@users.noreply.github.com> Date: Thu, 30 Jul 2026 17:49:49 -0400 Subject: [PATCH 16/26] revert(cli-integ): back to 1-hour OIDC session for Windows integ jobs The Atmosphere OIDC role's MaxSessionDuration is 1 hour: requesting 4 hours fails the assume-role call itself ('The requested DurationSeconds exceeds the MaxSessionDuration set for this role'). Revert to the 1-hour request and document why. Long-running Windows jobs will still 403 on tests running past expiry; fixing that needs a MaxSessionDuration bump on the role or credential refresh in the Atmosphere client. --- .github/workflows/integ.yml | 10 +++++----- projenrc/cdk-cli-integ-tests.ts | 14 ++++++++------ 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/.github/workflows/integ.yml b/.github/workflows/integ.yml index 33f44426f..8b6fa1c92 100644 --- a/.github/workflows/integ.yml +++ b/.github/workflows/integ.yml @@ -740,7 +740,7 @@ jobs: uses: aws-actions/configure-aws-credentials@v6 with: aws-region: us-east-1 - role-duration-seconds: 14400 + role-duration-seconds: 3600 role-to-assume: ${{ vars.CDK_ATMOSPHERE_PROD_OIDC_ROLE }} role-session-name: run-tests@aws-cdk-cli-integ output-credentials: true @@ -876,7 +876,7 @@ jobs: uses: aws-actions/configure-aws-credentials@v6 with: aws-region: us-east-1 - role-duration-seconds: 14400 + role-duration-seconds: 3600 role-to-assume: ${{ vars.CDK_ATMOSPHERE_PROD_OIDC_ROLE }} role-session-name: run-tests@aws-cdk-cli-integ output-credentials: true @@ -1002,7 +1002,7 @@ jobs: uses: aws-actions/configure-aws-credentials@v6 with: aws-region: us-east-1 - role-duration-seconds: 14400 + role-duration-seconds: 3600 role-to-assume: ${{ vars.CDK_ATMOSPHERE_PROD_OIDC_ROLE }} role-session-name: run-tests@aws-cdk-cli-integ output-credentials: true @@ -1125,7 +1125,7 @@ jobs: uses: aws-actions/configure-aws-credentials@v6 with: aws-region: us-east-1 - role-duration-seconds: 14400 + role-duration-seconds: 3600 role-to-assume: ${{ vars.CDK_ATMOSPHERE_PROD_OIDC_ROLE }} role-session-name: run-tests@aws-cdk-cli-integ output-credentials: true @@ -1262,7 +1262,7 @@ jobs: uses: aws-actions/configure-aws-credentials@v6 with: aws-region: us-east-1 - role-duration-seconds: 14400 + role-duration-seconds: 3600 role-to-assume: ${{ vars.CDK_ATMOSPHERE_PROD_OIDC_ROLE }} role-session-name: run-tests@aws-cdk-cli-integ output-credentials: true diff --git a/projenrc/cdk-cli-integ-tests.ts b/projenrc/cdk-cli-integ-tests.ts index 4046db50b..99f0f4143 100644 --- a/projenrc/cdk-cli-integ-tests.ts +++ b/projenrc/cdk-cli-integ-tests.ts @@ -53,14 +53,19 @@ function setupNodeStep(nodeVersion: string): github.workflows.JobStep { }; } -function awsAuthStep(props: CdkCliIntegTestsWorkflowProps, sessionName: string, durationSeconds?: number): github.workflows.JobStep { +function awsAuthStep(props: CdkCliIntegTestsWorkflowProps, sessionName: string): github.workflows.JobStep { return { name: 'Authenticate Via OIDC Role', id: 'creds', uses: 'aws-actions/configure-aws-credentials@v6', with: { 'aws-region': 'us-east-1', - 'role-duration-seconds': durationSeconds ?? (props.enableAtmosphere ? 60 * 60 : 4 * 60 * 60), + // The Atmosphere OIDC role's MaxSessionDuration is 1 hour; requesting more + // fails the assume-role call outright. Tests running past expiry in + // long-running (Windows) jobs will fail with 403s; the fix has to come + // from a longer MaxSessionDuration on the role or credential refresh in + // the Atmosphere client, not from this request. + 'role-duration-seconds': props.enableAtmosphere ? 60 * 60 : 4 * 60 * 60, // Expect this in Environment Variables 'role-to-assume': props.enableAtmosphere ? props.enableAtmosphere.oidcRoleArn : '${{ vars.AWS_ROLE_TO_ASSUME_FOR_TESTING }}', 'role-session-name': sessionName, @@ -696,10 +701,7 @@ export class CdkCliIntegTestsWorkflow extends Component { // Run AWS OIDC auth after everything else, so creds are only easily accessible then. // This is defense in depth. Since the workflow's ambient identify is trusted, any script at any point can assume the OIDC role. // The OIDC role is designed to not be able to do anything else but vending Atmosphere creds. - // Windows jobs run considerably longer than the 1-hour default session; - // tests still running past expiry fail with 403s. Request a longer - // session there (subject to the role's maximum session duration). - awsAuthStep(this.props, 'run-tests@aws-cdk-cli-integ', platform.windows ? 4 * 60 * 60 : undefined), + awsAuthStep(this.props, 'run-tests@aws-cdk-cli-integ'), { name: 'Run the test suite: ${{ matrix.suite }}', run: `npx run-suite${this.maxWorkersArg}${shardArg} --use-cli-release=\${{ steps.versions.outputs.cli_version }} --framework-version=\${{ steps.versions.outputs.lib_version }} \${{ matrix.suite }}`, From 9e0e648e126ea77ff4c89bf9b595ddaad53ddabb Mon Sep 17 00:00:00 2001 From: Ian Hou <45278651+iankhou@users.noreply.github.com> Date: Thu, 30 Jul 2026 17:54:22 -0400 Subject: [PATCH 17/26] chore(cli-integ): remove session duration comment --- projenrc/cdk-cli-integ-tests.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/projenrc/cdk-cli-integ-tests.ts b/projenrc/cdk-cli-integ-tests.ts index 99f0f4143..94eb8dcdf 100644 --- a/projenrc/cdk-cli-integ-tests.ts +++ b/projenrc/cdk-cli-integ-tests.ts @@ -60,11 +60,6 @@ function awsAuthStep(props: CdkCliIntegTestsWorkflowProps, sessionName: string): uses: 'aws-actions/configure-aws-credentials@v6', with: { 'aws-region': 'us-east-1', - // The Atmosphere OIDC role's MaxSessionDuration is 1 hour; requesting more - // fails the assume-role call outright. Tests running past expiry in - // long-running (Windows) jobs will fail with 403s; the fix has to come - // from a longer MaxSessionDuration on the role or credential refresh in - // the Atmosphere client, not from this request. 'role-duration-seconds': props.enableAtmosphere ? 60 * 60 : 4 * 60 * 60, // Expect this in Environment Variables 'role-to-assume': props.enableAtmosphere ? props.enableAtmosphere.oidcRoleArn : '${{ vars.AWS_ROLE_TO_ASSUME_FOR_TESTING }}', From bf0dd3fd5b2f78c945bd6195ee5b10c159ced5bb Mon Sep 17 00:00:00 2001 From: Ian Hou <45278651+iankhou@users.noreply.github.com> Date: Thu, 30 Jul 2026 23:51:16 +0000 Subject: [PATCH 18/26] fix(cli-integ): spawn TTY processes through the shell on Windows node-pty's ConPTY backend resolves the target with SearchPath, which only finds real executables, not the .cmd shims npm generates for CLI entrypoints. Spawning 'cdk' this way fails with 'File not found:', breaking every test that needs a TTY (cdk destroy/import prompts, tty-app). Route the command through cmd.exe /c, mirroring what Process.spawn does with shell: true. --- packages/@aws-cdk-testing/cli-integ/lib/process.ts | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/packages/@aws-cdk-testing/cli-integ/lib/process.ts b/packages/@aws-cdk-testing/cli-integ/lib/process.ts index 9b64ee585..7a987d51a 100644 --- a/packages/@aws-cdk-testing/cli-integ/lib/process.ts +++ b/packages/@aws-cdk-testing/cli-integ/lib/process.ts @@ -48,11 +48,18 @@ export class Process { * Spawn a process with a TTY attached. */ public static spawnTTY(command: string, args: string[], options: pty.IPtyForkOptions | pty.IWindowsPtyForkOptions = {}): IProcess { - const process = pty.spawn(command, args, { + // ConPTY resolves the spawned file with SearchPath, which only finds real + // executables — not the .cmd shims npm creates for CLI entrypoints. Route + // the command through the shell, like Process.spawn does with 'shell: true'. + if (process.platform === 'win32') { + args = ['/c', command, ...args]; + command = process.env.ComSpec ?? 'cmd.exe'; + } + const ptyProcess = pty.spawn(command, args, { name: 'xterm-color', ...options, }); - return new PtyProcess(process); + return new PtyProcess(ptyProcess); } /** From 24e98bc65d070dcb59bb68abb4057549709a005f Mon Sep 17 00:00:00 2001 From: Ian Hou <45278651+iankhou@users.noreply.github.com> Date: Thu, 30 Jul 2026 23:51:25 +0000 Subject: [PATCH 19/26] fix(cli-integ): make cdk watch tests work on Windows The watch tests spawned 'cdk' directly with child_process.spawn, which cannot start npm .cmd shims on Windows ('spawn cdk ENOENT'); the tests then sat in waitForOutput until the 120s poll timeout. Spawn through a shell on win32 (new spawnWatch helper), kill the resulting process tree with taskkill so the watcher does not outlive the test, and replace the POSIX-only 'touch' with fs.utimesSync. --- ...es-with-directory-scoped-glob.integtest.ts | 6 ++--- ...s-with-glob-patterns-negative.integtest.ts | 6 ++--- ...le-changes-with-glob-patterns.integtest.ts | 9 +++---- .../cli-integ-tests/watch/watch-helpers.ts | 25 +++++++++++++++++-- 4 files changed, 31 insertions(+), 15 deletions(-) diff --git a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/watch/cdk-watch-detects-file-changes-with-directory-scoped-glob.integtest.ts b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/watch/cdk-watch-detects-file-changes-with-directory-scoped-glob.integtest.ts index 62f2d6303..a3d226ede 100644 --- a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/watch/cdk-watch-detects-file-changes-with-directory-scoped-glob.integtest.ts +++ b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/watch/cdk-watch-detects-file-changes-with-directory-scoped-glob.integtest.ts @@ -1,7 +1,6 @@ -import * as child_process from 'child_process'; import * as fs from 'fs'; import * as path from 'path'; -import { waitForOutput, waitForCondition, safeKillProcess } from './watch-helpers'; +import { waitForOutput, waitForCondition, safeKillProcess, spawnWatch } from './watch-helpers'; import { integTest, withDefaultFixture } from '../../../lib'; jest.setTimeout(5 * 60 * 1000); // 5 minutes for watch tests @@ -34,11 +33,10 @@ integTest( let output = ''; // Start cdk watch - const watchProcess = child_process.spawn('cdk', [ + const watchProcess = spawnWatch([ 'watch', '--hotswap', '-v', fixture.fullStackName('test-1'), ], { cwd: fixture.integTestDir, - stdio: 'pipe', env: { ...process.env, ...fixture.cdkShellEnv() }, }); diff --git a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/watch/cdk-watch-detects-file-changes-with-glob-patterns-negative.integtest.ts b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/watch/cdk-watch-detects-file-changes-with-glob-patterns-negative.integtest.ts index 1dcab2a1a..a95f23482 100644 --- a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/watch/cdk-watch-detects-file-changes-with-glob-patterns-negative.integtest.ts +++ b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/watch/cdk-watch-detects-file-changes-with-glob-patterns-negative.integtest.ts @@ -1,7 +1,6 @@ -import * as child_process from 'child_process'; import * as fs from 'fs'; import * as path from 'path'; -import { waitForOutput, safeKillProcess } from './watch-helpers'; +import { waitForOutput, safeKillProcess, spawnWatch } from './watch-helpers'; import { integTest, withDefaultFixture, sleep } from '../../../lib'; jest.setTimeout(5 * 60 * 1000); // 5 minutes for watch tests @@ -27,11 +26,10 @@ integTest( let output = ''; // Start cdk watch - const watchProcess = child_process.spawn('cdk', [ + const watchProcess = spawnWatch([ 'watch', '--hotswap', '-v', fixture.fullStackName('test-1'), ], { cwd: fixture.integTestDir, - stdio: 'pipe', env: { ...process.env, ...fixture.cdkShellEnv() }, }); diff --git a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/watch/cdk-watch-detects-file-changes-with-glob-patterns.integtest.ts b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/watch/cdk-watch-detects-file-changes-with-glob-patterns.integtest.ts index 815a595fa..7b5f92bb8 100644 --- a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/watch/cdk-watch-detects-file-changes-with-glob-patterns.integtest.ts +++ b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/watch/cdk-watch-detects-file-changes-with-glob-patterns.integtest.ts @@ -1,7 +1,6 @@ -import * as child_process from 'child_process'; import * as fs from 'fs'; import * as path from 'path'; -import { waitForOutput, waitForCondition, safeKillProcess } from './watch-helpers'; +import { waitForOutput, waitForCondition, safeKillProcess, spawnWatch } from './watch-helpers'; import { integTest, withDefaultFixture } from '../../../lib'; jest.setTimeout(5 * 60 * 1000); // 5 minutes for watch tests @@ -26,11 +25,10 @@ integTest( let output = ''; // Start cdk watch - const watchProcess = child_process.spawn('cdk', [ + const watchProcess = spawnWatch([ 'watch', '--hotswap', '-v', fixture.fullStackName('test-1'), ], { cwd: fixture.integTestDir, - stdio: 'pipe', env: { ...process.env, ...fixture.cdkShellEnv() }, }); @@ -51,7 +49,8 @@ integTest( fixture.log('✓ Initial deployment completed'); // Update the test file timestamp to trigger a watch event - child_process.spawnSync('touch', [testFile]); + const now = new Date(); + fs.utimesSync(testFile, now, now); await waitForOutput(() => output, 'Detected change to'); fixture.log('✓ Watch detected file change'); diff --git a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/watch/watch-helpers.ts b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/watch/watch-helpers.ts index bbe2a918d..ca983fb96 100644 --- a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/watch/watch-helpers.ts +++ b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/watch/watch-helpers.ts @@ -1,4 +1,5 @@ -import type { ChildProcess } from 'node:child_process'; +import * as child_process from 'node:child_process'; +import type { ChildProcess, SpawnOptions } from 'node:child_process'; const DEFAULT_POLL_TIMEOUT = 120_000; // 2 minutes @@ -33,12 +34,32 @@ export async function waitForCondition(condition: () => boolean): Promise expect(condition()).toBe(true); } +/** + * Spawn a long-running `cdk watch` process. + * + * On Windows the CLI is an npm .cmd shim, which `spawn` can only start + * through a shell ('spawn cdk ENOENT' otherwise). + */ +export function spawnWatch(args: string[], options: SpawnOptions): ChildProcess { + return child_process.spawn('cdk', args, { + stdio: 'pipe', + shell: process.platform === 'win32', + ...options, + }); +} + /** * Kill a spawned process. */ export function safeKillProcess(proc: ChildProcess): void { try { - proc.kill('SIGKILL'); + if (process.platform === 'win32' && proc.pid !== undefined) { + // Kill the whole tree: the process was spawned through a shell, + // so proc.pid is the shell and 'cdk watch' is its child. + child_process.spawnSync('taskkill', ['/pid', proc.pid.toString(), '/T', '/F']); + } else { + proc.kill('SIGKILL'); + } } catch { // process may have already exited } From 2a949f914057c36d2df35b6d89508b5101c85265 Mon Sep 17 00:00:00 2001 From: Ian Hou <45278651+iankhou@users.noreply.github.com> Date: Thu, 30 Jul 2026 23:51:37 +0000 Subject: [PATCH 20/26] fix(cli-integ): deliver Windows skip list via file, add two docker tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two more tests build Linux docker images as a side effect and cannot pass on Windows-containers runners: 'test resource import with construct that requires bundling' (INCLUDE_NODEJS_FUNCTION_LAMBDA forces docker bundling of a NodejsFunction) and 'hotswap deployment supports Bedrock AgentCore Runtime' (builds a linux/arm64 DockerImageAsset). The session-tags skip name contains commas, which the comma-separated CDK_INTEG_SKIP_TESTS env var splits into fragments that match nothing — the test ran (and failed) despite being listed. Write the skip list to a newline-separated file and point CDK_INTEG_SKIP_TESTS_FILE at it instead. --- .github/workflows/integ.yml | 125 ++++++++++++++++++++++++++++++-- projenrc/cdk-cli-integ-tests.ts | 14 +++- 2 files changed, 133 insertions(+), 6 deletions(-) diff --git a/.github/workflows/integ.yml b/.github/workflows/integ.yml index 8b6fa1c92..5696908b4 100644 --- a/.github/workflows/integ.yml +++ b/.github/workflows/integ.yml @@ -681,7 +681,7 @@ jobs: MAVEN_ARGS: --no-transfer-progress IS_CANARY: "true" CI: "true" - CDK_INTEG_SKIP_TESTS: deploy same docker asset to multiple regions,deploy same docker asset to multiple stacks,deploy stack with multiple docker assets,deploy stack with docker asset,cdk-assets smoke test,deploy new style synthesis to new style bootstrap (with docker image),Garbage Collection untags in-use ecr images,Garbage Collection keeps in use ecr images,Garbage Collection deletes unused ecr images,Garbage Collection tags unused ecr images,all calls from isolated container go through proxy,docker-credential-cdk-assets can assume role and fetch ECR credentials,toolkit deploy stack with multiple docker assets,CDK synth bundled functions as expected,CDK synth add the metadata properties expected by sam,can deploy with session tags on the deploy, lookup, file asset, and image asset publishing roles,generating and loading assembly + CDK_INTEG_SKIP_TESTS_FILE: ${{ github.workspace }}\windows-skip-tests.txt defaults: run: shell: bash @@ -697,6 +697,29 @@ jobs: echo "TMP=${drive}:\temp" >> $env:GITHUB_ENV echo "npm_config_cache=${drive}:\npm-cache" >> $env:GITHUB_ENV shell: powershell + - name: Write Windows skip-tests file + run: |- + cat > windows-skip-tests.txt << 'EOF' + deploy same docker asset to multiple regions + deploy same docker asset to multiple stacks + deploy stack with multiple docker assets + deploy stack with docker asset + cdk-assets smoke test + deploy new style synthesis to new style bootstrap (with docker image) + Garbage Collection untags in-use ecr images + Garbage Collection keeps in use ecr images + Garbage Collection deletes unused ecr images + Garbage Collection tags unused ecr images + all calls from isolated container go through proxy + docker-credential-cdk-assets can assume role and fetch ECR credentials + toolkit deploy stack with multiple docker assets + CDK synth bundled functions as expected + CDK synth add the metadata properties expected by sam + can deploy with session tags on the deploy, lookup, file asset, and image asset publishing roles + generating and loading assembly + test resource import with construct that requires bundling + hotswap deployment supports Bedrock AgentCore Runtime + EOF - name: Download artifact uses: actions/download-artifact@v8 with: @@ -817,7 +840,7 @@ jobs: MAVEN_ARGS: --no-transfer-progress IS_CANARY: "true" CI: "true" - CDK_INTEG_SKIP_TESTS: deploy same docker asset to multiple regions,deploy same docker asset to multiple stacks,deploy stack with multiple docker assets,deploy stack with docker asset,cdk-assets smoke test,deploy new style synthesis to new style bootstrap (with docker image),Garbage Collection untags in-use ecr images,Garbage Collection keeps in use ecr images,Garbage Collection deletes unused ecr images,Garbage Collection tags unused ecr images,all calls from isolated container go through proxy,docker-credential-cdk-assets can assume role and fetch ECR credentials,toolkit deploy stack with multiple docker assets,CDK synth bundled functions as expected,CDK synth add the metadata properties expected by sam,can deploy with session tags on the deploy, lookup, file asset, and image asset publishing roles,generating and loading assembly + CDK_INTEG_SKIP_TESTS_FILE: ${{ github.workspace }}\windows-skip-tests.txt defaults: run: shell: bash @@ -833,6 +856,29 @@ jobs: echo "TMP=${drive}:\temp" >> $env:GITHUB_ENV echo "npm_config_cache=${drive}:\npm-cache" >> $env:GITHUB_ENV shell: powershell + - name: Write Windows skip-tests file + run: |- + cat > windows-skip-tests.txt << 'EOF' + deploy same docker asset to multiple regions + deploy same docker asset to multiple stacks + deploy stack with multiple docker assets + deploy stack with docker asset + cdk-assets smoke test + deploy new style synthesis to new style bootstrap (with docker image) + Garbage Collection untags in-use ecr images + Garbage Collection keeps in use ecr images + Garbage Collection deletes unused ecr images + Garbage Collection tags unused ecr images + all calls from isolated container go through proxy + docker-credential-cdk-assets can assume role and fetch ECR credentials + toolkit deploy stack with multiple docker assets + CDK synth bundled functions as expected + CDK synth add the metadata properties expected by sam + can deploy with session tags on the deploy, lookup, file asset, and image asset publishing roles + generating and loading assembly + test resource import with construct that requires bundling + hotswap deployment supports Bedrock AgentCore Runtime + EOF - name: Download artifact uses: actions/download-artifact@v8 with: @@ -943,7 +989,7 @@ jobs: MAVEN_ARGS: --no-transfer-progress IS_CANARY: "true" CI: "true" - CDK_INTEG_SKIP_TESTS: deploy same docker asset to multiple regions,deploy same docker asset to multiple stacks,deploy stack with multiple docker assets,deploy stack with docker asset,cdk-assets smoke test,deploy new style synthesis to new style bootstrap (with docker image),Garbage Collection untags in-use ecr images,Garbage Collection keeps in use ecr images,Garbage Collection deletes unused ecr images,Garbage Collection tags unused ecr images,all calls from isolated container go through proxy,docker-credential-cdk-assets can assume role and fetch ECR credentials,toolkit deploy stack with multiple docker assets,CDK synth bundled functions as expected,CDK synth add the metadata properties expected by sam,can deploy with session tags on the deploy, lookup, file asset, and image asset publishing roles,generating and loading assembly + CDK_INTEG_SKIP_TESTS_FILE: ${{ github.workspace }}\windows-skip-tests.txt defaults: run: shell: bash @@ -959,6 +1005,29 @@ jobs: echo "TMP=${drive}:\temp" >> $env:GITHUB_ENV echo "npm_config_cache=${drive}:\npm-cache" >> $env:GITHUB_ENV shell: powershell + - name: Write Windows skip-tests file + run: |- + cat > windows-skip-tests.txt << 'EOF' + deploy same docker asset to multiple regions + deploy same docker asset to multiple stacks + deploy stack with multiple docker assets + deploy stack with docker asset + cdk-assets smoke test + deploy new style synthesis to new style bootstrap (with docker image) + Garbage Collection untags in-use ecr images + Garbage Collection keeps in use ecr images + Garbage Collection deletes unused ecr images + Garbage Collection tags unused ecr images + all calls from isolated container go through proxy + docker-credential-cdk-assets can assume role and fetch ECR credentials + toolkit deploy stack with multiple docker assets + CDK synth bundled functions as expected + CDK synth add the metadata properties expected by sam + can deploy with session tags on the deploy, lookup, file asset, and image asset publishing roles + generating and loading assembly + test resource import with construct that requires bundling + hotswap deployment supports Bedrock AgentCore Runtime + EOF - name: Download artifact uses: actions/download-artifact@v8 with: @@ -1066,7 +1135,7 @@ jobs: MAVEN_ARGS: --no-transfer-progress IS_CANARY: "true" CI: "true" - CDK_INTEG_SKIP_TESTS: deploy same docker asset to multiple regions,deploy same docker asset to multiple stacks,deploy stack with multiple docker assets,deploy stack with docker asset,cdk-assets smoke test,deploy new style synthesis to new style bootstrap (with docker image),Garbage Collection untags in-use ecr images,Garbage Collection keeps in use ecr images,Garbage Collection deletes unused ecr images,Garbage Collection tags unused ecr images,all calls from isolated container go through proxy,docker-credential-cdk-assets can assume role and fetch ECR credentials,toolkit deploy stack with multiple docker assets,CDK synth bundled functions as expected,CDK synth add the metadata properties expected by sam,can deploy with session tags on the deploy, lookup, file asset, and image asset publishing roles,generating and loading assembly + CDK_INTEG_SKIP_TESTS_FILE: ${{ github.workspace }}\windows-skip-tests.txt defaults: run: shell: bash @@ -1082,6 +1151,29 @@ jobs: echo "TMP=${drive}:\temp" >> $env:GITHUB_ENV echo "npm_config_cache=${drive}:\npm-cache" >> $env:GITHUB_ENV shell: powershell + - name: Write Windows skip-tests file + run: |- + cat > windows-skip-tests.txt << 'EOF' + deploy same docker asset to multiple regions + deploy same docker asset to multiple stacks + deploy stack with multiple docker assets + deploy stack with docker asset + cdk-assets smoke test + deploy new style synthesis to new style bootstrap (with docker image) + Garbage Collection untags in-use ecr images + Garbage Collection keeps in use ecr images + Garbage Collection deletes unused ecr images + Garbage Collection tags unused ecr images + all calls from isolated container go through proxy + docker-credential-cdk-assets can assume role and fetch ECR credentials + toolkit deploy stack with multiple docker assets + CDK synth bundled functions as expected + CDK synth add the metadata properties expected by sam + can deploy with session tags on the deploy, lookup, file asset, and image asset publishing roles + generating and loading assembly + test resource import with construct that requires bundling + hotswap deployment supports Bedrock AgentCore Runtime + EOF - name: Download artifact uses: actions/download-artifact@v8 with: @@ -1203,7 +1295,7 @@ jobs: MAVEN_ARGS: --no-transfer-progress IS_CANARY: "true" CI: "true" - CDK_INTEG_SKIP_TESTS: deploy same docker asset to multiple regions,deploy same docker asset to multiple stacks,deploy stack with multiple docker assets,deploy stack with docker asset,cdk-assets smoke test,deploy new style synthesis to new style bootstrap (with docker image),Garbage Collection untags in-use ecr images,Garbage Collection keeps in use ecr images,Garbage Collection deletes unused ecr images,Garbage Collection tags unused ecr images,all calls from isolated container go through proxy,docker-credential-cdk-assets can assume role and fetch ECR credentials,toolkit deploy stack with multiple docker assets,CDK synth bundled functions as expected,CDK synth add the metadata properties expected by sam,can deploy with session tags on the deploy, lookup, file asset, and image asset publishing roles,generating and loading assembly + CDK_INTEG_SKIP_TESTS_FILE: ${{ github.workspace }}\windows-skip-tests.txt defaults: run: shell: bash @@ -1219,6 +1311,29 @@ jobs: echo "TMP=${drive}:\temp" >> $env:GITHUB_ENV echo "npm_config_cache=${drive}:\npm-cache" >> $env:GITHUB_ENV shell: powershell + - name: Write Windows skip-tests file + run: |- + cat > windows-skip-tests.txt << 'EOF' + deploy same docker asset to multiple regions + deploy same docker asset to multiple stacks + deploy stack with multiple docker assets + deploy stack with docker asset + cdk-assets smoke test + deploy new style synthesis to new style bootstrap (with docker image) + Garbage Collection untags in-use ecr images + Garbage Collection keeps in use ecr images + Garbage Collection deletes unused ecr images + Garbage Collection tags unused ecr images + all calls from isolated container go through proxy + docker-credential-cdk-assets can assume role and fetch ECR credentials + toolkit deploy stack with multiple docker assets + CDK synth bundled functions as expected + CDK synth add the metadata properties expected by sam + can deploy with session tags on the deploy, lookup, file asset, and image asset publishing roles + generating and loading assembly + test resource import with construct that requires bundling + hotswap deployment supports Bedrock AgentCore Runtime + EOF - name: Download artifact uses: actions/download-artifact@v8 with: diff --git a/projenrc/cdk-cli-integ-tests.ts b/projenrc/cdk-cli-integ-tests.ts index 94eb8dcdf..02fab0895 100644 --- a/projenrc/cdk-cli-integ-tests.ts +++ b/projenrc/cdk-cli-integ-tests.ts @@ -40,6 +40,8 @@ const DOCKER_TESTS_SKIPPED_ON_WINDOWS = [ 'CDK synth add the metadata properties expected by sam', 'can deploy with session tags on the deploy, lookup, file asset, and image asset publishing roles', 'generating and loading assembly', + 'test resource import with construct that requires bundling', + 'hotswap deployment supports Bedrock AgentCore Runtime', ]; function setupNodeStep(nodeVersion: string): github.workflows.JobStep { @@ -627,7 +629,10 @@ export class CdkCliIntegTestsWorkflow extends Component { IS_CANARY: 'true', CI: 'true', ...platform.windows ? { - CDK_INTEG_SKIP_TESTS: DOCKER_TESTS_SKIPPED_ON_WINDOWS.join(','), + // The skip file is newline-separated; the CDK_INTEG_SKIP_TESTS + // environment variable is comma-separated and cannot express + // test names that contain commas. + CDK_INTEG_SKIP_TESTS_FILE: '${{ github.workspace }}\\windows-skip-tests.txt', } : {}, // add extra env at end so it can override ...props.extraEnv, @@ -667,6 +672,13 @@ export class CdkCliIntegTestsWorkflow extends Component { // reads and writes the cache, so move it onto the Dev Drive too 'echo "npm_config_cache=${drive}:\\npm-cache" >> $env:GITHUB_ENV', ].join('\n'), + }, { + name: 'Write Windows skip-tests file', + run: [ + 'cat > windows-skip-tests.txt << \'EOF\'', + ...DOCKER_TESTS_SKIPPED_ON_WINDOWS, + 'EOF', + ].join('\n'), }] : [], github.WorkflowSteps.downloadArtifact({ with: { From 4f786e8eb5cb16e7ecff060ec469bf1c90b1b881 Mon Sep 17 00:00:00 2001 From: Ian Hou <45278651+iankhou@users.noreply.github.com> Date: Fri, 31 Jul 2026 06:55:43 +0000 Subject: [PATCH 21/26] fix(cli-integ): share one npm install across tests on Windows Every test installs aws-cdk-lib into its own temp directory. Writing out that package's tens of thousands of files takes ~3s on Linux but 10-12 minutes on Windows, and all ~15 jest workers doing it concurrently is the bulk of the Windows wall-clock (the same install takes 41s when a single worker runs it alone). On win32, install each distinct package set once per machine into a shared directory (workers coordinate through an atomic mkdir lock) and junction it into the test directory. rimraf now removes links without recursing into them so test cleanup cannot delete the shared install. --- .../@aws-cdk-testing/cli-integ/lib/shell.ts | 17 ++++- .../cli-integ/lib/with-cdk-app.ts | 72 ++++++++++++++++++- 2 files changed, 86 insertions(+), 3 deletions(-) diff --git a/packages/@aws-cdk-testing/cli-integ/lib/shell.ts b/packages/@aws-cdk-testing/cli-integ/lib/shell.ts index 1be3c53f4..73c5eb4e6 100644 --- a/packages/@aws-cdk-testing/cli-integ/lib/shell.ts +++ b/packages/@aws-cdk-testing/cli-integ/lib/shell.ts @@ -282,7 +282,22 @@ export class ShellHelper { export function rimraf(fsPath: string): boolean { try { let success = true; - const isDir = fs.lstatSync(fsPath).isDirectory(); + const stat = fs.lstatSync(fsPath); + + // Remove links without recursing into their target: a directory may + // link to shared content that other tests are still using (e.g. the + // shared 'node_modules' on Windows). + if (stat.isSymbolicLink()) { + try { + fs.unlinkSync(fsPath); + } catch { + // On Windows, directory links (junctions) must be removed with rmdir + fs.rmdirSync(fsPath); + } + return true; + } + + const isDir = stat.isDirectory(); if (isDir) { for (const file of fs.readdirSync(fsPath)) { diff --git a/packages/@aws-cdk-testing/cli-integ/lib/with-cdk-app.ts b/packages/@aws-cdk-testing/cli-integ/lib/with-cdk-app.ts index bddfc3650..4f46dda3c 100644 --- a/packages/@aws-cdk-testing/cli-integ/lib/with-cdk-app.ts +++ b/packages/@aws-cdk-testing/cli-integ/lib/with-cdk-app.ts @@ -1,5 +1,6 @@ /* eslint-disable no-console */ import assert from 'assert'; +import * as crypto from 'crypto'; import * as fs from 'fs'; import * as os from 'os'; import * as path from 'path'; @@ -13,7 +14,7 @@ import type { ITestCliSource, ITestLibrarySource } from './package-sources/sourc import { testSource } from './package-sources/subprocess'; import { RESOURCES_DIR } from './resources'; import type { ShellOptions } from './shell'; -import { ShellHelper, rimraf } from './shell'; +import { shell, ShellHelper, rimraf } from './shell'; import type { AwsContext, AwsContextOptions } from './with-aws'; import { atmosphereEnabled, withAws } from './with-aws'; import { withTimeout } from './with-timeout'; @@ -1064,6 +1065,70 @@ export async function installNpmPackages(fixture: TestFixture, packages: Record< devDependencies: packages, }, undefined, 2), { encoding: 'utf-8' }); + if (process.platform === 'win32') { + // Installing aws-cdk-lib means writing out tens of thousands of small + // files, which is very slow on Windows (minutes instead of seconds), + // and every concurrent jest worker doing so at once makes it slower + // still. Install every distinct package set only once per machine and + // junction it into the test directory. + const sharedNodeModules = await sharedPackageSetInstall(fixture, packages); + fs.symlinkSync(sharedNodeModules, path.join(fixture.integTestDir, 'node_modules'), 'junction'); + return; + } + + await npmInstallWithRetry(fixture, fixture.integTestDir); +} + +/** + * Install the given package set into a machine-shared directory, once. + * + * Concurrent callers (jest workers are separate processes) coordinate via an + * atomically-created lock directory; whoever wins installs while the rest + * poll for the completion marker. + * + * @returns the path of the installed `node_modules` directory. + */ +async function sharedPackageSetInstall(fixture: TestFixture, packages: Record): Promise { + const hash = crypto.createHash('sha256').update(JSON.stringify(packages)).digest('hex').slice(0, 16); + const sharedDir = path.join(os.tmpdir(), `cdk-integ-shared-${hash}`); + const nodeModules = path.join(sharedDir, 'node_modules'); + const completeMarker = path.join(sharedDir, '.install-complete'); + const lockDir = `${sharedDir}.lock`; + + const deadline = Date.now() + 30 * 60 * 1000; + while (true) { + if (fs.existsSync(completeMarker)) { + return nodeModules; + } + if (Date.now() > deadline) { + throw new Error(`Timed out waiting for shared install of ${JSON.stringify(packages)} in '${sharedDir}'`); + } + + try { + fs.mkdirSync(lockDir); + } catch { + // Another worker is installing; wait for it to finish. + await sleep(5_000); + continue; + } + + try { + if (fs.existsSync(completeMarker)) { + return nodeModules; + } + fixture.log(`Installing shared package set into '${sharedDir}'`); + fs.mkdirSync(sharedDir, { recursive: true }); + fs.copyFileSync(path.join(fixture.integTestDir, 'package.json'), path.join(sharedDir, 'package.json')); + await npmInstallWithRetry(fixture, sharedDir); + fs.writeFileSync(completeMarker, ''); + return nodeModules; + } finally { + fs.rmdirSync(lockDir); + } + } +} + +async function npmInstallWithRetry(fixture: TestFixture, cwd: string) { // we often ECONNRESET from NPM so lets retry. this might be because of high concurrency // which overwhelmes system resources. const timeoutMinutes = 10; @@ -1073,7 +1138,10 @@ export async function installNpmPackages(fixture: TestFixture, packages: Record< while (true) { try { // Now install that `package.json` using NPM7 - await fixture.shell(['node', require.resolve('npm'), 'install']); + await shell(['node', require.resolve('npm'), 'install'], { + cwd, + outputs: [fixture.output], + }); break; } catch (e: any) { if (Date.now() < timeoutDate.getTime() && fixture.output.toString().includes('ECONNRESET' )) { From 6ca1d69e40454b243830f8c4ae6f3b02288b19f5 Mon Sep 17 00:00:00 2001 From: Ian Hou <45278651+iankhou@users.noreply.github.com> Date: Fri, 31 Jul 2026 14:50:29 +0000 Subject: [PATCH 22/26] fix(cli-integ): widen ConPTY terminal so long prompts match 'cdk import prompts the user for sns topic arns' hangs on Windows: the prompt line (resource path + ARN hint) is longer than the default 80 columns, and ConPTY hard-wraps output at the terminal width, so the line-based prompt regex never matches and the test waits until the 20-minute timeout. Use 512 columns so no real prompt ever wraps. --- packages/@aws-cdk-testing/cli-integ/lib/process.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/@aws-cdk-testing/cli-integ/lib/process.ts b/packages/@aws-cdk-testing/cli-integ/lib/process.ts index 7a987d51a..5e08f966e 100644 --- a/packages/@aws-cdk-testing/cli-integ/lib/process.ts +++ b/packages/@aws-cdk-testing/cli-integ/lib/process.ts @@ -57,6 +57,11 @@ export class Process { } const ptyProcess = pty.spawn(command, args, { name: 'xterm-color', + // Wide enough that no output line ever hits the terminal width: ConPTY + // (unlike Unix ptys) renders the screen buffer and inserts hard line + // breaks at the width, which splits long prompts across lines and + // breaks the line-based prompt matching in shell(). + cols: 512, ...options, }); return new PtyProcess(ptyProcess); From f65a95bb057c408e47280dc873a3bd8aa48dcdd3 Mon Sep 17 00:00:00 2001 From: Ian Hou <45278651+iankhou@users.noreply.github.com> Date: Fri, 31 Jul 2026 14:50:29 +0000 Subject: [PATCH 23/26] fix(cli-integ): skip sam local test on Windows Synthesizing the SAM fixture app bundles a python lambda with aws-lambda-python-alpha, which builds the Linux image public.ecr.aws/sam/build-python3.12 - impossible in Windows-containers mode. Skip list is now 20. --- .github/workflows/integ.yml | 5 +++++ projenrc/cdk-cli-integ-tests.ts | 1 + 2 files changed, 6 insertions(+) diff --git a/.github/workflows/integ.yml b/.github/workflows/integ.yml index 5696908b4..9343da9a2 100644 --- a/.github/workflows/integ.yml +++ b/.github/workflows/integ.yml @@ -719,6 +719,7 @@ jobs: generating and loading assembly test resource import with construct that requires bundling hotswap deployment supports Bedrock AgentCore Runtime + sam can locally test the synthesized cdk application EOF - name: Download artifact uses: actions/download-artifact@v8 @@ -878,6 +879,7 @@ jobs: generating and loading assembly test resource import with construct that requires bundling hotswap deployment supports Bedrock AgentCore Runtime + sam can locally test the synthesized cdk application EOF - name: Download artifact uses: actions/download-artifact@v8 @@ -1027,6 +1029,7 @@ jobs: generating and loading assembly test resource import with construct that requires bundling hotswap deployment supports Bedrock AgentCore Runtime + sam can locally test the synthesized cdk application EOF - name: Download artifact uses: actions/download-artifact@v8 @@ -1173,6 +1176,7 @@ jobs: generating and loading assembly test resource import with construct that requires bundling hotswap deployment supports Bedrock AgentCore Runtime + sam can locally test the synthesized cdk application EOF - name: Download artifact uses: actions/download-artifact@v8 @@ -1333,6 +1337,7 @@ jobs: generating and loading assembly test resource import with construct that requires bundling hotswap deployment supports Bedrock AgentCore Runtime + sam can locally test the synthesized cdk application EOF - name: Download artifact uses: actions/download-artifact@v8 diff --git a/projenrc/cdk-cli-integ-tests.ts b/projenrc/cdk-cli-integ-tests.ts index 02fab0895..43d76239a 100644 --- a/projenrc/cdk-cli-integ-tests.ts +++ b/projenrc/cdk-cli-integ-tests.ts @@ -42,6 +42,7 @@ const DOCKER_TESTS_SKIPPED_ON_WINDOWS = [ 'generating and loading assembly', 'test resource import with construct that requires bundling', 'hotswap deployment supports Bedrock AgentCore Runtime', + 'sam can locally test the synthesized cdk application', ]; function setupNodeStep(nodeVersion: string): github.workflows.JobStep { From dba01673a4e5aa907df2a03bf1a6b7c0facfd972 Mon Sep 17 00:00:00 2001 From: Ian Hou <45278651+iankhou@users.noreply.github.com> Date: Fri, 31 Jul 2026 18:39:25 +0000 Subject: [PATCH 24/26] fix(cli-integ): match prompts in ConPTY screen-buffer output The second prompt of 'cdk import prompts the user for sns topic arns' never matched on Windows: ConPTY draws it with an absolute cursor-positioning sequence, pads it with spaces to the terminal width, and follows it with lines containing only control sequences. The line-based matcher saw a control-only 'last line' and waited forever. On win32, strip ANSI escape sequences and match against the last line with visible content. --- .../@aws-cdk-testing/cli-integ/lib/shell.ts | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/packages/@aws-cdk-testing/cli-integ/lib/shell.ts b/packages/@aws-cdk-testing/cli-integ/lib/shell.ts index 73c5eb4e6..b1d2cee66 100644 --- a/packages/@aws-cdk-testing/cli-integ/lib/shell.ts +++ b/packages/@aws-cdk-testing/cli-integ/lib/shell.ts @@ -354,7 +354,28 @@ export function addToShellPath(x: string) { class LastLine { private lastLine: string = ''; + // win32 only: the last completed line that had visible content, see below + private lastVisibleLine: string = ''; + public append(chunk: string): void { + if (process.platform === 'win32') { + // ConPTY renders the screen buffer instead of streaming plain text: + // prompts are drawn with cursor-positioning escape sequences, padded + // with spaces to the terminal width, and followed by "lines" that + // contain nothing but more escape sequences. Match against the last + // line that had visible content, so control-only lines don't erase a + // prompt that was just drawn. + const lines = stripAnsi(chunk).split(/\r?\n/); + this.lastLine += lines[0]; + for (const line of lines.slice(1)) { + if (this.lastLine.trim().length > 0) { + this.lastVisibleLine = this.lastLine; + } + this.lastLine = line; + } + return; + } + const lines = chunk.split(os.EOL); if (lines.length === 1) { // chunk doesn't contain a new line so just append @@ -366,10 +387,30 @@ class LastLine { } public get(): string { + if (process.platform === 'win32' && this.lastLine.trim().length === 0) { + return this.lastVisibleLine; + } return this.lastLine; } public reset() { this.lastLine = ''; + this.lastVisibleLine = ''; } } + +const ESC = '\u001b'; +// CSI sequences (cursor movement, erase, colors) and OSC sequences (window title) +const ANSI_REGEX = new RegExp(`${ESC}\\[[0-9;?]*[@-~]|${ESC}\\][^${ESC}\\u0007]*(?:\\u0007|${ESC}\\\\)`, 'g'); + +/** + * Remove ANSI escape sequences from terminal output. + * + * Windows ConPTY renders the screen buffer rather than streaming plain text: + * once the cursor reaches the bottom of the buffer, lines arrive as absolute + * cursor-positioning sequences instead of newline-terminated text. Prompt + * matching must look at the text only. + */ +function stripAnsi(chunk: string): string { + return chunk.replace(ANSI_REGEX, ''); +} From 0bf5af05d7923cc5ddbb6ab25122e33f464b9163 Mon Sep 17 00:00:00 2001 From: Ian Hou <45278651+iankhou@users.noreply.github.com> Date: Sat, 1 Aug 2026 00:55:32 +0000 Subject: [PATCH 25/26] fix(cli-integ): start Verdaccio without pm2, poll for readiness Every integ job installs Verdaccio globally before running tests. pm2 roughly doubled that install (77 of 398 packages) and only served to daemonize a process that just needs to outlive the job step; a nohup'd node process does the same. Replace the fixed 'sleep 5' with a poll of the registry endpoint (typically ready in 1-2s), and quiet the install with --no-audit --no-fund. Saves roughly 60-90s on Windows jobs and ~10s on Linux jobs, per job. --- projenrc/cdk-cli-integ-tests.ts | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/projenrc/cdk-cli-integ-tests.ts b/projenrc/cdk-cli-integ-tests.ts index 43d76239a..acffbc9ff 100644 --- a/projenrc/cdk-cli-integ-tests.ts +++ b/projenrc/cdk-cli-integ-tests.ts @@ -333,16 +333,22 @@ export class CdkCliIntegTestsWorkflow extends Component { committed: false, lines: [ '#!/bin/bash', - 'npm install -g verdaccio pm2', + // No process manager: Verdaccio only has to outlive this job, and the + // runner kills leftover processes at job teardown. A plain detached + // spawn avoids installing pm2, which used to double the install time. + 'npm install -g --no-audit --no-fund --loglevel=error verdaccio', 'mkdir -p $HOME/.config/verdaccio', `echo '${JSON.stringify(verdaccioConfig)}' > $HOME/.config/verdaccio/config.yaml`, - // Start Verdaccio through pm2 by pointing at its JS entrypoint with an - // explicit Node interpreter. On Windows the global `verdaccio` bin is a - // `.cmd` shim, which pm2's fork mode cannot execute; the resolved JS - // file works on every platform. + // Point at Verdaccio's JS entrypoint: on Windows the global bin is a + // `.cmd` shim that cannot be spawned from bash directly. 'VERDACCIO_BIN="$(npm root -g)/verdaccio/bin/verdaccio"', - 'pm2 start "$VERDACCIO_BIN" --interpreter node -- --config $HOME/.config/verdaccio/config.yaml', - 'sleep 5', // Wait for Verdaccio to start + 'nohup node "$VERDACCIO_BIN" --config $HOME/.config/verdaccio/config.yaml > verdaccio.log 2>&1 &', + // Wait for Verdaccio to accept requests instead of sleeping a fixed time + 'for i in $(seq 1 60); do', + ' if curl -fsS -o /dev/null http://localhost:4873/; then break; fi', + ' if [ $i -eq 60 ]; then echo "Verdaccio did not start:"; cat verdaccio.log; exit 1; fi', + ' sleep 1', + 'done', // Configure NPM to use local registry 'echo \'//localhost:4873/:_authToken="MWRjNDU3OTE1NTljYWUyOTFkMWJkOGUyYTIwZWMwNTI6YTgwZjkyNDE0NzgwYWQzNQ=="\' > ~/.npmrc', 'echo \'registry=http://localhost:4873/\' >> ~/.npmrc', From 2e0a1f1979e029e886e037d05ad84157f918377d Mon Sep 17 00:00:00 2001 From: Ian Hou <45278651+iankhou@users.noreply.github.com> Date: Sat, 1 Aug 2026 01:57:15 +0000 Subject: [PATCH 26/26] feat(cli-integ): share a weekly npm cache across integ jobs Every integ job downloads the same packages from scratch: the Verdaccio global install, @aws-cdk-testing/cli-integ and its large dependency tree (Jest, 14 AWS SDK clients, npm itself), and aws-cdk-lib per test. Cache the npm cache directory (the Dev Drive path on Windows, ~/.npm on Linux) with a weekly rotating key shared per platform. Locally published '.999' candidates change content under an unchanging version number, so jobs set npm_config_prefer_online: metadata is always revalidated against the registry and only integrity-matching tarballs are served from the cache. The npm cache is content-addressed, so a stale tarball can never satisfy a new integrity hash. --- .github/workflows/integ.yml | 100 ++++++++++++++++++++++++++++++++ projenrc/cdk-cli-integ-tests.ts | 30 ++++++++++ 2 files changed, 130 insertions(+) diff --git a/.github/workflows/integ.yml b/.github/workflows/integ.yml index 9343da9a2..b19a9be04 100644 --- a/.github/workflows/integ.yml +++ b/.github/workflows/integ.yml @@ -91,11 +91,21 @@ jobs: MAVEN_ARGS: --no-transfer-progress IS_CANARY: "true" CI: "true" + npm_config_prefer_online: "true" defaults: run: shell: bash if: github.event_name != 'merge_group' && !contains(github.event.pull_request.labels.*.name, 'pr/exempt-integ-test') steps: + - name: Compute cache key + id: cachekey + run: echo "week=$(date +%G-%V)" >> $GITHUB_OUTPUT + - name: Restore npm cache + uses: actions/cache@v4 + with: + path: ~/.npm + key: npm-cache-linux-${{ steps.cachekey.outputs.week }} + restore-keys: npm-cache-linux- - name: Download artifact uses: actions/download-artifact@v8 with: @@ -216,11 +226,21 @@ jobs: MAVEN_ARGS: --no-transfer-progress IS_CANARY: "true" CI: "true" + npm_config_prefer_online: "true" defaults: run: shell: bash if: github.event_name != 'merge_group' && !contains(github.event.pull_request.labels.*.name, 'pr/exempt-integ-test') steps: + - name: Compute cache key + id: cachekey + run: echo "week=$(date +%G-%V)" >> $GITHUB_OUTPUT + - name: Restore npm cache + uses: actions/cache@v4 + with: + path: ~/.npm + key: npm-cache-linux-${{ steps.cachekey.outputs.week }} + restore-keys: npm-cache-linux- - name: Download artifact uses: actions/download-artifact@v8 with: @@ -331,11 +351,21 @@ jobs: MAVEN_ARGS: --no-transfer-progress IS_CANARY: "true" CI: "true" + npm_config_prefer_online: "true" defaults: run: shell: bash if: github.event_name != 'merge_group' && !contains(github.event.pull_request.labels.*.name, 'pr/exempt-integ-test') steps: + - name: Compute cache key + id: cachekey + run: echo "week=$(date +%G-%V)" >> $GITHUB_OUTPUT + - name: Restore npm cache + uses: actions/cache@v4 + with: + path: ~/.npm + key: npm-cache-linux-${{ steps.cachekey.outputs.week }} + restore-keys: npm-cache-linux- - name: Download artifact uses: actions/download-artifact@v8 with: @@ -443,11 +473,21 @@ jobs: MAVEN_ARGS: --no-transfer-progress IS_CANARY: "true" CI: "true" + npm_config_prefer_online: "true" defaults: run: shell: bash if: github.event_name != 'merge_group' && !contains(github.event.pull_request.labels.*.name, 'pr/exempt-integ-test') steps: + - name: Compute cache key + id: cachekey + run: echo "week=$(date +%G-%V)" >> $GITHUB_OUTPUT + - name: Restore npm cache + uses: actions/cache@v4 + with: + path: ~/.npm + key: npm-cache-linux-${{ steps.cachekey.outputs.week }} + restore-keys: npm-cache-linux- - name: Download artifact uses: actions/download-artifact@v8 with: @@ -569,11 +609,21 @@ jobs: MAVEN_ARGS: --no-transfer-progress IS_CANARY: "true" CI: "true" + npm_config_prefer_online: "true" defaults: run: shell: bash if: github.event_name != 'merge_group' && !contains(github.event.pull_request.labels.*.name, 'pr/exempt-integ-test') steps: + - name: Compute cache key + id: cachekey + run: echo "week=$(date +%G-%V)" >> $GITHUB_OUTPUT + - name: Restore npm cache + uses: actions/cache@v4 + with: + path: ~/.npm + key: npm-cache-linux-${{ steps.cachekey.outputs.week }} + restore-keys: npm-cache-linux- - name: Download artifact uses: actions/download-artifact@v8 with: @@ -681,6 +731,7 @@ jobs: MAVEN_ARGS: --no-transfer-progress IS_CANARY: "true" CI: "true" + npm_config_prefer_online: "true" CDK_INTEG_SKIP_TESTS_FILE: ${{ github.workspace }}\windows-skip-tests.txt defaults: run: @@ -721,6 +772,15 @@ jobs: hotswap deployment supports Bedrock AgentCore Runtime sam can locally test the synthesized cdk application EOF + - name: Compute cache key + id: cachekey + run: echo "week=$(date +%G-%V)" >> $GITHUB_OUTPUT + - name: Restore npm cache + uses: actions/cache@v4 + with: + path: ${{ env.npm_config_cache }} + key: npm-cache-windows-${{ steps.cachekey.outputs.week }} + restore-keys: npm-cache-windows- - name: Download artifact uses: actions/download-artifact@v8 with: @@ -841,6 +901,7 @@ jobs: MAVEN_ARGS: --no-transfer-progress IS_CANARY: "true" CI: "true" + npm_config_prefer_online: "true" CDK_INTEG_SKIP_TESTS_FILE: ${{ github.workspace }}\windows-skip-tests.txt defaults: run: @@ -881,6 +942,15 @@ jobs: hotswap deployment supports Bedrock AgentCore Runtime sam can locally test the synthesized cdk application EOF + - name: Compute cache key + id: cachekey + run: echo "week=$(date +%G-%V)" >> $GITHUB_OUTPUT + - name: Restore npm cache + uses: actions/cache@v4 + with: + path: ${{ env.npm_config_cache }} + key: npm-cache-windows-${{ steps.cachekey.outputs.week }} + restore-keys: npm-cache-windows- - name: Download artifact uses: actions/download-artifact@v8 with: @@ -991,6 +1061,7 @@ jobs: MAVEN_ARGS: --no-transfer-progress IS_CANARY: "true" CI: "true" + npm_config_prefer_online: "true" CDK_INTEG_SKIP_TESTS_FILE: ${{ github.workspace }}\windows-skip-tests.txt defaults: run: @@ -1031,6 +1102,15 @@ jobs: hotswap deployment supports Bedrock AgentCore Runtime sam can locally test the synthesized cdk application EOF + - name: Compute cache key + id: cachekey + run: echo "week=$(date +%G-%V)" >> $GITHUB_OUTPUT + - name: Restore npm cache + uses: actions/cache@v4 + with: + path: ${{ env.npm_config_cache }} + key: npm-cache-windows-${{ steps.cachekey.outputs.week }} + restore-keys: npm-cache-windows- - name: Download artifact uses: actions/download-artifact@v8 with: @@ -1138,6 +1218,7 @@ jobs: MAVEN_ARGS: --no-transfer-progress IS_CANARY: "true" CI: "true" + npm_config_prefer_online: "true" CDK_INTEG_SKIP_TESTS_FILE: ${{ github.workspace }}\windows-skip-tests.txt defaults: run: @@ -1178,6 +1259,15 @@ jobs: hotswap deployment supports Bedrock AgentCore Runtime sam can locally test the synthesized cdk application EOF + - name: Compute cache key + id: cachekey + run: echo "week=$(date +%G-%V)" >> $GITHUB_OUTPUT + - name: Restore npm cache + uses: actions/cache@v4 + with: + path: ${{ env.npm_config_cache }} + key: npm-cache-windows-${{ steps.cachekey.outputs.week }} + restore-keys: npm-cache-windows- - name: Download artifact uses: actions/download-artifact@v8 with: @@ -1299,6 +1389,7 @@ jobs: MAVEN_ARGS: --no-transfer-progress IS_CANARY: "true" CI: "true" + npm_config_prefer_online: "true" CDK_INTEG_SKIP_TESTS_FILE: ${{ github.workspace }}\windows-skip-tests.txt defaults: run: @@ -1339,6 +1430,15 @@ jobs: hotswap deployment supports Bedrock AgentCore Runtime sam can locally test the synthesized cdk application EOF + - name: Compute cache key + id: cachekey + run: echo "week=$(date +%G-%V)" >> $GITHUB_OUTPUT + - name: Restore npm cache + uses: actions/cache@v4 + with: + path: ${{ env.npm_config_cache }} + key: npm-cache-windows-${{ steps.cachekey.outputs.week }} + restore-keys: npm-cache-windows- - name: Download artifact uses: actions/download-artifact@v8 with: diff --git a/projenrc/cdk-cli-integ-tests.ts b/projenrc/cdk-cli-integ-tests.ts index acffbc9ff..de0e3ee47 100644 --- a/projenrc/cdk-cli-integ-tests.ts +++ b/projenrc/cdk-cli-integ-tests.ts @@ -635,6 +635,12 @@ export class CdkCliIntegTestsWorkflow extends Component { // assumptions about the availability of source packages. IS_CANARY: 'true', CI: 'true', + // The npm cache is shared across runs (see the cache step below), but + // the locally published '.999' candidate packages change content while + // keeping the same version number. Always revalidate metadata against + // the registry (it is localhost, so this is cheap); tarballs that + // still match their integrity hash are served from the cache. + npm_config_prefer_online: 'true', ...platform.windows ? { // The skip file is newline-separated; the CDK_INTEG_SKIP_TESTS // environment variable is comma-separated and cannot express @@ -687,6 +693,30 @@ export class CdkCliIntegTestsWorkflow extends Component { 'EOF', ].join('\n'), }] : [], + { + name: 'Compute cache key', + id: 'cachekey', + // Rotate the cache weekly so it follows dependency updates without + // growing unboundedly; correctness does not depend on freshness + // because npm revalidates metadata (npm_config_prefer_online). + run: 'echo "week=$(date +%G-%V)" >> $GITHUB_OUTPUT', + }, + { + // The npm cache makes the Verdaccio global install, the cli-integ + // install, and the per-test framework installs mostly network-free. + // All jobs of a platform share one weekly cache entry: the first + // job to finish on a cache miss saves it, concurrent saves of the + // same key are rejected and harmless. + name: 'Restore npm cache', + uses: 'actions/cache@v4', + with: { + // On Windows this is the Dev Drive path exported by the setup + // step above; on Linux it is npm's default cache location. + 'path': platform.windows ? '${{ env.npm_config_cache }}' : '~/.npm', + 'key': `npm-cache-${platform.windows ? 'windows' : 'linux'}-\${{ steps.cachekey.outputs.week }}`, + 'restore-keys': `npm-cache-${platform.windows ? 'windows' : 'linux'}-`, + }, + }, github.WorkflowSteps.downloadArtifact({ with: { artifactIds: [`\${{needs.${this.JOB_PREPARE}.outputs.packagesArtifact}}`],