@andrian.yablonskyy/thub-common 1.0.29 → 1.0.31

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@andrian.yablonskyy/thub-common",
3
- "version": "1.0.29",
3
+ "version": "1.0.31",
4
4
  "description": "Shared job-spec schema, state enums and a thin API client used by the Agent and Client",
5
5
  "main": "src/index.js",
6
6
  "files": [
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 helper; `sudo` is the caller's job otherwise). Returns npm's exit code.
94
- function npmInstallGlobal(pkg, version, { env = process.env, stdio = 'inherit' } = {}){
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 result = spawnSync(npmBin(), ['i', '-g', `${pkg}@${version}`], { env, stdio });
102
- if (result.error){
103
- throw result.error;
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
  };
@@ -55,7 +55,7 @@ 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.
58
+ // THUB_* and JOB_* — the job's own parameters): git's safety settings.
59
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,
@@ -69,9 +69,9 @@ 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));
72
+ reserved = envNames.filter((n) => /^(THUB|JOB)_/.test(n) || RESERVED_ENV.includes(n));
73
73
  if (reserved.length){
74
- errors.push(`/env ${reserved.join(', ')}: set by the Client itself (THUB_*, ${RESERVED_ENV.join(', ')}) — use other names`);
74
+ errors.push(`/env ${reserved.join(', ')}: set by the Client itself (THUB_*, JOB_*, ${RESERVED_ENV.join(', ')}) — use other names`);
75
75
  }
76
76
  if (spec.git?.options){
77
77
  try {