@percepta/create 3.1.2 → 3.1.3

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 (43) hide show
  1. package/README.md +3 -4
  2. package/dist/{chunk-CG7IJSB4.js → chunk-CO3YWUD6.js} +2 -2
  3. package/dist/{chunk-WMJT7CB5.js → chunk-V5EJIUBJ.js} +5 -2
  4. package/dist/index.js +21 -53
  5. package/dist/{init-XDWSYHYK.js → init-EQZ2TCSJ.js} +2 -2
  6. package/dist/{status-BTHGN6QH.js → status-QW5TQDYY.js} +1 -1
  7. package/dist/{sync-3Q27L7XZ.js → sync-RLBZDOFB.js} +1 -1
  8. package/dist/{upstream-C5KFAHVR.js → upstream-TQFVPMEG.js} +1 -1
  9. package/package.json +1 -1
  10. package/templates/monorepo/.dockerignore +18 -0
  11. package/templates/webapp/.github/workflows/__APP_NAME__-ryvn-release.yaml +6 -2
  12. package/templates/webapp/.github/workflows/__APP_NAME__-terraform-ryvn-release.yaml +98 -0
  13. package/templates/webapp/AGENTS.md +17 -5
  14. package/templates/webapp/Dockerfile +16 -7
  15. package/templates/webapp/README.md +64 -2
  16. package/templates/webapp/agent-skills/deploy.md +48 -65
  17. package/templates/webapp/agent-skills/inngest.md +4 -4
  18. package/templates/webapp/agent-skills/langfuse.md +15 -14
  19. package/templates/webapp/agent-skills/llm.md +59 -0
  20. package/templates/webapp/agent-skills/oneshot.md +14 -1
  21. package/templates/webapp/agent-skills/ryvn.md +1 -1
  22. package/templates/webapp/deploy/README.md +34 -33
  23. package/templates/webapp/deploy/ryvn/__APP_NAME__-terraform.service.yaml +10 -0
  24. package/templates/webapp/deploy/ryvn/__APP_NAME__.service.yaml +2 -2
  25. package/templates/webapp/deploy/ryvn/environments/percepta-test/installations/__APP_NAME__-terraform.env.percepta-test.serviceinstallation.yaml +11 -0
  26. package/templates/webapp/deploy/ryvn/environments/percepta-test/installations/__APP_NAME__.env.percepta-test.serviceinstallation.yaml +45 -9
  27. package/templates/webapp/env.example.template +20 -2
  28. package/templates/webapp/eslint.config.mjs +6 -0
  29. package/templates/webapp/next.config.ts +9 -0
  30. package/templates/webapp/package.json.template +6 -2
  31. package/templates/webapp/scripts/deploy-percepta-test.ts +837 -0
  32. package/templates/webapp/scripts/migrate.ts +3 -0
  33. package/templates/webapp/scripts/open-ryvn-deploy-pr.ts +5 -3
  34. package/templates/webapp/scripts/with-local-env.ts +75 -0
  35. package/templates/webapp/src/config/getEnvConfig.ts +14 -0
  36. package/templates/webapp/src/instrumentation.ts +102 -10
  37. package/templates/webapp/src/services/llm/LLMService.ts +88 -0
  38. package/templates/webapp/src/services/llm/LlmProviderService.ts +85 -0
  39. package/templates/webapp/terraform/schema/main.tf +4 -0
  40. package/templates/webapp/terraform/schema/outputs.tf +9 -0
  41. package/templates/webapp/terraform/schema/variables.tf +19 -0
  42. package/templates/webapp/terraform/schema/versions.tf +38 -0
  43. package/templates/webapp/.github/workflows/__APP_NAME__-terraform.yml +0 -28
@@ -6,10 +6,13 @@ async function main(): Promise<void> {
6
6
  loadEnvConfig(process.cwd());
7
7
 
8
8
  // Dynamically load because we need to load the environment variables before importing modules that depend on them.
9
+ const { getEnvConfig } = await import("../src/config/getEnvConfig");
9
10
  const { client } = await import("../src/drizzle/db");
11
+ const { DATABASE_SCHEMA: databaseSchema } = getEnvConfig();
10
12
 
11
13
  await migrate(drizzle(client), {
12
14
  migrationsFolder: "./src/drizzle/migrations",
15
+ ...(databaseSchema ? { migrationsSchema: databaseSchema } : {}),
13
16
  });
14
17
 
15
18
  await client.end();
@@ -32,7 +32,9 @@ function parseArgs(argv: string[]): Options {
32
32
 
33
33
  for (let index = 0; index < argv.length; index++) {
34
34
  const arg = argv[index];
35
- if (arg === "--yes" || arg === "-y") {
35
+ if (arg === "--") {
36
+ continue;
37
+ } else if (arg === "--yes" || arg === "-y") {
36
38
  options.yes = true;
37
39
  } else if (arg === "--no-clone") {
38
40
  options.noClone = true;
@@ -357,7 +359,7 @@ function getPrBody(phase: DeployPhase): string {
357
359
  "1. Let GitOps import the service.",
358
360
  "2. Approve/apply the percepta-internal Terraform task if one is created.",
359
361
  "3. Push the app to main or run the release workflow so Ryvn has a first release.",
360
- `4. Open the installation PR with pnpm deploy:percepta-test -- --phase installation --yes.`,
362
+ `4. Open the installation PR with pnpm deploy:percepta-test:pr -- --phase installation --yes.`,
361
363
  ].join("\n");
362
364
  }
363
365
 
@@ -383,7 +385,7 @@ function printNextSteps(phase: DeployPhase): void {
383
385
  );
384
386
  console.log("4. Push the app to main or run the release workflow.");
385
387
  console.log(
386
- "5. Run: pnpm deploy:percepta-test -- --phase installation --yes",
388
+ "5. Run: pnpm deploy:percepta-test:pr -- --phase installation --yes",
387
389
  );
388
390
  return;
389
391
  }
@@ -0,0 +1,75 @@
1
+ #!/usr/bin/env tsx
2
+
3
+ import { spawn } from "node:child_process";
4
+ import { existsSync, readFileSync } from "node:fs";
5
+ import { homedir } from "node:os";
6
+ import path from "node:path";
7
+
8
+ type EnvMap = Record<string, string>;
9
+
10
+ const LOCAL_ENV_PATH = path.join(
11
+ homedir(),
12
+ ".config",
13
+ "percepta",
14
+ "create.env",
15
+ );
16
+
17
+ function parseEnvFile(filePath: string): EnvMap {
18
+ if (!existsSync(filePath)) return {};
19
+
20
+ const env: EnvMap = {};
21
+ const content = readFileSync(filePath, "utf8");
22
+ for (const rawLine of content.split(/\r?\n/)) {
23
+ const line = rawLine.trim();
24
+ if (!line || line.startsWith("#")) continue;
25
+
26
+ const normalized = line.startsWith("export ") ? line.slice(7).trim() : line;
27
+ const separatorIndex = normalized.indexOf("=");
28
+ if (separatorIndex === -1) continue;
29
+
30
+ const key = normalized.slice(0, separatorIndex).trim();
31
+ const rawValue = normalized.slice(separatorIndex + 1).trim();
32
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) continue;
33
+
34
+ env[key] = unquote(rawValue);
35
+ }
36
+
37
+ return env;
38
+ }
39
+
40
+ function unquote(value: string): string {
41
+ if (
42
+ (value.startsWith('"') && value.endsWith('"')) ||
43
+ (value.startsWith("'") && value.endsWith("'"))
44
+ ) {
45
+ return value.slice(1, -1);
46
+ }
47
+
48
+ return value;
49
+ }
50
+
51
+ const [command, ...args] = process.argv.slice(2);
52
+ if (!command) {
53
+ throw new Error("Usage: tsx scripts/with-local-env.ts <command> [...args]");
54
+ }
55
+
56
+ const child = spawn(command, args, {
57
+ env: {
58
+ ...parseEnvFile(LOCAL_ENV_PATH),
59
+ ...process.env,
60
+ },
61
+ stdio: "inherit",
62
+ });
63
+
64
+ child.on("error", (error) => {
65
+ throw error;
66
+ });
67
+
68
+ child.on("exit", (code, signal) => {
69
+ if (signal) {
70
+ process.kill(process.pid, signal);
71
+ return;
72
+ }
73
+
74
+ process.exit(code ?? 1);
75
+ });
@@ -44,12 +44,26 @@ export const { getEnvConfig, schema: ENV_CONFIG_SCHEMA } = createEnvConfig(
44
44
  LANGFUSE_PUBLIC_KEY: z.string().optional(),
45
45
  LANGFUSE_SECRET_KEY: z.string().optional(),
46
46
 
47
+ // OpenTelemetry:
48
+ OTEL_SERVICE_NAME: z.string().optional(),
49
+ OTEL_RESOURCE_ATTRIBUTES: z.string().optional(),
50
+ OTEL_TRACES_EXPORTER: z.string().optional(),
51
+ OTEL_METRICS_EXPORTER: z.string().optional(),
52
+ OTEL_EXPORTER_OTLP_PROTOCOL: z.string().optional(),
53
+ OTEL_EXPORTER_OTLP_ENDPOINT: z.string().optional(),
54
+ OTEL_EXPORTER_OTLP_TRACES_ENDPOINT: z.string().optional(),
55
+ OTEL_EXPORTER_OTLP_METRICS_ENDPOINT: z.string().optional(),
56
+ OTEL_METRIC_EXPORT_INTERVAL: z.string().optional(),
57
+
47
58
  // Security:
48
59
  ENCRYPTION_SECRET_KEY: z.string().optional(),
49
60
  TRIGGER_ENDPOINT_API_KEY: z.string().optional(),
50
61
 
51
62
  // LLM/AI providers:
63
+ ANTHROPIC_API_KEY: z.string().optional(),
52
64
  OPENAI_API_KEY: z.string().optional(),
65
+ LLM_PROVIDER: z.enum(["anthropic", "openai"]).optional(),
66
+ LLM_MODEL: z.string().optional(),
53
67
 
54
68
  // Readonly database (scripts):
55
69
  READONLY_SECRET_NAME: z.string().optional(),
@@ -1,34 +1,126 @@
1
1
  import { LangfuseSpanProcessor } from "@langfuse/otel";
2
2
  import { getNodeAutoInstrumentations } from "@opentelemetry/auto-instrumentations-node";
3
- import { NodeSDK } from "@opentelemetry/sdk-node";
3
+ import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-proto";
4
+ import { NodeSDK, tracing } from "@opentelemetry/sdk-node";
4
5
  import { compact } from "lodash-es";
5
6
  import { getEnvConfig } from "./config/getEnvConfig";
6
7
  import { getLogger } from "./services/logger/AppLogger";
7
8
 
8
- function getSpanProcessor(): LangfuseSpanProcessor | undefined {
9
+ type SpanProcessor = tracing.SpanProcessor;
10
+ type ReadableSpan = Parameters<SpanProcessor["onEnd"]>[0];
11
+ type OnStartArgs = Parameters<SpanProcessor["onStart"]>;
12
+
13
+ class FilteringSpanProcessor implements SpanProcessor {
14
+ public constructor(
15
+ private delegate: SpanProcessor,
16
+ private shouldExport: (span: ReadableSpan) => boolean,
17
+ ) {}
18
+
19
+ public forceFlush(): Promise<void> {
20
+ return this.delegate.forceFlush();
21
+ }
22
+
23
+ public onStart(...args: OnStartArgs): void {
24
+ this.delegate.onStart(...args);
25
+ }
26
+
27
+ public onEnd(span: ReadableSpan): void {
28
+ if (this.shouldExport(span)) {
29
+ this.delegate.onEnd(span);
30
+ }
31
+ }
32
+
33
+ public shutdown(): Promise<void> {
34
+ return this.delegate.shutdown();
35
+ }
36
+ }
37
+
38
+ function isAiSdkSpan(span: ReadableSpan): boolean {
39
+ const operationId = span.attributes["ai.operationId"];
40
+ if (typeof operationId === "string" && operationId.startsWith("ai.")) {
41
+ return true;
42
+ }
43
+
44
+ if (span.name.startsWith("ai.")) {
45
+ return true;
46
+ }
47
+
48
+ return Object.keys(span.attributes).some((key) => key.startsWith("gen_ai."));
49
+ }
50
+
51
+ function getOtlpTracesEndpoint(): string | undefined {
52
+ const {
53
+ OTEL_EXPORTER_OTLP_TRACES_ENDPOINT: tracesEndpoint,
54
+ OTEL_EXPORTER_OTLP_ENDPOINT: baseEndpoint,
55
+ } = getEnvConfig();
56
+
57
+ if (tracesEndpoint) return tracesEndpoint;
58
+
59
+ if (!baseEndpoint) return undefined;
60
+
61
+ return `${baseEndpoint.replace(/\/$/, "")}/v1/traces`;
62
+ }
63
+
64
+ function getOtlpSpanProcessor(): tracing.BatchSpanProcessor | undefined {
65
+ const { OTEL_TRACES_EXPORTER: tracesExporter } = getEnvConfig();
66
+ if (tracesExporter === "none") {
67
+ getLogger().debug(
68
+ undefined,
69
+ "OTEL_TRACES_EXPORTER=none. Skipping OTLP trace export.",
70
+ );
71
+ return undefined;
72
+ }
73
+
74
+ const tracesEndpoint = getOtlpTracesEndpoint();
75
+ if (!tracesEndpoint) {
76
+ getLogger().debug(
77
+ undefined,
78
+ "No OTLP trace endpoint found. Skipping OTLP trace export.",
79
+ );
80
+ return undefined;
81
+ }
82
+
83
+ getLogger().debug(
84
+ { safe: { tracesEndpoint } },
85
+ "Registering OTLP trace exporter.",
86
+ );
87
+ return new tracing.BatchSpanProcessor(
88
+ new OTLPTraceExporter({ url: tracesEndpoint }),
89
+ );
90
+ }
91
+
92
+ function getLangfuseSpanProcessor(): SpanProcessor | undefined {
9
93
  const {
10
94
  LANGFUSE_BASE_URL: baseUrl,
11
95
  LANGFUSE_PUBLIC_KEY: publicKey,
12
96
  LANGFUSE_SECRET_KEY: secretKey,
13
97
  } = getEnvConfig();
14
- if (baseUrl == null) {
98
+ if (!baseUrl || !publicKey || !secretKey) {
15
99
  getLogger().debug(
16
100
  undefined,
17
- "No Langfuse base URL found. Skipping Langfuse OpenTelemetry.",
101
+ "Langfuse environment is incomplete. Skipping Langfuse OpenTelemetry.",
18
102
  );
19
103
  return undefined;
20
104
  }
21
105
 
22
106
  getLogger().debug(undefined, "Registering Langfuse OpenTelemetry.");
23
- return new LangfuseSpanProcessor({
24
- baseUrl,
25
- publicKey,
26
- secretKey,
27
- });
107
+ return new FilteringSpanProcessor(
108
+ new LangfuseSpanProcessor({
109
+ baseUrl,
110
+ publicKey,
111
+ secretKey,
112
+ }),
113
+ isAiSdkSpan,
114
+ );
28
115
  }
29
116
 
117
+ const spanProcessors: tracing.SpanProcessor[] = compact([
118
+ getOtlpSpanProcessor(),
119
+ getLangfuseSpanProcessor(),
120
+ ]);
121
+
30
122
  const sdk = new NodeSDK({
31
- spanProcessors: compact([getSpanProcessor()]),
123
+ spanProcessors,
32
124
  instrumentations: [getNodeAutoInstrumentations()],
33
125
  });
34
126
 
@@ -0,0 +1,88 @@
1
+ import { generateText, streamText } from "ai";
2
+ import { type LlmProviderName, LlmProviderService } from "./LlmProviderService";
3
+
4
+ type GenerateTextOptions = Omit<Parameters<typeof generateText>[0], "model"> & {
5
+ modelId?: string;
6
+ provider?: LlmProviderName;
7
+ telemetryFunctionId?: string;
8
+ };
9
+
10
+ type StreamTextOptions = Omit<Parameters<typeof streamText>[0], "model"> & {
11
+ modelId?: string;
12
+ provider?: LlmProviderName;
13
+ telemetryFunctionId?: string;
14
+ };
15
+
16
+ export class LLMService {
17
+ private static SINGLETON: LLMService | undefined;
18
+
19
+ public static create(): LLMService {
20
+ if (LLMService.SINGLETON == null) {
21
+ LLMService.SINGLETON = new LLMService(LlmProviderService.create());
22
+ }
23
+
24
+ return LLMService.SINGLETON;
25
+ }
26
+
27
+ private constructor(private llmProviderService: LlmProviderService) {}
28
+
29
+ public generateText(
30
+ options: GenerateTextOptions,
31
+ ): ReturnType<typeof generateText> {
32
+ const { modelId, provider, telemetryFunctionId, ...generateOptions } =
33
+ options;
34
+ const selection = this.llmProviderService.getLanguageModel({
35
+ modelId,
36
+ provider,
37
+ });
38
+
39
+ const aiOptions = {
40
+ ...generateOptions,
41
+ model: selection.model,
42
+ experimental_telemetry: {
43
+ ...generateOptions.experimental_telemetry,
44
+ isEnabled: generateOptions.experimental_telemetry?.isEnabled ?? true,
45
+ functionId:
46
+ telemetryFunctionId ??
47
+ generateOptions.experimental_telemetry?.functionId ??
48
+ "llm.generateText",
49
+ metadata: {
50
+ ...generateOptions.experimental_telemetry?.metadata,
51
+ llmProvider: selection.provider,
52
+ llmModel: selection.modelId,
53
+ },
54
+ },
55
+ } as Parameters<typeof generateText>[0];
56
+
57
+ return generateText(aiOptions);
58
+ }
59
+
60
+ public streamText(options: StreamTextOptions): ReturnType<typeof streamText> {
61
+ const { modelId, provider, telemetryFunctionId, ...streamOptions } =
62
+ options;
63
+ const selection = this.llmProviderService.getLanguageModel({
64
+ modelId,
65
+ provider,
66
+ });
67
+
68
+ const aiOptions = {
69
+ ...streamOptions,
70
+ model: selection.model,
71
+ experimental_telemetry: {
72
+ ...streamOptions.experimental_telemetry,
73
+ isEnabled: streamOptions.experimental_telemetry?.isEnabled ?? true,
74
+ functionId:
75
+ telemetryFunctionId ??
76
+ streamOptions.experimental_telemetry?.functionId ??
77
+ "llm.streamText",
78
+ metadata: {
79
+ ...streamOptions.experimental_telemetry?.metadata,
80
+ llmProvider: selection.provider,
81
+ llmModel: selection.modelId,
82
+ },
83
+ },
84
+ } as Parameters<typeof streamText>[0];
85
+
86
+ return streamText(aiOptions);
87
+ }
88
+ }
@@ -0,0 +1,85 @@
1
+ import { createAnthropic } from "@ai-sdk/anthropic";
2
+ import { createOpenAI } from "@ai-sdk/openai";
3
+ import { type LanguageModel } from "ai";
4
+ import { getEnvConfig } from "../../config/getEnvConfig";
5
+
6
+ export type LlmProviderName = "anthropic" | "openai";
7
+
8
+ export interface LanguageModelSelection {
9
+ model: LanguageModel;
10
+ modelId: string;
11
+ provider: LlmProviderName;
12
+ }
13
+
14
+ interface GetLanguageModelOptions {
15
+ modelId?: string;
16
+ provider?: LlmProviderName;
17
+ }
18
+
19
+ const DEFAULT_ANTHROPIC_MODEL = "claude-sonnet-4-5-20250929";
20
+ const DEFAULT_OPENAI_MODEL = "gpt-4.1";
21
+
22
+ export class LlmProviderService {
23
+ private static SINGLETON: LlmProviderService | undefined;
24
+
25
+ public static create(): LlmProviderService {
26
+ if (LlmProviderService.SINGLETON == null) {
27
+ LlmProviderService.SINGLETON = new LlmProviderService();
28
+ }
29
+
30
+ return LlmProviderService.SINGLETON;
31
+ }
32
+
33
+ private constructor() {}
34
+
35
+ public getLanguageModel(
36
+ options: GetLanguageModelOptions = {},
37
+ ): LanguageModelSelection {
38
+ const config = getEnvConfig();
39
+ const provider =
40
+ options.provider ??
41
+ config.LLM_PROVIDER ??
42
+ (config.ANTHROPIC_API_KEY ? "anthropic" : undefined) ??
43
+ (config.OPENAI_API_KEY ? "openai" : undefined);
44
+
45
+ if (provider === "anthropic") {
46
+ if (!config.ANTHROPIC_API_KEY) {
47
+ throw new Error(
48
+ "LLM_PROVIDER=anthropic but ANTHROPIC_API_KEY is not configured.",
49
+ );
50
+ }
51
+
52
+ const modelId =
53
+ options.modelId ?? config.LLM_MODEL ?? DEFAULT_ANTHROPIC_MODEL;
54
+ return {
55
+ provider,
56
+ modelId,
57
+ model: createAnthropic({
58
+ apiKey: config.ANTHROPIC_API_KEY,
59
+ }).languageModel(modelId),
60
+ };
61
+ }
62
+
63
+ if (provider === "openai") {
64
+ if (!config.OPENAI_API_KEY) {
65
+ throw new Error(
66
+ "LLM_PROVIDER=openai but OPENAI_API_KEY is not configured.",
67
+ );
68
+ }
69
+
70
+ const modelId =
71
+ options.modelId ?? config.LLM_MODEL ?? DEFAULT_OPENAI_MODEL;
72
+ return {
73
+ provider,
74
+ modelId,
75
+ model: createOpenAI({
76
+ apiKey: config.OPENAI_API_KEY,
77
+ }).languageModel(modelId),
78
+ };
79
+ }
80
+
81
+ throw new Error(
82
+ "No LLM provider is configured. Set ANTHROPIC_API_KEY or OPENAI_API_KEY locally, or deploy to an environment with a shared LLM variable group.",
83
+ );
84
+ }
85
+ }
@@ -0,0 +1,4 @@
1
+ resource "postgresql_schema" "demo" {
2
+ database = var.database_name
3
+ name = var.schema_name
4
+ }
@@ -0,0 +1,9 @@
1
+ output "database_name" {
2
+ description = "Database containing the demo schema."
3
+ value = var.database_name
4
+ }
5
+
6
+ output "schema_name" {
7
+ description = "Created demo schema name."
8
+ value = postgresql_schema.demo.name
9
+ }
@@ -0,0 +1,19 @@
1
+ variable "aws_region" {
2
+ description = "AWS region containing the shared Percepta internal database secret."
3
+ type = string
4
+ }
5
+
6
+ variable "database_secret_name" {
7
+ description = "AWS Secrets Manager secret name containing shared Postgres credentials."
8
+ type = string
9
+ }
10
+
11
+ variable "database_name" {
12
+ description = "Database where the demo app schema should be created."
13
+ type = string
14
+ }
15
+
16
+ variable "schema_name" {
17
+ description = "Postgres schema name for this demo app."
18
+ type = string
19
+ }
@@ -0,0 +1,38 @@
1
+ terraform {
2
+ required_version = ">= 1.5.0"
3
+
4
+ required_providers {
5
+ aws = {
6
+ source = "hashicorp/aws"
7
+ version = "~> 5.0"
8
+ }
9
+ postgresql = {
10
+ source = "cyrilgdn/postgresql"
11
+ version = "~> 1.22"
12
+ }
13
+ }
14
+
15
+ backend "kubernetes" {}
16
+ }
17
+
18
+ provider "aws" {
19
+ region = var.aws_region
20
+ }
21
+
22
+ data "aws_secretsmanager_secret_version" "database" {
23
+ secret_id = var.database_secret_name
24
+ }
25
+
26
+ locals {
27
+ database_credentials = jsondecode(data.aws_secretsmanager_secret_version.database.secret_string)
28
+ }
29
+
30
+ provider "postgresql" {
31
+ host = local.database_credentials.host
32
+ port = tonumber(local.database_credentials.port)
33
+ username = local.database_credentials.username
34
+ password = local.database_credentials.password
35
+ sslmode = "require"
36
+ connect_timeout = 15
37
+ superuser = false
38
+ }
@@ -1,28 +0,0 @@
1
- name: Terraform Validate (__APP_NAME__)
2
-
3
- on:
4
- push:
5
- paths:
6
- - "packages/__APP_NAME__/terraform/**"
7
- - ".github/workflows/__APP_NAME__-terraform.yml"
8
-
9
- jobs:
10
- terraform-validate:
11
- name: Validate Terraform
12
- runs-on: ubuntu-latest
13
-
14
- steps:
15
- - name: Checkout repository
16
- uses: actions/checkout@v4
17
-
18
- - name: Setup Terraform
19
- uses: hashicorp/setup-terraform@v3
20
-
21
- - name: Terraform Init
22
- run: terraform -chdir=packages/__APP_NAME__/terraform init -backend=false
23
-
24
- - name: Terraform Format Check
25
- run: terraform fmt -check -recursive packages/__APP_NAME__/terraform/
26
-
27
- - name: Terraform Validate
28
- run: terraform -chdir=packages/__APP_NAME__/terraform validate