suitescript-verify 1.0.0 → 1.1.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/README.md +37 -18
- package/dist/cli.js +15 -11
- package/dist/config.js +38 -16
- package/dist/doctor.js +38 -0
- package/dist/netsuite.js +17 -4
- package/dist/run.js +1 -1
- package/dist/sdf.js +23 -9
- package/package.json +1 -1
- package/templates/sdf/deploy.xml +1 -1
- package/templates/sdf/runner.xml +5 -5
package/README.md
CHANGED
|
@@ -113,16 +113,33 @@ Every step that creates a credential or accepts a license is the person's. `init
|
|
|
113
113
|
Oracle allows one certificate per integration, role and entity, so the two pairs cannot be shared, and an EC key fails the SDF login (see [Troubleshooting](#setup-ci-fails-with-an-error-with-the-private-key)).
|
|
114
114
|
5. **In the sandbox, create the integration record, the role and the certificate mappings.**
|
|
115
115
|
- The integration needs more than the machine-to-machine grant: also enable the token-based authentication flags and the RESTlets and REST Web Services scopes (see [Troubleshooting](#every-call-fails-with-401-invalid_login_attempt)).
|
|
116
|
-
- The runner is limited to a role with the id `customrole_suitescript_verify`, which you create. Use your own id by setting `SUITESCRIPT_VERIFY_ROLE_ID`; do not rely on a fixed id, since a custom role's id may be read-only after saving.
|
|
116
|
+
- The runner is limited to a role with the id `customrole_suitescript_verify`, which you create. Use your own id by setting `SUITESCRIPT_VERIFY_ROLE_ID`; do not rely on a fixed id, since a custom role's id may be read-only after saving. The platform rejects a role id over 40 characters, `customrole_` included, so the id after the prefix is at most 29 characters; the package refuses a longer one before it reaches the sandbox.
|
|
117
117
|
- Keep the role minimal (see [Probes](#probes)).
|
|
118
118
|
6. **Save the settings.** `suitescript-verify passkey` generates the SuiteCloud passkey into `verification/.env.local`. `suitescript-verify env set NAME=value` saves any other key there (values are never printed; a different existing value is kept unless `--overwrite`). See [Settings](#settings) for the names.
|
|
119
|
-
7. **Run `suitescript-verify doctor`** until every step passes. It checks the SuiteCloud CLI, account, sandbox, certificates, passkey and project files, says which failing steps are the maintainer's own, and exits 1 while any fails.
|
|
119
|
+
7. **Run `suitescript-verify doctor`** until every step passes. It checks the SuiteCloud CLI, Java 17 or 21, the SuiteCloud SDK jar and login, account, sandbox, certificates, passkey and project files, says which failing steps are the maintainer's own, and exits 1 while any fails.
|
|
120
120
|
8. **`suitescript-verify setup-ci`** (for a pipeline) registers the SDF credentials (auth id `suitescript-verify`) with your project's SuiteCloud CLI, passing the passkey through the environment, never an argument. On success it prints the `SUITESCRIPT_VERIFY_` names the pipeline must supply as its own secrets, never a value. It writes no file, no credential variable and no license variable. Accepting Oracle's license in the pipeline is yours.
|
|
121
121
|
|
|
122
|
+
**Note:** projects on one machine that register the same SDF auth id (this package always uses `suitescript-verify`) share one stored credential entry. That is harmless while their certificate and passkey values match; when they differ, the later `setup-ci` replaces the earlier entry.
|
|
123
|
+
|
|
122
124
|
**Note:** never put `SUITECLOUD_CI_PASSKEY` in your user-wide environment. It stops browser login in your other SuiteCloud projects. Keep the passkey in `.env.local`, where the package hands it to the CLI for its own child process only.
|
|
123
125
|
|
|
124
126
|
Every command that reaches the sandbox refuses an account that is not a sandbox (`digits_SBn`).
|
|
125
127
|
|
|
128
|
+
### Keeping the tooling in a subfolder
|
|
129
|
+
|
|
130
|
+
A project that publishes its own package from the repository root may keep the tooling in a subfolder that is an npm project of its own, with its own `package.json` and `node_modules` holding `suitescript-verify` and `@oracle/suitecloud-cli`. The package treats the folder it runs in as the project, so run it from the subfolder and pass `--dir .`, which makes the subfolder itself the verification folder (its `.env`, `.env.local`, `probes` and `sdf`):
|
|
131
|
+
|
|
132
|
+
```json
|
|
133
|
+
{
|
|
134
|
+
"scripts": {
|
|
135
|
+
"verify:doctor": "suitescript-verify doctor --dir .",
|
|
136
|
+
"verify:run": "suitescript-verify run --dir ."
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
A script at the repository root then calls it with `npm --prefix <subfolder> run verify:doctor --`. Give `init`, `deploy`, `cleanup` and the other commands the same `--dir .`. The subfolder's `.env.local` is checked against the repository's `.gitignore`.
|
|
142
|
+
|
|
126
143
|
## Deploy
|
|
127
144
|
|
|
128
145
|
```sh
|
|
@@ -131,8 +148,9 @@ suitescript-verify deploy --approved # the real deploy: a person at a terminal
|
|
|
131
148
|
suitescript-verify cleanup --approved # turns the runner's deployment off: the person types CLEANUP
|
|
132
149
|
```
|
|
133
150
|
|
|
134
|
-
`deploy` generates the runner and an SDF project into `verification/sdf` (the generated `src/` there is yours to gitignore) from the RESTlet probes in `verification/probes`, then dry-runs it through your project's SuiteCloud CLI. `cleanup` does the same with the deployment switched off, and without `--approved` it is a dry run. The runner's script id is `customscript_suitescript_verify_runner
|
|
151
|
+
`deploy` generates the runner and an SDF project into `verification/sdf` (the generated `src/` there is yours to gitignore) from the RESTlet probes in `verification/probes`, then dry-runs it through your project's SuiteCloud CLI. `cleanup` does the same with the deployment switched off, and without `--approved` it is a dry run. The runner's script id is `customscript_suitescript_verify_runner` and its deployment id `customdeploy_suitescript_verify_runner`; both come from the runner id (`SUITESCRIPT_VERIFY_RUNNER_ID`, default `suitescript_verify`).
|
|
135
152
|
|
|
153
|
+
- **Projects that share a sandbox need their own runner id.** A deploy replaces the runner's probe list, so two projects with the same runner id overwrite each other: whichever deployed last decides which probes exist, and the other's answer `unknown probe` until it deploys again. Set a different `SUITESCRIPT_VERIFY_RUNNER_ID` in each project's committed `.env` (for example `my_project`). The runner, its deployment and its File Cabinet folder are named from it (`customscript_my_project_runner`, `my-project`), while the projects can still share one role and token mapping. The id is at most 20 characters because the platform rejects a script or deployment id over 40 characters, prefix included (`The script ID must not exceed 40 characters.`): `customscript_` and `customdeploy_` take 13 and `_runner` takes 7. Changing a project's id leaves its old runner deployed in the sandbox, because the SuiteCloud CLI cannot delete a script. The next `deploy` or `cleanup` removes the old runner from the generated SDF folder (so it is not deployed again) and warns with the old id; to turn the old runner off, run `cleanup --approved` once with `SUITESCRIPT_VERIFY_RUNNER_ID` set to the old id, then switch back. The warning only sees runners generated in this folder.
|
|
136
154
|
- **Every probe change needs a deploy.** The runner's probe list is generated into its dependency list, so a new, renamed or edited probe only exists in the sandbox after a deploy. Write and review **all** the probes of a batch first, dry-run once, and ask for one deploy. A deploy is the person's, in their own terminal, so a wasted one costs a person's time.
|
|
137
155
|
- **After every deploy, run the chain-proving probe first** (a trivial probe, for example `probe-zero`, that only proves the runner loads). If it fails, the runner is broken and no other result means anything (see [Troubleshooting](#every-probe-returns-http-400-and-names-no-probe)).
|
|
138
156
|
- **A sandbox refresh clears the token mappings and, presumably, the deployed runner.** After one, redo the role, mapping and deploy steps.
|
|
@@ -190,21 +208,22 @@ The package validates the tags in a probe's leading JSDoc block: `@scriptType`,
|
|
|
190
208
|
|
|
191
209
|
Every setting is a `SUITESCRIPT_VERIFY_` key. They layer from the committed `verification/.env`, the gitignored `verification/.env.local` and the process environment, in that order. The committed file may hold only the keys marked "committed", and its values must not look like a secret. A bad value stops every command that reaches the sandbox, and the error names the key, never the value.
|
|
192
210
|
|
|
193
|
-
| Key | Where | What it is
|
|
194
|
-
| ----------------------------------------- | --------- |
|
|
195
|
-
| `SUITESCRIPT_VERIFY_PROBES_DIR` | committed | Folder of probes (a path in the project)
|
|
196
|
-
| `SUITESCRIPT_VERIFY_RESULTS_DIR` | committed | Folder of results pages (a path in the project). No default: you choose
|
|
197
|
-
| `SUITESCRIPT_VERIFY_SDF_DIR` | committed | Folder for the generated SDF project (a path in the project)
|
|
198
|
-
| `SUITESCRIPT_VERIFY_ROLE_ID` | committed | The runner's role id, matching `^customrole_[a-z0-9_]
|
|
199
|
-
| `
|
|
200
|
-
| `
|
|
201
|
-
| `
|
|
202
|
-
| `
|
|
203
|
-
| `
|
|
204
|
-
| `
|
|
205
|
-
| `
|
|
206
|
-
| `
|
|
207
|
-
| `
|
|
211
|
+
| Key | Where | What it is |
|
|
212
|
+
| ----------------------------------------- | --------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
213
|
+
| `SUITESCRIPT_VERIFY_PROBES_DIR` | committed | Folder of probes (a path in the project) |
|
|
214
|
+
| `SUITESCRIPT_VERIFY_RESULTS_DIR` | committed | Folder of results pages (a path in the project). No default: you choose |
|
|
215
|
+
| `SUITESCRIPT_VERIFY_SDF_DIR` | committed | Folder for the generated SDF project (a path in the project) |
|
|
216
|
+
| `SUITESCRIPT_VERIFY_ROLE_ID` | committed | The runner's role id, matching `^customrole_[a-z0-9_]{1,29}$` (40 characters at most in all); default `customrole_suitescript_verify`. Used in the generated manifest and the runner, and reported by `doctor` |
|
|
217
|
+
| `SUITESCRIPT_VERIFY_RUNNER_ID` | committed | The runner's name: 1 to 20 lowercase letters, digits and underscores, starting with a letter; default `suitescript_verify`. Names the script (`customscript_<id>_runner`), the deployment and the File Cabinet folder, so projects sharing a sandbox can each have their own runner |
|
|
218
|
+
| `SUITESCRIPT_VERIFY_EXTRA_TAGS` | committed | Extra probe tag names, comma-separated, without the `@` |
|
|
219
|
+
| `SUITESCRIPT_VERIFY_ACCOUNT` | local | The sandbox account (`digits_SBn`) |
|
|
220
|
+
| `SUITESCRIPT_VERIFY_CLIENT_ID` | local | The integration's client id |
|
|
221
|
+
| `SUITESCRIPT_VERIFY_CERTIFICATE_ID` | local | Certificate id of the runner pair |
|
|
222
|
+
| `SUITESCRIPT_VERIFY_PRIVATE_KEY_PATH` | local | Path to the runner pair's private key (outside the project) |
|
|
223
|
+
| `SUITESCRIPT_VERIFY_SDF_CERTIFICATE_ID` | local | Certificate id of the SDF pair |
|
|
224
|
+
| `SUITESCRIPT_VERIFY_SDF_PRIVATE_KEY_PATH` | local | Path to the SDF pair's private key (outside the project) |
|
|
225
|
+
| `SUITESCRIPT_VERIFY_PASSKEY` | local | The SuiteCloud passkey, saved by `passkey` |
|
|
226
|
+
| `SUITESCRIPT_VERIFY_DOMAIN` | local | Optional NetSuite domain passed to the SuiteCloud CLI |
|
|
208
227
|
|
|
209
228
|
"Local" keys go in `.env.local` or the process environment (a pipeline's secrets), never in the committed file.
|
|
210
229
|
|
package/dist/cli.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { randomBytes } from "node:crypto";
|
|
2
2
|
import { createInterface } from "node:readline/promises";
|
|
3
3
|
import { certificate } from "./certificate.js";
|
|
4
|
-
import { loadConfig, resultsDirGuidance, resultsDirKey, suggestedResultsDir, } from "./config.js";
|
|
4
|
+
import { loadConfig, resultsDirGuidance, resultsDirKey, runnerIdKey, suggestedResultsDir, } from "./config.js";
|
|
5
5
|
import { formatChecks, names, runChecks } from "./doctor.js";
|
|
6
6
|
import { envNames } from "./envfile.js";
|
|
7
7
|
import { init } from "./init.js";
|
|
@@ -73,15 +73,23 @@ async function initCommand(args) {
|
|
|
73
73
|
console.log(init(process.cwd(), config, await chooseResultsDir(config, args)));
|
|
74
74
|
return 0;
|
|
75
75
|
}
|
|
76
|
+
/**
|
|
77
|
+
* Generates the runner and its SDF project. A runner generated here under
|
|
78
|
+
* another id may still be deployed (the SuiteCloud CLI cannot delete a script),
|
|
79
|
+
* so say how to turn it off.
|
|
80
|
+
*/
|
|
81
|
+
function buildRunner(config, deployed) {
|
|
82
|
+
const built = buildSdfProject(config, { deployed });
|
|
83
|
+
for (const id of built.replacedRunners) {
|
|
84
|
+
console.warn(`This folder held a runner for the id "${id}", which may still be deployed in the sandbox. To turn it off, run cleanup with ${runnerIdKey}=${id}, then switch back.`);
|
|
85
|
+
}
|
|
86
|
+
return built;
|
|
87
|
+
}
|
|
76
88
|
async function deploy(args) {
|
|
77
89
|
const project = process.cwd();
|
|
78
90
|
const config = configure(args, true);
|
|
79
91
|
requireSandbox(config);
|
|
80
|
-
const { files, slugs } =
|
|
81
|
-
deployed: true,
|
|
82
|
-
extraTags: config.extraTags,
|
|
83
|
-
roleId: config.roleId,
|
|
84
|
-
});
|
|
92
|
+
const { files, slugs } = buildRunner(config, true);
|
|
85
93
|
console.log(`Probes in the runner: ${slugs.join(", ") || "(none)"}`);
|
|
86
94
|
console.log(`Files to upload: ${files.join(", ")}`);
|
|
87
95
|
const approved = args.includes("--approved");
|
|
@@ -106,11 +114,7 @@ async function cleanup(args) {
|
|
|
106
114
|
else {
|
|
107
115
|
console.log("Dry run only. This turns the runner's deployment off in the sandbox, so ask the user for approval, then rerun with --approved.\nTo remove access entirely, the user revokes the certificate mapping (Setup > Integration > Manage Authentication > OAuth 2.0 Client Credentials (M2M) Setup).");
|
|
108
116
|
}
|
|
109
|
-
|
|
110
|
-
deployed: false,
|
|
111
|
-
extraTags: config.extraTags,
|
|
112
|
-
roleId: config.roleId,
|
|
113
|
-
});
|
|
117
|
+
buildRunner(config, false);
|
|
114
118
|
return suitecloud(project, config, [
|
|
115
119
|
"project:deploy",
|
|
116
120
|
...(approved ? [] : ["--dryrun"]),
|
package/dist/config.js
CHANGED
|
@@ -15,10 +15,27 @@ export const suggestedResultsDir = "docs/verification/results";
|
|
|
15
15
|
export const resultsDirGuidance = `Choose where results pages live in your project, then run suitescript-verify init --results-dir <path> (${suggestedResultsDir} is the suggested value) or set ${resultsDirKey} in the committed .env.`;
|
|
16
16
|
/** The committed `.env` may also register extra probe tags: a comma-separated list of tag names, not a path. */
|
|
17
17
|
export const extraTagsKey = "SUITESCRIPT_VERIFY_EXTRA_TAGS";
|
|
18
|
-
/** The committed `.env` may
|
|
18
|
+
/** The committed `.env` may name the runner role: a NetSuite custom role id. */
|
|
19
19
|
export const roleIdKey = "SUITESCRIPT_VERIFY_ROLE_ID";
|
|
20
20
|
export const defaultRoleId = "customrole_suitescript_verify";
|
|
21
|
-
const
|
|
21
|
+
const roleSetting = {
|
|
22
|
+
// `customrole_` is 11 characters, so 29 more make 40.
|
|
23
|
+
expects: "a NetSuite custom role id like customrole_suitescript_verify (lowercase letters, digits and underscores after customrole_, 40 characters at most in all)",
|
|
24
|
+
fallback: defaultRoleId,
|
|
25
|
+
key: roleIdKey,
|
|
26
|
+
pattern: /^customrole_[a-z0-9_]{1,29}$/,
|
|
27
|
+
};
|
|
28
|
+
/** The committed `.env` may name the runner: a short stem for its script id, deployment id and File Cabinet folder. */
|
|
29
|
+
export const runnerIdKey = "SUITESCRIPT_VERIFY_RUNNER_ID";
|
|
30
|
+
export const defaultRunnerId = "suitescript_verify";
|
|
31
|
+
const runnerSetting = {
|
|
32
|
+
// `customscript_` and `customdeploy_` are 13 characters and `_runner` is 7, so 20 more make 40.
|
|
33
|
+
expects: "a runner id like my_project (a lowercase letter, then lowercase letters, digits and underscores, 20 characters at most)",
|
|
34
|
+
fallback: defaultRunnerId,
|
|
35
|
+
key: runnerIdKey,
|
|
36
|
+
pattern: /^[a-z][a-z0-9_]{0,19}$/,
|
|
37
|
+
};
|
|
38
|
+
const idSettings = [roleSetting, runnerSetting];
|
|
22
39
|
const tagName = /^[A-Za-z][A-Za-z0-9_-]*$/;
|
|
23
40
|
/** The tag names in a registration value, without the `@`; blank entries are dropped. */
|
|
24
41
|
export const parseTagList = (value) => value
|
|
@@ -58,17 +75,19 @@ export const insideProject = (project, path) => {
|
|
|
58
75
|
const looksSecret = (value) => /-----BEGIN /.test(value) ||
|
|
59
76
|
/^eyJ[\w-]+\.[\w-]+\.[\w-]+$/.test(value) ||
|
|
60
77
|
/^[A-Za-z0-9+_=-]{32,}$/.test(value);
|
|
61
|
-
const
|
|
62
|
-
message: `${
|
|
78
|
+
const idProblem = (setting, file) => ({
|
|
79
|
+
message: `${setting.key} in ${file} is not ${setting.expects}.`,
|
|
63
80
|
});
|
|
64
81
|
/** Checks the committed `.env` against the allowlist. Messages name the key, never the value. */
|
|
65
82
|
function checkCommitted(project, committed, file) {
|
|
66
83
|
const problems = [];
|
|
67
84
|
for (const [key, value] of committed) {
|
|
68
|
-
|
|
85
|
+
const idSetting = idSettings.find((setting) => setting.key === key);
|
|
86
|
+
if (idSetting) {
|
|
69
87
|
// The pattern is the whole check: a valid id may be long enough to look like a token.
|
|
70
|
-
if (!
|
|
71
|
-
problems.push(
|
|
88
|
+
if (!idSetting.pattern.test(value)) {
|
|
89
|
+
problems.push(idProblem(idSetting, file));
|
|
90
|
+
}
|
|
72
91
|
}
|
|
73
92
|
else if (key === extraTagsKey) {
|
|
74
93
|
if (!parseTagList(value).every((name) => tagName.test(name))) {
|
|
@@ -84,7 +103,7 @@ function checkCommitted(project, committed, file) {
|
|
|
84
103
|
}
|
|
85
104
|
else if (!layoutKeys.includes(key)) {
|
|
86
105
|
problems.push({
|
|
87
|
-
message: `${key} in ${file} is not a layout key. The committed file holds layout only (${[...layoutKeys, extraTagsKey,
|
|
106
|
+
message: `${key} in ${file} is not a layout key. The committed file holds layout only (${[...layoutKeys, extraTagsKey, ...idSettings.map((setting) => setting.key)].join(", ")}); put anything else in .env.local or the process environment.`,
|
|
88
107
|
});
|
|
89
108
|
}
|
|
90
109
|
else if (!insideProject(project, value)) {
|
|
@@ -142,13 +161,15 @@ export function loadConfig(project, dir, processEnv = process.env) {
|
|
|
142
161
|
}
|
|
143
162
|
}
|
|
144
163
|
const path = (key, fallback) => resolve(project, values[key] || fallback);
|
|
145
|
-
const
|
|
164
|
+
const idValue = (setting) => values[setting.key] || setting.fallback;
|
|
146
165
|
// The committed file reports its own bad value; this covers .env.local and the environment.
|
|
147
|
-
const
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
166
|
+
const idProblems = idSettings.flatMap((setting) => {
|
|
167
|
+
const committedValue = committed.get(setting.key);
|
|
168
|
+
return setting.pattern.test(idValue(setting)) ||
|
|
169
|
+
(committedValue !== undefined && !setting.pattern.test(committedValue))
|
|
170
|
+
? []
|
|
171
|
+
: [idProblem(setting, ".env.local or the environment")];
|
|
172
|
+
});
|
|
152
173
|
const shown = relative(project, envFile).replaceAll("\\", "/");
|
|
153
174
|
return {
|
|
154
175
|
extraTags: parseTagList(values[extraTagsKey] ?? ""),
|
|
@@ -156,13 +177,14 @@ export function loadConfig(project, dir, processEnv = process.env) {
|
|
|
156
177
|
problems: [
|
|
157
178
|
...checkCommitted(project, committed, shown),
|
|
158
179
|
...checkLocalIgnored(project, localFile),
|
|
159
|
-
...
|
|
180
|
+
...idProblems,
|
|
160
181
|
],
|
|
161
182
|
resultsDir: values[resultsDirKey]
|
|
162
183
|
? resolve(project, values[resultsDirKey])
|
|
163
184
|
: undefined,
|
|
164
|
-
roleId,
|
|
185
|
+
roleId: idValue(roleSetting),
|
|
165
186
|
root,
|
|
187
|
+
runnerId: idValue(runnerSetting),
|
|
166
188
|
sdfDir: path("SUITESCRIPT_VERIFY_SDF_DIR", join(root, "sdf")),
|
|
167
189
|
values,
|
|
168
190
|
};
|
package/dist/doctor.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
1
2
|
import { createPrivateKey } from "node:crypto";
|
|
2
3
|
import { existsSync, readFileSync } from "node:fs";
|
|
4
|
+
import { homedir } from "node:os";
|
|
3
5
|
import { join } from "node:path";
|
|
4
6
|
import { insideProject, resultsDirGuidance, resultsDirKey, } from "./config.js";
|
|
5
7
|
export const names = {
|
|
@@ -11,6 +13,33 @@ export const names = {
|
|
|
11
13
|
sdfCertificateId: "SUITESCRIPT_VERIFY_SDF_CERTIFICATE_ID",
|
|
12
14
|
sdfPrivateKeyPath: "SUITESCRIPT_VERIFY_SDF_PRIVATE_KEY_PATH",
|
|
13
15
|
};
|
|
16
|
+
/**
|
|
17
|
+
* The Java versions Oracle's SDK runs on: the SuiteCloud CLI's own
|
|
18
|
+
* `SDK_COMPATIBLE_JAVA_VERSIONS`, which refuses to run on any other.
|
|
19
|
+
*/
|
|
20
|
+
const javaVersions = ["17", "21"];
|
|
21
|
+
/** Where Oracle's SDK keeps its jar (`cli/`) and its saved logins, under the home folder. */
|
|
22
|
+
const sdkFolder = ".suitecloud-sdk";
|
|
23
|
+
const loginFiles = ["credentials_browser_based.p12", "credentials_ci.p12"];
|
|
24
|
+
/** The installed Java's version, read the way the SuiteCloud CLI reads it (`java -version`, no shell); empty when there is none. */
|
|
25
|
+
function installedJavaVersion() {
|
|
26
|
+
const result = spawnSync("java", ["-version"], {
|
|
27
|
+
encoding: "utf8",
|
|
28
|
+
shell: false,
|
|
29
|
+
});
|
|
30
|
+
const match = /(?:\bjava version\b|\bopenjdk version\b) "([\d.]+)"/.exec(result.stderr ?? "");
|
|
31
|
+
return match?.[1] ?? "";
|
|
32
|
+
}
|
|
33
|
+
/** The SDK jar the installed SuiteCloud CLI downloads, from its package.json; empty when unknown. */
|
|
34
|
+
function sdkFilename(project) {
|
|
35
|
+
try {
|
|
36
|
+
const manifest = JSON.parse(readFileSync(join(project, "node_modules", "@oracle", "suitecloud-cli", "package.json"), "utf8"));
|
|
37
|
+
return manifest.sdkFilename ?? "";
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return "";
|
|
41
|
+
}
|
|
42
|
+
}
|
|
14
43
|
/** `ec:<curve>`, `rsa:<bits>` or `other`, from a private key file. Never the key itself. */
|
|
15
44
|
function keyType(path) {
|
|
16
45
|
try {
|
|
@@ -34,6 +63,14 @@ export function runChecks(project, config, env = process.env) {
|
|
|
34
63
|
const add = (name, ok, owner, next) => checks.push({ name, next, ok, owner });
|
|
35
64
|
const v = config.values;
|
|
36
65
|
add("SuiteCloud CLI (@oracle/suitecloud-cli) is installed in the project", existsSync(join(project, "node_modules", "@oracle", "suitecloud-cli", "src", "suitecloud.js")), "maintainer", "Run npm install --save-dev @oracle/suitecloud-cli, read Oracle's license and accept it yourself. This package never accepts it.");
|
|
66
|
+
const javaVersion = installedJavaVersion();
|
|
67
|
+
add(`Java ${javaVersions.join(" or ")} is on the PATH`,
|
|
68
|
+
// The major version: "17.0.4" is Java 17, but "170.0.1" is not.
|
|
69
|
+
javaVersions.includes(javaVersion.split(".")[0] ?? ""), "maintainer", `Install Java ${javaVersions.join(" or ")} and make sure \`java -version\` works: the SuiteCloud SDK runs on no other version.`);
|
|
70
|
+
const sdkRoot = join(homedir(), sdkFolder);
|
|
71
|
+
const jarName = sdkFilename(project);
|
|
72
|
+
add("The SuiteCloud SDK jar is downloaded", jarName !== "" && existsSync(join(sdkRoot, "cli", jarName)), "maintainer", "Run the SuiteCloud CLI's postinstall script in your project (node node_modules/@oracle/suitecloud-cli/postinstall.js) and read and accept Oracle's license yourself when it asks. This package never accepts it. A headless machine needs a desktop install, or the maintainer to accept the license first.");
|
|
73
|
+
add("SuiteCloud login is set up (browser-based once, or CI)", loginFiles.some((file) => existsSync(join(sdkRoot, file))), "maintainer", "With SUITECLOUD_CI_PASSKEY unset, run the project's SuiteCloud CLI account:setup from the SDF folder, log in to the sandbox and choose a role. This one-time browser login installs Oracle's SuiteCloud Development Integration in the account.");
|
|
37
74
|
const missing = [
|
|
38
75
|
names.account,
|
|
39
76
|
names.clientId,
|
|
@@ -84,6 +121,7 @@ export function runChecks(project, config, env = process.env) {
|
|
|
84
121
|
? "Run suitescript-verify init to create the missing folders."
|
|
85
122
|
: "Run account:setup:ci from the SDF folder once the steps above pass, so project.json gets a default auth id.");
|
|
86
123
|
add(`Runner role id: ${config.roleId}`, true, "agent", "");
|
|
124
|
+
add(`Runner id: ${config.runnerId}`, true, "agent", "");
|
|
87
125
|
return checks;
|
|
88
126
|
}
|
|
89
127
|
export function formatChecks(checks) {
|
package/dist/netsuite.js
CHANGED
|
@@ -1,10 +1,23 @@
|
|
|
1
1
|
import { createPrivateKey, sign } from "node:crypto";
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
export const
|
|
2
|
+
import { defaultRunnerId } from "./config.js";
|
|
3
|
+
/** The names a runner takes from its id: the script, the deployment, the File Cabinet folder and the title shown in the account. */
|
|
4
|
+
export const runnerNames = (runnerId) => ({
|
|
5
|
+
deployId: `customdeploy_${runnerId}_runner`,
|
|
6
|
+
folder: runnerId.replaceAll("_", "-"),
|
|
7
|
+
scriptId: `customscript_${runnerId}_runner`,
|
|
8
|
+
// A runner of another id says so, so two runners in one account can be told apart.
|
|
9
|
+
title: runnerId === defaultRunnerId
|
|
10
|
+
? "suitescript-verify runner"
|
|
11
|
+
: `suitescript-verify runner (${runnerId})`,
|
|
12
|
+
});
|
|
13
|
+
/** The id of the runner that wrote an object file, from its name; undefined for any other file. */
|
|
14
|
+
export const runnerIdOfObjectFile = (file) => /^customscript_([a-z0-9_]+)_runner.xml$/.exec(file)?.[1];
|
|
5
15
|
const host = (account) => account.trim().toLowerCase().replace(/_/g, "-");
|
|
6
16
|
export const tokenUrl = (account) => `https://${host(account)}.suitetalk.api.netsuite.com/services/rest/auth/oauth2/v1/token`;
|
|
7
|
-
export const restletUrl = (account) =>
|
|
17
|
+
export const restletUrl = (account, runnerId) => {
|
|
18
|
+
const { deployId, scriptId } = runnerNames(runnerId);
|
|
19
|
+
return `https://${host(account)}.restlets.api.netsuite.com/app/site/hosting/restlet.nl?script=${scriptId}&deploy=${deployId}`;
|
|
20
|
+
};
|
|
8
21
|
const base64url = (data) => Buffer.from(data).toString("base64url");
|
|
9
22
|
/** The signed JWT NetSuite's OAuth 2.0 client credentials flow expects: ES256 with an EC P-256 key. */
|
|
10
23
|
export function clientAssertion(options) {
|
package/dist/run.js
CHANGED
|
@@ -115,7 +115,7 @@ export async function runProbe(project, config, args) {
|
|
|
115
115
|
if (!tokenResponse.ok || !tokenBody.access_token) {
|
|
116
116
|
throw new UsageError(safe(`Token request failed: HTTP ${tokenResponse.status} ${tokenBody.error ?? ""} ${tokenBody.error_description ?? ""}`.trim()));
|
|
117
117
|
}
|
|
118
|
-
const response = await fetch(restletUrl(creds.account), {
|
|
118
|
+
const response = await fetch(restletUrl(creds.account, config.runnerId), {
|
|
119
119
|
body: JSON.stringify(probe.params.length > 0
|
|
120
120
|
? { params, probe: probe.slug }
|
|
121
121
|
: { probe: probe.slug }),
|
package/dist/sdf.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
import { mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
1
|
+
import { mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync, } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
|
-
import {
|
|
4
|
-
import { fileCabinetFolder } from "./netsuite.js";
|
|
3
|
+
import { runnerIdOfObjectFile, runnerNames } from "./netsuite.js";
|
|
5
4
|
import { isRestlet, listProbes, read, tagProblems, } from "./probes.js";
|
|
6
5
|
import { UsageError } from "./shared.js";
|
|
7
6
|
const templates = new URL("../templates/", import.meta.url);
|
|
@@ -23,9 +22,11 @@ export function generateRunner(probes, source) {
|
|
|
23
22
|
/**
|
|
24
23
|
* Writes the generated runner, the probe copies and the SDF project files into
|
|
25
24
|
* the SDF folder. Only RESTlet probes go in: the runner is a RESTlet. With
|
|
26
|
-
* `deployed: false` the runner's deployment is switched off (cleanup).
|
|
25
|
+
* `deployed: false` the runner's deployment is switched off (cleanup). A runner
|
|
26
|
+
* generated here under another id is removed so it is not deployed again;
|
|
27
|
+
* `replacedRunners` names those ids, since each may still be deployed in the sandbox.
|
|
27
28
|
*/
|
|
28
|
-
export function buildSdfProject(
|
|
29
|
+
export function buildSdfProject({ extraTags, probesDir, roleId, runnerId, sdfDir, }, { deployed }) {
|
|
29
30
|
const probes = listProbes(probesDir, extraTags).filter(isRestlet);
|
|
30
31
|
const bad = probes.filter((probe) => tagProblems(probe).length > 0);
|
|
31
32
|
if (bad.length > 0) {
|
|
@@ -34,15 +35,28 @@ export function buildSdfProject(sdfDir, probesDir, { deployed, extraTags = [], r
|
|
|
34
35
|
.join("\n")}`);
|
|
35
36
|
}
|
|
36
37
|
const src = join(sdfDir, "src");
|
|
37
|
-
const
|
|
38
|
+
const names = runnerNames(runnerId);
|
|
39
|
+
const folder = join(src, "FileCabinet", "SuiteScripts", names.folder);
|
|
38
40
|
rmSync(join(src, "FileCabinet"), { force: true, recursive: true });
|
|
39
41
|
mkdirSync(join(folder, "probes"), { recursive: true });
|
|
40
42
|
mkdirSync(join(src, "Objects"), { recursive: true });
|
|
43
|
+
const replacedRunners = [];
|
|
44
|
+
for (const file of readdirSync(join(src, "Objects"))) {
|
|
45
|
+
const id = runnerIdOfObjectFile(file);
|
|
46
|
+
if (id !== undefined && id !== runnerId) {
|
|
47
|
+
rmSync(join(src, "Objects", file));
|
|
48
|
+
replacedRunners.push(id);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
41
51
|
writeFileSync(join(sdfDir, "package.json"), template("sdf/package.json"));
|
|
42
52
|
writeFileSync(join(sdfDir, "suitecloud.config.js"), template("sdf/suitecloud.config.js"));
|
|
43
53
|
writeFileSync(join(src, "manifest.xml"), template("sdf/manifest.xml").replaceAll("%%ROLE_ID%%", roleId));
|
|
44
|
-
writeFileSync(join(src, "deploy.xml"), template("sdf/deploy.xml"));
|
|
45
|
-
writeFileSync(join(src, "Objects",
|
|
54
|
+
writeFileSync(join(src, "deploy.xml"), template("sdf/deploy.xml").replaceAll("%%FOLDER%%", names.folder));
|
|
55
|
+
writeFileSync(join(src, "Objects", `${names.scriptId}.xml`), template("sdf/runner.xml")
|
|
56
|
+
.replaceAll("%%SCRIPT_ID%%", names.scriptId)
|
|
57
|
+
.replaceAll("%%DEPLOY_ID%%", names.deployId)
|
|
58
|
+
.replaceAll("%%FOLDER%%", names.folder)
|
|
59
|
+
.replaceAll("%%TITLE%%", names.title)
|
|
46
60
|
.replace("%%DEPLOYED%%", deployed ? "T" : "F")
|
|
47
61
|
.replaceAll("%%ROLE_ID%%", roleId));
|
|
48
62
|
const slugs = probes.map((probe) => probe.slug);
|
|
@@ -52,5 +66,5 @@ export function buildSdfProject(sdfDir, probesDir, { deployed, extraTags = [], r
|
|
|
52
66
|
writeFileSync(join(folder, "probes", `${slug}.js`), read(join(probesDir, `${slug}.js`)));
|
|
53
67
|
files.push(`probes/${slug}.js`);
|
|
54
68
|
}
|
|
55
|
-
return { files, slugs };
|
|
69
|
+
return { files, replacedRunners, slugs };
|
|
56
70
|
}
|
package/package.json
CHANGED
package/templates/sdf/deploy.xml
CHANGED
package/templates/sdf/runner.xml
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
|
-
<restlet scriptid="
|
|
1
|
+
<restlet scriptid="%%SCRIPT_ID%%">
|
|
2
2
|
<description>Runs one version-controlled probe by name in a sandbox. Source: the project's verification folder, through suitescript-verify.</description>
|
|
3
3
|
<isinactive>F</isinactive>
|
|
4
|
-
<name
|
|
4
|
+
<name>%%TITLE%%</name>
|
|
5
5
|
<notifyowner>F</notifyowner>
|
|
6
|
-
<scriptfile>[/SuiteScripts
|
|
6
|
+
<scriptfile>[/SuiteScripts/%%FOLDER%%/runner.js]</scriptfile>
|
|
7
7
|
<scriptdeployments>
|
|
8
|
-
<scriptdeployment scriptid="
|
|
8
|
+
<scriptdeployment scriptid="%%DEPLOY_ID%%">
|
|
9
9
|
<allroles>F</allroles>
|
|
10
10
|
<audslctrole>[%%ROLE_ID%%]</audslctrole>
|
|
11
11
|
<isdeployed>%%DEPLOYED%%</isdeployed>
|
|
12
12
|
<loglevel>DEBUG</loglevel>
|
|
13
13
|
<status>RELEASED</status>
|
|
14
|
-
<title
|
|
14
|
+
<title>%%TITLE%%</title>
|
|
15
15
|
</scriptdeployment>
|
|
16
16
|
</scriptdeployments>
|
|
17
17
|
</restlet>
|