Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions src/deploy/adapters/awsSsm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,14 +75,18 @@ export function parameterNameFor(varName: string, opts: AwsSsmOptions): string {
// ── Helpers ────────────────────────────────────────────────────────────────

function which(bin: string): string | null {
const r = spawnSync('which', [bin], { encoding: 'utf-8' });
const r = spawnSync('which', [bin], { encoding: 'utf-8', env: process.env });
return r.status === 0 ? r.stdout.trim() || null : null;
}

/** Region from the ambient aws CLI config, for picker defaults. */
export function detectAwsRegion(): string | null {
// env is passed on purpose (here, in which() and in spawnAsync): Bun only
// looks up the current PATH when env is given, and the tests put a stub aws
// first on PATH. Node behaves the same either way.
const r = spawnSync('aws', ['configure', 'get', 'region'], {
encoding: 'utf-8',
env: process.env,
});
return r.status === 0 ? r.stdout.trim() || null : null;
}
Expand All @@ -93,7 +97,7 @@ function spawnAsync(
stdin?: string,
): Promise<{ stdout: string; stderr: string; code: number }> {
return new Promise((resolve) => {
const child = spawn(cmd, args, { stdio: ['pipe', 'pipe', 'pipe'] });
const child = spawn(cmd, args, { stdio: ['pipe', 'pipe', 'pipe'], env: process.env });
let stdout = '';
let stderr = '';
child.stdout.on('data', (d) => (stdout += d.toString()));
Expand Down
40 changes: 32 additions & 8 deletions tests/deploy/awsSsm.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import { describe, test, expect, beforeEach, afterEach } from 'bun:test';
import { describe, test, expect, beforeEach, afterEach, beforeAll, afterAll } from 'bun:test';
import { mkdirSync, writeFileSync, rmSync, existsSync } from 'fs';
import { join } from 'path';
import { tmpdir } from 'os';
import { spawnSync } from 'child_process';
import {
awsSsmAdapter,
leafFor,
Expand All @@ -13,13 +12,29 @@ import {
} from '../../src/deploy/adapters/awsSsm';
import { TargetConfig } from '../../src/deploy/adapter';

// "Happy path" preflight needs the aws CLI on PATH (the binary check is the
// last step before ok:true). CI doesn't ship it, so gate that test.
// Config-error tests short-circuit before the binary check.
const HAS_AWS = spawnSync('which', ['aws']).status === 0;

const ROOT = join(tmpdir(), `capy-aws-ssm-adapter-${process.pid}`);

// No test here may start the real aws CLI: on CI runners it can take more
// than 5 s to start, and a deploy test would reach out to AWS. A stub aws goes
// first on PATH for the whole file. It answers `configure get region` with a
// fixed region and fails every other command at once.
const STUB_BIN = join(tmpdir(), `capy-aws-stub-${process.pid}`);
const STUB_REGION = 'stub-region-1';
const realPath = process.env.PATH;
beforeAll(() => {
mkdirSync(STUB_BIN, { recursive: true });
writeFileSync(
join(STUB_BIN, 'aws'),
`#!/bin/sh\nif [ "$1" = configure ]; then echo ${STUB_REGION}; exit 0; fi\necho "stub aws: no AWS calls in tests" >&2\nexit 1\n`,
{ mode: 0o755 },
);
process.env.PATH = `${STUB_BIN}:${realPath}`;
});
afterAll(() => {
process.env.PATH = realPath;
rmSync(STUB_BIN, { recursive: true, force: true });
});

beforeEach(() => {
if (existsSync(ROOT)) rmSync(ROOT, { recursive: true, force: true });
mkdirSync(ROOT, { recursive: true });
Expand Down Expand Up @@ -86,6 +101,15 @@ describe('awsSsm — name mapping', () => {
});

describe('awsSsm — detect', () => {
test('uses the region from the aws CLI (the stub, never the real aws)', async () => {
writeFileSync(
join(ROOT, 'main.tf'),
'data "aws_ssm_parameter" "db" {\n name = "/capy/prod/database-url"\n}\n',
);
const r = await awsSsmAdapter.detect(ROOT);
expect(r.options?.region).toBe(STUB_REGION);
});

test('infers prefix + kebab naming from literal tf parameter names', async () => {
mkdirSync(join(ROOT, 'infra'), { recursive: true });
writeFileSync(
Expand Down Expand Up @@ -193,7 +217,7 @@ describe('awsSsm — preflight (config errors, no aws CLI needed)', () => {
expect(r.reason).toContain('no vars');
});

test.if(HAS_AWS)('valid config passes with aws on PATH', async () => {
test('valid config passes with aws on PATH', async () => {
const r = await awsSsmAdapter.preflight(baseTarget(), { cwd: ROOT });
expect(r.ok).toBe(true);
});
Expand Down
Loading