@capawesome/cli 4.26.1 → 4.27.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.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,18 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file. See [commit-and-tag-version](https://github.com/absolute-version/commit-and-tag-version) for commit guidelines.
4
4
 
5
+ ## [4.27.0](https://github.com/capawesome-team/cli/compare/v4.26.1...v4.27.0) (2026-10-09)
6
+
7
+
8
+ ### Features
9
+
10
+ * **apps:** add `--apple-api-key-id` to `apps:destinations:create` ([#249](https://github.com/capawesome-team/cli/issues/249)) ([f8efdd8](https://github.com/capawesome-team/cli/commit/f8efdd8ff401ab4dbb1eb954db5ca044b6819501))
11
+
12
+
13
+ ### Bug Fixes
14
+
15
+ * **apps:** upload Apple API key file only after all destination options are validated ([#250](https://github.com/capawesome-team/cli/issues/250)) ([ad0fadf](https://github.com/capawesome-team/cli/commit/ad0fadfa9b38792764d962ad7a5803448082ff1b))
16
+
5
17
  ## [4.26.1](https://github.com/capawesome-team/cli/compare/v4.26.0...v4.26.1) (2026-10-07)
6
18
 
7
19
 
@@ -34,6 +34,7 @@ export default defineCommand({
34
34
  appleTeamId: z.string().optional().describe('Apple Team ID for the destination.'),
35
35
  appleAppPassword: z.string().optional().describe('Apple app-specific password for the destination.'),
36
36
  appleApiKeyFile: z.string().optional().describe('Path to the Apple API key (.p8) file.'),
37
+ appleApiKeyId: z.string().optional().describe('Apple API Key ID for the destination.'),
37
38
  appleIssuerId: z.string().optional().describe('Apple Issuer ID for the destination.'),
38
39
  appleBetaGroup: z
39
40
  .array(z.string())
@@ -77,12 +78,11 @@ export default defineCommand({
77
78
  defaultLanguage: stringOption.describe('Language of the default release notes text, e.g. `en-US` (Google Play and Huawei AppGallery only).'),
78
79
  })),
79
80
  action: withAuth(async (options, args) => {
80
- let { appId, json, name, platform, type, appleId, appleAppId, appleTeamId, appleAppPassword, appleApiKeyFile, appleIssuerId, appleBetaGroup, appleSubmitForReview, appleReleaseType, appleRejectIfPossible, androidPackageName, androidBuildArtifactType, androidReleaseStatus, googleServiceAccountKeyFile, googlePlayTrack, firebaseAppId, firebaseTesterGroup, huaweiAppId, huaweiClientId, huaweiClientSecret, defaultLanguage, } = options;
81
+ let { appId, json, name, platform, type, appleId, appleAppId, appleTeamId, appleAppPassword, appleApiKeyFile, appleApiKeyId, appleIssuerId, appleBetaGroup, appleSubmitForReview, appleReleaseType, appleRejectIfPossible, androidPackageName, androidBuildArtifactType, androidReleaseStatus, googleServiceAccountKeyFile, googlePlayTrack, firebaseAppId, firebaseTesterGroup, huaweiAppId, huaweiClientId, huaweiClientSecret, defaultLanguage, } = options;
81
82
  if (type === 'app-store-connect') {
82
83
  consola.warn('The destination type `app-store-connect` is deprecated. Use `apple-app-store-connect` instead.');
83
84
  type = 'apple-app-store-connect';
84
85
  }
85
- let appleApiKeyId;
86
86
  let appAppleApiKeyId;
87
87
  let appGoogleServiceAccountKeyId;
88
88
  // 1. Select organization and app
@@ -231,7 +231,7 @@ export default defineCommand({
231
231
  if (type === 'apple-app-store-connect') {
232
232
  // 9. Ask for authentication method
233
233
  let authMethod;
234
- if (appleApiKeyFile || appleIssuerId) {
234
+ if (appleApiKeyFile || appleApiKeyId || appleIssuerId) {
235
235
  authMethod = 'apiKey';
236
236
  }
237
237
  else if (appleId || appleAppId || appleAppPassword) {
@@ -252,7 +252,7 @@ export default defineCommand({
252
252
  }
253
253
  }
254
254
  else {
255
- consola.error('You must provide authentication options when running in non-interactive environment. Either pass --apple-api-key-file and --apple-issuer-id for API Key authentication or --apple-id, --apple-app-id, and --apple-app-password for Password authentication.');
255
+ consola.error('You must provide authentication options when running in non-interactive environment. Either pass --apple-api-key-file, --apple-api-key-id, and --apple-issuer-id for API Key authentication or --apple-id, --apple-app-id, and --apple-app-password for Password authentication.');
256
256
  process.exit(1);
257
257
  }
258
258
  if (authMethod === 'apiKey') {
@@ -270,20 +270,6 @@ export default defineCommand({
270
270
  process.exit(1);
271
271
  }
272
272
  }
273
- // Upload Apple API key file
274
- const appleApiKeyFileReadable = await isReadable(appleApiKeyFile);
275
- if (!appleApiKeyFileReadable) {
276
- consola.error(`The Apple API key file does not exist or is not accessible: ${appleApiKeyFile}`);
277
- process.exit(1);
278
- }
279
- const buffer = fs.readFileSync(appleApiKeyFile);
280
- const fileName = path.basename(appleApiKeyFile);
281
- const key = await appAppleApiKeysService.create({
282
- appId,
283
- buffer,
284
- fileName,
285
- });
286
- appAppleApiKeyId = key.id;
287
273
  // 11. Ask for key ID
288
274
  if (!appleApiKeyId) {
289
275
  if (!isInteractive()) {
@@ -359,6 +345,9 @@ export default defineCommand({
359
345
  process.exit(1);
360
346
  }
361
347
  }
348
+ if (appleApiKeyFile) {
349
+ appAppleApiKeyId = await uploadAppleApiKeyFile(appId, appleApiKeyFile);
350
+ }
362
351
  }
363
352
  const response = await appDestinationsService.create({
364
353
  appId,
@@ -491,3 +480,18 @@ const uploadGoogleServiceAccountKeyFile = async (appId, googleServiceAccountKeyF
491
480
  });
492
481
  return key.id;
493
482
  };
483
+ const uploadAppleApiKeyFile = async (appId, appleApiKeyFile) => {
484
+ const appleApiKeyFileReadable = await isReadable(appleApiKeyFile);
485
+ if (!appleApiKeyFileReadable) {
486
+ consola.error(`The Apple API key file does not exist or is not accessible: ${appleApiKeyFile}`);
487
+ process.exit(1);
488
+ }
489
+ const buffer = fs.readFileSync(appleApiKeyFile);
490
+ const fileName = path.basename(appleApiKeyFile);
491
+ const key = await appAppleApiKeysService.create({
492
+ appId,
493
+ buffer,
494
+ fileName,
495
+ });
496
+ return key.id;
497
+ };
@@ -25,7 +25,7 @@ describe('apps-destinations-create', () => {
25
25
  const appId = 'app-123';
26
26
  const destinationId = 'destination-456';
27
27
  const googleServiceAccountKeyId = 'key-789';
28
- const appleApiKeyId = 'apple-key-012';
28
+ const appAppleApiKeyId = 'apple-key-012';
29
29
  const testToken = 'test-token';
30
30
  let tempDirectory;
31
31
  let googleServiceAccountKeyFile;
@@ -100,7 +100,7 @@ describe('apps-destinations-create', () => {
100
100
  const keyScope = nock(DEFAULT_API_BASE_URL)
101
101
  .post(`/v1/apps/${appId}/apple-api-keys`)
102
102
  .matchHeader('Authorization', `Bearer ${testToken}`)
103
- .reply(201, { id: appleApiKeyId });
103
+ .reply(201, { id: appAppleApiKeyId });
104
104
  const destinationScope = nock(DEFAULT_API_BASE_URL)
105
105
  .post(`/v1/apps/${appId}/destinations`, {
106
106
  appId,
@@ -110,7 +110,7 @@ describe('apps-destinations-create', () => {
110
110
  appleTeamId: 'team-id',
111
111
  appleApiKeyId: 'ABC123DEFG',
112
112
  appleIssuerId: 'issuer-id',
113
- appAppleApiKeyId: appleApiKeyId,
113
+ appAppleApiKeyId,
114
114
  appleBetaGroups: ['External Testers', 'QA'],
115
115
  appleRejectIfPossible: false,
116
116
  appleReleaseType: 'after-approval',
@@ -123,6 +123,70 @@ describe('apps-destinations-create', () => {
123
123
  expect(destinationScope.isDone()).toBe(true);
124
124
  expect(mockPrompt).toHaveBeenCalledTimes(1);
125
125
  });
126
+ it('should create an App Store Connect destination with the Apple API key ID non-interactively', async () => {
127
+ const appleApiKeyFile = path.join(tempDirectory, 'AuthKey.p8');
128
+ await fs.writeFile(appleApiKeyFile, 'private-key');
129
+ const options = {
130
+ appId,
131
+ name: 'App Store',
132
+ platform: 'ios',
133
+ type: 'apple-app-store-connect',
134
+ appleApiKeyFile,
135
+ appleApiKeyId: 'ABC123DEFG',
136
+ appleIssuerId: 'issuer-id',
137
+ appleTeamId: 'team-id',
138
+ };
139
+ const keyScope = nock(DEFAULT_API_BASE_URL)
140
+ .post(`/v1/apps/${appId}/apple-api-keys`)
141
+ .matchHeader('Authorization', `Bearer ${testToken}`)
142
+ .reply(201, { id: appAppleApiKeyId });
143
+ const destinationScope = nock(DEFAULT_API_BASE_URL)
144
+ .post(`/v1/apps/${appId}/destinations`, {
145
+ appId,
146
+ name: 'App Store',
147
+ platform: 'ios',
148
+ type: 'apple-app-store-connect',
149
+ appleTeamId: 'team-id',
150
+ appleApiKeyId: 'ABC123DEFG',
151
+ appleIssuerId: 'issuer-id',
152
+ appAppleApiKeyId,
153
+ })
154
+ .matchHeader('Authorization', `Bearer ${testToken}`)
155
+ .reply(201, { id: destinationId });
156
+ await createDestinationCommand.action(options, undefined);
157
+ expect(keyScope.isDone()).toBe(true);
158
+ expect(destinationScope.isDone()).toBe(true);
159
+ expect(mockPrompt).not.toHaveBeenCalled();
160
+ });
161
+ it('should require the Apple API key file when only the Apple API key ID is provided non-interactively', async () => {
162
+ const options = {
163
+ appId,
164
+ name: 'App Store',
165
+ platform: 'ios',
166
+ type: 'apple-app-store-connect',
167
+ appleApiKeyId: 'ABC123DEFG',
168
+ };
169
+ await expect(createDestinationCommand.action(options, undefined)).rejects.toThrow('Process exited with code 1');
170
+ expect(mockConsola.error).toHaveBeenCalledWith('You must provide the Apple API key file when running in non-interactive environment.');
171
+ });
172
+ it('should not upload the Apple API key file when required options are missing non-interactively', async () => {
173
+ const appleApiKeyFile = path.join(tempDirectory, 'AuthKey.p8');
174
+ await fs.writeFile(appleApiKeyFile, 'private-key');
175
+ const options = {
176
+ appId,
177
+ name: 'App Store',
178
+ platform: 'ios',
179
+ type: 'apple-app-store-connect',
180
+ appleApiKeyFile,
181
+ appleApiKeyId: 'ABC123DEFG',
182
+ appleIssuerId: 'issuer-id',
183
+ };
184
+ const keyScope = nock(DEFAULT_API_BASE_URL)
185
+ .post(`/v1/apps/${appId}/apple-api-keys`)
186
+ .reply(201, { id: appAppleApiKeyId });
187
+ await expect(createDestinationCommand.action(options, undefined)).rejects.toThrow('Process exited with code 1');
188
+ expect(keyScope.isDone()).toBe(false);
189
+ });
126
190
  it('should map the deprecated destination type app-store-connect to apple-app-store-connect', async () => {
127
191
  mockIsInteractive.mockReturnValue(true);
128
192
  mockPrompt.mockResolvedValueOnce('ABC123DEFG');
@@ -144,7 +208,7 @@ describe('apps-destinations-create', () => {
144
208
  const keyScope = nock(DEFAULT_API_BASE_URL)
145
209
  .post(`/v1/apps/${appId}/apple-api-keys`)
146
210
  .matchHeader('Authorization', `Bearer ${testToken}`)
147
- .reply(201, { id: appleApiKeyId });
211
+ .reply(201, { id: appAppleApiKeyId });
148
212
  const destinationScope = nock(DEFAULT_API_BASE_URL)
149
213
  .post(`/v1/apps/${appId}/destinations`, {
150
214
  appId,
@@ -154,7 +218,7 @@ describe('apps-destinations-create', () => {
154
218
  appleTeamId: 'team-id',
155
219
  appleApiKeyId: 'ABC123DEFG',
156
220
  appleIssuerId: 'issuer-id',
157
- appAppleApiKeyId: appleApiKeyId,
221
+ appAppleApiKeyId,
158
222
  appleBetaGroups: ['External Testers', 'QA'],
159
223
  appleRejectIfPossible: false,
160
224
  appleReleaseType: 'after-approval',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@capawesome/cli",
3
- "version": "4.26.1",
3
+ "version": "4.27.0",
4
4
  "description": "The Capawesome Cloud Command Line Interface (CLI) to manage Live Updates and more.",
5
5
  "type": "module",
6
6
  "scripts": {