@vaharoni/devops 1.1.0 → 1.1.2

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 (116) hide show
  1. package/dist/cli/init.d.ts.map +1 -1
  2. package/dist/cli/init.js +196 -46
  3. package/dist/libs/init-generator.d.ts +41 -0
  4. package/dist/libs/init-generator.d.ts.map +1 -0
  5. package/dist/libs/init-generator.js +123 -0
  6. package/package.json +3 -1
  7. package/src/cli/init.ts +221 -48
  8. package/src/libs/init-generator.ts +165 -0
  9. package/src/target-templates/README.md +1 -0
  10. package/src/target-templates/cluster-resource-options/README.md +2 -0
  11. package/src/target-templates/{.devops → cluster-resource-options}/postgres/production/configurations/07-SGObjectStorage.yaml +1 -1
  12. package/src/target-templates/{.devops → cluster-resource-options}/postgres/production/configurations/08-SGScript.yaml +1 -1
  13. package/src/target-templates/{.devops → cluster-resource-options}/postgres/staging/configurations/07-SGObjectStorage.yaml +1 -1
  14. package/src/target-templates/{.devops → cluster-resource-options}/postgres/staging/configurations/08-SGScript.yaml +1 -1
  15. package/src/target-templates/infra-variants/README.md +2 -0
  16. package/src/target-templates/infra-variants/digitalocean/.devops/config/constants.yaml +18 -0
  17. package/src/target-templates/infra-variants/digitalocean/.github/workflows/k8s-build.yaml +73 -0
  18. package/src/target-templates/infra-variants/gcloud/.devops/config/constants.yaml +15 -0
  19. package/src/target-templates/infra-variants/gcloud/.devops/manifests/ingress.yaml.hb +22 -0
  20. package/src/target-templates/infra-variants/gcloud/.github/workflows/k8s-build.yaml +77 -0
  21. package/src/target-templates/{.devops → infra-variants/hetzner/.devops}/config/constants.yaml +4 -4
  22. package/src/target-templates/{.devops → infra-variants/hetzner/.devops}/infra/hetzner/abandoned/harbor-values.yaml +2 -2
  23. package/src/target-templates/{.devops → infra-variants/hetzner/.devops}/infra/hetzner/abandoned/hcloud-config.yaml +1 -1
  24. package/src/target-templates/{.devops → infra-variants/hetzner/.devops}/infra/hetzner/harbor-cert.yaml +2 -2
  25. package/src/target-templates/{.devops → infra-variants/hetzner/.devops}/infra/hetzner/harbor-values.yaml +2 -2
  26. package/src/target-templates/{.devops → infra-variants/hetzner/.devops}/infra/hetzner/hcloud-config.yaml +1 -1
  27. package/src/target-templates/{.github → infra-variants/hetzner/.github}/workflows/k8s-build.yaml +0 -4
  28. package/src/target-templates/lang-variants-common/README.md +4 -0
  29. package/src/target-templates/{.devops → lang-variants-common/python/.devops}/config/images.yaml +4 -4
  30. package/src/target-templates/{.devops → lang-variants-common/python/.devops}/manifests/prefect.yaml.hb +1 -0
  31. package/src/target-templates/{pyproject.toml → lang-variants-common/python/pyproject.toml} +1 -1
  32. package/src/target-templates/lang-variants-common/typescript/.devops/config/images.yaml +69 -0
  33. package/src/target-templates/lang-variants-common/typescript/.devops/manifests/_index.yaml +19 -0
  34. package/src/target-templates/{.devops → lang-variants-common/typescript/.devops}/manifests/cron-jobs.yaml.hb +1 -0
  35. package/src/target-templates/{.devops → lang-variants-common/typescript/.devops}/manifests/db-migrate-job.yaml.hb +1 -0
  36. package/src/target-templates/{.devops → lang-variants-common/typescript/.devops}/manifests/deployment-debug.yaml.hb +1 -0
  37. package/src/target-templates/{.devops → lang-variants-common/typescript/.devops}/manifests/deployment-process.yaml.hb +1 -0
  38. package/src/target-templates/{.devops → lang-variants-common/typescript/.devops}/manifests/deployment-web.yaml.hb +1 -0
  39. package/src/target-templates/lang-variants-common/typescript/.github/actions/connect-to-gke@v1/action.yaml +43 -0
  40. package/src/target-templates/{.github → lang-variants-common/typescript/.github}/actions/connect-to-infra@v1/action.yaml +22 -1
  41. package/src/target-templates/lang-variants-prisma/README.md +3 -0
  42. package/src/target-templates/config/.env.development +0 -1
  43. package/src/target-templates/config/.env.global +0 -4
  44. package/src/target-templates/config/.env.test +0 -1
  45. package/src/target-templates/libs/example-node-lib/bun.lock +0 -27
  46. /package/src/target-templates/{.devops/infra/test.yaml → cluster-resource-options/dns-test/dns-test.yaml} +0 -0
  47. /package/src/target-templates/{.devops → cluster-resource-options}/milvus/production/milvus-values.yaml +0 -0
  48. /package/src/target-templates/{.devops → cluster-resource-options}/milvus/staging/milvus-values.yaml +0 -0
  49. /package/src/target-templates/{.devops/infra → cluster-resource-options/monitoring-ingress}/monitoring-ingress.yaml +0 -0
  50. /package/src/target-templates/{.devops/postgres/DailyOperatorRestart.yaml → cluster-resource-options/postgres/daily-operator-restart.yaml} +0 -0
  51. /package/src/target-templates/{.devops → cluster-resource-options}/postgres/production/cluster/PodDisruptionBudget.yaml +0 -0
  52. /package/src/target-templates/{.devops → cluster-resource-options}/postgres/production/cluster/SGCluster.yaml +0 -0
  53. /package/src/target-templates/{.devops → cluster-resource-options}/postgres/production/cluster/StackGres-alerts.yaml +0 -0
  54. /package/src/target-templates/{.devops → cluster-resource-options}/postgres/production/configurations/06-SGDistributedLogs.yaml +0 -0
  55. /package/src/target-templates/{.devops/infra → cluster-resource-options/postgres}/stackgres-ui-ingress.yaml +0 -0
  56. /package/src/target-templates/{.devops → cluster-resource-options}/postgres/staging/cluster/SGCluster.yaml +0 -0
  57. /package/src/target-templates/{.devops → cluster-resource-options}/prefect/production/prefect-values.yaml +0 -0
  58. /package/src/target-templates/{.devops → cluster-resource-options}/prefect/staging/prefect-values.yaml +0 -0
  59. /package/src/target-templates/{.devops → cluster-resource-options}/redis/production/redis-values.yaml +0 -0
  60. /package/src/target-templates/{.devops → cluster-resource-options}/redis/staging/redis-values.yaml +0 -0
  61. /package/src/target-templates/{.devops → infra-variants/hetzner/.devops}/infra/hetzner/cert-manager.yaml +0 -0
  62. /package/src/target-templates/{.devops → infra-variants/hetzner/.devops}/infra/hetzner/ingress-nginx-annotations.yaml +0 -0
  63. /package/src/target-templates/{.devops → infra-variants/hetzner/.devops}/infra/hetzner/ingress-nginx-configmap.yaml +0 -0
  64. /package/src/target-templates/{.devops → infra-variants/hetzner/.devops}/infra/hetzner/retain-storage-class.yaml +0 -0
  65. /package/src/target-templates/{.devops → lang-variants-common/python/.devops}/docker-images/python-services/python-exec.sh +0 -0
  66. /package/src/target-templates/{.devops → lang-variants-common/python/.devops}/docker-images/python-services/python-run.sh +0 -0
  67. /package/src/target-templates/{.devops → lang-variants-common/python/.devops}/docker-images/python-services.Dockerfile +0 -0
  68. /package/src/target-templates/{.devops → lang-variants-common/python/.devops}/manifests/_index.yaml +0 -0
  69. /package/src/target-templates/{applications → lang-variants-common/python/applications}/example-data-pipeline/pyproject.toml +0 -0
  70. /package/src/target-templates/{applications → lang-variants-common/python/applications}/example-data-pipeline/src/example_data_pipeline/main.py +0 -0
  71. /package/src/target-templates/{applications → lang-variants-common/python/applications}/example-python/pyproject.toml +0 -0
  72. /package/src/target-templates/{applications → lang-variants-common/python/applications}/example-python/src/example_python/__init__.py +0 -0
  73. /package/src/target-templates/{applications → lang-variants-common/python/applications}/example-python/src/example_python/main.py +0 -0
  74. /package/src/target-templates/{applications → lang-variants-common/python/applications}/example-python/src/example_python/scripts.py +0 -0
  75. /package/src/target-templates/{applications → lang-variants-common/python/applications}/example-python/tests/__init__.py +0 -0
  76. /package/src/target-templates/{devopspy → lang-variants-common/python/devopspy} +0 -0
  77. /package/src/target-templates/{libs → lang-variants-common/python/libs}/example-python-lib/pyproject.toml +0 -0
  78. /package/src/target-templates/{libs → lang-variants-common/python/libs}/example-python-lib/src/example_python_lib/__init__.py +0 -0
  79. /package/src/target-templates/{.devops → lang-variants-common/typescript/.devops}/docker-images/common/docker-common.sh +0 -0
  80. /package/src/target-templates/{.devops → lang-variants-common/typescript/.devops}/docker-images/node-services/node-exec.sh +0 -0
  81. /package/src/target-templates/{.devops → lang-variants-common/typescript/.devops}/docker-images/node-services/node-run.sh +0 -0
  82. /package/src/target-templates/{.devops → lang-variants-common/typescript/.devops}/docker-images/node-services.Dockerfile +0 -0
  83. /package/src/target-templates/{.devops → lang-variants-common/typescript/.devops}/env.example.yaml +0 -0
  84. /package/src/target-templates/{.devops → lang-variants-common/typescript/.devops}/manifests/ingress.yaml.hb +0 -0
  85. /package/src/target-templates/{.devops → lang-variants-common/typescript/.devops}/manifests/service.yaml.hb +0 -0
  86. /package/src/target-templates/{.github → lang-variants-common/typescript/.github}/actions/build-image@v1/action.yaml +0 -0
  87. /package/src/target-templates/{.github → lang-variants-common/typescript/.github}/actions/connect-to-digital-ocean@v1/action.yaml +0 -0
  88. /package/src/target-templates/{.github → lang-variants-common/typescript/.github}/actions/connect-to-hetzner@v1/action.yaml +0 -0
  89. /package/src/target-templates/{.github → lang-variants-common/typescript/.github}/actions/db-migrate@v1/action.yaml +0 -0
  90. /package/src/target-templates/{.github → lang-variants-common/typescript/.github}/actions/deploy-image@v1/action.yaml +0 -0
  91. /package/src/target-templates/{.github → lang-variants-common/typescript/.github}/actions/setup-prereq@v1/action.yaml +0 -0
  92. /package/src/target-templates/{applications → lang-variants-common/typescript/applications}/example-node/index.ts +0 -0
  93. /package/src/target-templates/{applications → lang-variants-common/typescript/applications}/example-node/package.json +0 -0
  94. /package/src/target-templates/{applications → lang-variants-common/typescript/applications}/example-node/tsconfig.json +0 -0
  95. /package/src/target-templates/{applications → lang-variants-common/typescript/applications}/jobs/README.md +0 -0
  96. /package/src/target-templates/{applications → lang-variants-common/typescript/applications}/jobs/index.ts +0 -0
  97. /package/src/target-templates/{applications → lang-variants-common/typescript/applications}/jobs/package.json +0 -0
  98. /package/src/target-templates/{applications → lang-variants-common/typescript/applications}/jobs/tsconfig.json +0 -0
  99. /package/src/target-templates/{devops → lang-variants-common/typescript/devops} +0 -0
  100. /package/src/target-templates/{libs → lang-variants-common/typescript/libs}/example-node-lib/index.ts +0 -0
  101. /package/src/target-templates/{libs → lang-variants-common/typescript/libs}/example-node-lib/package.json +0 -0
  102. /package/src/target-templates/{libs → lang-variants-common/typescript/libs}/example-node-lib/tsconfig.json +0 -0
  103. /package/src/target-templates/{tmp → lang-variants-common/typescript/tmp}/.gitkeep +0 -0
  104. /package/src/target-templates/{tsconfig.json → lang-variants-common/typescript/tsconfig.json} +0 -0
  105. /package/src/target-templates/{db → lang-variants-prisma/python/db}/db/__init__.py +0 -0
  106. /package/src/target-templates/{db → lang-variants-prisma/python/db}/db/db_client_test.py +0 -0
  107. /package/src/target-templates/{db → lang-variants-prisma/python/db}/pyproject.toml +0 -0
  108. /package/src/target-templates/{db → lang-variants-prisma/typescript/db}/db-client-test.ts +0 -0
  109. /package/src/target-templates/{db → lang-variants-prisma/typescript/db}/db-client.ts +0 -0
  110. /package/src/target-templates/{db → lang-variants-prisma/typescript/db}/env.yaml +0 -0
  111. /package/src/target-templates/{db → lang-variants-prisma/typescript/db}/package.json +0 -0
  112. /package/src/target-templates/{db → lang-variants-prisma/typescript/db}/prisma/schema.prisma +0 -0
  113. /package/src/target-templates/{db → lang-variants-prisma/typescript/db}/prisma-setup-vitest.ts +0 -0
  114. /package/src/target-templates/{db → lang-variants-prisma/typescript/db}/tsconfig.json +0 -0
  115. /package/src/target-templates/{dml → lang-variants-prisma/typescript/dml}/package.json +0 -0
  116. /package/src/target-templates/{dml → lang-variants-prisma/typescript/dml}/tsconfig.json +0 -0
package/src/cli/init.ts CHANGED
@@ -1,13 +1,9 @@
1
- import url from "url";
2
- import path from "path";
3
- import fs from "fs-extra";
4
-
5
- const __file__ = url.fileURLToPath(import.meta.url);
6
- const __root__ = path.join(path.dirname(__file__), "../..");
7
- const templatesDir = path.join(__root__, "src/target-templates");
8
- const targetDir = process.cwd(); // User's current working directory
9
-
1
+ import inquirer from "inquirer";
2
+ import { InitGenerator, type InitGeneratorFileInfo } from "../libs/init-generator";
10
3
  import { CLICommandParser, printUsageAndExit } from "./common";
4
+ import chalk from "chalk";
5
+ import fs from 'fs-extra';
6
+ import type { ConstFileSchema } from "../types";
11
7
 
12
8
  const oneLiner =
13
9
  "Initializes the devops utility by copying template files to the current folder";
@@ -25,56 +21,233 @@ EXAMPLES
25
21
 
26
22
  async function run(cmdObj: CLICommandParser) {
27
23
  if (cmdObj.help) printUsageAndExit(usage);
28
- copyTemplates();
24
+ createFiles();
29
25
  }
30
26
 
31
27
  export default {
32
28
  init: { oneLiner, keyExamples, run },
33
29
  };
34
30
 
35
- async function copyTemplates() {
36
- console.log(`Initializing devops files from ${templatesDir} to ${targetDir}`);
37
-
38
- try {
39
- // Copy files without overriding existing ones
40
- await fs.copy(templatesDir, targetDir, {
41
- overwrite: false,
42
- errorOnExist: false,
43
- dereference: false,
44
- });
45
-
46
- console.log(successMessage);
47
- } catch (error) {
48
- console.error("❌ Failed to initialize devops files:", error);
49
- process.exit(1);
31
+ async function createFiles() {
32
+ const tc = new InitGenerator();
33
+ const userChoices = await getUserChoices(tc.projectName);
34
+
35
+ // Language variants
36
+ tc.addCopiedFolder("lang-variants-common/typescript", ".");
37
+ if (userChoices.usePython) {
38
+ tc.addCopiedFolder("lang-variants-common/python", ".");
39
+ tc.enableSubtitution("pyproject.toml");
40
+ }
41
+ tc.enableSubtitution(".devops/config/images.yaml");
42
+ tc.setMessageGenerator(".envrc", envrcMessage);
43
+
44
+ // gitignore
45
+ const gitIgnore = gitIgnoreContent(userChoices.infraVariant, userChoices.usePython)
46
+ tc.addGeneratedFile(".gitignore", gitIgnore);
47
+ tc.setMessageGenerator(".gitignore", gitignoreMessageGen(gitIgnore));
48
+
49
+ // Infra variants
50
+ tc.addCopiedFolder(`infra-variants/${userChoices.infraVariant}`, ".");
51
+ tc.enableSubtitution(".devops/config/constants.yaml");
52
+ if (userChoices.infraVariant === "hetzner") {
53
+ tc.enableSubtitution(".devops/infra/hetzner/harbor-cert.yaml");
54
+ tc.enableSubtitution(".devops/infra/hetzner/harbor-values.yaml");
55
+ tc.enableSubtitution(".devops/infra/hetzner/hcloud-config.yaml");
50
56
  }
51
- }
52
57
 
53
- const successMessage = `
54
- Devops files initialized successfully!
58
+ // Prisma
59
+ if (userChoices.usePrisma) {
60
+ tc.addCopiedFolder("lang-variants-prisma/typescript", ".");
61
+ if (userChoices.usePython) {
62
+ tc.addCopiedFolder("lang-variants-prisma/python", ".");
63
+ }
64
+ }
55
65
 
56
- To finish the setup:
66
+ // Cluster resources
67
+ const clusterResources = new Set(userChoices.clusterResources);
68
+ if (clusterResources.has("dns-test")) {
69
+ tc.addCopiedFolder("cluster-resource-options/dns-test", ".devops/infra/dns-test");
70
+ }
71
+ if (clusterResources.has("monitoring-ingress")) {
72
+ tc.addCopiedFolder("cluster-resource-options/monitoring-ingress", ".devops/infra/monitoring-ingress");
73
+ }
74
+ if (clusterResources.has("postgres")) {
75
+ tc.addCopiedFolder("cluster-resource-options/postgres", ".devops/infra/postgres");
76
+ // prettier-ignore
77
+ tc.enableSubtitution(".devops/infra/postgres/staging/configurations/07-SGObjectStorage.yaml");
78
+ // prettier-ignore
79
+ tc.enableSubtitution(".devops/infra/postgres/staging/configurations/08-SGScript.yaml");
80
+ // prettier-ignore
81
+ tc.enableSubtitution(".devops/infra/postgres/production/configurations/07-SGObjectStorage.yaml");
82
+ // prettier-ignore
83
+ tc.enableSubtitution(".devops/infra/postgres/production/configurations/08-SGScript.yaml");
84
+ }
85
+ if (clusterResources.has("redis")) {
86
+ tc.addCopiedFolder("cluster-resource-options/redis", ".devops/infra/redis");
87
+ }
88
+ if (clusterResources.has("milvus")) {
89
+ tc.addCopiedFolder("cluster-resource-options/milvus", ".devops/infra/milvus");
90
+ }
91
+ if (clusterResources.has("prefect") && userChoices.usePython) {
92
+ tc.addCopiedFolder("cluster-resource-options/prefect", ".devops/infra/prefect");
93
+ }
57
94
 
58
- 1. add the following entry to the main package.json:
59
- "workspaces": [
60
- "libs/**",
61
- "applications/**",
95
+ tc.run({
96
+ substitution: {
97
+ 'PROJECT_NAME': userChoices.projectName,
98
+ 'STAGING_DOMAIN': userChoices.stagingDomain,
99
+ 'PRODUCTION_DOMAIN': userChoices.productionDomain,
100
+ 'GCLOUD_PROJECT_ID': userChoices.gcloudProjectId,
101
+ 'REGISTRY_IMAGE_PATH_PREFIX': userChoices.registryImagePathPrefix,
102
+ 'REGISTRY_BASE_URL': userChoices.registryBaseUrl,
103
+ },
104
+ messages: [
105
+ packageJsonMessage(userChoices.usePrisma)
106
+ ]
107
+ })
108
+ }
109
+
110
+ function packageJsonMessage(usePrisma: boolean) {
111
+ const prismaMessage = usePrisma
112
+ ? `,
62
113
  "db/**",
63
- "dml/**"
64
- ],
114
+ "dml/**"`
115
+ : "";
65
116
 
66
- 2. add the following to your .gitignore:
67
- **/.env*
117
+ return `add the following entry to the main ${chalk.blue("package.json")}:
118
+ ${chalk.yellow(`"workspaces": [
119
+ "libs/**",
120
+ "applications/**"${prismaMessage}
121
+ ],`)}`
122
+ }
123
+
124
+ function gitIgnoreContent(infraVariant: UserChoices["infraVariant"], usePython: boolean) {
125
+ const common = `**/.env*
68
126
  config/kubeconfig
69
127
  tmp/**
70
- !tmp/**/.gitkeep
71
- venv/
72
- **/__pycache__
73
-
74
- 3. optionally create an .envrc file with the following content and run direnv allow:
75
- if [ -f "$PWD/config/kubeconfig" ]; then
76
- export KUBECONFIG="$PWD/config/kubeconfig"
77
- else
78
- export KUBECONFIG="$HOME/.kube/config"
79
- fi
80
- `;
128
+ !tmp/**/.gitkeep`;
129
+
130
+ const gcloud = infraVariant === 'gcloud'
131
+ ? 'config/gke_gcloud_auth_plugin_cache'
132
+ : null;
133
+
134
+ const python = usePython
135
+ ? `venv/
136
+ **/__pycache__`
137
+ : null;
138
+
139
+ return [common, gcloud, python].filter(Boolean).join('\n');
140
+ }
141
+
142
+ function gitignoreMessageGen(content: string) {
143
+ return (exists: boolean) => {
144
+ if (!exists) return;
145
+ return `add the following to your ${chalk.blue(".gitignore")}:
146
+ ${chalk.yellow(content)}`;
147
+ }
148
+ }
149
+
150
+ function envrcMessage(targetExists: boolean, fileInfo: InitGeneratorFileInfo) {
151
+ if (fileInfo.type !== 'copied') throw new Error(`envrcMessage() expects a copied file, got ${fileInfo.type}`);
152
+ if (targetExists) {
153
+ const content = fs.readFileSync(fileInfo.sourceAbs, 'utf-8');
154
+ return `add the following to your ${chalk.blue(".envrc")} and run ${chalk.yellow("direnv allow")}:
155
+ ${chalk.yellow(content)}`;
156
+ } else {
157
+ return `Enable ${chalk.blue(".envrc")} by installing ${chalk.blue('direnv')} and running ${chalk.yellow("direnv allow")}`;
158
+ }
159
+ }
160
+
161
+ type UserChoices = {
162
+ projectName: string;
163
+ stagingDomain: string;
164
+ productionDomain: string;
165
+ infraVariant: ConstFileSchema["infra"];
166
+ gcloudProjectId?: string;
167
+ registryImagePathPrefix?: string;
168
+ registryBaseUrl?: string;
169
+ usePython: boolean;
170
+ usePrisma: boolean;
171
+ clusterResources: string[];
172
+ };
173
+
174
+ function getUserChoices(projectName: string | undefined): Promise<UserChoices> {
175
+ const defaultProjectName = projectName || "changeme";
176
+
177
+ return inquirer.prompt([
178
+ {
179
+ type: "input",
180
+ name: "projectName",
181
+ message: `Enter the project name (default: '${defaultProjectName}')`,
182
+ default: defaultProjectName,
183
+ },
184
+ {
185
+ type: "input",
186
+ name: "stagingDomain",
187
+ message: "Enter the staging domain (default: 'staging.com')",
188
+ default: "staging.com",
189
+ },
190
+ {
191
+ type: "input",
192
+ name: "productionDomain",
193
+ message: "Enter the production domain (default: 'production.com')",
194
+ default: "production.com",
195
+ },
196
+ {
197
+ type: "list",
198
+ name: "infraVariant",
199
+ message: "Where does your cluster run?",
200
+ choices: [
201
+ { name: "Google Cloud", value: "gcloud" },
202
+ { name: "Digital Ocean", value: "digitalocean" },
203
+ { name: "Hetzner", value: "hetzner" },
204
+ ],
205
+ },
206
+ {
207
+ type: "input",
208
+ name: "gcloudProjectId",
209
+ message: "Enter the GCP project ID (default: 'changeme')",
210
+ default: "changeme",
211
+ when: (answers) => answers.infraVariant === "gcloud",
212
+ },
213
+ {
214
+ type: "input",
215
+ name: "registryImagePathPrefix",
216
+ message: (answers) => `Enter your Digital Ocean container registry name (default: '${answers.projectName}')`,
217
+ default: (answers) => answers.projectName,
218
+ when: (answers) => answers.infraVariant === "digitalocean",
219
+ },
220
+ {
221
+ type: "input",
222
+ name: "registryBaseUrl",
223
+ message: (answers) => `Enter your registry base URL (default: 'registry.${answers.stagingDomain}')`,
224
+ default: (answers) => `registry.${answers.stagingDomain}`,
225
+ when: (answers) => answers.infraVariant === "hetzner",
226
+ },
227
+ {
228
+ type: "confirm",
229
+ name: "usePython",
230
+ message: "Add support for Python?",
231
+ default: true,
232
+ },
233
+ {
234
+ type: "confirm",
235
+ name: "usePrisma",
236
+ message: "Add support for Prisma?",
237
+ default: true,
238
+ },
239
+ {
240
+ type: "checkbox",
241
+ name: "clusterResources",
242
+ message: "Optional manifests and helm charts to add",
243
+ choices: (answers) => [
244
+ { name: "Manifest to test DNS setup", value: "dns-test" },
245
+ { name: "Manifest to setup ingress for graphana and prometheus", value: "monitoring-ingress" },
246
+ { name: "Stackgres CRDs and manifests for Postgres", value: "postgres" },
247
+ { name: "Redis Helm chart values", value: "redis" },
248
+ { name: "Milvus helm chart values", value: "milvus" },
249
+ ...(answers.usePython ? [{ name: "Prefect Helm chart values", value: "prefect" }] : [])
250
+ ]
251
+ }
252
+ ])
253
+ }
@@ -0,0 +1,165 @@
1
+ import url from "url";
2
+ import path from "path";
3
+ import fs from "fs-extra";
4
+ import chalk from "chalk";
5
+ import fg from 'fast-glob';
6
+
7
+ const __file__ = url.fileURLToPath(import.meta.url);
8
+ const __root__ = path.join(path.dirname(__file__), "../..");
9
+ const templatesDir = path.join(__root__, "src/target-templates");
10
+ const targetDir = process.cwd(); // User's current working directory
11
+
12
+ type MessageGeneratorFn = (targetExists: boolean, fileInfo: InitGeneratorFileInfo) => string | null | undefined;
13
+
14
+ type CommonFileInfo = {
15
+ targetRel: string;
16
+ targetAbs: string;
17
+ targetFolderAbs: string;
18
+ targetExists: boolean;
19
+ messageGenerator?: MessageGeneratorFn;
20
+ }
21
+
22
+ export type InitGeneratorCopiedFileInfo = CommonFileInfo &{
23
+ type: "copied";
24
+ sourceRel: string;
25
+ sourceAbs: string;
26
+ enableSubstitution?: boolean;
27
+ }
28
+
29
+ type InitGeneratorGeneratedFileInfo = CommonFileInfo & {
30
+ type: "generated";
31
+ content: string;
32
+ }
33
+
34
+ export type InitGeneratorFileInfo = InitGeneratorCopiedFileInfo | InitGeneratorGeneratedFileInfo;
35
+
36
+ export class InitGenerator {
37
+ projectName?: string;
38
+
39
+ /** The key is targetRel */
40
+ files: Record<string, InitGeneratorFileInfo> = {};
41
+
42
+ constructor() {
43
+ if (fs.existsSync("package.json")) {
44
+ const packageJson = fs.readJSONSync("package.json");
45
+ this.projectName = packageJson.name;
46
+ }
47
+ }
48
+
49
+ _ensureFileExists(targetRel: string) {
50
+ if (!this.files[targetRel]) {
51
+ throw new Error(`File for target "${targetRel}" not found.`);
52
+ }
53
+ }
54
+
55
+ enableSubtitution(targetRel: string) {
56
+ this._ensureFileExists(targetRel);
57
+ if (this.files[targetRel].type !== "copied") {
58
+ throw new Error(`File for target "${targetRel}" is not a copied file.`);
59
+ }
60
+ this.files[targetRel].enableSubstitution = true;
61
+ }
62
+
63
+ setMessageGenerator(targetRel: string, messageGen: MessageGeneratorFn) {
64
+ this._ensureFileExists(targetRel);
65
+ this.files[targetRel].messageGenerator = messageGen;
66
+ }
67
+
68
+ addGeneratedFile(targetRel: string, content: string) {
69
+ const targetAbs = path.join(targetDir, targetRel);
70
+ const targetFolderAbs = path.dirname(targetAbs);
71
+ const exists = fs.existsSync(targetAbs);
72
+ this.files[targetRel] = {
73
+ type: "generated",
74
+ targetRel,
75
+ targetAbs,
76
+ targetFolderAbs,
77
+ targetExists: exists,
78
+ content,
79
+ };
80
+ }
81
+
82
+ /**
83
+ * @param source relative path under the templates folder. All files and folders under `source` are copied directly under `target`.
84
+ * @param target relative path under the project root folder.
85
+ * If the target file exists already in `files`, it will be overridden.
86
+ */
87
+ addCopiedFolder(source: string, target: string) {
88
+ const pathPrefix = path.join(templatesDir, source);
89
+ const glob = path.join(pathPrefix, '**/*');
90
+ fg.globSync(glob, { dot: true }).forEach((sourceAbs) => {
91
+ const sourceRel = path.relative(templatesDir, sourceAbs);
92
+ const pathUnderSource = path.relative(pathPrefix, sourceAbs);
93
+ const targetRel = path.join(target, pathUnderSource);
94
+ const targetAbs = path.join(targetDir, targetRel);
95
+ const targetFolderAbs = path.dirname(targetAbs);
96
+ const exists = fs.existsSync(targetAbs);
97
+ this.files[targetRel] = {
98
+ type: "copied",
99
+ sourceRel,
100
+ targetRel,
101
+ sourceAbs,
102
+ targetAbs,
103
+ targetFolderAbs,
104
+ targetExists: exists,
105
+ };
106
+ })
107
+ }
108
+
109
+ run({
110
+ substitution = {},
111
+ messages = [],
112
+ } : {
113
+ substitution?: Record<string, string | undefined>;
114
+ messages?: string[];
115
+ }) {
116
+ const fileMessages: string[] = [];
117
+ Object.values(this.files).forEach((fileInfo) => {
118
+ if (fileInfo.messageGenerator) {
119
+ const message = fileInfo.messageGenerator(fileInfo.targetExists, fileInfo);
120
+ if (message) {
121
+ fileMessages.push(message);
122
+ }
123
+ }
124
+
125
+ if (fileInfo.targetExists) {
126
+ console.log(`Skipped ${chalk.yellow(fileInfo.targetRel)} (exists)`);
127
+ return;
128
+ }
129
+
130
+ // Create or copy
131
+ if (!fs.existsSync(fileInfo.targetFolderAbs)) {
132
+ fs.mkdirSync(fileInfo.targetFolderAbs, { recursive: true });
133
+ }
134
+
135
+ if (fileInfo.type === 'generated') {
136
+ fs.writeFileSync(fileInfo.targetAbs, fileInfo.content, 'utf8');
137
+ } else if (fileInfo.enableSubstitution) {
138
+ const content = fs.readFileSync(fileInfo.sourceAbs, 'utf8');
139
+ const substitutedContent = content.replace(/\$([A-Z_]+)/g, (_, varName) => {
140
+ const value = substitution[varName];
141
+ if (!value) {
142
+ throw new Error(`${chalk.blue("TemplateCopier.run()")}: Variable ${chalk.yellow(varName)} is needed by ${chalk.yellow(fileInfo.targetRel)} but is undefined.`);
143
+ }
144
+ return value;
145
+ });
146
+ fs.writeFileSync(fileInfo.targetAbs, substitutedContent);
147
+ } else {
148
+ fs.copySync(fileInfo.sourceAbs, fileInfo.targetAbs, {
149
+ overwrite: false,
150
+ errorOnExist: false,
151
+ dereference: false,
152
+ });
153
+ }
154
+ console.log(`Created ${chalk.green(fileInfo.targetRel)}`);
155
+ });
156
+
157
+ const allMessages = [...messages, ...fileMessages];
158
+ if (!allMessages.length) return;
159
+
160
+ console.log(chalk.blue("\nNext steps:"));
161
+ allMessages.forEach((msg, i) => {
162
+ console.log(`${i + 1}. ${msg}\n`);
163
+ });
164
+ }
165
+ }
@@ -0,0 +1 @@
1
+ This folder contains the templates that are copied to the target project's folder based on answers the user provides during `./devops init`. Some of these answers may change the content of files, which is done by substituting variables prefixed by `$`. To avoid potential issues, such substitution is not pursued globally for all files in this folder. Files requiring substitution must be referenced by name in `./devops init` code.
@@ -0,0 +1,2 @@
1
+ During `./devops init`, the user is asked to select options for generating manifests of various cluster resources.
2
+ The files of each selected option `$OPTION` is copied under `.devops/infra/$OPTION`.
@@ -6,7 +6,7 @@ metadata:
6
6
  spec:
7
7
  type: s3Compatible
8
8
  s3Compatible:
9
- bucket: changeme-backups
9
+ bucket: $PROJECT_NAME-backups
10
10
  endpoint: https://hel1.your-objectstorage.com
11
11
  awsCredentials:
12
12
  secretKeySelectors:
@@ -9,4 +9,4 @@ spec:
9
9
  scripts:
10
10
  - name: create-database
11
11
  script: |
12
- CREATE DATABASE changeme WITH OWNER postgres;
12
+ CREATE DATABASE $PROJECT_NAME WITH OWNER postgres;
@@ -6,7 +6,7 @@ metadata:
6
6
  spec:
7
7
  type: s3Compatible
8
8
  s3Compatible:
9
- bucket: changeme-backups
9
+ bucket: $PROJECT_NAME-backups
10
10
  endpoint: https://hel1.your-objectstorage.com
11
11
  awsCredentials:
12
12
  secretKeySelectors:
@@ -9,4 +9,4 @@ spec:
9
9
  scripts:
10
10
  - name: create-database
11
11
  script: |
12
- CREATE DATABASE changeme WITH OWNER postgres;
12
+ CREATE DATABASE $PROJECT_NAME WITH OWNER postgres;
@@ -0,0 +1,2 @@
1
+ During `./devops init`, the user is asked to select an infra variant.
2
+ The files under the selected `$OPTION` are copied under the root project folder.
@@ -0,0 +1,18 @@
1
+ # These will be used when generating kubernetes entities
2
+ project-name: $PROJECT_NAME
3
+
4
+ # Supported: hetzner, digitalocean, or gcloud
5
+ infra: digitalocean
6
+
7
+ # Only relevant for Digital Ocean. Determines the number of versions to keep for each docker image.
8
+ image-versions-to-keep: 5
9
+
10
+ registry-base-url: registry.digitalocean.com
11
+ # What comes before <image-name>:<tag>. Can be empty.
12
+ registry-image-path-prefix: $REGISTRY_IMAGE_PATH_PREFIX
13
+
14
+ # production and staging are supported by default
15
+ extra-remote-environments: []
16
+
17
+ # development and test are supported by default
18
+ extra-local-environments: []
@@ -0,0 +1,73 @@
1
+ name: "Monorepo Build and Deploy"
2
+
3
+ on:
4
+ push:
5
+ branches:
6
+ - staging
7
+ - production
8
+
9
+ permissions:
10
+ contents: read
11
+ packages: read
12
+
13
+ jobs:
14
+ build_images:
15
+ runs-on: ubuntu-latest
16
+ strategy:
17
+ matrix:
18
+ include:
19
+ - image_name: main-node
20
+ - image_name: main-python
21
+ cache_path: /root/.cache/uv
22
+ steps:
23
+ # Fetch the last 50 commits so that devops affected works
24
+ - name: Checkout repo and history
25
+ uses: actions/checkout@v4
26
+ with:
27
+ fetch-depth: 50
28
+
29
+ - name: Setup prerequesites
30
+ uses: ./.github/actions/setup-prereq@v1
31
+
32
+ - name: Connect to infrastructure
33
+ uses: ./.github/actions/connect-to-infra@v1
34
+ with:
35
+ do_access_token: ${{ secrets.DIGITALOCEAN_ACCESS_TOKEN }}
36
+ do_cluster_name: ${{ secrets.DIGITALOCEAN_CLUSTER_NAME }}
37
+
38
+ - name: Build image
39
+ uses: ./.github/actions/build-image@v1
40
+ with:
41
+ image_name: ${{ matrix.image_name }}
42
+ cache_path: ${{ matrix.cache_path || '/root/.bun/install/cache' }}
43
+
44
+ db_migrate_and_deploy:
45
+ needs: [build_images]
46
+ runs-on: ubuntu-latest
47
+ steps:
48
+ # Fetch the last 50 commits so that devops affected works
49
+ - name: Checkout repo and history
50
+ uses: actions/checkout@v4
51
+ with:
52
+ fetch-depth: 50
53
+
54
+ - name: Setup prerequesites
55
+ uses: ./.github/actions/setup-prereq@v1
56
+
57
+ - name: Connect to infrastructure
58
+ uses: ./.github/actions/connect-to-infra@v1
59
+ with:
60
+ do_access_token: ${{ secrets.DIGITALOCEAN_ACCESS_TOKEN }}
61
+ do_cluster_name: ${{ secrets.DIGITALOCEAN_CLUSTER_NAME }}
62
+
63
+ - name: Run DB Migrate
64
+ uses: ./.github/actions/db-migrate@v1
65
+
66
+ # Repeat per image (it checks if the image is affected and deploys it if it is)
67
+ - name: Deploy main node
68
+ uses: ./.github/actions/deploy-image@v1
69
+ with: { "image_name": "main-node" }
70
+
71
+ - name: Deploy main python
72
+ uses: ./.github/actions/deploy-image@v1
73
+ with: { "image_name": "main-python" }
@@ -0,0 +1,15 @@
1
+ # These will be used when generating kubernetes entities
2
+ project-name: $PROJECT_NAME
3
+
4
+ # Supported: hetzner, digitalocean, or gcloud
5
+ infra: gcloud
6
+
7
+ registry-base-url: gcr.io
8
+ # What comes before <image-name>:<tag>. Can be empty.
9
+ registry-image-path-prefix: $GCLOUD_PROJECT_ID
10
+
11
+ # production and staging are supported by default
12
+ extra-remote-environments: []
13
+
14
+ # development and test are supported by default
15
+ extra-local-environments: []
@@ -0,0 +1,22 @@
1
+ apiVersion: networking.k8s.io/v1
2
+ kind: Ingress
3
+ metadata:
4
+ name: {{app_name}}
5
+ namespace: {{namespace}}
6
+ labels:
7
+ app: {{app_name}}
8
+ env: {{monorepo_env}}
9
+ annotations:
10
+ kubernetes.io/ingress.class: "gce"
11
+ spec:
12
+ rules:
13
+ - host: {{subdomain}}.{{domain_name}}
14
+ http:
15
+ paths:
16
+ - path: /
17
+ pathType: Prefix
18
+ backend:
19
+ service:
20
+ name: {{service_name}}
21
+ port:
22
+ number: 80