@andrian.yablonskyy/thub-common 1.0.28 → 1.0.30
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 +1 -1
- package/src/job-spec.schema.js +6 -10
- package/src/updates.js +38 -7
- package/src/validate-job-spec.js +5 -10
package/package.json
CHANGED
package/src/job-spec.schema.js
CHANGED
|
@@ -102,11 +102,10 @@ const DOCKER_IMAGE_PATTERN =
|
|
|
102
102
|
}
|
|
103
103
|
},
|
|
104
104
|
// Environment variables (`--env NAME=value`) the Client sets for every
|
|
105
|
-
// command it runs for the job — git
|
|
106
|
-
//
|
|
107
|
-
//
|
|
108
|
-
//
|
|
109
|
-
// from its database once the job ends.
|
|
105
|
+
// command it runs for the job — git and the job's command. No name
|
|
106
|
+
// means anything special. Values are secrets as far as the Coordinator
|
|
107
|
+
// is concerned: masked in the Agent API and dropped from its database
|
|
108
|
+
// once the job ends.
|
|
110
109
|
env: {
|
|
111
110
|
type: 'object',
|
|
112
111
|
maxProperties: 64,
|
|
@@ -128,9 +127,6 @@ const DOCKER_IMAGE_PATTERN =
|
|
|
128
127
|
// see README §7.1 "Dry-run the pipeline".
|
|
129
128
|
dryRun: { type: 'boolean', default: false }
|
|
130
129
|
}
|
|
131
|
-
}
|
|
130
|
+
};
|
|
132
131
|
|
|
133
|
-
|
|
134
|
-
DOCKER_LOGIN_ENV = ['DOCKER_REGISTRY', 'DOCKER_USERNAME', 'DOCKER_PASSWORD'];
|
|
135
|
-
|
|
136
|
-
module.exports = { jobSpecSchema, DOCKER_IMAGE_PATTERN, ENV_NAME_PATTERN, DOCKER_LOGIN_ENV };
|
|
132
|
+
module.exports = { jobSpecSchema, DOCKER_IMAGE_PATTERN, ENV_NAME_PATTERN };
|
package/src/updates.js
CHANGED
|
@@ -89,20 +89,50 @@ function npmBin(){
|
|
|
89
89
|
return fs.existsSync(candidate) ? candidate : 'npm';
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
+
// npm's errors for a version that isn't downloadable (yet): right after a
|
|
93
|
+
// publish, the registry can list a version whose tarball still 404s for a
|
|
94
|
+
// few minutes — worth waiting for, unlike any other failure.
|
|
95
|
+
const NOT_YET_AVAILABLE = /\b(E404|ETARGET|ENOVERSIONS)\b/,
|
|
96
|
+
// How long to wait before each retry of such a failure: ~7.5 min in all.
|
|
97
|
+
NPM_RETRY_DELAYS_SEC = [30, 60, 120, 240];
|
|
98
|
+
|
|
99
|
+
function sleepSync(ms){
|
|
100
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
101
|
+
}
|
|
102
|
+
|
|
92
103
|
// `npm i -g <pkg>@<version>` (as the current user — root for the Client's
|
|
93
|
-
// update
|
|
94
|
-
|
|
104
|
+
// and Coordinator's update helpers; `sudo` is the caller's job otherwise).
|
|
105
|
+
// Always with --prefer-online, so a stale cached packument can't hide or
|
|
106
|
+
// misplace a fresh version. A "not found" failure is retried after each of
|
|
107
|
+
// `retryDelaysSec` (npm's stderr is read to tell, and passed on); anything
|
|
108
|
+
// else fails at once. Returns npm's exit code. `spawn`/`sleep`: for tests.
|
|
109
|
+
function npmInstallGlobal(pkg, version, {
|
|
110
|
+
env = process.env, stdio = 'inherit', retryDelaysSec = NPM_RETRY_DELAYS_SEC, log = (line) => console.error(line),
|
|
111
|
+
spawn = spawnSync, sleep = sleepSync
|
|
112
|
+
} = {}){
|
|
95
113
|
if (!Object.values(PACKAGES).includes(pkg)){
|
|
96
114
|
throw new Error(`Refusing to install unknown package ${pkg}`);
|
|
97
115
|
}
|
|
98
116
|
if (!isValidVersion(version)){
|
|
99
117
|
throw new Error(`Invalid version "${version}"`);
|
|
100
118
|
}
|
|
101
|
-
const
|
|
102
|
-
|
|
103
|
-
|
|
119
|
+
const [stdin, stdout] = Array.isArray(stdio) ? stdio : [stdio, stdio],
|
|
120
|
+
args = ['i', '-g', '--prefer-online', `${pkg}@${version}`];
|
|
121
|
+
for (let attempt = 0; ; attempt++){
|
|
122
|
+
const result = spawn(npmBin(), args, { env, stdio: [stdin, stdout, 'pipe'], encoding: 'utf8' });
|
|
123
|
+
if (result.error){
|
|
124
|
+
throw result.error;
|
|
125
|
+
}
|
|
126
|
+
const stderr = result.stderr || '';
|
|
127
|
+
if (stderr){
|
|
128
|
+
process.stderr.write(stderr);
|
|
129
|
+
}
|
|
130
|
+
if (result.status === 0 || !NOT_YET_AVAILABLE.test(stderr) || attempt >= retryDelaysSec.length){
|
|
131
|
+
return result.status;
|
|
132
|
+
}
|
|
133
|
+
log(`${pkg}@${version} isn't downloadable from the registry yet (just published?) — retrying in ${retryDelaysSec[attempt]} s`);
|
|
134
|
+
sleep(retryDelaysSec[attempt] * 1000);
|
|
104
135
|
}
|
|
105
|
-
return result.status;
|
|
106
136
|
}
|
|
107
137
|
|
|
108
138
|
module.exports = {
|
|
@@ -113,5 +143,6 @@ module.exports = {
|
|
|
113
143
|
defaultRegistry,
|
|
114
144
|
fetchLatestVersion,
|
|
115
145
|
npmBin,
|
|
116
|
-
npmInstallGlobal
|
|
146
|
+
npmInstallGlobal,
|
|
147
|
+
NPM_RETRY_DELAYS_SEC
|
|
117
148
|
};
|
package/src/validate-job-spec.js
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
|
|
16
16
|
const Ajv = require('ajv'),
|
|
17
17
|
addFormats = require('ajv-formats'),
|
|
18
|
-
{ jobSpecSchema
|
|
18
|
+
{ jobSpecSchema } = require('./job-spec.schema'),
|
|
19
19
|
{ splitArgs } = require('./split-args');
|
|
20
20
|
|
|
21
21
|
const ajv = new Ajv({ useDefaults: true, allErrors: true, strict: false });
|
|
@@ -55,8 +55,8 @@ function legacyShapeError(spec){
|
|
|
55
55
|
const GIT_URL = /^(?:(?:https?|ssh|git):\/\/[^\s]+|[A-Za-z0-9._-]+@[A-Za-z0-9.-]+:[^\s]+)$/,
|
|
56
56
|
|
|
57
57
|
// Job env names the Client sets itself for a job's commands (besides
|
|
58
|
-
// THUB_*): git's safety settings
|
|
59
|
-
RESERVED_ENV = ['GIT_TERMINAL_PROMPT', 'GIT_ALLOW_PROTOCOL'
|
|
58
|
+
// THUB_*): git's safety settings.
|
|
59
|
+
RESERVED_ENV = ['GIT_TERMINAL_PROMPT', 'GIT_ALLOW_PROTOCOL'];
|
|
60
60
|
|
|
61
61
|
// Rules across fields, spelled out here rather than as schema if/then,
|
|
62
62
|
// whose errors ("must match a schema in then") say little.
|
|
@@ -69,15 +69,10 @@ function crossFieldErrors(spec){
|
|
|
69
69
|
errors.push('/git/url must be an https://, http://, ssh:// or git:// URL, or user@host:path');
|
|
70
70
|
}
|
|
71
71
|
const envNames = Object.keys(spec.env && typeof spec.env === 'object' ? spec.env : {}),
|
|
72
|
-
reserved = envNames.filter((n) => /^THUB_/.test(n) || RESERVED_ENV.includes(n))
|
|
73
|
-
login = DOCKER_LOGIN_ENV.filter((n) => envNames.includes(n));
|
|
72
|
+
reserved = envNames.filter((n) => /^THUB_/.test(n) || RESERVED_ENV.includes(n));
|
|
74
73
|
if (reserved.length){
|
|
75
74
|
errors.push(`/env ${reserved.join(', ')}: set by the Client itself (THUB_*, ${RESERVED_ENV.join(', ')}) — use other names`);
|
|
76
75
|
}
|
|
77
|
-
if (login.length && login.length < DOCKER_LOGIN_ENV.length){
|
|
78
|
-
errors.push(`/env a Docker registry login needs all of ${DOCKER_LOGIN_ENV.join(', ')} — missing ` +
|
|
79
|
-
DOCKER_LOGIN_ENV.filter((n) => !login.includes(n)).join(', '));
|
|
80
|
-
}
|
|
81
76
|
if (spec.git?.options){
|
|
82
77
|
try {
|
|
83
78
|
splitArgs(spec.git.options);
|
|
@@ -97,4 +92,4 @@ function maskEnv(env){
|
|
|
97
92
|
|
|
98
93
|
const MASKED = '***';
|
|
99
94
|
|
|
100
|
-
module.exports = { validateJobSpec, maskEnv,
|
|
95
|
+
module.exports = { validateJobSpec, maskEnv, JOB_ENV_MASK: MASKED };
|