@fjall/components-infrastructure 3.2.1 → 3.3.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.
package/dist/lib/app.d.ts CHANGED
@@ -16,7 +16,7 @@ import { type RemovalPolicyString } from "./resources/aws/messaging/utils.js";
16
16
  import { type ServiceRegistrationProps } from "./resources/aws/networking/serviceDiscovery.js";
17
17
  import { type IPrivateDnsNamespace, type IService } from "aws-cdk-lib/aws-servicediscovery";
18
18
  import { type AnyCompute } from "./patterns/aws/compute.js";
19
- import { Buildkite, type BuildkitePropsInput } from "./patterns/aws/buildkite.js";
19
+ import { type Buildkite, type BuildkiteFactoryFn, type BuildkitePropsInput } from "./patterns/aws/buildkite.js";
20
20
  import { DevSubstrate, type IDevSubstrateProps } from "./patterns/aws/devSubstrate.js";
21
21
  import { type Storage, type StorageFactoryFn } from "./patterns/aws/storage.js";
22
22
  import { type AnyPattern, type PatternFactoryFn } from "./patterns/aws/pattern.js";
@@ -288,19 +288,19 @@ export declare class App extends CdkApp {
288
288
  * same registry posture — Fjall-managed platform infrastructure,
289
289
  * deliberately NOT on the customer create/scaffold surface (design § D3).
290
290
  *
291
- * Cost-allocation environment resolves from `getConfig()` (design § D10) so
292
- * the fleet's environment tag matches the deploying config rather than the
293
- * construct's `management` fallback; an explicit
294
- * `props.costAllocationEnvironment` still wins.
295
- *
296
- * Alarm actions default to the account's `SharedAlarmTopicArn` export
297
- * (design § D14 alerting ships WITH the fleet): every fjall-governed
298
- * account's Account/Platform stack produces that export, so alarms page
299
- * out of the box. An explicit `props.alarmSnsTopicArn` still wins.
300
- * `applicationId` defaults to the app name so the alarm webhook can map
301
- * pages to the application; an explicit prop still wins.
291
+ * App-level defaults (cost-allocation environment from `getConfig()`,
292
+ * alarm actions to the account's `SharedAlarmTopicArn` export,
293
+ * `applicationId` to the app name — designs § D10/§ D14) are resolved by
294
+ * `resolveBuildkiteConstructProps`; explicit props always win.
295
+ *
296
+ * The props overload pins construct id `"Buildkite"` (byte-identical
297
+ * logical IDs for pre-factory consumers); the factory overload accepts
298
+ * `BuildkiteFactory.build(id, props)` the shape the codemod add path
299
+ * emits. Both overloads resolve placement here, so the deploy-core
300
+ * fixed-stack contract cannot be bypassed from an emitted statement.
302
301
  */
303
302
  addBuildkite(props: BuildkitePropsInput): Buildkite;
303
+ addBuildkite(fn: BuildkiteFactoryFn): Buildkite;
304
304
  /**
305
305
  * Add a database resource to the default database stack using the factory pattern.
306
306
  * Returns the appropriate database type based on the factory used.
package/dist/lib/app.js CHANGED
@@ -9,14 +9,12 @@ import { NetworkFactory } from "./patterns/aws/network.js";
9
9
  import { MessagingFactory } from "./patterns/aws/messaging.js";
10
10
  import { Schedule } from "./resources/aws/messaging/schedule.js";
11
11
  import { ServiceDiscoveryNamespace } from "./resources/aws/networking/serviceDiscovery.js";
12
- import { Buildkite } from "./patterns/aws/buildkite.js";
12
+ import { BuildkiteFactory } from "./patterns/aws/buildkite.js";
13
13
  import { DevSubstrate } from "./patterns/aws/devSubstrate.js";
14
14
  import { StandardTagsAspect } from "./utils/standardTagsAspect.js";
15
15
  import { BACKUP_TIER_TAG_KEY, BACKUP_TIER_TAG_MAP } from "./utils/backupTierMapping.js";
16
16
  import { randomBytes } from "crypto";
17
17
  import { getConfig } from "./utils/getConfig.js";
18
- import { UNKNOWN_ENVIRONMENT } from "./utils/env.js";
19
- import { SHARED_ALARM_TOPIC_EXPORT_NAME } from "./config/aws/alarmTopic.js";
20
18
  import AuditRoleFactory from "./resources/aws/audit/auditRole.js";
21
19
  import { FJALL_AUDIT_CONFIG } from "./config/audit.js";
22
20
  import { FjallLogger } from "./utils/validationLogger.js";
@@ -483,46 +481,12 @@ export class App extends CdkApp {
483
481
  computeStack.addConstruct(substrate);
484
482
  return substrate;
485
483
  }
486
- /**
487
- * Instantiate the self-hosted Buildkite agent fleet on the default compute
488
- * stack (design `aiDocs/designs/2026-07-18-buildkite-selfhosted-agents.md`).
489
- *
490
- * Same placement contract as `addDevSubstrate` above: deploy-core selects
491
- * stacks by the fixed six-category enum, so scoping under
492
- * `${stackPrefix}Compute` is what makes the fleet actually deploy. Also the
493
- * same registry posture — Fjall-managed platform infrastructure,
494
- * deliberately NOT on the customer create/scaffold surface (design § D3).
495
- *
496
- * Cost-allocation environment resolves from `getConfig()` (design § D10) so
497
- * the fleet's environment tag matches the deploying config rather than the
498
- * construct's `management` fallback; an explicit
499
- * `props.costAllocationEnvironment` still wins.
500
- *
501
- * Alarm actions default to the account's `SharedAlarmTopicArn` export
502
- * (design § D14 — alerting ships WITH the fleet): every fjall-governed
503
- * account's Account/Platform stack produces that export, so alarms page
504
- * out of the box. An explicit `props.alarmSnsTopicArn` still wins.
505
- * `applicationId` defaults to the app name so the alarm webhook can map
506
- * pages to the application; an explicit prop still wins.
507
- */
508
- addBuildkite(props) {
484
+ addBuildkite(input) {
485
+ const fn = typeof input === "function"
486
+ ? input
487
+ : BuildkiteFactory.build("Buildkite", input);
509
488
  const computeStack = this.getDefaultComputeStack();
510
- const configEnvironment = getConfig().environment;
511
- const constructProps = { ...props, vpc: this.getVpc() };
512
- // Post-spread injection so a caller-materialised `costAllocationEnvironment:
513
- // undefined` counts as absent rather than clobbering the resolved value.
514
- if (constructProps.costAllocationEnvironment === undefined &&
515
- configEnvironment !== "" &&
516
- configEnvironment !== UNKNOWN_ENVIRONMENT) {
517
- constructProps.costAllocationEnvironment = configEnvironment;
518
- }
519
- if (constructProps.alarmSnsTopicArn === undefined) {
520
- constructProps.alarmSnsTopicArn = `import:${SHARED_ALARM_TOPIC_EXPORT_NAME}`;
521
- }
522
- if (constructProps.applicationId === undefined) {
523
- constructProps.applicationId = this.getName();
524
- }
525
- const fleet = new Buildkite(computeStack.getStack(), "Buildkite", constructProps);
489
+ const fleet = fn(this, computeStack.getStack());
526
490
  computeStack.addConstruct(fleet);
527
491
  return fleet;
528
492
  }
@@ -307,7 +307,14 @@ function buildDevIsolationGuardrails() {
307
307
  Sid: "DenyCrossAccountAssumeRole",
308
308
  Effect: "Deny",
309
309
  Action: ["sts:AssumeRole", "sts:AssumeRoot"],
310
- Resource: "*",
310
+ // NotResource carves delegation roles (the DelegationRole construct's
311
+ // fixed `<label>DelegateHostedZoneRole` naming contract) out of the
312
+ // deny — a delegated child domain deployed into a dev account must
313
+ // assume the parent zone's delegation role cross-account to write its
314
+ // NS. Those roles are NS-write-scoped by construction
315
+ // (delegationRole.ts). AssumeRoot stays denied everywhere: root-user
316
+ // ARNs never match the role pattern.
317
+ NotResource: "arn:aws:iam::*:role/*DelegateHostedZoneRole",
311
318
  Condition: {
312
319
  StringNotEquals: {
313
320
  // IAM policy variable, substituted per-request by AWS — the
@@ -1,4 +1,5 @@
1
1
  import { CfnOutput, Duration, RemovalPolicy, Stack, Token } from "aws-cdk-lib";
2
+ import { Monitoring } from "aws-cdk-lib/aws-autoscaling";
2
3
  import { InstanceArchitecture, InstanceType, MachineImage, SubnetType } from "aws-cdk-lib/aws-ec2";
3
4
  import { PolicyStatement } from "aws-cdk-lib/aws-iam";
4
5
  import { Source } from "aws-cdk-lib/aws-s3-deployment";
@@ -106,6 +107,12 @@ export class Buildkite extends Construct {
106
107
  userData,
107
108
  role: agentRole,
108
109
  ssmSessionPermissions: false,
110
+ // Upstream parity: EnableDetailedMonitoring defaults "false" in the
111
+ // pinned v6.68.1 template, and the wrapper's absent-default is DETAILED
112
+ // — omitting this silently deviated. The fleet's alarms and scaler run
113
+ // on Buildkite queue metrics, not EC2 metrics, so 1-minute granularity
114
+ // buys nothing here.
115
+ instanceMonitoring: Monitoring.BASIC,
109
116
  blockDevices: [
110
117
  {
111
118
  deviceName: "/dev/xvda",
@@ -0,0 +1,27 @@
1
+ import { type Construct } from "constructs";
2
+ import type App from "../../../app.js";
3
+ import { Buildkite, type BuildkiteConstructProps } from "./buildkite.js";
4
+ import type { BuildkitePropsInput } from "./schema.js";
5
+ /**
6
+ * Thunk returned by `BuildkiteFactory.build` — materialised only by
7
+ * `App.addBuildkite`, which owns stack placement (the fleet has exactly one
8
+ * legal home, `${stackPrefix}Compute`; design
9
+ * `aiDocs/designs/2026-07-18-fjall-add-buildkite.md` § D2).
10
+ */
11
+ export type BuildkiteFactoryFn = (app: App, scope: Construct) => Buildkite;
12
+ /**
13
+ * Resolve construct props from plain-data input: VPC injection plus the three
14
+ * app-level defaults (cost-allocation environment, shared alarm topic,
15
+ * applicationId). Single source for both `App.addBuildkite` entry paths
16
+ * (design § D2) — explicit props always win.
17
+ */
18
+ export declare function resolveBuildkiteConstructProps(app: App, props: BuildkitePropsInput): BuildkiteConstructProps;
19
+ /**
20
+ * Factory for the self-hosted Buildkite agent fleet — the codemod add path
21
+ * emits `app.addBuildkite(BuildkiteFactory.build("Buildkite", { … }))`
22
+ * (design § D2/§ D3). Validation, the region guard, cost tags, and pinned-AMI
23
+ * resolution all live in the `Buildkite` constructor and fire on every path.
24
+ */
25
+ export declare class BuildkiteFactory {
26
+ static build(id: string, props: BuildkitePropsInput): BuildkiteFactoryFn;
27
+ }
@@ -0,0 +1,39 @@
1
+ import { SHARED_ALARM_TOPIC_EXPORT_NAME } from "../../../config/aws/alarmTopic.js";
2
+ import { UNKNOWN_ENVIRONMENT } from "../../../utils/env.js";
3
+ import { getConfig } from "../../../utils/getConfig.js";
4
+ import { Buildkite } from "./buildkite.js";
5
+ /**
6
+ * Resolve construct props from plain-data input: VPC injection plus the three
7
+ * app-level defaults (cost-allocation environment, shared alarm topic,
8
+ * applicationId). Single source for both `App.addBuildkite` entry paths
9
+ * (design § D2) — explicit props always win.
10
+ */
11
+ export function resolveBuildkiteConstructProps(app, props) {
12
+ const configEnvironment = getConfig().environment;
13
+ const constructProps = { ...props, vpc: app.getVpc() };
14
+ // Post-spread injection so a caller-materialised `costAllocationEnvironment:
15
+ // undefined` counts as absent rather than clobbering the resolved value.
16
+ if (constructProps.costAllocationEnvironment === undefined &&
17
+ configEnvironment !== "" &&
18
+ configEnvironment !== UNKNOWN_ENVIRONMENT) {
19
+ constructProps.costAllocationEnvironment = configEnvironment;
20
+ }
21
+ if (constructProps.alarmSnsTopicArn === undefined) {
22
+ constructProps.alarmSnsTopicArn = `import:${SHARED_ALARM_TOPIC_EXPORT_NAME}`;
23
+ }
24
+ if (constructProps.applicationId === undefined) {
25
+ constructProps.applicationId = app.getName();
26
+ }
27
+ return constructProps;
28
+ }
29
+ /**
30
+ * Factory for the self-hosted Buildkite agent fleet — the codemod add path
31
+ * emits `app.addBuildkite(BuildkiteFactory.build("Buildkite", { … }))`
32
+ * (design § D2/§ D3). Validation, the region guard, cost tags, and pinned-AMI
33
+ * resolution all live in the `Buildkite` constructor and fire on every path.
34
+ */
35
+ export class BuildkiteFactory {
36
+ static build(id, props) {
37
+ return (app, scope) => new Buildkite(scope, id, resolveBuildkiteConstructProps(app, props));
38
+ }
39
+ }
@@ -116,6 +116,10 @@ function buildAgentPolicyDocuments(params) {
116
116
  logging: new PolicyDocument({
117
117
  statements: [
118
118
  new PolicyStatement({
119
+ // PutRetentionPolicy pairs with ENABLE_EC2_LOG_RETENTION_POLICY
120
+ // ='true' in the install part (userData.ts) — the booting instance
121
+ // applies agentLogRetentionDays to the /buildkite/* groups. Remove
122
+ // only together with that flag.
119
123
  actions: [
120
124
  "logs:CreateLogGroup",
121
125
  "logs:CreateLogStream",
@@ -1,81 +1,8 @@
1
- import { z } from "zod";
2
1
  /**
3
- * Plain-data props for the `Buildkite` pattern, validated at construct time.
4
- * Deliberately CDK-free so the same schema can later back the customer
5
- * scaffold surface if the pattern is ever promoted to `PATTERN_TYPE_VALUES`
6
- * (design § D3 — promotion must be additive).
7
- *
8
- * Defaults encode the design's deploy-fleet posture
9
- * (`aiDocs/designs/2026-07-18-buildkite-selfhosted-agents.md`):
10
- * on-demand-only (`spotCapacityPercentage` 0 — ASG mixed-instances has no
11
- * spot→on-demand fallback, and a spot reclaim mid-deploy is a rollback
12
- * incident, § D7), scale-to-zero, whole-instance agents, bounded instance
13
- * staleness, and no ECR / docker-login instance-level credentials (§ D9 —
14
- * deploy AWS access stays server-minted; the instance profile confers none).
2
+ * Buildkite props schema re-export barrel. The canonical declaration lives
3
+ * in `@fjall/generator` (`src/schemas/buildkiteSchemas.ts`) so construct
4
+ * validation, the codemod fragment, and the LLM JSON schema all consume one
5
+ * source (design `aiDocs/designs/2026-07-18-fjall-add-buildkite.md` § D4).
6
+ * Do NOT redeclare here — the cross-package reference-identity test pins it.
15
7
  */
16
- export declare const BuildkitePropsSchema: z.ZodObject<{
17
- buildkiteQueue: z.ZodString;
18
- buildkiteOrgSlug: z.ZodString;
19
- agentTokenSsmParameterName: z.ZodString;
20
- agentTokenKmsKeyArn: z.ZodOptional<z.ZodString>;
21
- fjallApiKeySsmParameterName: z.ZodOptional<z.ZodString>;
22
- instanceType: z.ZodDefault<z.ZodString>;
23
- agentVolumeSizeGib: z.ZodDefault<z.ZodNumber>;
24
- agentMinInstances: z.ZodDefault<z.ZodNumber>;
25
- agentMaxInstances: z.ZodDefault<z.ZodNumber>;
26
- agentsPerInstance: z.ZodDefault<z.ZodNumber>;
27
- spotCapacityPercentage: z.ZodDefault<z.ZodNumber>;
28
- scaleInIdlePeriodSeconds: z.ZodDefault<z.ZodNumber>;
29
- disconnectAfterUptimeSeconds: z.ZodDefault<z.ZodNumber>;
30
- maxInstanceLifetimeDays: z.ZodDefault<z.ZodNumber>;
31
- terminateInstanceAfterJob: z.ZodDefault<z.ZodBoolean>;
32
- purgeBuildsOnDiskFull: z.ZodDefault<z.ZodBoolean>;
33
- terminateInstanceOnDiskFull: z.ZodDefault<z.ZodBoolean>;
34
- logRetentionDays: z.ZodDefault<z.ZodNumber>;
35
- buildkiteAgentRelease: z.ZodDefault<z.ZodEnum<{
36
- stable: "stable";
37
- beta: "beta";
38
- edge: "edge";
39
- }>>;
40
- buildkiteAgentTags: z.ZodDefault<z.ZodString>;
41
- buildkiteAgentTimestampLines: z.ZodDefault<z.ZodBoolean>;
42
- buildkiteAgentExperiments: z.ZodDefault<z.ZodString>;
43
- buildkiteAgentTracingBackend: z.ZodDefault<z.ZodEnum<{
44
- "": "";
45
- datadog: "datadog";
46
- opentelemetry: "opentelemetry";
47
- }>>;
48
- buildkiteAgentCancelGracePeriodSeconds: z.ZodDefault<z.ZodNumber>;
49
- enableSecretsPlugin: z.ZodDefault<z.ZodBoolean>;
50
- enableEcrPlugin: z.ZodDefault<z.ZodBoolean>;
51
- enableDockerLoginPlugin: z.ZodDefault<z.ZodBoolean>;
52
- enableDockerUserNamespaceRemap: z.ZodDefault<z.ZodBoolean>;
53
- enableDockerExperimental: z.ZodDefault<z.ZodBoolean>;
54
- dockerNetworkingProtocol: z.ZodDefault<z.ZodEnum<{
55
- ipv4: "ipv4";
56
- dualstack: "dualstack";
57
- }>>;
58
- enableInstanceStorage: z.ZodDefault<z.ZodBoolean>;
59
- mountTmpfsAtTmp: z.ZodDefault<z.ZodBoolean>;
60
- buildkiteAgentEnableGitMirrors: z.ZodDefault<z.ZodBoolean>;
61
- bootstrapScriptUrl: z.ZodDefault<z.ZodString>;
62
- agentEnvFileUrl: z.ZodDefault<z.ZodString>;
63
- scalerEventSchedulePeriod: z.ZodDefault<z.ZodString>;
64
- scalerMinPollInterval: z.ZodDefault<z.ZodString>;
65
- scaleOutFactor: z.ZodDefault<z.ZodString>;
66
- scaleOutWaitingForJobs: z.ZodDefault<z.ZodBoolean>;
67
- rolePermissionsBoundaryArn: z.ZodOptional<z.ZodString>;
68
- alarmSnsTopicArn: z.ZodOptional<z.ZodString>;
69
- applicationId: z.ZodOptional<z.ZodString>;
70
- costAllocationEnvironment: z.ZodOptional<z.ZodString>;
71
- costAllocationOwner: z.ZodOptional<z.ZodString>;
72
- }, z.core.$strict>;
73
- export type BuildkiteProps = z.infer<typeof BuildkitePropsSchema>;
74
- /** Caller-facing shape: fields with defaults are optional at the call site. */
75
- export type BuildkitePropsInput = z.input<typeof BuildkitePropsSchema>;
76
- /**
77
- * Validate + default the plain-data props at construct time. Throws a
78
- * synth-time error listing every violation — the pattern's constructor is the
79
- * validation boundary, mirroring `ClickHouseDatabase`'s Stage-1 shape.
80
- */
81
- export declare function validateBuildkiteProps(props: BuildkitePropsInput): BuildkiteProps;
8
+ export { BuildkitePropsObjectSchema, BuildkitePropsSchema, validateBuildkiteProps, type BuildkiteProps, type BuildkitePropsInput, type BuildkitePropsObject } from "@fjall/generator";
@@ -1,155 +1,8 @@
1
- import { z } from "zod";
2
1
  /**
3
- * Plain-data props for the `Buildkite` pattern, validated at construct time.
4
- * Deliberately CDK-free so the same schema can later back the customer
5
- * scaffold surface if the pattern is ever promoted to `PATTERN_TYPE_VALUES`
6
- * (design § D3 — promotion must be additive).
7
- *
8
- * Defaults encode the design's deploy-fleet posture
9
- * (`aiDocs/designs/2026-07-18-buildkite-selfhosted-agents.md`):
10
- * on-demand-only (`spotCapacityPercentage` 0 — ASG mixed-instances has no
11
- * spot→on-demand fallback, and a spot reclaim mid-deploy is a rollback
12
- * incident, § D7), scale-to-zero, whole-instance agents, bounded instance
13
- * staleness, and no ECR / docker-login instance-level credentials (§ D9 —
14
- * deploy AWS access stays server-minted; the instance profile confers none).
2
+ * Buildkite props schema re-export barrel. The canonical declaration lives
3
+ * in `@fjall/generator` (`src/schemas/buildkiteSchemas.ts`) so construct
4
+ * validation, the codemod fragment, and the LLM JSON schema all consume one
5
+ * source (design `aiDocs/designs/2026-07-18-fjall-add-buildkite.md` § D4).
6
+ * Do NOT redeclare here — the cross-package reference-identity test pins it.
15
7
  */
16
- export const BuildkitePropsSchema = z
17
- .object({
18
- /** Buildkite queue this fleet serves (cluster-scoped via the token). */
19
- buildkiteQueue: z
20
- .string()
21
- .min(1, "buildkiteQueue cannot be empty")
22
- .max(100)
23
- .regex(/^[a-zA-Z0-9-_]+$/, "buildkiteQueue must be alphanumeric with hyphens/underscores"),
24
- /**
25
- * Buildkite organisation slug (e.g. `fjall-tech`). The
26
- * buildkite-agent-scaler publishes its CloudWatch metrics dimensioned by
27
- * {Org, Queue} — the fleet's alarms must query the same pair or they read
28
- * no data at all (heartbeat permanently ALARM, queued-with-zero-capacity
29
- * permanently inert).
30
- */
31
- buildkiteOrgSlug: z
32
- .string()
33
- .min(1, "buildkiteOrgSlug cannot be empty")
34
- .max(100)
35
- .regex(/^[a-z0-9-]+$/, "buildkiteOrgSlug must be a lowercase Buildkite organisation slug"),
36
- /**
37
- * Name of the pre-provisioned SSM SecureString holding the cluster-scoped
38
- * agent token (e.g. `/Buildkite/agents/agent-token`). Provisioned
39
- * out-of-band via `fjall secrets` tooling — the construct receives the
40
- * identifier only; no secret value ever transits synth (design § D4).
41
- */
42
- agentTokenSsmParameterName: z
43
- .string()
44
- .min(2)
45
- .regex(/^\//, "agentTokenSsmParameterName must be a full path (leading /)"),
46
- /**
47
- * KMS key ARN encrypting the agent-token parameter, when a customer
48
- * managed key is used. Omit for the AWS-managed `aws/ssm` key.
49
- */
50
- agentTokenKmsKeyArn: z.string().min(1).optional(),
51
- /**
52
- * Name of the SSM SecureString holding FJALL_API_KEY. When set, the
53
- * construct ships a per-job `env` hook into the managed secrets bucket
54
- * that reads the parameter at job start (rotation takes effect on the
55
- * next job, no instance replacement — design § D4(iii)) and grants the
56
- * instance profile read on exactly this parameter.
57
- */
58
- fjallApiKeySsmParameterName: z
59
- .string()
60
- .min(2)
61
- .regex(/^\//, "fjallApiKeySsmParameterName must be a full path (leading /)")
62
- .optional(),
63
- /** EC2 instance type. Graviton default per design § D7. */
64
- instanceType: z.string().min(1).default("c8g.xlarge"),
65
- agentVolumeSizeGib: z.number().int().min(20).max(1000).default(250),
66
- agentMinInstances: z.number().int().min(0).default(0),
67
- agentMaxInstances: z.number().int().min(1).default(2),
68
- agentsPerInstance: z.number().int().min(1).default(1),
69
- /**
70
- * Percentage of capacity on spot. Deploy fleets MUST stay 0 (on-demand
71
- * only): no spot→on-demand fallback mechanism exists, and a reclaim
72
- * mid-deploy lands in the rollback-wedge class (design § D7). Non-zero is
73
- * for Phase-2 CI fleets running idempotent, auto-retried jobs.
74
- */
75
- spotCapacityPercentage: z.number().int().min(0).max(100).default(0),
76
- /** Agent-driven scale-in: idle seconds before an instance self-terminates. */
77
- scaleInIdlePeriodSeconds: z.number().int().min(60).default(600),
78
- /** Bounded instance staleness; also caps stale-token propagation (§ D5). */
79
- disconnectAfterUptimeSeconds: z.number().int().min(3600).default(86_400),
80
- maxInstanceLifetimeDays: z.number().int().min(1).max(365).default(7),
81
- terminateInstanceAfterJob: z.boolean().default(false),
82
- purgeBuildsOnDiskFull: z.boolean().default(true),
83
- terminateInstanceOnDiskFull: z.boolean().default(false),
84
- /** CloudWatch retention for the scaler's log group (days). */
85
- logRetentionDays: z.number().int().min(1).default(30),
86
- buildkiteAgentRelease: z.enum(["stable", "beta", "edge"]).default("stable"),
87
- buildkiteAgentTags: z.string().default(""),
88
- buildkiteAgentTimestampLines: z.boolean().default(false),
89
- buildkiteAgentExperiments: z.string().default(""),
90
- buildkiteAgentTracingBackend: z
91
- .enum(["", "datadog", "opentelemetry"])
92
- .default(""),
93
- buildkiteAgentCancelGracePeriodSeconds: z
94
- .number()
95
- .int()
96
- .min(10)
97
- .default(60),
98
- /** S3 secrets-hooks plugin — required for the FJALL_API_KEY env hook. */
99
- enableSecretsPlugin: z.boolean().default(true),
100
- /**
101
- * ECR + docker-login plugins default OFF: the instance profile carries no
102
- * registry credentials (design § D9); deploy jobs authenticate through
103
- * server-minted credentials exactly as on hosted agents.
104
- */
105
- enableEcrPlugin: z.boolean().default(false),
106
- enableDockerLoginPlugin: z.boolean().default(false),
107
- enableDockerUserNamespaceRemap: z.boolean().default(true),
108
- enableDockerExperimental: z.boolean().default(false),
109
- dockerNetworkingProtocol: z.enum(["ipv4", "dualstack"]).default("ipv4"),
110
- enableInstanceStorage: z.boolean().default(false),
111
- mountTmpfsAtTmp: z.boolean().default(true),
112
- buildkiteAgentEnableGitMirrors: z.boolean().default(false),
113
- bootstrapScriptUrl: z.string().default(""),
114
- agentEnvFileUrl: z.string().default(""),
115
- scalerEventSchedulePeriod: z.string().min(1).default("1 minute"),
116
- scalerMinPollInterval: z.string().min(1).default("10s"),
117
- scaleOutFactor: z.string().min(1).default("1.0"),
118
- scaleOutWaitingForJobs: z.boolean().default(false),
119
- /** IAM permissions boundary for the scaler's roles; emitted only when set. */
120
- rolePermissionsBoundaryArn: z.string().min(1).optional(),
121
- /**
122
- * SNS topic receiving the fleet's two alarms (scaler heartbeat + queued
123
- * with zero capacity — design § D14). Omit to create the alarms without
124
- * actions (visible on the console, silent).
125
- */
126
- alarmSnsTopicArn: z.string().min(1).optional(),
127
- /**
128
- * Fjall application the fleet belongs to — appended to alarm
129
- * descriptions and tagged (`fjall:applicationId`) so the alarm webhook
130
- * can map pages to the application. `App.addBuildkite` injects the app
131
- * name automatically; set only to override.
132
- */
133
- applicationId: z.string().min(1).optional(),
134
- costAllocationEnvironment: z.string().min(1).optional(),
135
- costAllocationOwner: z.string().min(1).optional()
136
- })
137
- .strict()
138
- .refine((props) => props.agentMinInstances <= props.agentMaxInstances, {
139
- message: "agentMinInstances must be <= agentMaxInstances"
140
- });
141
- /**
142
- * Validate + default the plain-data props at construct time. Throws a
143
- * synth-time error listing every violation — the pattern's constructor is the
144
- * validation boundary, mirroring `ClickHouseDatabase`'s Stage-1 shape.
145
- */
146
- export function validateBuildkiteProps(props) {
147
- const result = BuildkitePropsSchema.safeParse(props);
148
- if (!result.success) {
149
- const details = result.error.issues
150
- .map((issue) => ` - ${issue.path.join(".") || "(root)"}: ${issue.message}`)
151
- .join("\n");
152
- throw new Error(`Buildkite: invalid props:\n${details}`);
153
- }
154
- return result.data;
155
- }
8
+ export { BuildkitePropsObjectSchema, BuildkitePropsSchema, validateBuildkiteProps } from "@fjall/generator";
@@ -42,7 +42,7 @@ export declare function buildBuildkiteDockerCommands(props: BuildkiteProps): str
42
42
  * `bk-install-elastic-stack.sh`, mirroring the upstream block key-for-key in
43
43
  * upstream order (so a fixture-bump diff reads line-by-line). Keys with no
44
44
  * schema knob carry the upstream v6.68.1 parameter default verbatim, with
45
- * three deliberate deviations:
45
+ * four deliberate deviations:
46
46
  *
47
47
  * - `BUILDKITE_S3_ACL`: our artifact bucket is BucketOwnerEnforced (ACLs
48
48
  * disabled), where S3 rejects every canned ACL EXCEPT
@@ -54,5 +54,16 @@ export declare function buildBuildkiteDockerCommands(props: BuildkiteProps): str
54
54
  * is out of scope for the Phase-1 deploy fleet; a future signing knob must
55
55
  * land WITH the kms:Sign/Verify/GetPublicKey grants it needs (the prior
56
56
  * knob shipped without them — a dead toggle that broke agents when set).
57
+ * - `ENABLE_EC2_LOG_RETENTION_POLICY`: hard-pinned `true` (upstream default
58
+ * `false`, "preserve all logs"). Fjall's log-group lifecycle posture is
59
+ * bounded retention everywhere: each booting instance applies
60
+ * `agentLogRetentionDays` to the agent-created `/buildkite/*` groups via
61
+ * the instance role's `logs:PutRetentionPolicy` grant (which this flag
62
+ * makes live — it was stranded while `false`). Retro-applies to existing
63
+ * groups on the next boot after enabling, expiring events older than the
64
+ * window. Residual: runtime-created groups stay untagged — the namespace
65
+ * is account-global (shared across fleets and stacks), so CFN pre-creation
66
+ * would fight cross-stack ownership; cost exposure is bounded by the
67
+ * retention itself.
57
68
  */
58
69
  export declare function buildBuildkiteInstallCommands(props: BuildkiteProps, context: BuildkiteUserDataContext): string[];
@@ -149,7 +149,7 @@ export function buildBuildkiteDockerCommands(props) {
149
149
  * `bk-install-elastic-stack.sh`, mirroring the upstream block key-for-key in
150
150
  * upstream order (so a fixture-bump diff reads line-by-line). Keys with no
151
151
  * schema knob carry the upstream v6.68.1 parameter default verbatim, with
152
- * three deliberate deviations:
152
+ * four deliberate deviations:
153
153
  *
154
154
  * - `BUILDKITE_S3_ACL`: our artifact bucket is BucketOwnerEnforced (ACLs
155
155
  * disabled), where S3 rejects every canned ACL EXCEPT
@@ -161,6 +161,17 @@ export function buildBuildkiteDockerCommands(props) {
161
161
  * is out of scope for the Phase-1 deploy fleet; a future signing knob must
162
162
  * land WITH the kms:Sign/Verify/GetPublicKey grants it needs (the prior
163
163
  * knob shipped without them — a dead toggle that broke agents when set).
164
+ * - `ENABLE_EC2_LOG_RETENTION_POLICY`: hard-pinned `true` (upstream default
165
+ * `false`, "preserve all logs"). Fjall's log-group lifecycle posture is
166
+ * bounded retention everywhere: each booting instance applies
167
+ * `agentLogRetentionDays` to the agent-created `/buildkite/*` groups via
168
+ * the instance role's `logs:PutRetentionPolicy` grant (which this flag
169
+ * makes live — it was stranded while `false`). Retro-applies to existing
170
+ * groups on the next boot after enabling, expiring events older than the
171
+ * window. Residual: runtime-created groups stay untagged — the namespace
172
+ * is account-global (shared across fleets and stacks), so CFN pre-creation
173
+ * would fight cross-stack ownership; cost exposure is bounded by the
174
+ * retention itself.
164
175
  */
165
176
  export function buildBuildkiteInstallCommands(props, context) {
166
177
  return [
@@ -219,8 +230,8 @@ export function buildBuildkiteInstallCommands(props, context) {
219
230
  "RESOURCE_LIMITS_CPU_WEIGHT='100' \\",
220
231
  "RESOURCE_LIMITS_CPU_QUOTA='90%' \\",
221
232
  "RESOURCE_LIMITS_IO_WEIGHT='80' \\",
222
- "ENABLE_EC2_LOG_RETENTION_POLICY='false' \\",
223
- "EC2_LOG_RETENTION_DAYS='7' \\",
233
+ "ENABLE_EC2_LOG_RETENTION_POLICY='true' \\",
234
+ `EC2_LOG_RETENTION_DAYS='${props.agentLogRetentionDays}' \\`,
224
235
  "/usr/local/bin/bk-install-elastic-stack.sh"
225
236
  ];
226
237
  }
@@ -7,5 +7,6 @@
7
7
  export { Buildkite, FJALL_ENV_HOOK_OBJECT_KEY, buildFjallApiKeyEnvHookScript, type BuildkiteConstructProps } from "./buildkite/buildkite.js";
8
8
  export { BUILDKITE_AMI_OWNER_ACCOUNT_ID, BUILDKITE_CPU_ARCHITECTURES, BUILDKITE_STACK_PINS, resolvePinnedAmiId, resolveScalerSarApplicationArn, type BuildkiteCpuArchitecture } from "./buildkite/pins.js";
9
9
  export { BuildkitePropsSchema, validateBuildkiteProps, type BuildkiteProps, type BuildkitePropsInput } from "./buildkite/schema.js";
10
+ export { BuildkiteFactory, type BuildkiteFactoryFn } from "./buildkite/factory.js";
10
11
  export { BUILDKITE_AGENT_IAM_ACTION_ALLOWLIST, buildAgentRole, type BuildkiteAgentRoleParams } from "./buildkite/iam.js";
11
12
  export { BUILDKITE_DOCKER_PART_ENV_KEYS, BUILDKITE_MOUNT_PART_ENV_KEYS, BUILDKITE_USER_DATA_ENV_KEYS, buildBuildkiteDockerCommands, buildBuildkiteInstallCommands, buildBuildkiteMountCommands, buildBuildkiteUserData, type BuildkiteUserDataContext } from "./buildkite/userData.js";
@@ -7,5 +7,6 @@
7
7
  export { Buildkite, FJALL_ENV_HOOK_OBJECT_KEY, buildFjallApiKeyEnvHookScript } from "./buildkite/buildkite.js";
8
8
  export { BUILDKITE_AMI_OWNER_ACCOUNT_ID, BUILDKITE_CPU_ARCHITECTURES, BUILDKITE_STACK_PINS, resolvePinnedAmiId, resolveScalerSarApplicationArn } from "./buildkite/pins.js";
9
9
  export { BuildkitePropsSchema, validateBuildkiteProps } from "./buildkite/schema.js";
10
+ export { BuildkiteFactory } from "./buildkite/factory.js";
10
11
  export { BUILDKITE_AGENT_IAM_ACTION_ALLOWLIST, buildAgentRole } from "./buildkite/iam.js";
11
12
  export { BUILDKITE_DOCKER_PART_ENV_KEYS, BUILDKITE_MOUNT_PART_ENV_KEYS, BUILDKITE_USER_DATA_ENV_KEYS, buildBuildkiteDockerCommands, buildBuildkiteInstallCommands, buildBuildkiteMountCommands, buildBuildkiteUserData } from "./buildkite/userData.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fjall/components-infrastructure",
3
- "version": "3.2.1",
3
+ "version": "3.3.0",
4
4
  "license": "SEE LICENSE IN LICENSE",
5
5
  "type": "module",
6
6
  "bin": {
@@ -67,8 +67,8 @@
67
67
  },
68
68
  "dependencies": {
69
69
  "@aws-sdk/client-organizations": "^3.1038.0",
70
- "@fjall/generator": "^3.2.1",
71
- "@fjall/util": "^3.2.1",
70
+ "@fjall/generator": "^3.3.0",
71
+ "@fjall/util": "^3.3.0",
72
72
  "constructs": "^10.6.0"
73
73
  },
74
74
  "overrides": {
@@ -82,5 +82,5 @@
82
82
  "engines": {
83
83
  "node": ">=18.0.0"
84
84
  },
85
- "gitHead": "1a249298df7bf3104d9d49b47700234db815ebb1"
85
+ "gitHead": "cc1e0f520f4b6b5312e16df3751d60ea746dfb7d"
86
86
  }