@git.zone/cli 3.2.0 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (126) hide show
  1. package/dist_ts/00_commitinfo_data.js +2 -2
  2. package/dist_ts/classes.gitzoneconfig.js +1 -1
  3. package/dist_ts/classes.project.js +1 -1
  4. package/dist_ts/gitzone.cli.js +1 -1
  5. package/dist_ts/gitzone.logging.js +1 -1
  6. package/dist_ts/helpers.changelog.js +1 -1
  7. package/dist_ts/helpers.climode.js +1 -1
  8. package/dist_ts/helpers.smartconfig.js +61 -11
  9. package/dist_ts/helpers.smartconfigmigrations.js +1 -1
  10. package/dist_ts/helpers.tsdocker.js +1 -1
  11. package/dist_ts/helpers.workflow.js +1 -1
  12. package/dist_ts/index.js +1 -1
  13. package/dist_ts/mod_audit/index.js +1 -1
  14. package/dist_ts/mod_commit/index.js +1 -1
  15. package/dist_ts/mod_commit/mod.helpers.js +1 -1
  16. package/dist_ts/mod_commit/mod.plugins.js +1 -1
  17. package/dist_ts/mod_commit/mod.ui.js +1 -1
  18. package/dist_ts/mod_config/classes.commitconfig.js +1 -1
  19. package/dist_ts/mod_config/classes.releaseconfig.js +1 -1
  20. package/dist_ts/mod_config/index.js +2 -2
  21. package/dist_ts/mod_config/mod.plugins.js +1 -1
  22. package/dist_ts/mod_deprecate/index.js +1 -1
  23. package/dist_ts/mod_deprecate/mod.plugins.js +1 -1
  24. package/dist_ts/mod_docker/classes.dockerpruner.js +7 -1
  25. package/dist_ts/mod_docker/index.js +1 -1
  26. package/dist_ts/mod_docker/mod.plugins.js +1 -1
  27. package/dist_ts/mod_format/classes.baseformatter.js +1 -1
  28. package/dist_ts/mod_format/classes.diffreporter.js +1 -1
  29. package/dist_ts/mod_format/classes.formatcontext.js +1 -1
  30. package/dist_ts/mod_format/classes.formatplanner.js +1 -1
  31. package/dist_ts/mod_format/classes.formatstats.js +1 -1
  32. package/dist_ts/mod_format/formatters/assets.formatter.js +1 -1
  33. package/dist_ts/mod_format/formatters/cleanup.formatter.js +1 -1
  34. package/dist_ts/mod_format/formatters/copy.formatter.js +1 -1
  35. package/dist_ts/mod_format/formatters/gitignore.formatter.js +1 -1
  36. package/dist_ts/mod_format/formatters/license.formatter.js +1 -1
  37. package/dist_ts/mod_format/formatters/packagejson.formatter.js +1 -1
  38. package/dist_ts/mod_format/formatters/prettier.formatter.js +1 -1
  39. package/dist_ts/mod_format/formatters/readme.formatter.js +1 -1
  40. package/dist_ts/mod_format/formatters/smartconfig.formatter.js +1 -1
  41. package/dist_ts/mod_format/formatters/templates.formatter.js +1 -1
  42. package/dist_ts/mod_format/formatters/tsconfig.formatter.js +1 -1
  43. package/dist_ts/mod_format/index.js +1 -1
  44. package/dist_ts/mod_format/interfaces.format.js +1 -1
  45. package/dist_ts/mod_format/mod.plugins.js +1 -1
  46. package/dist_ts/mod_helpers/index.js +1 -1
  47. package/dist_ts/mod_helpers/mod.plugins.js +1 -1
  48. package/dist_ts/mod_meta/index.js +1 -1
  49. package/dist_ts/mod_meta/meta.classes.meta.js +1 -1
  50. package/dist_ts/mod_meta/meta.interfaces.js +1 -1
  51. package/dist_ts/mod_meta/meta.plugins.js +1 -1
  52. package/dist_ts/mod_open/index.js +1 -1
  53. package/dist_ts/mod_open/mod.plugins.js +1 -1
  54. package/dist_ts/mod_release/index.js +4 -4
  55. package/dist_ts/mod_release/mod.plugins.js +1 -1
  56. package/dist_ts/mod_services/classes.dockercontainer.d.ts +50 -3
  57. package/dist_ts/mod_services/classes.dockercontainer.js +646 -84
  58. package/dist_ts/mod_services/classes.globalregistry.d.ts +10 -24
  59. package/dist_ts/mod_services/classes.globalregistry.js +210 -62
  60. package/dist_ts/mod_services/classes.globalregistrystore.d.ts +48 -0
  61. package/dist_ts/mod_services/classes.globalregistrystore.js +156 -0
  62. package/dist_ts/mod_services/classes.interprocesslock.d.ts +21 -0
  63. package/dist_ts/mod_services/classes.interprocesslock.js +141 -0
  64. package/dist_ts/mod_services/classes.serviceconfiguration.d.ts +15 -12
  65. package/dist_ts/mod_services/classes.serviceconfiguration.js +188 -91
  66. package/dist_ts/mod_services/classes.servicedatamarker.d.ts +32 -7
  67. package/dist_ts/mod_services/classes.servicedatamarker.js +145 -49
  68. package/dist_ts/mod_services/classes.servicemanager.d.ts +70 -28
  69. package/dist_ts/mod_services/classes.servicemanager.js +995 -421
  70. package/dist_ts/mod_services/classes.servicenames.d.ts +8 -0
  71. package/dist_ts/mod_services/classes.servicenames.js +54 -0
  72. package/dist_ts/mod_services/classes.serviceoptions.js +1 -1
  73. package/dist_ts/mod_services/classes.servicepruner.d.ts +3 -0
  74. package/dist_ts/mod_services/classes.servicepruner.js +208 -46
  75. package/dist_ts/mod_services/helpers.js +1 -1
  76. package/dist_ts/mod_services/index.d.ts +2 -0
  77. package/dist_ts/mod_services/index.js +81 -58
  78. package/dist_ts/mod_services/mod.plugins.js +1 -1
  79. package/dist_ts/mod_standard/index.js +2 -2
  80. package/dist_ts/mod_standard/mod.plugins.js +1 -1
  81. package/dist_ts/mod_start/index.js +1 -1
  82. package/dist_ts/mod_start/mod.plugins.js +1 -1
  83. package/dist_ts/mod_template/index.js +1 -1
  84. package/dist_ts/mod_template/mod.plugins.js +1 -1
  85. package/dist_ts/mod_tools/classes.packagemanager.js +1 -1
  86. package/dist_ts/mod_tools/index.js +1 -1
  87. package/dist_ts/mod_tools/mod.plugins.js +1 -1
  88. package/dist_ts/paths.js +1 -1
  89. package/dist_ts/plugins.d.ts +3 -1
  90. package/dist_ts/plugins.js +4 -2
  91. package/dist_ts_migration/001.service-selection.d.ts +9 -0
  92. package/dist_ts_migration/001.service-selection.js +51 -0
  93. package/dist_ts_migration/002.runtime-config.d.ts +19 -0
  94. package/dist_ts_migration/002.runtime-config.js +170 -0
  95. package/dist_ts_migration/003.service-data-marker.d.ts +9 -0
  96. package/dist_ts_migration/003.service-data-marker.js +156 -0
  97. package/dist_ts_migration/004.global-registry.d.ts +13 -0
  98. package/dist_ts_migration/004.global-registry.js +165 -0
  99. package/dist_ts_migration/index.d.ts +11 -0
  100. package/dist_ts_migration/index.js +59 -0
  101. package/package.json +6 -3
  102. package/readme.hints.md +102 -25
  103. package/readme.md +123 -28
  104. package/readme.plan.md +29 -16
  105. package/ts/00_commitinfo_data.ts +1 -1
  106. package/ts/helpers.smartconfig.ts +63 -10
  107. package/ts/mod_config/index.ts +1 -1
  108. package/ts/mod_docker/classes.dockerpruner.ts +6 -0
  109. package/ts/mod_release/index.ts +3 -3
  110. package/ts/mod_services/classes.dockercontainer.ts +912 -96
  111. package/ts/mod_services/classes.globalregistry.ts +290 -89
  112. package/ts/mod_services/classes.globalregistrystore.ts +279 -0
  113. package/ts/mod_services/classes.interprocesslock.ts +189 -0
  114. package/ts/mod_services/classes.serviceconfiguration.ts +215 -108
  115. package/ts/mod_services/classes.servicedatamarker.ts +210 -52
  116. package/ts/mod_services/classes.servicemanager.ts +1332 -512
  117. package/ts/mod_services/classes.servicenames.ts +76 -0
  118. package/ts/mod_services/classes.servicepruner.ts +310 -52
  119. package/ts/mod_services/index.ts +89 -62
  120. package/ts/mod_standard/index.ts +1 -1
  121. package/ts/plugins.ts +4 -0
  122. package/ts_migration/001.service-selection.ts +77 -0
  123. package/ts_migration/002.runtime-config.ts +218 -0
  124. package/ts_migration/003.service-data-marker.ts +216 -0
  125. package/ts_migration/004.global-registry.ts +272 -0
  126. package/ts_migration/index.ts +83 -0
@@ -1,4 +1,5 @@
1
1
  import * as plugins from './mod.plugins.js';
2
+ import { withInterProcessLock } from './classes.interprocesslock.js';
2
3
 
3
4
  /**
4
5
  * Canonical service identifiers used across mod_services.
@@ -8,9 +9,13 @@ import * as plugins from './mod.plugins.js';
8
9
  * `@git.zone/tsdeploy` (`deriveRequiredCapabilities`), so the strings and the
9
10
  * array shape must never change here.
10
11
  */
11
- export type TServiceName = 'mongodb' | 'minio' | 'elasticsearch';
12
+ export type TServiceName = 'mongodb' | 'objectstorage' | 'elasticsearch';
13
+ export type TLegacyServiceName = 'minio';
12
14
 
13
- export const serviceNames: TServiceName[] = ['mongodb', 'minio', 'elasticsearch'];
15
+ export const serviceNames: TServiceName[] = ['mongodb', 'objectstorage', 'elasticsearch'];
16
+
17
+ export const objectStorageImage =
18
+ 'code.foss.global/lossless.zone/objectstorage@sha256:43b60897eed17b54feb4e363808ce540ed1d9d27201871c5c8a869300d41e1b8';
14
19
 
15
20
  /** Owner recorded in markers and container labels. */
16
21
  export const serviceDataOwner = '@git.zone/cli';
@@ -20,6 +25,10 @@ export const serviceToolLabel = 'gitzone-services';
20
25
 
21
26
  /** Marker file name, stored in `<projectPath>/.nogit/`. */
22
27
  export const serviceDataMarkerFile = '.gitzone-services.json';
28
+ export const serviceDataMarkerSchemaVersion = 2;
29
+ export const legacyMinioDataDirectoryName = 'miniodata';
30
+ export const serviceOperationLabel = 'git.zone.operation';
31
+ export const serviceBindOperation = 'service-bind-operation';
23
32
 
24
33
  /**
25
34
  * Directory basename used below `.nogit/` for each service's bind-mounted data.
@@ -28,18 +37,18 @@ export const serviceDataMarkerFile = '.gitzone-services.json';
28
37
  */
29
38
  export const serviceDataDirectoryNames: { [key in TServiceName]: string } = {
30
39
  mongodb: 'mongodata',
31
- minio: 'miniodata',
40
+ objectstorage: 'objectstoragedata',
32
41
  elasticsearch: 'esdata',
33
42
  };
34
43
 
35
44
  /**
36
45
  * Image used to run each service, and therefore the image available to perform
37
- * a privileged cleanup of that service's data directory: if the data exists,
46
+ * a root-helper cleanup of that service's data directory: if the data exists,
38
47
  * the service ran, so its image is present locally.
39
48
  */
40
49
  export const serviceImages: { [key in TServiceName]: string } = {
41
50
  mongodb: 'mongo:7.0',
42
- minio: 'minio/minio',
51
+ objectstorage: objectStorageImage,
43
52
  elasticsearch: 'elasticsearch:8.11.0',
44
53
  };
45
54
 
@@ -51,22 +60,36 @@ export interface IServiceDataMarkerEntry {
51
60
  export interface IServiceDataMarker {
52
61
  owner: string;
53
62
  kind: 'service-data';
54
- schemaVersion: 1;
63
+ schemaVersion: 2;
55
64
  projectPath: string;
56
65
  safeToPrune: true;
57
66
  createdAt: string;
58
67
  dataDirectories: IServiceDataMarkerEntry[];
68
+ legacyDataDirectories?: ILegacyServiceDataMarkerEntry[];
69
+ }
70
+
71
+ export interface ILegacyServiceDataMarkerEntry {
72
+ service: TLegacyServiceName;
73
+ path: string;
74
+ safeToPrune: false;
59
75
  }
60
76
 
61
77
  const isPlainObject = (value: unknown): value is Record<string, any> => {
62
78
  return typeof value === 'object' && value !== null && !Array.isArray(value);
63
79
  };
64
80
 
81
+ const hasOnlyKeys = (valueArg: Record<string, any>, allowedKeysArg: readonly string[]): boolean =>
82
+ Object.keys(valueArg).every((keyArg) => allowedKeysArg.includes(keyArg));
83
+
65
84
  /** Absolute path of the marker file for a project. */
66
85
  export const getServiceDataMarkerPath = (projectPathArg: string): string => {
67
86
  return plugins.path.join(plugins.path.resolve(projectPathArg), '.nogit', serviceDataMarkerFile);
68
87
  };
69
88
 
89
+ export const getServiceDataMarkerLockPath = (projectPathArg: string): string => {
90
+ return `${getServiceDataMarkerPath(projectPathArg)}.lock`;
91
+ };
92
+
70
93
  /**
71
94
  * Expected absolute data directory path for a service in a project.
72
95
  * `mod_services` must always derive data paths through this helper so that
@@ -83,8 +106,17 @@ export const getServiceDataDirectory = (
83
106
  );
84
107
  };
85
108
 
109
+ /** Exact legacy path retained only for migration detection. */
110
+ export const getLegacyMinioDataDirectory = (projectPathArg: string): string => {
111
+ return plugins.path.join(
112
+ plugins.path.resolve(projectPathArg),
113
+ '.nogit',
114
+ legacyMinioDataDirectoryName,
115
+ );
116
+ };
117
+
86
118
  /**
87
- * True only when `candidatePathArg` is exactly one of the three data
119
+ * True only when `candidatePathArg` is exactly one of the canonical data
88
120
  * directories `gitzone services` creates for `projectPathArg`.
89
121
  *
90
122
  * This is an allowlist, not a denylist: any path that is not byte-for-byte one
@@ -104,31 +136,44 @@ export const isSafeServiceDataPath = (
104
136
  return false;
105
137
  };
106
138
 
107
- /** Validate an unknown value as a usable marker. */
139
+ const hasValidMarkerEnvelope = (markerArg: Record<string, any>): boolean => {
140
+ return (
141
+ markerArg.owner === serviceDataOwner &&
142
+ markerArg.kind === 'service-data' &&
143
+ markerArg.safeToPrune === true &&
144
+ typeof markerArg.projectPath === 'string' &&
145
+ markerArg.projectPath.length > 0 &&
146
+ typeof markerArg.createdAt === 'string' &&
147
+ markerArg.createdAt.length > 0 &&
148
+ Array.isArray(markerArg.dataDirectories)
149
+ );
150
+ };
151
+
152
+ /** Validate an unknown value as a canonical v2 marker. */
108
153
  export const isValidServiceDataMarker = (markerArg: unknown): markerArg is IServiceDataMarker => {
109
154
  if (!isPlainObject(markerArg)) {
110
155
  return false;
111
156
  }
112
- if (markerArg.owner !== serviceDataOwner) {
113
- return false;
114
- }
115
- if (markerArg.kind !== 'service-data') {
116
- return false;
117
- }
118
- if (markerArg.schemaVersion !== 1) {
119
- return false;
120
- }
121
- if (markerArg.safeToPrune !== true) {
122
- return false;
123
- }
124
- if (typeof markerArg.projectPath !== 'string' || !markerArg.projectPath) {
125
- return false;
126
- }
127
- if (!Array.isArray(markerArg.dataDirectories)) {
157
+ if (
158
+ !hasOnlyKeys(markerArg, [
159
+ 'owner',
160
+ 'kind',
161
+ 'schemaVersion',
162
+ 'projectPath',
163
+ 'safeToPrune',
164
+ 'createdAt',
165
+ 'dataDirectories',
166
+ 'legacyDataDirectories',
167
+ ]) ||
168
+ !hasValidMarkerEnvelope(markerArg) ||
169
+ markerArg.schemaVersion !== serviceDataMarkerSchemaVersion
170
+ ) {
128
171
  return false;
129
172
  }
173
+ const projectPath = plugins.path.resolve(markerArg.projectPath);
174
+ const seenServices = new Set<TServiceName>();
130
175
  for (const entry of markerArg.dataDirectories) {
131
- if (!isPlainObject(entry)) {
176
+ if (!isPlainObject(entry) || !hasOnlyKeys(entry, ['service', 'path'])) {
132
177
  return false;
133
178
  }
134
179
  if (!serviceNames.includes(entry.service)) {
@@ -137,10 +182,60 @@ export const isValidServiceDataMarker = (markerArg: unknown): markerArg is IServ
137
182
  if (typeof entry.path !== 'string' || !entry.path) {
138
183
  return false;
139
184
  }
185
+ if (
186
+ seenServices.has(entry.service) ||
187
+ plugins.path.resolve(entry.path) !== getServiceDataDirectory(projectPath, entry.service)
188
+ ) {
189
+ return false;
190
+ }
191
+ seenServices.add(entry.service);
192
+ }
193
+ if (markerArg.legacyDataDirectories !== undefined) {
194
+ if (!Array.isArray(markerArg.legacyDataDirectories)) {
195
+ return false;
196
+ }
197
+ for (const entry of markerArg.legacyDataDirectories) {
198
+ if (
199
+ !isPlainObject(entry) ||
200
+ !hasOnlyKeys(entry, ['service', 'path', 'safeToPrune']) ||
201
+ entry.service !== 'minio' ||
202
+ entry.safeToPrune !== false ||
203
+ typeof entry.path !== 'string' ||
204
+ plugins.path.resolve(entry.path) !== getLegacyMinioDataDirectory(projectPath)
205
+ ) {
206
+ return false;
207
+ }
208
+ }
209
+ if (markerArg.legacyDataDirectories.length > 1) {
210
+ return false;
211
+ }
140
212
  }
141
213
  return true;
142
214
  };
143
215
 
216
+ const writeServiceDataMarker = async (
217
+ projectPathArg: string,
218
+ markerArg: IServiceDataMarker,
219
+ ): Promise<void> => {
220
+ const markerPath = getServiceDataMarkerPath(projectPathArg);
221
+ await plugins.smartfs.directory(plugins.path.dirname(markerPath)).recursive().create();
222
+ const tempPath = `${markerPath}.tmp-${process.pid}-${plugins.crypto
223
+ .randomBytes(12)
224
+ .toString('hex')}`;
225
+ try {
226
+ await plugins.fs.writeFile(tempPath, `${JSON.stringify(markerArg, null, 2)}\n`, {
227
+ encoding: 'utf8',
228
+ flag: 'wx',
229
+ });
230
+ await plugins.fs.rename(tempPath, markerPath);
231
+ } catch (error) {
232
+ await plugins.fs.rm(tempPath, { force: true }).catch(() => {});
233
+ throw new Error(`Could not persist service data marker atomically: ${markerPath}`, {
234
+ cause: error,
235
+ });
236
+ }
237
+ };
238
+
144
239
  /** Read and validate the marker for a project, or undefined when absent/invalid. */
145
240
  export const readServiceDataMarker = async (
146
241
  projectPathArg: string,
@@ -152,18 +247,32 @@ export const readServiceDataMarker = async (
152
247
  }
153
248
  const content = (await plugins.smartfs.file(markerPath).encoding('utf8').read()) as string;
154
249
  const parsed = JSON.parse(content);
155
- return isValidServiceDataMarker(parsed) ? parsed : undefined;
250
+ return isValidServiceDataMarker(parsed) &&
251
+ plugins.path.resolve(parsed.projectPath) === plugins.path.resolve(projectPathArg)
252
+ ? parsed
253
+ : undefined;
156
254
  } catch {
157
255
  return undefined;
158
256
  }
159
257
  };
160
258
 
259
+ export const markerProvesServiceDataDirectory = (
260
+ markerArg: IServiceDataMarker | undefined,
261
+ projectPathArg: string,
262
+ serviceArg: TServiceName,
263
+ ): boolean => {
264
+ const expectedPath = getServiceDataDirectory(projectPathArg, serviceArg);
265
+ return !!markerArg?.dataDirectories.some(
266
+ (entry) => entry.service === serviceArg && plugins.path.resolve(entry.path) === expectedPath,
267
+ );
268
+ };
269
+
161
270
  /**
162
271
  * Record that `gitzone services` owns a data directory for this project.
163
272
  *
164
273
  * The marker lives in `.nogit/` rather than inside the bind-mounted data
165
- * directory itself, so it can never confuse mongod, MinIO or Elasticsearch at
166
- * runtime. Entries are merged so enabling a service later does not drop the
274
+ * directory itself, so it can never confuse a service runtime. Entries are
275
+ * merged so enabling a service later does not drop the
167
276
  * claim on previously created directories.
168
277
  */
169
278
  export const recordServiceDataDirectory = async (
@@ -171,31 +280,44 @@ export const recordServiceDataDirectory = async (
171
280
  serviceArg: TServiceName,
172
281
  ): Promise<void> => {
173
282
  const projectPath = plugins.path.resolve(projectPathArg);
174
- const dataPath = getServiceDataDirectory(projectPath, serviceArg);
175
- const existingMarker = await readServiceDataMarker(projectPath);
176
-
177
- const entries: IServiceDataMarkerEntry[] = existingMarker
178
- ? existingMarker.dataDirectories.filter((entry) => entry.service !== serviceArg)
179
- : [];
180
- entries.push({ service: serviceArg, path: dataPath });
181
- entries.sort((entryA, entryB) => entryA.service.localeCompare(entryB.service));
182
-
183
- const marker: IServiceDataMarker = {
184
- owner: serviceDataOwner,
185
- kind: 'service-data',
186
- schemaVersion: 1,
187
- projectPath,
188
- safeToPrune: true,
189
- createdAt: existingMarker?.createdAt || new Date().toISOString(),
190
- dataDirectories: entries,
191
- };
283
+ await withInterProcessLock(
284
+ {
285
+ lockPath: getServiceDataMarkerLockPath(projectPath),
286
+ description: 'Service data marker',
287
+ },
288
+ async () => {
289
+ const dataPath = getServiceDataDirectory(projectPath, serviceArg);
290
+ const markerPath = getServiceDataMarkerPath(projectPath);
291
+ const markerExists = await plugins.smartfs.file(markerPath).exists();
292
+ const existingMarker = await readServiceDataMarker(projectPath);
293
+ if (markerExists && !existingMarker) {
294
+ throw new Error(
295
+ `Refusing to overwrite invalid or foreign service data marker: ${markerPath}`,
296
+ );
297
+ }
192
298
 
193
- const markerPath = getServiceDataMarkerPath(projectPath);
194
- await plugins.smartfs.directory(plugins.path.dirname(markerPath)).recursive().create();
195
- await plugins.smartfs
196
- .file(markerPath)
197
- .encoding('utf8')
198
- .write(`${JSON.stringify(marker, null, 2)}\n`);
299
+ const entries: IServiceDataMarkerEntry[] = existingMarker
300
+ ? existingMarker.dataDirectories.filter((entry) => entry.service !== serviceArg)
301
+ : [];
302
+ entries.push({ service: serviceArg, path: dataPath });
303
+ entries.sort((entryA, entryB) => entryA.service.localeCompare(entryB.service));
304
+
305
+ const marker: IServiceDataMarker = {
306
+ owner: serviceDataOwner,
307
+ kind: 'service-data',
308
+ schemaVersion: serviceDataMarkerSchemaVersion,
309
+ projectPath,
310
+ safeToPrune: true,
311
+ createdAt: existingMarker?.createdAt || new Date().toISOString(),
312
+ dataDirectories: entries,
313
+ ...(existingMarker?.legacyDataDirectories
314
+ ? { legacyDataDirectories: existingMarker.legacyDataDirectories }
315
+ : {}),
316
+ };
317
+
318
+ await writeServiceDataMarker(projectPath, marker);
319
+ },
320
+ );
199
321
  };
200
322
 
201
323
  /**
@@ -219,6 +341,22 @@ export const getServiceContainerLabels = (optionsArg: {
219
341
  };
220
342
  };
221
343
 
344
+ export const hasExactServiceContainerLabels = (
345
+ labelsArg: Record<string, string>,
346
+ expectedArg: Record<string, string>,
347
+ ): boolean => {
348
+ const actualGitZoneLabels = Object.entries(labelsArg).filter(([key]) =>
349
+ key.startsWith('git.zone.'),
350
+ );
351
+ const expectedGitZoneLabels = Object.entries(expectedArg).filter(([key]) =>
352
+ key.startsWith('git.zone.'),
353
+ );
354
+ return (
355
+ actualGitZoneLabels.length === expectedGitZoneLabels.length &&
356
+ expectedGitZoneLabels.every(([key, value]) => labelsArg[key] === value)
357
+ );
358
+ };
359
+
222
360
  /** True when a container's labels prove `gitzone services` created it. */
223
361
  export const isServiceOwnedByLabels = (labelsArg: { [key: string]: string }): boolean => {
224
362
  return (
@@ -226,3 +364,23 @@ export const isServiceOwnedByLabels = (labelsArg: { [key: string]: string }): bo
226
364
  labelsArg['git.zone.safe-to-prune'] === 'true'
227
365
  );
228
366
  };
367
+
368
+ /** Legacy provider evidence is retained for migration, never active lifecycle ownership. */
369
+ export const isLegacyMinioContainerLabels = (labelsArg: {
370
+ [key: string]: string;
371
+ }): boolean => {
372
+ return (
373
+ labelsArg['git.zone.tool'] === serviceToolLabel &&
374
+ labelsArg['git.zone.service'] === 'minio'
375
+ );
376
+ };
377
+
378
+ /** Transient bind-operation containers are owned by their invocation, not by prune. */
379
+ export const isServiceOperationHelperLabels = (labelsArg: {
380
+ [key: string]: string;
381
+ }): boolean => {
382
+ return (
383
+ labelsArg['git.zone.tool'] === serviceToolLabel &&
384
+ labelsArg[serviceOperationLabel] === serviceBindOperation
385
+ );
386
+ };