@cloudcommerce/cli 2.9.0 → 2.10.1
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/package.json +10 -3
- package/.turbo/turbo-build.log +0 -5
- package/CHANGELOG.md +0 -1
- package/src/build.ts +0 -56
- package/src/cli.ts +0 -228
- package/src/create-auth.ts +0 -51
- package/src/login.ts +0 -45
- package/src/setup-gcloud.ts +0 -228
- package/src/setup-gh.ts +0 -83
- package/tsconfig.json +0 -6
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cloudcommerce/cli",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "2.
|
|
4
|
+
"version": "2.10.1",
|
|
5
5
|
"description": "E-Com Plus Cloud Commerce CLI tools",
|
|
6
6
|
"bin": {
|
|
7
7
|
"cloudcommerce": "./bin/run.mjs"
|
|
@@ -11,6 +11,13 @@
|
|
|
11
11
|
".": "./lib/cli.js",
|
|
12
12
|
"./create-auth": "./create-auth.js"
|
|
13
13
|
},
|
|
14
|
+
"files": [
|
|
15
|
+
"/lib",
|
|
16
|
+
"/types",
|
|
17
|
+
"/*.{js,mjs,ts}",
|
|
18
|
+
"/ci/**/*.{sh,mjs}",
|
|
19
|
+
"/config/**/*.{json,rules,mjs,cjs}"
|
|
20
|
+
],
|
|
14
21
|
"repository": {
|
|
15
22
|
"type": "git",
|
|
16
23
|
"url": "git+https://github.com/ecomplus/cloud-commerce.git",
|
|
@@ -27,9 +34,9 @@
|
|
|
27
34
|
"dotenv": "^16.4.5",
|
|
28
35
|
"libsodium-wrappers": "^0.7.13",
|
|
29
36
|
"md5": "^2.3.0",
|
|
30
|
-
"typescript": "~5.
|
|
37
|
+
"typescript": "~5.4.3",
|
|
31
38
|
"zx": "^7.2.3",
|
|
32
|
-
"@cloudcommerce/api": "2.
|
|
39
|
+
"@cloudcommerce/api": "2.10.1"
|
|
33
40
|
},
|
|
34
41
|
"scripts": {
|
|
35
42
|
"build": "bash ../../scripts/build-lib.sh"
|
package/.turbo/turbo-build.log
DELETED
package/CHANGELOG.md
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
Please refer to GitHub [repository releases](https://github.com/ecomplus/cloud-commerce/releases) or monorepo unified [CHANGELOG.md](https://github.com/ecomplus/cloud-commerce/blob/main/CHANGELOG.md).
|
package/src/build.ts
DELETED
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
import { join as joinPath } from 'node:path';
|
|
2
|
-
import { $, fs } from 'zx';
|
|
3
|
-
|
|
4
|
-
const copyFunctionsConfig = async (isDev = false) => {
|
|
5
|
-
const functionsDir = joinPath(process.cwd(), 'functions');
|
|
6
|
-
if (isDev && !fs.existsSync(joinPath(functionsDir, '.env'))) {
|
|
7
|
-
try {
|
|
8
|
-
const { storeId } = JSON.parse(
|
|
9
|
-
fs.readFileSync(joinPath(functionsDir, 'config.json'), 'utf8'),
|
|
10
|
-
);
|
|
11
|
-
await fs.writeFile(joinPath(functionsDir, '.env'), `ECOM_STORE_ID=${storeId}\n`);
|
|
12
|
-
} catch {
|
|
13
|
-
//
|
|
14
|
-
}
|
|
15
|
-
}
|
|
16
|
-
const filesToCopy = ['.env', 'config.json', 'ssr/content/settings.json'];
|
|
17
|
-
const dirents = await fs.readdir(functionsDir, { withFileTypes: true });
|
|
18
|
-
/* eslint-disable no-await-in-loop */
|
|
19
|
-
for (let i = 0; i < dirents.length; i++) {
|
|
20
|
-
if (dirents[i].isDirectory() && dirents[i].name.charAt(0) !== '.') {
|
|
21
|
-
const codebase = dirents[i].name;
|
|
22
|
-
const codebaseDir = joinPath(functionsDir, codebase);
|
|
23
|
-
const isSSR = codebase === 'ssr';
|
|
24
|
-
await fs.ensureDir(joinPath(codebaseDir, 'content'));
|
|
25
|
-
if (isDev && isSSR && !fs.existsSync(joinPath(functionsDir, 'ssr', 'node_modules'))) {
|
|
26
|
-
await $`npm --prefix "functions/ssr" i`;
|
|
27
|
-
}
|
|
28
|
-
for (let ii = 0; ii < filesToCopy.length; ii++) {
|
|
29
|
-
const fileToCopy = filesToCopy[ii];
|
|
30
|
-
if (!isSSR || !fileToCopy.includes('ssr/')) {
|
|
31
|
-
const srcPath = joinPath(functionsDir, fileToCopy);
|
|
32
|
-
if (fs.existsSync(srcPath) && srcPath) {
|
|
33
|
-
await fs.copy(
|
|
34
|
-
srcPath,
|
|
35
|
-
joinPath(codebaseDir, fileToCopy.replace('ssr/', '')),
|
|
36
|
-
);
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
};
|
|
43
|
-
|
|
44
|
-
const buildCodebase = async (codebase?: string) => {
|
|
45
|
-
copyFunctionsConfig();
|
|
46
|
-
if (codebase === 'ssr') {
|
|
47
|
-
await $`npm --prefix "functions/ssr" run build 2>ssr-build-warns.log &&
|
|
48
|
-
printf '\n--- // ---\n' && cat ssr-build-warns.log`;
|
|
49
|
-
}
|
|
50
|
-
};
|
|
51
|
-
|
|
52
|
-
export default buildCodebase;
|
|
53
|
-
|
|
54
|
-
export { buildCodebase, copyFunctionsConfig };
|
|
55
|
-
|
|
56
|
-
export const prepareCodebases = copyFunctionsConfig;
|
package/src/cli.ts
DELETED
|
@@ -1,228 +0,0 @@
|
|
|
1
|
-
import { fileURLToPath } from 'node:url';
|
|
2
|
-
import { join as joinPath } from 'node:path';
|
|
3
|
-
import {
|
|
4
|
-
$,
|
|
5
|
-
argv,
|
|
6
|
-
fs,
|
|
7
|
-
echo,
|
|
8
|
-
chalk,
|
|
9
|
-
} from 'zx';
|
|
10
|
-
import * as dotenv from 'dotenv';
|
|
11
|
-
import Deepmerge from '@fastify/deepmerge';
|
|
12
|
-
import login from './login';
|
|
13
|
-
import build, { prepareCodebases } from './build';
|
|
14
|
-
import { siginGcloudAndSetIAM, createServiceAccountKey } from './setup-gcloud';
|
|
15
|
-
import createGhSecrets from './setup-gh';
|
|
16
|
-
|
|
17
|
-
if (!process.env.FIREBASE_PROJECT_ID && !process.env.GOOGLE_APPLICATION_CREDENTIALS) {
|
|
18
|
-
const pwd = process.cwd();
|
|
19
|
-
dotenv.config();
|
|
20
|
-
dotenv.config({ path: joinPath(pwd, 'functions/.env') });
|
|
21
|
-
}
|
|
22
|
-
const {
|
|
23
|
-
FIREBASE_PROJECT_ID,
|
|
24
|
-
GOOGLE_APPLICATION_CREDENTIALS,
|
|
25
|
-
GITHUB_TOKEN,
|
|
26
|
-
} = process.env;
|
|
27
|
-
|
|
28
|
-
// https://github.com/google/zx/issues/124
|
|
29
|
-
process.env.FORCE_COLOR = '3';
|
|
30
|
-
|
|
31
|
-
const __dirname = fileURLToPath(new URL('.', import.meta.url));
|
|
32
|
-
const pwd = process.cwd();
|
|
33
|
-
|
|
34
|
-
let projectId = FIREBASE_PROJECT_ID;
|
|
35
|
-
if (projectId) {
|
|
36
|
-
if (!fs.existsSync(joinPath(pwd, '.firebaserc'))) {
|
|
37
|
-
fs.writeFileSync(
|
|
38
|
-
joinPath(pwd, '.firebaserc'),
|
|
39
|
-
JSON.stringify({ projects: { default: projectId } }, null, 2),
|
|
40
|
-
);
|
|
41
|
-
}
|
|
42
|
-
} else {
|
|
43
|
-
if (GOOGLE_APPLICATION_CREDENTIALS) {
|
|
44
|
-
try {
|
|
45
|
-
const gac = fs.readJSONSync(joinPath(pwd, GOOGLE_APPLICATION_CREDENTIALS));
|
|
46
|
-
projectId = gac.project_id;
|
|
47
|
-
} catch (e) {
|
|
48
|
-
//
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
if (!projectId) {
|
|
52
|
-
try {
|
|
53
|
-
const firebaserc = fs.readJSONSync(joinPath(pwd, '.firebaserc'));
|
|
54
|
-
projectId = firebaserc.projects.default;
|
|
55
|
-
} catch (e) {
|
|
56
|
-
projectId = 'ecom2-demo';
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
export default async () => {
|
|
62
|
-
const baseConfigDir = joinPath(__dirname, '..', 'config');
|
|
63
|
-
await fs.copy(baseConfigDir, pwd);
|
|
64
|
-
const userConfigDir = joinPath(pwd, 'conf');
|
|
65
|
-
if (fs.existsSync(userConfigDir) && fs.lstatSync(userConfigDir).isDirectory()) {
|
|
66
|
-
await fs.copy(userConfigDir, pwd);
|
|
67
|
-
const userFirebaseJsonPath = joinPath(userConfigDir, 'firebase.json');
|
|
68
|
-
if (fs.existsSync(userFirebaseJsonPath)) {
|
|
69
|
-
let userFirebaseConfig: Record<string, any> | undefined;
|
|
70
|
-
try {
|
|
71
|
-
userFirebaseConfig = JSON.parse(
|
|
72
|
-
fs.readFileSync(userFirebaseJsonPath, 'utf8'),
|
|
73
|
-
);
|
|
74
|
-
} catch {
|
|
75
|
-
//
|
|
76
|
-
}
|
|
77
|
-
if (userFirebaseConfig) {
|
|
78
|
-
const deepmerge = Deepmerge();
|
|
79
|
-
const baseFirebaseConfig = JSON.parse(
|
|
80
|
-
fs.readFileSync(joinPath(baseConfigDir, 'firebase.json'), 'utf8'),
|
|
81
|
-
);
|
|
82
|
-
const mergedConfig = deepmerge(baseFirebaseConfig, userFirebaseConfig);
|
|
83
|
-
fs.writeFileSync(
|
|
84
|
-
joinPath(pwd, 'firebase.json'),
|
|
85
|
-
JSON.stringify(mergedConfig, null, 2),
|
|
86
|
-
);
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
const options: string[] = [];
|
|
92
|
-
Object.keys(argv).forEach((key) => {
|
|
93
|
-
if (key !== '_' && argv[key] !== false) {
|
|
94
|
-
if (argv[key] !== true || (key !== 'codebase' && key !== 'only')) {
|
|
95
|
-
options.push(`--${key}`, argv[key]);
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
});
|
|
99
|
-
const $firebase = (cmd: string) => {
|
|
100
|
-
if (cmd === 'deploy' && !options.length) {
|
|
101
|
-
return $`firebase --project=${projectId} ${cmd} --force`;
|
|
102
|
-
}
|
|
103
|
-
return $`firebase --project=${projectId} ${cmd} ${options}`;
|
|
104
|
-
};
|
|
105
|
-
|
|
106
|
-
if (argv._.includes('serve') || argv._.includes('preview')) {
|
|
107
|
-
if (argv.build !== false) {
|
|
108
|
-
await build(argv.codebase);
|
|
109
|
-
}
|
|
110
|
-
return $firebase('emulators:start').catch(async (err: any) => {
|
|
111
|
-
await echo`
|
|
112
|
-
Try killing open emulators with:
|
|
113
|
-
${chalk.bold('npx kill-port 4000 9099 5001 8080 5000 8085 9199 4400 4500')}
|
|
114
|
-
`;
|
|
115
|
-
if (err.stdout.includes('port taken')) {
|
|
116
|
-
return process.exit(1);
|
|
117
|
-
}
|
|
118
|
-
throw err;
|
|
119
|
-
});
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
if (argv._.find((cmd) => /^(\w+:)?shell$/.test(cmd))) {
|
|
123
|
-
return $firebase('functions:shell');
|
|
124
|
-
}
|
|
125
|
-
if (argv._.find((cmd) => /^(\w+:)?logs?$/.test(cmd))) {
|
|
126
|
-
return $firebase('functions:log');
|
|
127
|
-
}
|
|
128
|
-
if (argv._.includes('build')) {
|
|
129
|
-
return build(argv.codebase);
|
|
130
|
-
}
|
|
131
|
-
if (argv._.includes('deploy')) {
|
|
132
|
-
return $firebase('deploy');
|
|
133
|
-
}
|
|
134
|
-
if (argv._.includes('login')) {
|
|
135
|
-
await $firebase('login');
|
|
136
|
-
return login();
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
if (argv._.includes('setup')) {
|
|
140
|
-
const { storeId, authenticationId, apiKey } = await login();
|
|
141
|
-
await fs.writeFile(
|
|
142
|
-
joinPath(pwd, 'functions', '.env'),
|
|
143
|
-
`ECOM_AUTHENTICATION_ID=${authenticationId}
|
|
144
|
-
ECOM_API_KEY=${apiKey}
|
|
145
|
-
ECOM_STORE_ID=${storeId}
|
|
146
|
-
`,
|
|
147
|
-
);
|
|
148
|
-
|
|
149
|
-
if (argv.deploy !== false) {
|
|
150
|
-
await $firebase('deploy');
|
|
151
|
-
}
|
|
152
|
-
if (argv.commit !== false) {
|
|
153
|
-
await fs.writeFile(
|
|
154
|
-
joinPath(pwd, 'functions', 'config.json'),
|
|
155
|
-
JSON.stringify({ storeId }, null, 2),
|
|
156
|
-
);
|
|
157
|
-
|
|
158
|
-
await build(argv.codebase);
|
|
159
|
-
try {
|
|
160
|
-
await $`git add .firebaserc functions/config.json`;
|
|
161
|
-
await $`git commit -m "Setup store [skip ci]"`;
|
|
162
|
-
await $`git push`;
|
|
163
|
-
} catch (e) {
|
|
164
|
-
//
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
let serviceAccountJSON: string | null = null;
|
|
168
|
-
if (argv.gcloud !== false) {
|
|
169
|
-
try {
|
|
170
|
-
await siginGcloudAndSetIAM(projectId as string, pwd);
|
|
171
|
-
serviceAccountJSON = await createServiceAccountKey(projectId as string, pwd);
|
|
172
|
-
} catch (e) {
|
|
173
|
-
//
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
let hasCreatedAllSecrets = false;
|
|
177
|
-
if (GITHUB_TOKEN && argv.github !== false) {
|
|
178
|
-
try {
|
|
179
|
-
hasCreatedAllSecrets = await createGhSecrets(
|
|
180
|
-
apiKey,
|
|
181
|
-
authenticationId,
|
|
182
|
-
serviceAccountJSON,
|
|
183
|
-
GITHUB_TOKEN,
|
|
184
|
-
);
|
|
185
|
-
} catch (e) {
|
|
186
|
-
//
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
if (hasCreatedAllSecrets) {
|
|
191
|
-
return echo`
|
|
192
|
-
****
|
|
193
|
-
|
|
194
|
-
CloudCommerce setup completed successfully.
|
|
195
|
-
Your store repository on GitHub is ready, the first deploy will automatically start with GH Actions.
|
|
196
|
-
|
|
197
|
-
-- More info at https://github.com/ecomplus/store#getting-started
|
|
198
|
-
`;
|
|
199
|
-
}
|
|
200
|
-
return echo`
|
|
201
|
-
****
|
|
202
|
-
|
|
203
|
-
Finish by saving the following secrets to your GitHub repository:
|
|
204
|
-
|
|
205
|
-
${chalk.bold('ECOM_AUTHENTICATION_ID')} = ${chalk.bgMagenta(authenticationId)}
|
|
206
|
-
|
|
207
|
-
${chalk.bold('ECOM_API_KEY')} = ${chalk.bgMagenta(apiKey)}
|
|
208
|
-
|
|
209
|
-
${chalk.bold('FIREBASE_SERVICE_ACCOUNT')} = ${chalk.bgMagenta(serviceAccountJSON || '{YOUR_SERVICE_ACCOUNT_JSON}')}
|
|
210
|
-
|
|
211
|
-
-- More info at https://github.com/ecomplus/store#getting-started
|
|
212
|
-
`;
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
if (argv._.includes('prepare')) {
|
|
216
|
-
return prepareCodebases();
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
if (argv._.includes('dev') || argv._.includes('start') || !argv._.length) {
|
|
220
|
-
await prepareCodebases(true);
|
|
221
|
-
const prefix = joinPath(pwd, 'functions/ssr');
|
|
222
|
-
// https://docs.astro.build/en/reference/cli-reference/#astro-dev
|
|
223
|
-
const host = typeof argv.host === 'string' ? argv.host : '';
|
|
224
|
-
const port = typeof argv.port === 'string' || typeof argv.port === 'number' ? argv.port : '';
|
|
225
|
-
return $`npm --prefix "${prefix}" run dev -- --host ${host} --port ${port}`;
|
|
226
|
-
}
|
|
227
|
-
return $`echo 'Hello from @cloudcommerce/cli'`;
|
|
228
|
-
};
|
package/src/create-auth.ts
DELETED
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
import api from '@cloudcommerce/api';
|
|
2
|
-
|
|
3
|
-
const defaultAgent = {
|
|
4
|
-
name: 'Cloud Commerce default agent',
|
|
5
|
-
email: 'cloudcommerce-noreply@e-com.plus',
|
|
6
|
-
};
|
|
7
|
-
|
|
8
|
-
export default async (storeId: number, accessToken: string) => {
|
|
9
|
-
const apiConfig = {
|
|
10
|
-
storeId,
|
|
11
|
-
accessToken,
|
|
12
|
-
};
|
|
13
|
-
const { data } = await api.get('authentications', {
|
|
14
|
-
...apiConfig,
|
|
15
|
-
params: {
|
|
16
|
-
email: defaultAgent.email,
|
|
17
|
-
limit: 1,
|
|
18
|
-
},
|
|
19
|
-
});
|
|
20
|
-
|
|
21
|
-
let authenticationId = data.result[0]?._id;
|
|
22
|
-
if (!authenticationId) {
|
|
23
|
-
const { data: { _id } } = await api.post('authentications', {
|
|
24
|
-
...defaultAgent,
|
|
25
|
-
username: `cloudcomm${Date.now()}`,
|
|
26
|
-
permissions: {
|
|
27
|
-
applications: ['all'],
|
|
28
|
-
brands: ['all'],
|
|
29
|
-
categories: ['all'],
|
|
30
|
-
collections: ['all'],
|
|
31
|
-
grids: ['all'],
|
|
32
|
-
products: ['all'],
|
|
33
|
-
customers: ['all'],
|
|
34
|
-
carts: ['all'],
|
|
35
|
-
orders: ['GET', 'POST', 'PATCH'],
|
|
36
|
-
stores: ['GET', 'PATCH'],
|
|
37
|
-
},
|
|
38
|
-
}, apiConfig);
|
|
39
|
-
authenticationId = _id;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
const { data: apiKey } = await api.get(
|
|
43
|
-
`authentications/${authenticationId}/api_key`,
|
|
44
|
-
apiConfig,
|
|
45
|
-
) as { data: string };
|
|
46
|
-
return {
|
|
47
|
-
storeId,
|
|
48
|
-
authenticationId,
|
|
49
|
-
apiKey,
|
|
50
|
-
};
|
|
51
|
-
};
|
package/src/login.ts
DELETED
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
import * as readline from 'node:readline';
|
|
2
|
-
import { question, echo } from 'zx';
|
|
3
|
-
import md5 from 'md5';
|
|
4
|
-
import api from '@cloudcommerce/api';
|
|
5
|
-
import createAuth from './create-auth';
|
|
6
|
-
|
|
7
|
-
export default async () => {
|
|
8
|
-
await echo`-- Login with your E-Com Plus store admin account.
|
|
9
|
-
(i) same credentials used to enter the dashboard (https://ecomplus.app/)
|
|
10
|
-
`;
|
|
11
|
-
const username = await question('E-Com Plus username: ');
|
|
12
|
-
const passMd5 = await new Promise((resolve) => {
|
|
13
|
-
const rl = readline.createInterface({
|
|
14
|
-
input: process.stdin,
|
|
15
|
-
output: process.stdout,
|
|
16
|
-
}) as any;
|
|
17
|
-
rl.stdoutMuted = true;
|
|
18
|
-
rl.question('Password: ', (password) => {
|
|
19
|
-
rl.close();
|
|
20
|
-
rl.history = rl.history.slice(1);
|
|
21
|
-
resolve(md5(password));
|
|
22
|
-
});
|
|
23
|
-
rl._writeToOutput = function _writeToOutput(stringToWrite) {
|
|
24
|
-
if (rl.stdoutMuted) {
|
|
25
|
-
rl.output.write('*');
|
|
26
|
-
} else {
|
|
27
|
-
rl.output.write(stringToWrite);
|
|
28
|
-
}
|
|
29
|
-
};
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
const { data: login } = await api.post('login', {
|
|
33
|
-
username,
|
|
34
|
-
pass_md5_hash: passMd5,
|
|
35
|
-
});
|
|
36
|
-
const storeId = login.store_ids[0];
|
|
37
|
-
|
|
38
|
-
const {
|
|
39
|
-
data: {
|
|
40
|
-
access_token: accessToken,
|
|
41
|
-
},
|
|
42
|
-
} = await api.post('authenticate', login);
|
|
43
|
-
|
|
44
|
-
return createAuth(storeId, accessToken);
|
|
45
|
-
};
|
package/src/setup-gcloud.ts
DELETED
|
@@ -1,228 +0,0 @@
|
|
|
1
|
-
import path from 'node:path';
|
|
2
|
-
import {
|
|
3
|
-
$,
|
|
4
|
-
fs,
|
|
5
|
-
question,
|
|
6
|
-
echo,
|
|
7
|
-
} from 'zx';
|
|
8
|
-
|
|
9
|
-
let gcpAccessToken: string | undefined;
|
|
10
|
-
const serviceAccountId = 'cloud-commerce-gh-actions';
|
|
11
|
-
const getAccountEmail = (projectId: string) => {
|
|
12
|
-
return `${serviceAccountId}@${projectId}.iam.gserviceaccount.com`;
|
|
13
|
-
};
|
|
14
|
-
|
|
15
|
-
const requestApi = async (
|
|
16
|
-
projectId: string,
|
|
17
|
-
options?: {
|
|
18
|
-
baseURL?: string,
|
|
19
|
-
url?: string,
|
|
20
|
-
method: string,
|
|
21
|
-
body?: string,
|
|
22
|
-
},
|
|
23
|
-
) => {
|
|
24
|
-
const body = options?.body;
|
|
25
|
-
let url = options?.baseURL
|
|
26
|
-
|| `https://iam.googleapis.com/v1/projects/${projectId}/serviceAccounts`;
|
|
27
|
-
url += options?.url || '';
|
|
28
|
-
const data = await (await fetch(
|
|
29
|
-
url,
|
|
30
|
-
{
|
|
31
|
-
method: options?.method || 'GET',
|
|
32
|
-
headers: {
|
|
33
|
-
Authorization: `Bearer ${gcpAccessToken}`,
|
|
34
|
-
'Content-Type': 'application/json; charset=utf-8',
|
|
35
|
-
},
|
|
36
|
-
body,
|
|
37
|
-
},
|
|
38
|
-
)).json() as any;
|
|
39
|
-
const { error } = data;
|
|
40
|
-
if (error) {
|
|
41
|
-
let msgErr = 'Unexpected error in request';
|
|
42
|
-
msgErr = error.message ? `Code: ${error.code} - ${error.message}` : msgErr;
|
|
43
|
-
const err = new Error(msgErr);
|
|
44
|
-
throw err;
|
|
45
|
-
}
|
|
46
|
-
return data;
|
|
47
|
-
};
|
|
48
|
-
|
|
49
|
-
const getGcpAccessToken = async () => {
|
|
50
|
-
await echo`-- Get the Google Cloud account credentials:
|
|
51
|
-
1. Access https://shell.cloud.google.com/?fromcloudshell=true&show=terminal
|
|
52
|
-
2. Execute \`gcloud auth application-default print-access-token\` in Cloud Shell
|
|
53
|
-
`;
|
|
54
|
-
return question('Google Cloud access token: ');
|
|
55
|
-
};
|
|
56
|
-
|
|
57
|
-
const checkServiceAccountExists = async (projectId: string) => {
|
|
58
|
-
let hasServiceAccount: boolean;
|
|
59
|
-
try {
|
|
60
|
-
if (!gcpAccessToken) {
|
|
61
|
-
const { stderr } = await $`gcloud iam service-accounts describe ${getAccountEmail(projectId)}`;
|
|
62
|
-
hasServiceAccount = !/not_?found/i.test(stderr);
|
|
63
|
-
} else {
|
|
64
|
-
// https://cloud.google.com/iam/docs/creating-managing-service-accounts?hl=pt-br#listing
|
|
65
|
-
const { accounts: listAccounts } = await requestApi(projectId);
|
|
66
|
-
const accountFound = listAccounts
|
|
67
|
-
&& listAccounts.find(({ email }) => email === getAccountEmail(projectId));
|
|
68
|
-
hasServiceAccount = Boolean(accountFound);
|
|
69
|
-
}
|
|
70
|
-
} catch {
|
|
71
|
-
return null;
|
|
72
|
-
}
|
|
73
|
-
return hasServiceAccount;
|
|
74
|
-
};
|
|
75
|
-
|
|
76
|
-
const siginGcloudAndSetIAM = async (projectId: string, pwd: string) => {
|
|
77
|
-
let hasGcloud: boolean;
|
|
78
|
-
try {
|
|
79
|
-
hasGcloud = Boolean(await $`command -v gcloud`);
|
|
80
|
-
} catch {
|
|
81
|
-
hasGcloud = false;
|
|
82
|
-
}
|
|
83
|
-
if (hasGcloud) {
|
|
84
|
-
if (/no credential/i.test((await $`gcloud auth list`).stderr)) {
|
|
85
|
-
await $`gcloud auth login`;
|
|
86
|
-
}
|
|
87
|
-
await $`gcloud config set project ${projectId}`;
|
|
88
|
-
} else {
|
|
89
|
-
gcpAccessToken = await getGcpAccessToken();
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
const serviceAccount = await checkServiceAccountExists(projectId);
|
|
93
|
-
if (!serviceAccount) {
|
|
94
|
-
const description = 'A service account with permission to deploy Cloud Commerce'
|
|
95
|
-
+ ' from the GitHub repository to Firebase';
|
|
96
|
-
const displayName = 'Cloud Commerce GH Actions';
|
|
97
|
-
if (hasGcloud) {
|
|
98
|
-
await $`gcloud iam service-accounts create ${serviceAccountId} \
|
|
99
|
-
--description=${description} --display-name=${displayName}`;
|
|
100
|
-
} else if (gcpAccessToken) {
|
|
101
|
-
const body = JSON.stringify({
|
|
102
|
-
accountId: serviceAccountId,
|
|
103
|
-
serviceAccount: {
|
|
104
|
-
description,
|
|
105
|
-
displayName,
|
|
106
|
-
},
|
|
107
|
-
});
|
|
108
|
-
await requestApi(projectId, { method: 'POST', body });
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
await fs.ensureDir(path.join(pwd, '.cloudcommerce'));
|
|
112
|
-
const pathPolicyIAM = path.join(pwd, '.cloudcommerce', 'policyIAM.json');
|
|
113
|
-
let policyIAM: Record<string, any> = {};
|
|
114
|
-
const version = 3; // most recent
|
|
115
|
-
const baseURL = `https://cloudresourcemanager.googleapis.com/v1/projects/${projectId}`;
|
|
116
|
-
if (hasGcloud) {
|
|
117
|
-
await $`gcloud projects get-iam-policy ${projectId} --format json > ${pathPolicyIAM}`;
|
|
118
|
-
policyIAM = fs.readJSONSync(pathPolicyIAM);
|
|
119
|
-
} else if (gcpAccessToken) {
|
|
120
|
-
// https://cloud.google.com/iam/docs/granting-changing-revoking-access?hl=pt-br#view-access
|
|
121
|
-
// POST https://cloudresourcemanager.googleapis.com/API_VERSION/RESOURCE_TYPE/RESOURCE_ID:getIamPolicy
|
|
122
|
-
policyIAM = await requestApi(
|
|
123
|
-
projectId,
|
|
124
|
-
{
|
|
125
|
-
baseURL,
|
|
126
|
-
url: ':getIamPolicy',
|
|
127
|
-
method: 'POST',
|
|
128
|
-
body: JSON.stringify({ options: { requestedPolicyVersion: version } }),
|
|
129
|
-
},
|
|
130
|
-
);
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
const roles = [
|
|
134
|
-
'roles/firebase.admin',
|
|
135
|
-
'roles/appengine.appAdmin',
|
|
136
|
-
'roles/appengine.appCreator',
|
|
137
|
-
'roles/artifactregistry.admin',
|
|
138
|
-
'roles/cloudfunctions.admin',
|
|
139
|
-
'roles/cloudscheduler.admin',
|
|
140
|
-
'roles/iam.serviceAccountUser',
|
|
141
|
-
'roles/run.viewer',
|
|
142
|
-
'roles/serviceusage.apiKeysViewer',
|
|
143
|
-
'roles/serviceusage.serviceUsageAdmin',
|
|
144
|
-
];
|
|
145
|
-
let { bindings } = policyIAM;
|
|
146
|
-
if (!bindings) {
|
|
147
|
-
bindings = [];
|
|
148
|
-
}
|
|
149
|
-
let mustUpdatePolicy = false;
|
|
150
|
-
roles.forEach((role) => {
|
|
151
|
-
const roleFound = bindings.find((binding) => binding.role === role);
|
|
152
|
-
const memberServiceAccount = `serviceAccount:${getAccountEmail(projectId)}`;
|
|
153
|
-
if (!roleFound) {
|
|
154
|
-
const newBinding: { [key: string]: any } = {
|
|
155
|
-
members: [
|
|
156
|
-
memberServiceAccount,
|
|
157
|
-
],
|
|
158
|
-
role,
|
|
159
|
-
};
|
|
160
|
-
if (role === 'roles/serviceusage.serviceUsageAdmin') {
|
|
161
|
-
const roleExpiration = Date.now() + 1000 * 60 * 60 * 12;
|
|
162
|
-
newBinding.condition = {
|
|
163
|
-
expression: `request.time < timestamp("${new Date(roleExpiration).toISOString()}")`,
|
|
164
|
-
title: 'Enable APIs on first deploy',
|
|
165
|
-
description: null,
|
|
166
|
-
};
|
|
167
|
-
}
|
|
168
|
-
bindings.push(newBinding);
|
|
169
|
-
mustUpdatePolicy = true;
|
|
170
|
-
} else {
|
|
171
|
-
const serviceAccountHavePermission = roleFound.members.find(
|
|
172
|
-
(account: string) => account === memberServiceAccount,
|
|
173
|
-
);
|
|
174
|
-
if (!serviceAccountHavePermission) {
|
|
175
|
-
roleFound.members.push(memberServiceAccount);
|
|
176
|
-
mustUpdatePolicy = true;
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
});
|
|
180
|
-
if (mustUpdatePolicy) {
|
|
181
|
-
if (hasGcloud) {
|
|
182
|
-
fs.writeJSONSync(pathPolicyIAM, policyIAM);
|
|
183
|
-
return $`gcloud projects set-iam-policy ${projectId} ${pathPolicyIAM}`;
|
|
184
|
-
} if (gcpAccessToken) {
|
|
185
|
-
Object.assign(policyIAM, { version, bindings });
|
|
186
|
-
// POST https://cloudresourcemanager.googleapis.com/API_VERSION/RESOURCE_TYPE/RESOURCE_ID:setIamPolicy
|
|
187
|
-
return requestApi(
|
|
188
|
-
projectId,
|
|
189
|
-
{
|
|
190
|
-
baseURL,
|
|
191
|
-
url: ':setIamPolicy',
|
|
192
|
-
method: 'POST',
|
|
193
|
-
body: JSON.stringify({ policy: policyIAM }),
|
|
194
|
-
},
|
|
195
|
-
);
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
return null;
|
|
199
|
-
};
|
|
200
|
-
|
|
201
|
-
const createServiceAccountKey = async (projectId: string, pwd: string) => {
|
|
202
|
-
try {
|
|
203
|
-
const pathFileKey = path.join(pwd, '.cloudcommerce', 'serviceAccountKey.json');
|
|
204
|
-
if (!gcpAccessToken) {
|
|
205
|
-
await $`gcloud iam service-accounts keys create ${pathFileKey} \
|
|
206
|
-
--iam-account=${getAccountEmail(projectId)}`;
|
|
207
|
-
} else {
|
|
208
|
-
const { privateKeyData } = await requestApi(
|
|
209
|
-
projectId,
|
|
210
|
-
{
|
|
211
|
-
url: `/${getAccountEmail(projectId)}/keys`,
|
|
212
|
-
method: 'POST',
|
|
213
|
-
},
|
|
214
|
-
);
|
|
215
|
-
await $`echo '${privateKeyData}' | base64 --decode > ${pathFileKey}`;
|
|
216
|
-
}
|
|
217
|
-
return JSON.stringify(fs.readJSONSync(pathFileKey));
|
|
218
|
-
} catch (e) {
|
|
219
|
-
return null;
|
|
220
|
-
}
|
|
221
|
-
};
|
|
222
|
-
|
|
223
|
-
export default siginGcloudAndSetIAM;
|
|
224
|
-
|
|
225
|
-
export {
|
|
226
|
-
siginGcloudAndSetIAM,
|
|
227
|
-
createServiceAccountKey,
|
|
228
|
-
};
|
package/src/setup-gh.ts
DELETED
|
@@ -1,83 +0,0 @@
|
|
|
1
|
-
import { fetch, $ } from 'zx';
|
|
2
|
-
import libsodium from 'libsodium-wrappers';
|
|
3
|
-
|
|
4
|
-
const getRemoteRepo = async () => {
|
|
5
|
-
try {
|
|
6
|
-
return (await $`git config --get remote.origin.url`).stdout
|
|
7
|
-
.replace(/.*github.com[/:]/, '')
|
|
8
|
-
.replace('.git', '')
|
|
9
|
-
.replace('\n', '');
|
|
10
|
-
} catch (e) {
|
|
11
|
-
return null;
|
|
12
|
-
}
|
|
13
|
-
};
|
|
14
|
-
|
|
15
|
-
const createGhSecrets = async (
|
|
16
|
-
ecomApiKey: string,
|
|
17
|
-
ecomAuthentication: string,
|
|
18
|
-
firebaseServiceAccount: string | null,
|
|
19
|
-
ghToken: string,
|
|
20
|
-
ghRepo?: string,
|
|
21
|
-
) => {
|
|
22
|
-
const remoteRepo = ghRepo || await getRemoteRepo();
|
|
23
|
-
if (!remoteRepo) {
|
|
24
|
-
throw new Error("Can't define remote Git repository");
|
|
25
|
-
}
|
|
26
|
-
const baseUrl = `https://api.github.com/repos/${remoteRepo}/actions/secrets`;
|
|
27
|
-
|
|
28
|
-
const fetchGhSecrets = async (
|
|
29
|
-
resource: string,
|
|
30
|
-
body?: string,
|
|
31
|
-
method: string = 'GET',
|
|
32
|
-
) => {
|
|
33
|
-
const url = `${baseUrl}${resource}`;
|
|
34
|
-
const headers = {
|
|
35
|
-
Accept: 'application/vnd.github+json',
|
|
36
|
-
Authorization: `token ${ghToken}`,
|
|
37
|
-
};
|
|
38
|
-
return fetch(url, {
|
|
39
|
-
method,
|
|
40
|
-
headers,
|
|
41
|
-
body,
|
|
42
|
-
});
|
|
43
|
-
};
|
|
44
|
-
|
|
45
|
-
// https:// docs.github.com/pt/rest/actions/secrets#get-a-repository-public-key
|
|
46
|
-
const ghPublicKey = await (await fetchGhSecrets('/public-key')).json() as {
|
|
47
|
-
key_id: string,
|
|
48
|
-
key: string,
|
|
49
|
-
};
|
|
50
|
-
const ghKeyBuffer = Buffer.from(ghPublicKey.key, 'base64');
|
|
51
|
-
await libsodium.ready;
|
|
52
|
-
|
|
53
|
-
const createGhSecret = async (
|
|
54
|
-
secretName: string,
|
|
55
|
-
secretValue: string,
|
|
56
|
-
) => {
|
|
57
|
-
// https://docs.github.com/pt/rest/actions/secrets#example-encrypting-a-secret-using-nodejs
|
|
58
|
-
// Encryption example: https://github.com/github/tweetsodium
|
|
59
|
-
const encryptedBytes = libsodium.crypto_box_seal(
|
|
60
|
-
Buffer.from(secretValue),
|
|
61
|
-
ghKeyBuffer,
|
|
62
|
-
);
|
|
63
|
-
const body = {
|
|
64
|
-
encrypted_value: Buffer.from(encryptedBytes).toString('base64'),
|
|
65
|
-
key_id: ghPublicKey.key_id,
|
|
66
|
-
};
|
|
67
|
-
return fetchGhSecrets(`/${secretName}`, JSON.stringify(body), 'PUT');
|
|
68
|
-
};
|
|
69
|
-
|
|
70
|
-
try {
|
|
71
|
-
await createGhSecret('ECOM_API_KEY', ecomApiKey);
|
|
72
|
-
await createGhSecret('ECOM_AUTHENTICATION_ID', ecomAuthentication);
|
|
73
|
-
if (firebaseServiceAccount) {
|
|
74
|
-
await createGhSecret('FIREBASE_SERVICE_ACCOUNT', firebaseServiceAccount);
|
|
75
|
-
return true;
|
|
76
|
-
}
|
|
77
|
-
} catch (e) {
|
|
78
|
-
//
|
|
79
|
-
}
|
|
80
|
-
return false;
|
|
81
|
-
};
|
|
82
|
-
|
|
83
|
-
export default createGhSecrets;
|