Skip to content

Commit 5007a4f

Browse files
authored
feat!: add upsert-actions app-scripts command [EXT-5979] (#2332)
* feat: add create-actions app-scripts command * refactor: move resolveManifestFile() to shared utils * docs * refactor: pre validation for function and action IDs * refactor: create-action -> upsert-action * fix: create-actions -> upsert-actions leftovers * fix: error handling + tests * chore: use conventional-bump-prerelease for canary releases * feat!: remove hosted app action functionality from app-scripts BREAKING CHANGE: removes the ability to create hosted app actions from app-scripts * feat!: [BREAKING CHANGE] bump node and npm engines
1 parent b036293 commit 5007a4f

31 files changed

+1205
-264
lines changed

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@
1414
"lint": "lerna run lint",
1515
"build": "lerna run build",
1616
"version": "lerna version --no-private --conventional-commits --create-release github --yes --exact",
17-
"publish-packages": "lerna publish from-git --yes",
18-
"version:canary": "npm run version -- --conventional-prerelease",
17+
"publish-packages": "lerna publish from-git --yes --conventional-commits",
18+
"version:canary": "npm run version -- --conventional-prerelease --conventional-bump-prerelease --force-git-tag",
1919
"publish-packages:canary": "npm run publish-packages -- --canary",
2020
"pre-commit": "lerna run pre-commit",
2121
"prepare": "husky"

packages/contentful--app-scripts/README.md

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -310,3 +310,43 @@ Options:
310310
-m, --manifest-file <path> Contentful app manifest file path
311311
-w, --watch watch for changes
312312
-h, --help display help for command
313+
314+
### Upsert App Actions
315+
316+
Creates or updates Actions for an App using the configuration in a Contentful App Manifest file. Created resources will be synced back to your manifest file.
317+
318+
#### Interactive mode:
319+
320+
In the interactive mode, the CLI will ask for all required options.
321+
322+
> **Example**
323+
>
324+
> ```shell
325+
> $ npx --no-install @contentful/app-scripts upsert-actions
326+
> ```
327+
328+
#### Non-interactive mode:
329+
330+
When passing the `--ci` argument adding all variables as arguments is required.
331+
332+
> **Example**
333+
>
334+
> ```shell
335+
> $ npx --no-install @contentful/app-scripts upsert-actions --ci \
336+
> --manifest-file path/to/contentful-app-manifest.json \
337+
> --organization-id some-org-id \
338+
> --definition-id some-app-def-id \
339+
> --token $MY_CONTENTFUL_PAT
340+
> ```
341+
342+
**Options:**
343+
344+
| Argument | Description | Default value |
345+
| ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------ |
346+
| `--manifest-file` | The path to the Contentful app manifest file | `contentful-app-manifest.json` |
347+
| `--organization-id` | The ID of the organization which the app is defined in | |
348+
| `--definition-id` | The ID of the app to which to add the actions | |
349+
| `--token` | A personal [access token](https://www.contentful.com/developers/docs/references/content-management-api/#/reference/personal-access-tokens) | |
350+
| `--host` | (optional) Contentful CMA-endpoint to use | `api.contentful.com` |
351+
352+
**Note:** You can also pass all arguments in interactive mode to skip being asked for it.

packages/contentful--app-scripts/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@
1010
"directory": "packages/contentful--app-scripts"
1111
},
1212
"engines": {
13-
"node": ">=14",
14-
"npm": ">=6"
13+
"node": ">=18",
14+
"npm": ">=9"
1515
},
1616
"main": "lib/index.js",
1717
"types": "lib/index.d.ts",

packages/contentful--app-scripts/src/bin.ts

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
install,
1313
buildFunctions,
1414
generateFunction,
15+
upsertActions,
1516
} from './index';
1617
import { feedback } from './feedback';
1718

@@ -23,7 +24,8 @@ type Command =
2324
| typeof open
2425
| typeof install
2526
| typeof buildFunctions
26-
| typeof generateFunction;
27+
| typeof generateFunction
28+
| typeof upsertActions;
2729

2830
async function runCommand(command: Command, options?: any) {
2931
const { ci } = program.opts();
@@ -46,7 +48,7 @@ async function runCommand(command: Command, options?: any) {
4648
.description('Upload your build folder and create an AppBundle')
4749
.option('--bundle-dir [directory]', 'The directory of your build folder')
4850
.option('--organization-id [orgId]', 'The id of your organization')
49-
.option('--definition-id [defId]', 'The id of your apps definition')
51+
.option('--definition-id [defId]', 'The id of your app\'s definition')
5052
.option('--token [accessToken]', 'Your content management access token')
5153
.option('--comment [comment]', 'Optional comment for the created bundle')
5254
.option('--skip-activation', 'A Boolean flag to skip automatic activation')
@@ -60,7 +62,7 @@ async function runCommand(command: Command, options?: any) {
6062
.description('Mark an AppBundle as "active" for a given AppDefinition')
6163
.option('--bundle-id [bundleId]', 'The id of your bundle')
6264
.option('--organization-id [orgId]', 'The id of your organization')
63-
.option('--definition-id [defId]', 'The id of your apps definition')
65+
.option('--definition-id [defId]', 'The id of your app\'s definition')
6466
.option('--token [accessToken]', 'Your content management access token')
6567
.option('--host [host]', 'Contentful subdomain to use, e.g. "api.contentful.com"')
6668
.action(async (options) => {
@@ -70,7 +72,7 @@ async function runCommand(command: Command, options?: any) {
7072
program
7173
.command('open-settings')
7274
.description('Opens the app editor for a given AppDefinition')
73-
.option('--definition-id [defId]', 'The id of your apps definition')
75+
.option('--definition-id [defId]', 'The id of your app\'s definition')
7476
.option('--host [host]', 'Contentful subdomain to use, e.g. "api.contentful.com"')
7577
.action(async (options) => {
7678
await runCommand(open, options);
@@ -80,7 +82,7 @@ async function runCommand(command: Command, options?: any) {
8082
.command('bundle-cleanup')
8183
.description('Removes old, non-active bundles, only keeps the 50 most recent ones')
8284
.option('--organization-id [orgId]', 'The id of your organization')
83-
.option('--definition-id [defId]', 'The id of your apps definition')
85+
.option('--definition-id [defId]', 'The id of your app\'s definition')
8486
.option('--token [accessToken]', 'Your content management access token')
8587
.option('--keep [keepAmount]', 'The amount of bundles that should remain')
8688
.option('--host [host]', 'Contentful subdomain to use, e.g. "api.contentful.com"')
@@ -100,7 +102,7 @@ async function runCommand(command: Command, options?: any) {
100102
.description(
101103
'Opens a picker to select the space and environment for installing the app associated with a given AppDefinition'
102104
)
103-
.option('--definition-id [defId]', 'The id of your apps definition')
105+
.option('--definition-id [defId]', 'The id of your app\'s definition')
104106
.option('--host [host]', 'Contentful subdomain to use, e.g. "api.contentful.com"')
105107
.action(async (options) => {
106108
await runCommand(install, options);
@@ -127,6 +129,18 @@ async function runCommand(command: Command, options?: any) {
127129
await runCommand(generateFunction, options);
128130
});
129131

132+
program
133+
.command('upsert-actions')
134+
.description('Upsert Action(s) for an App')
135+
.option('-m, --manifest-file <path>', 'Contentful app manifest file path')
136+
.option('--organization-id [orgId]', 'The id of your organization')
137+
.option('--definition-id [defId]', 'The id of your app\'s definition')
138+
.option('--token [accessToken]', 'Your content management access token')
139+
.option('--host [host]', 'Contentful subdomain to use, e.g. "api.contentful.com"')
140+
.action(async (options) => {
141+
await runCommand(upsertActions, options);
142+
});
143+
130144
program.hook('preAction', (thisCommand) => {
131145
track({ command: thisCommand.args[0], ci: thisCommand.opts().ci });
132146
});

packages/contentful--app-scripts/src/build-functions/__tests__/build-functions.test.ts

Lines changed: 3 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
/* eslint-disable @typescript-eslint/no-var-requires */
2+
import assert from 'assert';
23
import path from 'path';
34
import { type BuildFunctionsOptions } from '../../types';
4-
import { resolveEsBuildConfig, resolveManifestFile, validateFunctions } from '../build-functions';
5-
import assert from 'assert';
5+
import { resolveEsBuildConfig, validateFunctions } from '../build-functions';
66

77
function assertPartialObject(actual: any, expected: any) {
88
for (const key in expected) {
@@ -14,28 +14,6 @@ function assertPartialObject(actual: any, expected: any) {
1414
}
1515
}
1616

17-
describe('resolveManifestFile', () => {
18-
it('should resolve the default manifest file if none is provided', async () => {
19-
const options: BuildFunctionsOptions = {};
20-
const manifest = await resolveManifestFile(options, __dirname);
21-
assert.strictEqual(Boolean(manifest), true);
22-
assert.strictEqual('functions' in manifest, true);
23-
assert.strictEqual(manifest.functions.length, 1);
24-
assert.strictEqual(manifest.functions[0].name, 'default');
25-
});
26-
27-
it('should resolve the manifest file at the provided path if present', async () => {
28-
const options: BuildFunctionsOptions = {
29-
manifestFile: 'fixtures/another-contentful-app-manifest.json',
30-
};
31-
const manifest = await resolveManifestFile(options, __dirname);
32-
assert.strictEqual(Boolean(manifest), true);
33-
assert.strictEqual('functions' in manifest, true);
34-
assert.strictEqual(manifest.functions.length, 1);
35-
assert.strictEqual(manifest.functions[0].name, 'custom');
36-
});
37-
});
38-
3917
describe('resolveEsBuildConfig', () => {
4018
it('should resolve the default esbuild config if none is provided', async () => {
4119
const options: BuildFunctionsOptions = {};
@@ -141,7 +119,7 @@ describe('validateFunctions', () => {
141119
const manifest = {
142120
functions: [
143121
{
144-
id: 'an-id',
122+
id: 'example',
145123
name: 'myFunc',
146124
entryFile: 'index.ts',
147125
description: 'My function',

packages/contentful--app-scripts/src/build-functions/build-functions.ts

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { NodeModulesPolyfillPlugin } from '@esbuild-plugins/node-modules-polyfil
55
import { NodeGlobalsPolyfillPlugin } from '@esbuild-plugins/node-globals-polyfill';
66
import { type BuildFunctionsOptions, type ContentfulFunction } from '../types';
77
import { z } from 'zod';
8+
import { ID_REGEX, resolveManifestFile } from '../utils';
89

910
type ContentfulFunctionToBuild = Omit<ContentfulFunction, 'entryFile'> & { entryFile: string };
1011

@@ -13,12 +14,14 @@ const functionManifestSchema = z
1314
functions: z.array(
1415
z
1516
.object({
16-
id: z.string(),
17+
id: z.string().regex(ID_REGEX, 'Invalid "id" (must only contain alphanumeric characters)'),
1718
name: z.string(),
1819
description: z.string(),
1920
path: z.string(),
2021
entryFile: z.string(),
2122
accepts: z.array(z.string()),
23+
}, {
24+
2225
})
2326
.required()
2427
),
@@ -58,12 +61,6 @@ export const validateFunctions = (manifest: Record<string, any>) => {
5861
});
5962
};
6063

61-
export const resolveManifestFile = (options: BuildFunctionsOptions, cwd = process.cwd()) => {
62-
return require(options.manifestFile
63-
? resolve(cwd, options.manifestFile)
64-
: resolve(cwd, 'contentful-app-manifest.json'));
65-
};
66-
6764
const getEntryPoints = (
6865
manifest: { functions: ContentfulFunctionToBuild[] },
6966
cwd = process.cwd()

packages/contentful--app-scripts/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,4 @@ export { feedback } from './feedback';
88
export { install } from './install';
99
export { buildFunctions } from './build-functions';
1010
export { generateFunction } from './generate-function';
11+
export { upsertActions } from './upsert-actions';

packages/contentful--app-scripts/src/types.ts

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,6 @@
11
import { Definition } from './definition-api';
22
import { Organization } from './organization-api';
33

4-
export interface FunctionAppAction {
5-
id: string;
6-
name: string;
7-
description: string;
8-
category: 'Custom';
9-
type: 'function';
10-
path: string;
11-
allowNetworks?: string[];
12-
entryFile?: string;
13-
}
14-
154
export interface ContentfulFunction {
165
id: string;
176
name: string;
@@ -84,7 +73,6 @@ export interface UploadSettings {
8473
skipActivation?: boolean;
8574
userAgentApplication?: string;
8675
host?: string;
87-
actions?: FunctionAppAction[];
8876
functions?: ContentfulFunction[];
8977
}
9078

packages/contentful--app-scripts/src/upload/build-upload-settings.test.ts

Lines changed: 6 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,12 @@ import * as utilsModule from '../utils';
1010
describe('buildAppUploadSettings', () => {
1111
let promptStub;
1212
let getAppInfoStub;
13-
let getEntityFromManifestStub;
13+
let getFunctionsFromManifestStub;
1414

1515
beforeEach(() => {
1616
promptStub = sinon.stub(inquirer, 'prompt');
1717
getAppInfoStub = sinon.stub(getAppInfoModule, 'getAppInfo');
18-
getEntityFromManifestStub = sinon.stub(utilsModule, 'getEntityFromManifest');
18+
getFunctionsFromManifestStub = sinon.stub(utilsModule, 'getFunctionsFromManifest');
1919
});
2020

2121
afterEach(() => {
@@ -39,16 +39,14 @@ describe('buildAppUploadSettings', () => {
3939

4040
getAppInfoStub.resolves({ appId: '123', appName: 'Test App' });
4141

42-
getEntityFromManifestStub.withArgs('actions').returns('actionsManifest');
43-
getEntityFromManifestStub.withArgs('functions').returns('functionsManifest');
42+
getFunctionsFromManifestStub.returns('functionsManifest');
4443

4544
const result = await buildAppUploadSettings(options);
4645

4746
assert.strictEqual(result.host, 'api.contentful.com', 'Protocol should be stripped from host');
4847
assert.strictEqual(result.bundleDirectory, './custom-build');
4948
assert.strictEqual(result.comment, 'Test comment');
5049
assert.strictEqual(result.skipActivation, false); // activateBundle is true, so skipActivation should be false
51-
assert.strictEqual(result.actions, 'actionsManifest');
5250
assert.strictEqual(result.functions, 'functionsManifest');
5351
});
5452
}
@@ -64,8 +62,7 @@ describe('buildAppUploadSettings', () => {
6462

6563
promptStub.resolves(prompts);
6664
getAppInfoStub.resolves({ appId: '123', appName: 'Test App' });
67-
getEntityFromManifestStub.withArgs('actions').returns('actionsManifest');
68-
getEntityFromManifestStub.withArgs('functions').returns('functionsManifest');
65+
getFunctionsFromManifestStub.returns('functionsManifest');
6966

7067
const result = await buildAppUploadSettings(options);
7168

@@ -82,8 +79,7 @@ describe('buildAppUploadSettings', () => {
8279

8380
promptStub.resolves({});
8481
getAppInfoStub.resolves({ appId: '123', appName: 'Test App' });
85-
getEntityFromManifestStub.withArgs('actions').returns('actionsManifest');
86-
getEntityFromManifestStub.withArgs('functions').returns('functionsManifest');
82+
getFunctionsFromManifestStub.returns('functionsManifest');
8783

8884
const result = await buildAppUploadSettings(options);
8985

@@ -99,8 +95,7 @@ describe('buildAppUploadSettings', () => {
9995

10096
promptStub.resolves(prompts);
10197
getAppInfoStub.resolves({ appId: '123', appName: 'Test App' });
102-
getEntityFromManifestStub.withArgs('actions').returns('actionsManifest');
103-
getEntityFromManifestStub.withArgs('functions').returns('functionsManifest');
98+
getFunctionsFromManifestStub.returns('functionsManifest');
10499

105100
const result = await buildAppUploadSettings(options);
106101

packages/contentful--app-scripts/src/upload/build-upload-settings.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
11
import { prompt } from 'inquirer';
22
import { getAppInfo } from '../get-app-info';
3-
import { getEntityFromManifest } from '../utils';
3+
import { getFunctionsFromManifest } from '../utils';
44
import { DEFAULT_CONTENTFUL_API_HOST } from '../constants';
55
import { UploadOptions, UploadSettings } from '../types';
66

77
export async function buildAppUploadSettings(options: UploadOptions): Promise<UploadSettings> {
8-
const actionsManifest = getEntityFromManifest('actions');
9-
const functionManifest = getEntityFromManifest('functions');
8+
const functionManifest = getFunctionsFromManifest();
109
const prompts = [];
1110
const { bundleDir, comment, skipActivation, host } = options;
1211

@@ -50,7 +49,6 @@ export async function buildAppUploadSettings(options: UploadOptions): Promise<Up
5049
skipActivation: skipActivation === undefined ? !activateBundle : skipActivation,
5150
comment,
5251
host: hostValue,
53-
actions: actionsManifest,
5452
functions: functionManifest,
5553
...appUploadSettings,
5654
...appInfo,

0 commit comments

Comments
 (0)