@mittwald/cli 1.11.0 → 1.11.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.
@@ -32,9 +32,10 @@ export function configureAxiosRetry(axios) {
32
32
  return true;
33
33
  }
34
34
  const isSafeRequest = error.config?.method?.toLowerCase() === "get";
35
+ const isConditionalRequest = !!error.config?.headers?.["if-event-reached"];
35
36
  const isPreconditionFailed = error.response?.status === 412;
36
37
  const isAccessDenied = error.response?.status === 403;
37
- if (isPreconditionFailed) {
38
+ if (isPreconditionFailed && isConditionalRequest) {
38
39
  return true;
39
40
  }
40
41
  return isSafeRequest && isAccessDenied && shouldRetryAccessDenied;
@@ -52,6 +52,7 @@ export class RenderBaseCommand extends ExtendedBaseCommand {
52
52
  useIncreaseInkStdoutColumns();
53
53
  return this.render();
54
54
  } }) }) }) }) }));
55
+ await handle.waitUntilExit();
55
56
  }
56
57
  useAppInstallationId(command) {
57
58
  return usePromise(() => this.withAppInstallationId(command), []);
@@ -14,8 +14,15 @@ async function setEnvironmentFromEnvFile(service) {
14
14
  return service;
15
15
  }
16
16
  const enriched = structuredClone(service);
17
- const envFileContent = await readFile(service.env_file, "utf-8");
18
- const envVars = parse(envFileContent);
17
+ const envFiles = Array.isArray(service.env_file)
18
+ ? service.env_file
19
+ : [service.env_file];
20
+ let envVars = {};
21
+ for (const envFile of envFiles) {
22
+ const envFileContent = await readFile(envFile, "utf-8");
23
+ const fileEnvVars = parse(envFileContent);
24
+ envVars = { ...envVars, ...fileEnvVars };
25
+ }
19
26
  delete enriched.env_file;
20
27
  enriched.envs = {
21
28
  ...envVars,
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,167 @@
1
+ import { describe, expect, it, jest, beforeEach } from "@jest/globals";
2
+ const mockReadFile = jest.fn();
3
+ jest.unstable_mockModule("fs/promises", () => ({
4
+ readFile: mockReadFile,
5
+ }));
6
+ const { enrichStackDefinition } = await import("./enrich.js");
7
+ describe("enrichStackDefinition", () => {
8
+ beforeEach(() => {
9
+ jest.clearAllMocks();
10
+ });
11
+ it("should handle service without env_file", async () => {
12
+ const input = {
13
+ services: {
14
+ nginx: {
15
+ image: "nginx:latest",
16
+ ports: ["80:80"],
17
+ },
18
+ },
19
+ };
20
+ const result = await enrichStackDefinition(input);
21
+ expect(result).toEqual(input);
22
+ expect(mockReadFile).not.toHaveBeenCalled();
23
+ });
24
+ it("should handle single env_file as string", async () => {
25
+ mockReadFile.mockResolvedValueOnce("FOO=bar\nBAZ=qux");
26
+ const input = {
27
+ services: {
28
+ nginx: {
29
+ image: "nginx:latest",
30
+ env_file: ".env",
31
+ },
32
+ },
33
+ };
34
+ const result = await enrichStackDefinition(input);
35
+ expect(mockReadFile).toHaveBeenCalledWith(".env", "utf-8");
36
+ expect(result).toEqual({
37
+ services: {
38
+ nginx: {
39
+ image: "nginx:latest",
40
+ envs: {
41
+ FOO: "bar",
42
+ BAZ: "qux",
43
+ },
44
+ },
45
+ },
46
+ });
47
+ });
48
+ it("should handle env_file as array", async () => {
49
+ mockReadFile
50
+ .mockResolvedValueOnce("FOO=bar\nBAZ=qux")
51
+ .mockResolvedValueOnce("FOO=overridden\nNEW=value");
52
+ const input = {
53
+ services: {
54
+ nginx: {
55
+ image: "nginx:latest",
56
+ env_file: [".env", ".env.local"],
57
+ },
58
+ },
59
+ };
60
+ const result = await enrichStackDefinition(input);
61
+ expect(mockReadFile).toHaveBeenCalledWith(".env", "utf-8");
62
+ expect(mockReadFile).toHaveBeenCalledWith(".env.local", "utf-8");
63
+ expect(result).toEqual({
64
+ services: {
65
+ nginx: {
66
+ image: "nginx:latest",
67
+ envs: {
68
+ FOO: "overridden", // Later file should override
69
+ BAZ: "qux",
70
+ NEW: "value",
71
+ },
72
+ },
73
+ },
74
+ });
75
+ });
76
+ it("should merge env_file variables with existing envs", async () => {
77
+ mockReadFile.mockResolvedValueOnce("FOO=from_file\nFILE_VAR=file_value");
78
+ const input = {
79
+ services: {
80
+ nginx: {
81
+ image: "nginx:latest",
82
+ env_file: ".env",
83
+ envs: {
84
+ FOO: "existing", // Should override env_file
85
+ EXISTING_VAR: "existing_value",
86
+ },
87
+ },
88
+ },
89
+ };
90
+ const result = await enrichStackDefinition(input);
91
+ expect(result).toEqual({
92
+ services: {
93
+ nginx: {
94
+ image: "nginx:latest",
95
+ envs: {
96
+ FOO: "existing", // Existing envs take precedence
97
+ FILE_VAR: "file_value",
98
+ EXISTING_VAR: "existing_value",
99
+ },
100
+ },
101
+ },
102
+ });
103
+ });
104
+ it("should handle multiple services with different env_file configurations", async () => {
105
+ mockReadFile
106
+ .mockResolvedValueOnce("NGINX_VAR=nginx_value")
107
+ .mockResolvedValueOnce("APP_VAR=app_value1")
108
+ .mockResolvedValueOnce("APP_VAR=app_value2\nOTHER=other");
109
+ const input = {
110
+ services: {
111
+ nginx: {
112
+ image: "nginx:latest",
113
+ env_file: ".env.nginx",
114
+ },
115
+ app: {
116
+ image: "app:latest",
117
+ env_file: [".env.app", ".env.app.local"],
118
+ },
119
+ db: {
120
+ image: "postgres:latest",
121
+ // No env_file
122
+ },
123
+ },
124
+ };
125
+ const result = await enrichStackDefinition(input);
126
+ expect(result).toEqual({
127
+ services: {
128
+ nginx: {
129
+ image: "nginx:latest",
130
+ envs: {
131
+ NGINX_VAR: "nginx_value",
132
+ },
133
+ },
134
+ app: {
135
+ image: "app:latest",
136
+ envs: {
137
+ APP_VAR: "app_value2", // Later file overrides
138
+ OTHER: "other",
139
+ },
140
+ },
141
+ db: {
142
+ image: "postgres:latest",
143
+ },
144
+ },
145
+ });
146
+ });
147
+ it("should handle empty env files", async () => {
148
+ mockReadFile.mockResolvedValueOnce("");
149
+ const input = {
150
+ services: {
151
+ nginx: {
152
+ image: "nginx:latest",
153
+ env_file: ".env",
154
+ },
155
+ },
156
+ };
157
+ const result = await enrichStackDefinition(input);
158
+ expect(result).toEqual({
159
+ services: {
160
+ nginx: {
161
+ image: "nginx:latest",
162
+ envs: {},
163
+ },
164
+ },
165
+ });
166
+ });
167
+ });
@@ -3,7 +3,7 @@ type ContainerServiceDeclareRequest = MittwaldAPIV2.Components.Schemas.Container
3
3
  export type ContainerServiceInput = ContainerServiceDeclareRequest & {
4
4
  command?: string[] | string;
5
5
  entrypoint?: string[] | string;
6
- env_file?: string;
6
+ env_file?: string | string[];
7
7
  environment?: {
8
8
  [k: string]: string;
9
9
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mittwald/cli",
3
- "version": "1.11.0",
3
+ "version": "1.11.2",
4
4
  "description": "Hand-crafted CLI for the mittwald API",
5
5
  "license": "MIT",
6
6
  "author": {
@@ -56,7 +56,7 @@
56
56
  "envfile": "^7.1.0",
57
57
  "fast-xml-parser": "^5.2.5",
58
58
  "ink": "^6.2.3",
59
- "ink-link": "^4.0.0",
59
+ "ink-link": "^5.0.0",
60
60
  "ink-text-input": "^6.0.0",
61
61
  "js-yaml": "^4.1.0",
62
62
  "marked": "^15.0.12",