@contrail/cli 3.5.5-alpha.960e382 → 3.5.5

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/README.md CHANGED
@@ -17,7 +17,7 @@ $ npm install -g @contrail/cli
17
17
  $ contrail COMMAND
18
18
  running command...
19
19
  $ contrail (--version)
20
- @contrail/cli/3.5.5-alpha.960e382 linux-x64 node-v24.21.0
20
+ @contrail/cli/3.5.5 linux-x64 node-v24.21.0
21
21
  $ contrail --help [COMMAND]
22
22
  USAGE
23
23
  $ contrail COMMAND
@@ -34,5 +34,9 @@ export declare function uploadFeatureFlagsIfPermitted(dir: string): Promise<void
34
34
  *
35
35
  * Failures are collected and rethrown so an import never reports success while the org is
36
36
  * missing flags.
37
+ *
38
+ * A file that lists no flags at all is skipped rather than applied: reconciling against an
39
+ * empty list would disable every flag in the target org, which is almost never what a copy
40
+ * is meant to do. Flags that should be off in the target have to be disabled by hand.
37
41
  */
38
42
  export declare function uploadFeatureFlags(dir: string): Promise<void>;
@@ -107,6 +107,10 @@ exports.uploadFeatureFlagsIfPermitted = uploadFeatureFlagsIfPermitted;
107
107
  *
108
108
  * Failures are collected and rethrown so an import never reports success while the org is
109
109
  * missing flags.
110
+ *
111
+ * A file that lists no flags at all is skipped rather than applied: reconciling against an
112
+ * empty list would disable every flag in the target org, which is almost never what a copy
113
+ * is meant to do. Flags that should be off in the target have to be disabled by hand.
110
114
  */
111
115
  async function uploadFeatureFlags(dir) {
112
116
  const filePath = path.join(dir, export_1.FEATURE_FLAGS_FILE);
@@ -114,11 +118,16 @@ async function uploadFeatureFlags(dir) {
114
118
  logger_1.default.info(`${chalk_1.default.yellow.bold('FEATURE FLAGS')} No feature flags found in ${dir}. Skipping.`);
115
119
  return;
116
120
  }
117
- const data = (0, file_helpers_1.loadData)(filePath);
121
+ const data = (0, file_helpers_1.loadData)(filePath) ?? [];
118
122
  if (!Array.isArray(data)) {
119
123
  throw new Error(`${filePath} must contain a YAML list of feature flags`);
120
124
  }
121
125
  const featureNamesToEnable = new Set(data.map(toFeatureName).filter(Boolean));
126
+ if (featureNamesToEnable.size === 0) {
127
+ logger_1.default.info(`${chalk_1.default.yellow.bold('FEATURE FLAGS')} ${filePath} lists no feature flags. Skipping, so ` +
128
+ 'the flags currently enabled in this org are left as they are.');
129
+ return;
130
+ }
122
131
  const existingFlags = (await (0, feature_flags_1.getFeatureFlags)()) ?? [];
123
132
  const enabledFeatureNames = new Set(existingFlags.map((flag) => flag?.featureName));
124
133
  const toCreate = [...featureNamesToEnable].filter((name) => !enabledFeatureNames.has(name));
@@ -58,6 +58,26 @@ describe('uploadFeatureFlags', () => {
58
58
  expect(mocked(feature_flags_1.getFeatureFlags)).not.toHaveBeenCalled();
59
59
  expect(mocked(feature_flags_1.createFeatureFlag)).not.toHaveBeenCalled();
60
60
  });
61
+ it('skips an export that lists no flags instead of disabling everything in the target org', async () => {
62
+ mocked(file_helpers_1.loadData).mockReturnValue([]);
63
+ mocked(feature_flags_1.getFeatureFlags).mockResolvedValue([
64
+ { id: '1', featureName: 'SHOWROOM' },
65
+ { id: '2', featureName: 'DEV_TABS' },
66
+ ]);
67
+ const infoSpy = jest.spyOn(logger_1.default, 'info').mockImplementation(() => undefined);
68
+ await (0, import_1.uploadFeatureFlags)('export-dir');
69
+ expect(mocked(feature_flags_1.deleteFeatureFlag)).not.toHaveBeenCalled();
70
+ expect(mocked(feature_flags_1.createFeatureFlag)).not.toHaveBeenCalled();
71
+ expect(infoSpy).toHaveBeenCalledWith(expect.stringContaining('lists no feature flags'));
72
+ infoSpy.mockRestore();
73
+ });
74
+ it('treats a blank feature-flags.yml the same as an empty list', async () => {
75
+ mocked(file_helpers_1.loadData).mockReturnValue(undefined);
76
+ mocked(feature_flags_1.getFeatureFlags).mockResolvedValue([{ id: '1', featureName: 'SHOWROOM' }]);
77
+ await (0, import_1.uploadFeatureFlags)('export-dir');
78
+ expect(mocked(feature_flags_1.getFeatureFlags)).not.toHaveBeenCalled();
79
+ expect(mocked(feature_flags_1.deleteFeatureFlag)).not.toHaveBeenCalled();
80
+ });
61
81
  it('enables every flag from the export that the target org is missing', async () => {
62
82
  mocked(file_helpers_1.loadData).mockReturnValue([{ featureName: 'SHOWROOM' }, { featureName: 'PROMOTE' }]);
63
83
  mocked(feature_flags_1.getFeatureFlags).mockResolvedValue([{ id: '1', featureName: 'SHOWROOM' }]);
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "3.5.5-alpha.960e382",
2
+ "version": "3.5.5",
3
3
  "commands": {
4
4
  "app:create": {
5
5
  "id": "app:create",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@contrail/cli",
3
- "version": "3.5.5-alpha.960e382",
3
+ "version": "3.5.5",
4
4
  "description": "VibeIQ's Contrail Platform CLI.",
5
5
  "bin": {
6
6
  "contrail-cli": "./bin/run",