Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
41aa0c4
fix(core): resolve npx/npm spawn ENOENT on Windows The deploy/sandbox…
pranavosu Jun 17, 2026
bb74ef1
ci: add TEMPORARY windows-latest spawn check for #19 Verifies the Win…
pranavosu Jun 17, 2026
5c90a93
ci: add TEMPORARY Windows template build + dev smoke (#19) Scaffolds …
pranavosu Jun 17, 2026
4aeb412
ci: generate version files before build:packages on Windows smoke bui…
pranavosu Jun 17, 2026
be98b8f
ci: scaffold Windows smoke under RUNNER_TEMP to avoid 8.3 short-path …
pranavosu Jun 17, 2026
4b93d94
ci: replace temp Windows checks with optional Windows E2E workflow Dr…
pranavosu Jun 17, 2026
8c07b59
fix(core): use file:// URLs for backend dynamic import on Windows The…
pranavosu Jun 17, 2026
1355657
test(comprehensive): spawn deploy/destroy via node --import tsx for W…
pranavosu Jun 17, 2026
4ec3c2d
ci(windows-e2e): shorten stack suffix to fit S3 bucket name limit The…
pranavosu Jun 17, 2026
a4ad0ff
ci(windows-e2e): combine dev + sandbox + production into one job Sing…
pranavosu Jun 17, 2026
b6e46d2
Merge branch 'main' into ci/win
pranavosu Jun 17, 2026
6362cc0
ci(windows-e2e): drive real npm run commands on a scaffolded template…
pranavosu Jun 17, 2026
8b90241
ci(windows-e2e): use backend template (no GSI); add BB republish chan…
pranavosu Jun 18, 2026
28fb925
ci(windows-e2e): base dev/sandbox readiness on artifacts, not log str…
pranavosu Jun 18, 2026
d1329af
chore: run Windows E2E on schedule+dispatch; trim comments/changeset …
pranavosu Jun 18, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions .changeset/bb-republish-patch.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
"@aws-blocks/bb-app-setting": patch
"@aws-blocks/bb-auth-cognito": patch
"@aws-blocks/bb-auth-oidc": patch
"@aws-blocks/bb-distributed-table": patch
"@aws-blocks/bb-email-client": patch
"@aws-blocks/bb-knowledge-base": patch
"@aws-blocks/bb-kv-store": patch
"@aws-blocks/bb-tracer": patch
---

Minor test improvements
5 changes: 5 additions & 0 deletions .changeset/windows-spawn-npx.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@aws-blocks/core": patch
---

Fix `deploy`, `sandbox`, and `destroy` failing on Windows: spawn `npm`/`npx`/`cdk` via `cross-spawn` (resolves the `.cmd` shims) and import the backend through a `file://` URL so absolute paths like `D:\...` work during CDK synth.
55 changes: 55 additions & 0 deletions .github/workflows/windows-e2e.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
name: Windows E2E

# Scaffolds the `backend` template from a local registry (packed off this repo)
# and runs the real `npm run dev`/`sandbox`/`deploy` on Windows, exercising the
# client tooling (spawning, CDK synth, paths). Deployed logic runs in Lambda and
# is OS-independent, so behavioral suites stay on Linux. Scheduled + manual only.

on:
workflow_dispatch:
schedule:
- cron: '0 8 * * *'
- cron: '0 14 * * *'

concurrency:
group: windows-e2e-${{ github.ref }}
cancel-in-progress: true

permissions:
id-token: write
contents: read

jobs:
windows-e2e:
name: Windows E2E (dev + sandbox + deploy)
runs-on: windows-latest
timeout-minutes: 120
environment: publish
env:
# Unique per run so a cancelled run can't collide with the next.
BLOCKS_STACK_SUFFIX: w${{ github.run_number }}
E2E_FROM_EMAIL: ${{ secrets.E2E_FROM_EMAIL }}
steps:
- uses: actions/checkout@v5
with:
persist-credentials: false

- uses: actions/setup-node@v5
with:
node-version-file: '.nvmrc'
cache: npm

- run: npm ci
- run: npm run build

- name: Pack to local registry
run: npm run publish:dry-run

- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v6
with:
role-to-assume: ${{ secrets.AWS_ROLE_ARN }}
aws-region: us-east-1

- name: Scaffold template and run user commands
run: node scripts/ci/windows-template-e2e.mjs
2 changes: 2 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@
"blocks-telemetry": "./dist/scripts/telemetry-cli.js"
},
"devDependencies": {
"@types/cross-spawn": "^6.0.6",
"@types/http-proxy": "^1.17.16",
"@types/node": "^20.0.0",
"typescript": "^5.3.0",
Expand All @@ -64,6 +65,7 @@
"@aws-blocks/hosting": "*",
"@aws-sdk/client-s3": "^3.700.0",
"@aws-sdk/client-ssm": "^3.700.0",
"cross-spawn": "^7.0.6",
"http-proxy": "^1.18.1"
},
"peerDependencies": {
Expand Down
8 changes: 6 additions & 2 deletions packages/core/src/cdk/blocks-backend.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import * as lambda from 'aws-cdk-lib/aws-lambda-nodejs';
import * as apigateway from 'aws-cdk-lib/aws-apigateway';
import { CfnGroup } from 'aws-cdk-lib/aws-resourcegroups';
import { Construct } from 'constructs';
import { pathToFileURL } from 'node:url';
import { DEFAULT_NODE_RUNTIME } from './node-version.js';
import { addBlocksStackMetadata } from './stack-metadata.js';
import { finalizeConfigRegistry, registerConfig } from './config-registry.js';
Expand Down Expand Up @@ -230,8 +231,11 @@ export class BlocksBackend extends Construct {
static async create(scope: Construct, id: string, props: BlocksBackendProps) {
assertCdkConditionActive();
const backend = new BlocksBackend(scope, id, props);
// ESM caches modules by URL — append a unique query string so each stage re-executes the module body
const mod = await import(`${props.backendCDKPath}?stack=${id}`);
// file:// URL (not a raw path) so the cache-busting query works on Windows,
// where an absolute path like `D:\...` is rejected as URL scheme `d:`.
const backendUrl = pathToFileURL(props.backendCDKPath);
backendUrl.searchParams.set('stack', id);
const mod = await import(backendUrl.href);
if (typeof mod.default === 'function') {
try {
await mod.default(backend);
Expand Down
8 changes: 6 additions & 2 deletions packages/core/src/cdk/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import { pathToFileURL } from 'node:url';
import {
type BlocksStackProps,
type BlocksStack as BaseBlocksStack,
Expand Down Expand Up @@ -51,8 +52,11 @@ export class BlocksStack extends cdk.Stack implements BaseBlocksStack {
const actualScope = pipelineScope || scope;

const stack = new BlocksStack(actualScope, id, props);
// ESM caches modules by URL — append a unique query string so each stage re-executes the module body
const mod = await import(`${props.backendCDKPath}?stack=${id}`);
// file:// URL (not a raw path) so the cache-busting query works on Windows,
// where an absolute path like `D:\...` is rejected as URL scheme `d:`.
const backendUrl = pathToFileURL(props.backendCDKPath);
backendUrl.searchParams.set('stack', id);
const mod = await import(backendUrl.href);
if (typeof mod.default === 'function') {
try {
await mod.default(stack);
Expand Down
7 changes: 5 additions & 2 deletions packages/core/src/pipeline/pipeline-construct.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,9 @@
ShellStep,
} from 'aws-cdk-lib/pipelines';
import { Construct } from 'constructs';
import * as fs from 'fs';

Check notice on line 16 in packages/core/src/pipeline/pipeline-construct.ts

View workflow job for this annotation

GitHub Actions / Build, Unit Tests, E2E Local

lint/style/useNodejsImportProtocol

A Node.js builtin module should be imported with the node: protocol.
import * as path from 'path';

Check notice on line 17 in packages/core/src/pipeline/pipeline-construct.ts

View workflow job for this annotation

GitHub Actions / Build, Unit Tests, E2E Local

lint/style/useNodejsImportProtocol

A Node.js builtin module should be imported with the node: protocol.
import { pathToFileURL } from 'node:url';
import type {
BranchConfig,
PipelineProps,
Expand Down Expand Up @@ -501,7 +502,7 @@
const stage = new DeployStage(construct, `${branchId}-Stage-${stageConfig.name}`, {
stageConfig,
});
const result = props.stageFactory!(stage, stageConfig);

Check warning on line 505 in packages/core/src/pipeline/pipeline-construct.ts

View workflow job for this annotation

GitHub Actions / Build, Unit Tests, E2E Local

lint/style/noNonNullAssertion

Forbidden non-null assertion.
if (result && typeof (result as any).then === 'function') {
throw new Error(
'Pipeline: stageFactory returned a Promise. ' +
Expand Down Expand Up @@ -542,7 +543,7 @@
if (resolvedAppFile) {
await importAppFileForStage(stage, stageConfig, resolvedAppFile);
} else {
await props.stageFactory!(stage, stageConfig);

Check warning on line 546 in packages/core/src/pipeline/pipeline-construct.ts

View workflow job for this annotation

GitHub Actions / Build, Unit Tests, E2E Local

lint/style/noNonNullAssertion

Forbidden non-null assertion.
}

validateStageStacks(stage, stageConfig.name, resolvedAppFile ? 'appFile' : 'stageFactory');
Expand Down Expand Up @@ -579,8 +580,10 @@
const listenersBefore = process.listeners('beforeExit').slice();

try {
// ESM caches modules by URL — append a unique query string so each stage re-executes the module body
await import(`${appFile}?stage=${encodeURIComponent(stageConfig.name)}`);
// file:// URL (not a raw path) so the cache-busting query works on Windows.
const appUrl = pathToFileURL(appFile);
appUrl.searchParams.set('stage', stageConfig.name);
await import(appUrl.href);
} finally {
// Remove any beforeExit listeners added during import.
// The imported file's cdk.App() registers a synth() handler that would
Expand Down
3 changes: 2 additions & 1 deletion packages/core/src/scripts/deploy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { ensureSecrets, loadProductionEnv } from './ensure-secrets.js';
import { applyExternalMigrations } from './external-migrations-step.js';
import { trackCommand } from '../telemetry/trackCommand.js';
import { getCdkTelemetryEnv } from './cdk-telemetry-env.js';
import { runSync } from './run-command.js';

export interface DeployOptions {
cdkAppPath: string;
Expand Down Expand Up @@ -56,7 +57,7 @@ export async function deploy(options: DeployOptions) {
console.log(' - Frontend hosting (S3 + CloudFront)');

try {
execFileSync(
runSync(
"npx",
[
"cdk", "deploy",
Expand Down
4 changes: 2 additions & 2 deletions packages/core/src/scripts/destroy.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

import { execFileSync } from 'node:child_process';
import { trackCommand } from '../telemetry/trackCommand.js';
import { getCdkTelemetryEnv } from './cdk-telemetry-env.js';
import { runSync } from './run-command.js';

export interface DestroyOptions {
cdkAppPath: string;
Expand All @@ -15,7 +15,7 @@ export async function destroy(options: DestroyOptions) {
console.log('🗑️ Destroying production stack...');

try {
execFileSync(
runSync(
"npx",
[
"cdk", "destroy",
Expand Down
4 changes: 2 additions & 2 deletions packages/core/src/scripts/external-migrations-step.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,10 @@
* here.
*/

import { execFileSync } from 'node:child_process';
import { existsSync, readdirSync, readFileSync } from 'node:fs';
import { findConnectionString } from './ensure-secrets.js';
import { extractDbRef, dbConnectionParameterName } from '../db-naming.js';
import { runSync } from './run-command.js';

const DEFAULT_MIGRATIONS_DIR = './migrations';
/** Default output dir for db-pull generated files (database.types.ts / database.meta.ts). */
Expand Down Expand Up @@ -59,7 +59,7 @@
migrationsDir: string,
regenerateTypesDir?: string,
): void {
execFileSync('npx', buildMigrateArgs(stage, migrationsDir, regenerateTypesDir), {
runSync('npx', buildMigrateArgs(stage, migrationsDir, regenerateTypesDir), {
stdio: 'inherit',
env: { ...process.env, BLOCKS_MIGRATE_URL: connValue },
});
Expand Down Expand Up @@ -158,7 +158,7 @@
* - the production SSM parameter for this ref (best-effort; skipped silently when
* offline or unauthenticated).
*/
async function productionRefs(devRef: string): Promise<Set<string>> {

Check warning on line 161 in packages/core/src/scripts/external-migrations-step.ts

View workflow job for this annotation

GitHub Actions / Build, Unit Tests, E2E Local

lint/correctness/noUnusedFunctionParameters

This parameter devRef is unused.
const refs = new Set<string>();

if (existsSync('.env.production')) {
Expand Down
45 changes: 45 additions & 0 deletions packages/core/src/scripts/run-command.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

import type {
ChildProcess,
SpawnOptions,
SpawnSyncOptions,
} from 'node:child_process';
import spawn from 'cross-spawn';

// `npm`/`npx`/`cdk`/`tsx` are `.cmd` shims on Windows, which Node's
// execFileSync/spawn can't resolve (spawnSync ENOENT) and won't run without a
// shell. cross-spawn resolves the shim and quotes args safely (array form, no
// shell injection), so these wrappers work on Windows too.

/**
* Run a command to completion (stdio inherited) and throw on failure — a
* cross-platform drop-in for `execFileSync` where only success/failure matters.
*/
export function runSync(
command: string,
args: string[],
options: SpawnSyncOptions = {},
): void {
const result = spawn.sync(command, args, { stdio: 'inherit', ...options });

if (result.error) {
throw result.error;
}
if (result.signal) {
throw new Error(`${command} was terminated by signal ${result.signal}`);
}
if (typeof result.status === 'number' && result.status !== 0) {
throw new Error(`${command} ${args.join(' ')} exited with code ${result.status}`);
}
}

/** Spawn a long-running command and return the `ChildProcess` (e.g. `cdk watch`). */
export function spawnCommand(
command: string,
args: string[],
options: SpawnOptions,
): ChildProcess {
return spawn(command, args, options);
}
11 changes: 6 additions & 5 deletions packages/core/src/scripts/sandbox.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

import { execFileSync, spawn } from "node:child_process";
import { execFileSync } from "node:child_process";
import { writeFileSync, mkdirSync, readFileSync } from "node:fs";
import { join, resolve, dirname } from "node:path";
import { fileURLToPath, pathToFileURL } from "node:url";
Expand All @@ -10,6 +10,7 @@ import { applyExternalMigrations } from './external-migrations-step.js';
import { trackCommand } from '../telemetry/trackCommand.js';
import { buildAndSendEvent } from '../telemetry/client.js';
import { getCdkTelemetryEnv } from './cdk-telemetry-env.js';
import { runSync, spawnCommand } from './run-command.js';

/**
* Import the backend definition to populate the Scope BB registry.
Expand Down Expand Up @@ -69,7 +70,7 @@ export async function startSandbox(options: SandboxOptions) {
console.log(" (This may take a few minutes on first deploy)");

try {
execFileSync(
runSync(
"npm",
[
"exec", "cdk", "--", "deploy",
Expand Down Expand Up @@ -138,7 +139,7 @@ export async function startSandbox(options: SandboxOptions) {
console.log("🌐 Starting local dev server (proxying to AWS)...");
console.log(`\n Open http://localhost:${clientPort}\n`);

const cdkWatch = spawn("npx", [
const cdkWatch = spawnCommand("npx", [
"cdk", "watch", "--hotswap",
`--outputs-file`, `${outDir}/outputs.json`,
`--context`, `projectRoot=${process.cwd()}`,
Expand All @@ -162,7 +163,7 @@ export async function startSandbox(options: SandboxOptions) {
const devServerCmd = devCommand || `npx tsx watch aws-blocks/scripts/server.ts`;
const [cmd, ...args] = devServerCmd.split(' ');

const devServer = spawn(cmd, args, {
const devServer = spawnCommand(cmd, args, {
stdio: "inherit",
shell: true,
env: {
Expand Down Expand Up @@ -212,7 +213,7 @@ export async function destroySandbox(backendPath: string) {

for (let attempt = 0; ; attempt++) {
try {
execFileSync("npm", cdkArgs, { stdio: "inherit", env: cdkEnv });
runSync("npm", cdkArgs, { stdio: "inherit", env: cdkEnv });
console.log(attempt === 0 ? "\n✅ Sandbox destroyed!" : "\n✅ Sandbox destroyed on retry!");
return;
} catch (error) {
Expand Down
Loading
Loading