@macedon-technologies/batman 1.32.1 → 1.33.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. package/.jsii +2 -2
  2. package/lib/BatmanProject.js +1 -1
  3. package/lib/BootcampProject.js +1 -1
  4. package/lib/CodeOwnersFile.js +1 -1
  5. package/lib/PrAutoMergeWorkflow.js +1 -1
  6. package/lib/ProductionDeployWorkflow.js +1 -1
  7. package/lib/SoftwareFactoryFiles.js +1 -1
  8. package/lib/StagingDeployWorkflow.js +1 -1
  9. package/package.json +2 -1
  10. package/templates/app/architecture-rules/README.md +97 -0
  11. package/templates/app/architecture-rules/check.ts +218 -0
  12. package/templates/app/architecture-rules/rules.json +3 -0
  13. package/templates/app/architecture-rules/tsconfig.json +14 -0
  14. package/templates/app/openspec-gates/check-tasks-complete.ts +44 -0
  15. package/templates/app/openspec-gates/tsconfig.json +13 -0
  16. package/templates/app/package.json +74 -0
  17. package/templates/app/pnpm-lock.yaml +3888 -0
  18. package/templates/app/pnpm-workspace.yaml +7 -0
  19. package/templates/app/src/aspects/DataDogAspect.ts +14 -0
  20. package/templates/app/src/constructs/Secret.ts +50 -0
  21. package/templates/app/src/constructs/handler/Secret.Function.ts +38 -0
  22. package/templates/app/src/constructs/handler/secret/client.ts +5 -0
  23. package/templates/app/src/constructs/handler/secret/createSecretValue.ts +30 -0
  24. package/templates/app/src/constructs/handler/secret/didRandomPropertiesChange.ts +13 -0
  25. package/templates/app/src/constructs/handler/secret/updateSecretValue.ts +71 -0
  26. package/templates/app/swamp-bootstrap/pull-extensions.ts +68 -0
  27. package/templates/app/swamp-bootstrap/tsconfig.json +13 -0
  28. package/templates/app/test/DataDogAspect.test.ts +31 -0
  29. package/templates/app/test/Secret.test.ts +40 -0
  30. package/templates/app/test/didRandomPropertiesChange.test.ts +19 -0
  31. package/templates/app/test/tsconfig.json +13 -0
  32. package/templates/app/tsconfig.json +50 -0
@@ -0,0 +1,7 @@
1
+ onlyBuiltDependencies:
2
+ - esbuild
3
+ ignoredBuiltDependencies:
4
+ - '@parcel/watcher'
5
+ allowBuilds:
6
+ esbuild: true
7
+ '@parcel/watcher': false
@@ -0,0 +1,14 @@
1
+ import { IAspect } from 'aws-cdk-lib';
2
+ import { Function } from 'aws-cdk-lib/aws-lambda';
3
+ import { IConstruct } from 'constructs';
4
+ import { DatadogLambda } from 'datadog-cdk-constructs-v2'; // or v1
5
+
6
+ export class DataDogAspect implements IAspect {
7
+ constructor(private props: { datadog: DatadogLambda }) {}
8
+
9
+ visit(node: IConstruct): void {
10
+ if (node instanceof Function) {
11
+ this.props.datadog.addLambdaFunctions([node]);
12
+ }
13
+ }
14
+ }
@@ -0,0 +1,50 @@
1
+ // Copied from @matthewbonig/secret (MIT, https://github.com/mbonig/secret).
2
+ // This file is generated by Batman and is yours to modify.
3
+ import { join } from 'path';
4
+ import { CustomResource } from 'aws-cdk-lib';
5
+ import { Effect, PolicyStatement } from 'aws-cdk-lib/aws-iam';
6
+ import { Runtime } from 'aws-cdk-lib/aws-lambda';
7
+ import { NodejsFunction } from 'aws-cdk-lib/aws-lambda-nodejs';
8
+ import { Secret as AWSSecret, SecretProps, SecretStringGenerator } from 'aws-cdk-lib/aws-secretsmanager';
9
+ import { Provider } from 'aws-cdk-lib/custom-resources';
10
+ import { Construct } from 'constructs';
11
+
12
+ export class Secret extends AWSSecret {
13
+ constructor(scope: Construct, id: string, props: SecretProps) {
14
+ super(scope, id, {
15
+ ...props,
16
+ generateSecretString: undefined, // explicitly set this to undefined so that we never use the built-in functionality
17
+ });
18
+
19
+ if (props.generateSecretString?.secretStringTemplate) {
20
+ // now we're going to use a custom resource for this instead
21
+ this.createCustomResource(props.generateSecretString);
22
+ }
23
+ }
24
+
25
+ private createCustomResource(generateSecretString: SecretStringGenerator) {
26
+ const eventHandler = new NodejsFunction(this, 'EventHandlerFunction', {
27
+ runtime: Runtime.NODEJS_24_X,
28
+ entry: join(__dirname, 'handler/Secret.Function.ts'),
29
+ });
30
+ this.grantWrite(eventHandler);
31
+ this.grantRead(eventHandler);
32
+ eventHandler.addToRolePolicy(new PolicyStatement({
33
+ effect: Effect.ALLOW,
34
+ resources: ['*'],
35
+ actions: ['secretsmanager:GetRandomPassword'],
36
+ }));
37
+ const provider = new Provider(this, 'CustomResourceProvider', {
38
+ onEventHandler: eventHandler,
39
+ });
40
+ const customResource = new CustomResource(this, 'GenerateSecretStringCR', {
41
+ resourceType: 'Custom::GenerateSecretString',
42
+ serviceToken: provider.serviceToken,
43
+ properties: {
44
+ Secret: this.secretArn,
45
+ GenerateSecretString: generateSecretString,
46
+ },
47
+ });
48
+ customResource.node.addDependency(this);
49
+ }
50
+ }
@@ -0,0 +1,38 @@
1
+ // Copied from @matthewbonig/secret (MIT, https://github.com/mbonig/secret).
2
+ // This file is generated by Batman and is yours to modify.
3
+ import { createSecretValue } from './secret/createSecretValue';
4
+ import { updateSecretValue } from './secret/updateSecretValue';
5
+
6
+ interface CustomResource {
7
+ RequestType: 'Create' | 'Update' | 'Delete';
8
+ LogicalResourceId: string;
9
+ ResourceProperties: any;
10
+ OldResourceProperties?: any;
11
+ PhysicalResourceId?: string;
12
+ }
13
+
14
+ export const handler = async (event: CustomResource) => {
15
+ console.log('Received event:', JSON.stringify(event, null, 2));
16
+
17
+ switch (event.RequestType) {
18
+ case 'Create':
19
+ console.log(`Create secret value for secret ${event.LogicalResourceId};`);
20
+ await createSecretValue({
21
+ secretArn: event.ResourceProperties.Secret,
22
+ generateSecretString: event.ResourceProperties.GenerateSecretString,
23
+ });
24
+ break;
25
+ case 'Update':
26
+ console.log('Updating secret with physical id:', event.PhysicalResourceId);
27
+ await updateSecretValue({
28
+ secretArn: event.ResourceProperties.Secret,
29
+ generateSecretString: event.ResourceProperties.GenerateSecretString,
30
+ oldGenerateSecretString: event.OldResourceProperties.GenerateSecretString,
31
+ });
32
+ break;
33
+ case 'Delete':
34
+ console.log('Deleting secret with physical id:', event.PhysicalResourceId);
35
+ console.log('Doing nothing. deleting secrets requires no work at all.');
36
+ break;
37
+ }
38
+ };
@@ -0,0 +1,5 @@
1
+ // Copied from @matthewbonig/secret (MIT, https://github.com/mbonig/secret).
2
+ // This file is generated by Batman and is yours to modify.
3
+ import { SecretsManager } from '@aws-sdk/client-secrets-manager';
4
+
5
+ export const client = new SecretsManager({});
@@ -0,0 +1,30 @@
1
+ // Copied from @matthewbonig/secret (MIT, https://github.com/mbonig/secret).
2
+ // This file is generated by Batman and is yours to modify.
3
+ import { GetRandomPasswordCommand, PutSecretValueCommand } from '@aws-sdk/client-secrets-manager';
4
+ import { client } from './client';
5
+
6
+ export async function createSecretValue({ generateSecretString, secretArn }: {
7
+ secretArn: any;
8
+ generateSecretString: any;
9
+ }) {
10
+ // since this is a create, we can just write the value as-is...
11
+ const secretValue = JSON.parse(generateSecretString.secretStringTemplate);
12
+ if (generateSecretString.generateStringKey) {
13
+ const randomValue = await client.send(new GetRandomPasswordCommand({
14
+ ExcludeCharacters: generateSecretString.excludeCharacters,
15
+ ExcludeLowercase: generateSecretString.excludeLowercase,
16
+ ExcludeNumbers: generateSecretString.excludeNumbers,
17
+ ExcludePunctuation: generateSecretString.excludePunctuation,
18
+ ExcludeUppercase: generateSecretString.excludeUppercase,
19
+ IncludeSpace: generateSecretString.includeSpace,
20
+ PasswordLength: generateSecretString.passwordLength,
21
+ RequireEachIncludedType: generateSecretString.requireEachIncludedType,
22
+ }));
23
+ secretValue[generateSecretString.generateStringKey] = randomValue.RandomPassword;
24
+ }
25
+
26
+ await client.send(new PutSecretValueCommand({
27
+ SecretId: secretArn,
28
+ SecretString: JSON.stringify(secretValue),
29
+ }));
30
+ }
@@ -0,0 +1,13 @@
1
+ // Copied from @matthewbonig/secret (MIT, https://github.com/mbonig/secret).
2
+ // This file is generated by Batman and is yours to modify.
3
+ export function didRandomPropertiesChange(oldGenerateSecretString: any, generateSecretString: any) {
4
+ return oldGenerateSecretString.excludeCharacters !== generateSecretString.excludeCharacters ||
5
+ oldGenerateSecretString.excludeLowercase !== generateSecretString.excludeLowercase ||
6
+ oldGenerateSecretString.excludeNumbers !== generateSecretString.excludeNumbers ||
7
+ oldGenerateSecretString.excludePunctuation !== generateSecretString.excludePunctuation ||
8
+ oldGenerateSecretString.excludeUppercase !== generateSecretString.excludeUppercase ||
9
+ oldGenerateSecretString.includeSpace !== generateSecretString.includeSpace ||
10
+ oldGenerateSecretString.passwordLength !== generateSecretString.passwordLength ||
11
+ oldGenerateSecretString.requireEachIncludedType !== generateSecretString.requireEachIncludedType;
12
+
13
+ }
@@ -0,0 +1,71 @@
1
+ // Copied from @matthewbonig/secret (MIT, https://github.com/mbonig/secret).
2
+ // This file is generated by Batman and is yours to modify.
3
+ import {
4
+ GetRandomPasswordCommand,
5
+ GetSecretValueCommand,
6
+ PutSecretValueCommand,
7
+ } from '@aws-sdk/client-secrets-manager';
8
+ import { client } from './client';
9
+ import { didRandomPropertiesChange } from './didRandomPropertiesChange';
10
+
11
+ export async function updateSecretValue({ oldGenerateSecretString, generateSecretString, secretArn }: {
12
+ secretArn: any;
13
+ generateSecretString: any;
14
+ oldGenerateSecretString: any;
15
+ }) {
16
+ // let's get the existing value
17
+ const existingSecretValue = await client.send(new GetSecretValueCommand({
18
+ SecretId: secretArn,
19
+ }));
20
+ // and parse the existing value
21
+ let parsedValue: any = {};
22
+
23
+ try {
24
+ parsedValue = JSON.parse(existingSecretValue.SecretString!);
25
+ } catch (err) {
26
+ // the existing value couldn't be parsed, it was probably a random string.
27
+ // let's go ahead and just start fresh then,
28
+ }
29
+
30
+ // now let's check what properties have changed
31
+ const newSecretStringTemplate = JSON.parse(generateSecretString.secretStringTemplate);
32
+ const oldSecretStringTemplate = JSON.parse(oldGenerateSecretString.secretStringTemplate);
33
+ for (const key in newSecretStringTemplate) {
34
+ if (oldSecretStringTemplate[key] === undefined) {
35
+ // ok, so here's something to add...
36
+ parsedValue[key] = newSecretStringTemplate[key];
37
+ }
38
+ if (oldSecretStringTemplate[key] !== newSecretStringTemplate[key]) {
39
+ // ok, so here's something to update...
40
+ parsedValue[key] = newSecretStringTemplate[key];
41
+ }
42
+ }
43
+ for (const key in oldSecretStringTemplate) {
44
+ if (newSecretStringTemplate[key] === undefined) {
45
+ // ok, so here's something to remove...
46
+ delete parsedValue[key];
47
+ }
48
+ }
49
+
50
+ // did anything around the GenerateStringKey change?
51
+ if (didRandomPropertiesChange(oldGenerateSecretString, generateSecretString)
52
+ || generateSecretString.generateStringKey !== oldGenerateSecretString.generateStringKey) {
53
+ const { RandomPassword: newRandomValue } = await client.send(new GetRandomPasswordCommand({
54
+ ExcludeLowercase: generateSecretString.excludeLowercase === 'true',
55
+ ExcludeNumbers: generateSecretString.excludeNumbers === 'true',
56
+ ExcludePunctuation: generateSecretString.excludePunctuation === 'true',
57
+ ExcludeUppercase: generateSecretString.excludeUppercase === 'true',
58
+ IncludeSpace: generateSecretString.includeSpace === 'true',
59
+ PasswordLength: +generateSecretString.passwordLength,
60
+ RequireEachIncludedType: generateSecretString.requireEachIncludedType === 'true',
61
+ }));
62
+ parsedValue[generateSecretString.generateStringKey] = newRandomValue;
63
+ }
64
+
65
+ // update the secret in AWS
66
+ await client.send(new PutSecretValueCommand({
67
+ SecretId: secretArn,
68
+ SecretString: JSON.stringify(parsedValue),
69
+ }));
70
+
71
+ }
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Pulls each swamp extension at the version pinned in
3
+ * extensions/models/upstream_extensions.json, so a fresh clone runs the
4
+ * same extensions as the rest of the team, including any the repo added
5
+ * itself. A repo with no lockfile yet pulls DEFAULT_EXTENSIONS at their
6
+ * latest stable versions, which creates the lockfile.
7
+ *
8
+ * A pull rewrites the lockfile even at an identical version, and on Windows
9
+ * it writes backslash paths and deletes the committed files it tracks (e.g.
10
+ * .claude/skills/software-factory/). So afterwards the lockfile and every
11
+ * git-tracked file it lists are restored from git.
12
+ */
13
+ import { execFileSync } from 'node:child_process';
14
+ import { existsSync, readFileSync } from 'node:fs';
15
+
16
+ const LOCKFILE = 'extensions/models/upstream_extensions.json';
17
+ const DEFAULT_EXTENSIONS = ['@swamp/software-factory', '@webframp/github', '@goodcraft/github'];
18
+
19
+ interface LockfileEntry {
20
+ version: string;
21
+ files?: string[];
22
+ }
23
+
24
+ function readLockfile(): Record<string, LockfileEntry> | undefined {
25
+ if (!existsSync(LOCKFILE)) {
26
+ return undefined;
27
+ }
28
+ return JSON.parse(readFileSync(LOCKFILE, 'utf8')) as Record<string, LockfileEntry>;
29
+ }
30
+
31
+ function run(command: string, args: string[]): string {
32
+ return execFileSync(command, args, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'inherit'] });
33
+ }
34
+
35
+ function pull(extension: string): void {
36
+ console.log(`swamp extension pull ${extension} --force`);
37
+ execFileSync('swamp', ['extension', 'pull', extension, '--force'], { stdio: 'inherit' });
38
+ }
39
+
40
+ function restoreTrackedFiles(lockfile: Record<string, LockfileEntry>): void {
41
+ const listed = Object.values(lockfile)
42
+ .flatMap((entry) => entry.files ?? [])
43
+ .map((file) => file.replace(/\\/g, '/'))
44
+ .filter((file) => !file.startsWith('.swamp/'));
45
+ const tracked = run('git', ['ls-files', '-z', '--', LOCKFILE, ...listed])
46
+ .split('\0')
47
+ .filter(Boolean);
48
+ if (tracked.length > 0) {
49
+ run('git', ['checkout', '--', ...tracked]);
50
+ console.log(`Restored ${tracked.length} tracked file(s) from git`);
51
+ }
52
+ }
53
+
54
+ function main(): void {
55
+ const lockfile = readLockfile();
56
+ if (!lockfile) {
57
+ console.log(`No ${LOCKFILE} yet: pulling the latest stable software-factory extensions`);
58
+ DEFAULT_EXTENSIONS.forEach(pull);
59
+ return;
60
+ }
61
+
62
+ for (const [name, entry] of Object.entries(lockfile)) {
63
+ pull(`${name}@${entry.version}`);
64
+ }
65
+ restoreTrackedFiles(lockfile);
66
+ }
67
+
68
+ main();
@@ -0,0 +1,13 @@
1
+ {
2
+ "extends": "../tsconfig.json",
3
+ "compilerOptions": {
4
+ "noEmit": true,
5
+ "rootDir": "."
6
+ },
7
+ "include": [
8
+ "*.ts"
9
+ ],
10
+ "exclude": [
11
+ "node_modules"
12
+ ]
13
+ }
@@ -0,0 +1,31 @@
1
+ import { App, Aspects, Stack } from 'aws-cdk-lib';
2
+ import { Match, Template } from 'aws-cdk-lib/assertions';
3
+ import { Code, Function, Runtime } from 'aws-cdk-lib/aws-lambda';
4
+ import { DatadogLambda } from 'datadog-cdk-constructs-v2';
5
+ import { DataDogAspect } from '../src/aspects/DataDogAspect';
6
+
7
+ describe('DataDogAspect', () => {
8
+ test('adds every Lambda function in the stack to Datadog', () => {
9
+ const app = new App();
10
+ const stack = new Stack(app, 'TestStack', { env: { account: '111111111111', region: 'us-east-1' } });
11
+ const datadog = new DatadogLambda(stack, 'Datadog', {
12
+ nodeLayerVersion: 120,
13
+ extensionLayerVersion: 80,
14
+ site: 'datadoghq.com',
15
+ apiKeySecretArn: 'arn:aws:secretsmanager:us-east-1:111111111111:secret:datadog',
16
+ });
17
+ new Function(stack, 'Handler', {
18
+ runtime: Runtime.NODEJS_22_X,
19
+ handler: 'index.handler',
20
+ code: Code.fromInline('exports.handler = async () => {};'),
21
+ });
22
+
23
+ Aspects.of(stack).add(new DataDogAspect({ datadog }));
24
+ const template = Template.fromStack(stack);
25
+
26
+ template.hasResourceProperties('AWS::Lambda::Function', {
27
+ Environment: { Variables: Match.objectLike({ DD_LAMBDA_HANDLER: 'index.handler' }) },
28
+ Layers: Match.arrayWith([Match.stringLikeRegexp('Datadog-Extension')]),
29
+ });
30
+ });
31
+ });
@@ -0,0 +1,40 @@
1
+ import { App, Stack } from 'aws-cdk-lib';
2
+ import { Match, Template } from 'aws-cdk-lib/assertions';
3
+ import { Secret } from '../src/constructs/Secret';
4
+
5
+ function synth(addSecret: (stack: Stack) => void): Template {
6
+ const app = new App({ context: { 'aws:cdk:bundling-stacks': [] } });
7
+ const stack = new Stack(app, 'TestStack');
8
+ addSecret(stack);
9
+ return Template.fromStack(stack);
10
+ }
11
+
12
+ describe('Secret', () => {
13
+ test('generates a templated secret value through a custom resource', () => {
14
+ const template = synth((stack) => new Secret(stack, 'Secret', {
15
+ generateSecretString: { secretStringTemplate: '{"username":"app"}', generateStringKey: 'password' },
16
+ }));
17
+
18
+ template.resourceCountIs('Custom::GenerateSecretString', 1);
19
+ template.hasResourceProperties('AWS::SecretsManager::Secret', {
20
+ GenerateSecretString: Match.objectEquals({}),
21
+ });
22
+ template.hasResourceProperties('AWS::Lambda::Function', { Runtime: 'nodejs24.x' });
23
+ template.hasResourceProperties('AWS::IAM::Policy', {
24
+ PolicyDocument: {
25
+ Statement: Match.arrayWith([
26
+ Match.objectLike({ Action: 'secretsmanager:GetRandomPassword', Effect: 'Allow', Resource: '*' }),
27
+ ]),
28
+ },
29
+ });
30
+ });
31
+
32
+ test('creates no custom resource without a secret string template', () => {
33
+ const template = synth((stack) => new Secret(stack, 'Secret', {
34
+ generateSecretString: { passwordLength: 32 },
35
+ }));
36
+
37
+ template.resourceCountIs('Custom::GenerateSecretString', 0);
38
+ template.resourceCountIs('AWS::Lambda::Function', 0);
39
+ });
40
+ });
@@ -0,0 +1,19 @@
1
+ import { didRandomPropertiesChange } from '../src/constructs/handler/secret/didRandomPropertiesChange';
2
+
3
+ describe('didRandomPropertiesChange', () => {
4
+ const base = { passwordLength: 32, excludePunctuation: true, secretStringTemplate: '{}', generateStringKey: 'password' };
5
+
6
+ test('is false when only non-random properties change', () => {
7
+ expect(didRandomPropertiesChange(base, { ...base, secretStringTemplate: '{"a":1}', generateStringKey: 'pw' })).toBe(false);
8
+ });
9
+
10
+ test.each([
11
+ ['passwordLength', 64],
12
+ ['excludePunctuation', false],
13
+ ['excludeCharacters', '"@/'],
14
+ ['includeSpace', true],
15
+ ['requireEachIncludedType', true],
16
+ ])('is true when %s changes', (key, value) => {
17
+ expect(didRandomPropertiesChange(base, { ...base, [key]: value })).toBe(true);
18
+ });
19
+ });
@@ -0,0 +1,13 @@
1
+ {
2
+ "extends": "../tsconfig.json",
3
+ "compilerOptions": {
4
+ "noEmit": true,
5
+ "rootDir": ".."
6
+ },
7
+ "include": [
8
+ "**/*.ts"
9
+ ],
10
+ "exclude": [
11
+ "node_modules"
12
+ ]
13
+ }
@@ -0,0 +1,50 @@
1
+ {
2
+ "compilerOptions": {
3
+ "rootDir": "src",
4
+ "outDir": "lib",
5
+ "alwaysStrict": true,
6
+ "declaration": true,
7
+ "esModuleInterop": true,
8
+ "experimentalDecorators": true,
9
+ "inlineSourceMap": true,
10
+ "inlineSources": true,
11
+ "lib": [
12
+ "es2022"
13
+ ],
14
+ "module": "NodeNext",
15
+ "noEmitOnError": false,
16
+ "noFallthroughCasesInSwitch": false,
17
+ "noImplicitAny": true,
18
+ "noImplicitReturns": true,
19
+ "noImplicitThis": true,
20
+ "noUnusedLocals": false,
21
+ "noUnusedParameters": false,
22
+ "resolveJsonModule": true,
23
+ "strict": true,
24
+ "strictNullChecks": true,
25
+ "strictPropertyInitialization": false,
26
+ "stripInternal": true,
27
+ "target": "ES2022",
28
+ "types": [
29
+ "jest",
30
+ "jsonwebtoken",
31
+ "node",
32
+ "aws-lambda",
33
+ "express",
34
+ "pg"
35
+ ],
36
+ "moduleResolution": "nodenext",
37
+ "typeRoots": [
38
+ "./node_modules/@types"
39
+ ],
40
+ "isolatedModules": true,
41
+ "allowImportingTsExtensions": true
42
+ },
43
+ "include": [
44
+ "src/**/*.ts"
45
+ ],
46
+ "exclude": [
47
+ "node_modules",
48
+ "cdk.out"
49
+ ]
50
+ }