arcadible 1.0.10 → 1.0.12

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/bin/arc-deploy.js CHANGED
@@ -136,14 +136,6 @@ const report = function (
136
136
 
137
137
  // [function]
138
138
 
139
- // the account, or null when there's none or it can't be refreshed: for a git
140
- // deploy, which needs none.
141
- const whoamiIfAny = function (opts) {
142
- return whoami(opts).catch(() => null);
143
- };
144
-
145
- // [function]
146
-
147
139
  // the release, as `--json` prints it.
148
140
  const releaseJson = function (row) {
149
141
  if (!row) {
@@ -168,7 +160,7 @@ const waitAndReport = async function (
168
160
  current,
169
161
  deployed,
170
162
  result,
171
- { git, json, owner, quiet, root, since, timeout },
163
+ { git, json, quiet, root, since, timeout },
172
164
  ) {
173
165
  let sw = new SpinnerWheel(phrase.releaseWaiting(git));
174
166
  sw.start(json || quiet);
@@ -178,7 +170,7 @@ const waitAndReport = async function (
178
170
  waited = await wait(
179
171
  current,
180
172
  deployed,
181
- { git, owner, root, since, timeout },
173
+ { git, root, since, timeout },
182
174
  {
183
175
  onRelease() {
184
176
  // found: GitHub Actions is done, and Arcadible publishes it now.
@@ -253,9 +245,6 @@ const start = function (argv) {
253
245
  const timeout = Number(argv.timeout ?? TIMEOUT[git ? "git" : "direct"]);
254
246
  const root = resolve(process.cwd(), argv.cwd);
255
247
  let pb = null;
256
- // the game's account, when Arcadible could say: a git deploy's owner waits
257
- // among their own releases.
258
- let owner = null;
259
248
  // whether the handle rules held the handle back from this release, so the
260
249
  // game's links are by id.
261
250
  let held = false;
@@ -277,14 +266,12 @@ const start = function (argv) {
277
266
  sw.stop(true);
278
267
  warn(warnings, { json, quiet });
279
268
  sw = new SpinnerWheel("Checking… %s");
280
- // git deploys need no login: anyone who can push to the repository can
281
- // release, and the Arcadible GitHub App publishes the release. Anyone
282
- // can wait for it too; the game's owner, logged in, waits among their
283
- // own releases, which also say when the repository isn't connected.
269
+ // git deploys need no login, and never use one: anyone who can push to
270
+ // the repository can release, and the Arcadible GitHub App publishes
271
+ // the release. Every call is public, so it's the same for whoever runs
272
+ // it, and the saved session isn't read, or refreshed.
284
273
  if (git) {
285
- return whoamiIfAny({
286
- handler: handler({ json, pb, quiet, sw, warnings }),
287
- });
274
+ return null;
288
275
  }
289
276
  return whoami({ handler: handler({ json, pb, quiet, sw, warnings }) });
290
277
  })
@@ -309,7 +296,6 @@ const start = function (argv) {
309
296
  })
310
297
  .then(([current, predeployed]) => {
311
298
  sw.stop(true);
312
- owner = predeployed.owner ?? null;
313
299
  held = predeployed.held ?? false;
314
300
  sw = new SpinnerWheel("Building… %s");
315
301
  sw.start(git || json || quiet);
@@ -394,7 +380,6 @@ const start = function (argv) {
394
380
  return waitAndReport(current, deployed, result, {
395
381
  git,
396
382
  json,
397
- owner,
398
383
  quiet,
399
384
  root,
400
385
  since,
package/package.json CHANGED
@@ -65,5 +65,5 @@
65
65
  "scripts": {
66
66
  "test": "node --test \"test/**/*.test.js\""
67
67
  },
68
- "version": "1.0.10"
68
+ "version": "1.0.12"
69
69
  }
@@ -63,27 +63,24 @@ const findDirect = function (id, headers) {
63
63
  // [function]
64
64
 
65
65
  // a git deploy's release: GitHub's, for this version, made since the tag was
66
- // pushed. Null until GitHub Actions has built it. The game's owner looks
67
- // among their account's releases, which include one with no game: a
68
- // repository that isn't connected fails that way (repo_not_connected).
69
- // anyone else, logged in or not, looks among the game's, which anyone can
70
- // read.
71
- const findGit = function ({ account, game, headers, since, version }) {
72
- const newest = account
73
- ? () => api.accountReleases(account, { limit: 20, select: SELECT }, headers)
74
- : () =>
75
- api.discoverGameReleases(game, { limit: 10, select: SELECT, version });
66
+ // pushed. Null until GitHub Actions has built it. Looked for among the
67
+ // game's releases, which anyone can read, with no token: a git deploy asks
68
+ // what's true for whoever runs it. A release with no game (a repository that
69
+ // isn't connected, which the predeploy refuses first) isn't among them.
70
+ const findGit = function ({ game, since, version }) {
76
71
  return () =>
77
- newest().then(
78
- (rows) =>
79
- (rows ?? []).find(
80
- (r) =>
81
- r.source === "github" &&
82
- r.version === version &&
83
- (r.game === game || r.game === null) &&
84
- Date.parse(r.created_at) >= since - SKEW,
85
- ) ?? null,
86
- );
72
+ api
73
+ .discoverGameReleases(game, { limit: 10, select: SELECT, version })
74
+ .then(
75
+ (rows) =>
76
+ (rows ?? []).find(
77
+ (r) =>
78
+ r.source === "github" &&
79
+ r.version === version &&
80
+ r.game === game &&
81
+ Date.parse(r.created_at) >= since - SKEW,
82
+ ) ?? null,
83
+ );
87
84
  };
88
85
 
89
86
  // [function]
@@ -123,28 +120,19 @@ const pollRelease = function (find, timeout, onRelease) {
123
120
  // [function]
124
121
 
125
122
  // waits on a deploy's release. `deployed` is what the deploy resolved: a
126
- // direct deploy's has the release's id, and a git deploy's its game.
127
- // `owner` is the game's account, when it's known: only they get Studio's
128
- // link, since only they can open it. A token's account isn't known.
123
+ // direct deploy's has the release's id, and a git deploy's its game. A git
124
+ // deploy has no `current`: it never uses a login. Studio's link is for
125
+ // everyone, since Studio asks whoever opens it to log in.
129
126
  const main = function (
130
127
  current,
131
128
  deployed,
132
- { git: isGit, owner, root, since, timeout },
129
+ { git: isGit, root, since, timeout },
133
130
  { onRelease } = {},
134
131
  ) {
135
- const headers = api.auth(current?.session?.access_token);
136
132
  const game = deployed.game;
137
- const account = current?.token ? null : (current?.user?.id ?? null);
138
- const owns = !isGit || (account !== null && account === owner);
139
133
  const find = isGit
140
- ? findGit({
141
- account: owns ? account : null,
142
- game,
143
- headers,
144
- since,
145
- version: deployed.version.to,
146
- })
147
- : findDirect(deployed.release, headers);
134
+ ? findGit({ game, since, version: deployed.version.to })
135
+ : findDirect(deployed.release, api.auth(current?.session?.access_token));
148
136
  return Promise.all([
149
137
  pollRelease(find, timeout, onRelease),
150
138
  isGit ? actionsUrl(root) : null,
@@ -152,7 +140,7 @@ const main = function (
152
140
  .then(([{ release, timedOut }, actions]) => ({
153
141
  actions,
154
142
  release,
155
- studio: owns ? studioUrl(game, "builds") : null,
143
+ studio: studioUrl(game, "builds"),
156
144
  timedOut,
157
145
  }))
158
146
  .catch(ended(current));
@@ -9,33 +9,22 @@ const path = require("node:path");
9
9
 
10
10
  const cfgcode = require("../../config/code");
11
11
  const error = require("../../util/error");
12
+ const lockfile = require("../../util/lockfile");
12
13
  const phrase = require("../../phrase");
13
14
 
14
15
  // [use]
15
16
 
16
17
  const ArcError = error.ArcError;
18
+ const detect = lockfile.detect;
17
19
  const existsSync = fs.existsSync; // eslint-disable-line no-sync
18
20
  const join = path.join;
19
21
 
20
- // [declare]
21
-
22
- const LOCK_FILES = [
23
- // npm
24
- "package-lock.json",
25
- "npm-shrinkwrap.json",
26
- // pnpm
27
- "pnpm-lock.yaml",
28
- // yarn
29
- "yarn.lock",
30
- ];
31
-
32
22
  // [function]
33
23
 
34
24
  // fails when there's a package.json without a lockfile: the release workflow
35
25
  // installs exactly what the lockfile pins.
36
26
  const ensureLockFile = function (root) {
37
- const has = (file) => existsSync(join(root, file));
38
- if (has("package.json") && !LOCK_FILES.some(has)) {
27
+ if (existsSync(join(root, "package.json")) && !detect(root)) {
39
28
  return Promise.reject(
40
29
  new ArcError(cfgcode.LOCKFILE_MISSING, phrase.lockfileMissing()),
41
30
  );
@@ -39,11 +39,12 @@ const answerOf = function (request) {
39
39
  // [function]
40
40
 
41
41
  // what Arcadible would refuse in the release, refused before the tag is pushed: a
42
- // game that was never deployed (a release can't create one), and a version
43
- // that's already published. The lookups are public, so this needs no login.
42
+ // game that was never deployed (a release can't create one), a version
43
+ // that's already published, and a handle that isn't the game's (only a
44
+ // direct deploy changes one, so a collaborator's push can't). The lookups
45
+ // are public, so this needs no login.
44
46
  // when Arcadible can't be reached, it warns and pushes anyway: a release
45
47
  // shouldn't depend on the API, and Arcadible still checks.
46
- // resolves with the game's owner, or null when Arcadible can't be reached.
47
48
  const ensurePublishable = function ({ handle, id, version }, handler) {
48
49
  return Promise.all([
49
50
  answerOf(api.discoverGame(id)),
@@ -64,9 +65,17 @@ const ensurePublishable = function ({ handle, id, version }, handler) {
64
65
  }
65
66
  return null;
66
67
  }
67
- return build.status === 200
68
- ? published.refuse(id, version)
69
- : (game.data?.account ?? null);
68
+ if (build.status === 200) {
69
+ return published.refuse(id, version);
70
+ }
71
+ const current = game.data?.handle ?? null;
72
+ if ((handle ?? null) !== current) {
73
+ throw new ArcError(
74
+ cfgcode.GAME_HANDLE_CHANGED,
75
+ phrase.gameHandleChanged(handle, current),
76
+ );
77
+ }
78
+ return null;
70
79
  });
71
80
  };
72
81
 
@@ -116,17 +125,20 @@ const ensureConnected = function (root, { handle, id }) {
116
125
  // [function]
117
126
 
118
127
  // the release's checks: its tag is free, Arcadible would publish it, and
119
- // origin is its connected repository. resolves with the version, and the
120
- // game's owner, who waits on the release among their own.
128
+ // origin is its connected repository. resolves with the version.
121
129
  const ensureRelease = function (root, game, handler) {
122
130
  return ensureTagFree(root, game.version)
123
131
  .then(() => ensurePublishable(game, handler))
124
- .then((owner) => Promise.all([owner, ensureConnected(root, game)]))
125
- .then(([owner]) => ({ owner, to: game.version }));
132
+ .then(() => ensureConnected(root, game))
133
+ .then(() => ({ to: game.version }));
126
134
  };
127
135
 
128
136
  // [function]
129
137
 
138
+ // a git deploy needs no login, and never uses one: anyone who can push can
139
+ // release, and so can tools other than arc. Every lookup is public, so its
140
+ // answer is the same whoever runs it, and true when it's asked; the release
141
+ // pipeline has the final word.
130
142
  const main = function (conf, { onPredeployBegin, ...handler }) {
131
143
  const root = conf.root;
132
144
  if (onPredeployBegin) {
@@ -12,6 +12,8 @@ const ensureUnpublished = ac.ensureUnpublished;
12
12
 
13
13
  // [function]
14
14
 
15
+ // a direct deploy needs a login. Each lookup uses its token or not, as it
16
+ // needs, and its answer is true when it's asked.
15
17
  const main = function (current, conf, { onPredeployBegin, ...handler }) {
16
18
  if (onPredeployBegin) {
17
19
  onPredeployBegin({ root: conf.root });
package/src/api.js CHANGED
@@ -136,19 +136,9 @@ const release = function (id, params, headers) {
136
136
  return request.get(endpoint(pathOf`/releases/${id}`, params), headers);
137
137
  };
138
138
 
139
- // [function]
140
-
141
- const accountReleases = function (id, params, headers) {
142
- return request.get(
143
- endpoint(pathOf`/accounts/${id}/releases`, params),
144
- headers,
145
- );
146
- };
147
-
148
139
  // [export]
149
140
 
150
141
  module.exports = {
151
- accountReleases,
152
142
  auth,
153
143
  discoverGame,
154
144
  discoverGameBuild,
@@ -49,10 +49,11 @@ const CLI = {
49
49
  };
50
50
  // a release's reasons, from the pipeline (the API's
51
51
  // db/type/release_reason.sql). The CLI shares them, so a name means the same
52
- // thing wherever it appears: GAME_UNAVAILABLE, INTERNAL, REPO_NOT_CONNECTED,
53
- // and VERSION_EXISTS are the CLI's too.
52
+ // thing wherever it appears: GAME_HANDLE_CHANGED, GAME_UNAVAILABLE, INTERNAL,
53
+ // as well as REPO_NOT_CONNECTED and VERSION_EXISTS, are the CLI's too.
54
54
  const RELEASE = {
55
55
  BUILD_RATE_LIMITED: "build_rate_limited",
56
+ GAME_HANDLE_CHANGED: "game_handle_changed",
56
57
  GAME_LIMIT_REACHED: "game_limit_reached",
57
58
  GAME_MISMATCH: "game_mismatch",
58
59
  GAME_UNAVAILABLE: "game_unavailable",
package/src/parser/env.js CHANGED
@@ -12,6 +12,7 @@ const fs = require("fs-extra");
12
12
 
13
13
  const cfgcode = require("../config/code");
14
14
  const error = require("../util/error");
15
+ const lockfile = require("../util/lockfile");
15
16
  const objs = require("../util/objs");
16
17
  const phrase = require("../phrase");
17
18
 
@@ -19,14 +20,13 @@ const phrase = require("../phrase");
19
20
 
20
21
  const ArcError = error.ArcError;
21
22
  const deepCompact = objs.deepCompact;
23
+ const detect = lockfile.detect;
22
24
  const join = path.join;
23
25
  const pathExists = fs.pathExists;
24
26
  const readFile = fs.readFile;
25
27
 
26
28
  // [declare]
27
29
 
28
- // the build a package.json build script stands for.
29
- const NPM_RUN_BUILD = "npm run build";
30
30
  const PACKAGE = "package.json";
31
31
 
32
32
  // [function]
@@ -53,13 +53,19 @@ const parseJson = function (buf) {
53
53
 
54
54
  // [function]
55
55
 
56
- // package.json's version, and its build script as `npm run build`: run
57
- // through npm, the script finds the project's own tools on the PATH
58
- // (node_modules/.bin), and its prebuild and postbuild run, as they do
59
- // locally. npm runs the scripts of a pnpm or Yarn project too.
60
- const extract = function (conf) {
56
+ // package.json's version, and its build script as `<manager> run build`, run
57
+ // with the package manager its lockfile names (npm without one): the script
58
+ // finds the project's own tools, and its prebuild and postbuild run, as they do
59
+ // locally. A Yarn project on Plug'n'Play has no node_modules, so only Yarn
60
+ // finds its tools.
61
+ const extract = function (conf, manager) {
62
+ if (!Object.keys(conf).length) {
63
+ return {};
64
+ }
61
65
  const game = { version: conf.version };
62
- const workflow = conf.scripts?.build ? { build: NPM_RUN_BUILD } : {};
66
+ const workflow = conf.scripts?.build
67
+ ? { build: `${manager || "npm"} run build` }
68
+ : {};
63
69
  return deepCompact({ game, workflow }) ?? {};
64
70
  };
65
71
 
@@ -70,7 +76,7 @@ const main = function (root) {
70
76
  return pathExists(file)
71
77
  .then((exists) => (exists ? readFile(file, "utf8") : null))
72
78
  .then((buf) => (buf === null ? {} : parseJson(buf)))
73
- .then((conf) => (Object.keys(conf).length ? extract(conf) : {}));
79
+ .then((conf) => extract(conf, detect(root)));
74
80
  };
75
81
 
76
82
  // [export]
package/src/phrase.js CHANGED
@@ -225,6 +225,17 @@ const gameHandleTaken = function (handle) {
225
225
 
226
226
  // [function]
227
227
 
228
+ // a git release that would change the game's handle, which only a direct
229
+ // deploy does.
230
+ const gameHandleChanged = function (handle, current) {
231
+ return blocks(
232
+ `The handle in arcadible.toml (${handle ?? "none"}) isn't the game's on ${TITLE} (${current ?? "none"}).`,
233
+ `Only ${cmdDeploy()} changes a handle: deploy with it, or set the handle back.`,
234
+ );
235
+ };
236
+
237
+ // [function]
238
+
228
239
  const gameHandleLapsed = function (handle) {
229
240
  return `Handle ${handle} lapsed, so deploying without it.\nA production release takes it back.`;
230
241
  };
@@ -764,12 +775,12 @@ const packageUncompressedTooLarge = function (size) {
764
775
 
765
776
  // a failed release's message, a sentence to a line, then its reason code
766
777
  // and where to see it.
767
- // `studio` is the game's builds in Studio, for its owner; anyone else can't
768
- // open it.
778
+ // `studio` is the game's builds in Studio, which asks whoever opens it to
779
+ // log in.
769
780
  const releaseFailed = function ({ message }, studio) {
770
781
  return blocks(
771
782
  (message ?? "No reason given.").split(/(?<=\.)\s+/u).join("\n"),
772
- ...(studio ? [[`${TITLE} Studio:`, studio]] : []),
783
+ [`${TITLE} Studio:`, studio],
773
784
  );
774
785
  };
775
786
 
@@ -791,18 +802,12 @@ const releaseTimedOut = function (
791
802
  { actions, found, git, studio },
792
803
  ) {
793
804
  const head = `Waited ${waited(seconds)} for v${version}.`;
794
- if ((found || !git) && studio) {
805
+ if (found || !git) {
795
806
  return blocks(head, [
796
807
  `It may still finish. See it in ${TITLE} Studio:`,
797
808
  studio,
798
809
  ]);
799
810
  }
800
- if (found) {
801
- return blocks(head, [
802
- "It may still finish.",
803
- `The ${TITLE} check on the tagged commit says how it went.`,
804
- ]);
805
- }
806
811
  return blocks(
807
812
  head,
808
813
  [
@@ -1092,6 +1097,7 @@ module.exports = {
1092
1097
  docMediaType,
1093
1098
  docTagUnallowed,
1094
1099
  errorCode,
1100
+ gameHandleChanged,
1095
1101
  gameHandleLapsed,
1096
1102
  gameHandleLimit,
1097
1103
  gameHandleTaken,
@@ -0,0 +1,35 @@
1
+ "use strict";
2
+
3
+ // [import]
4
+
5
+ const fs = require("node:fs");
6
+ const path = require("node:path");
7
+
8
+ // [use]
9
+
10
+ const existsSync = fs.existsSync; // eslint-disable-line no-sync
11
+ const join = path.join;
12
+
13
+ // [declare]
14
+
15
+ // each package manager, and the lockfiles that mean a project uses it, in the
16
+ // order they're checked, as the release tools' install checks them.
17
+ const LOCKFILES = [
18
+ ["npm", ["package-lock.json", "npm-shrinkwrap.json"]],
19
+ ["pnpm", ["pnpm-lock.yaml"]],
20
+ ["yarn", ["yarn.lock"]],
21
+ ];
22
+
23
+ // [function]
24
+
25
+ // the package manager a project's lockfile names, or "" when it has none.
26
+ const detect = function (root) {
27
+ const found = LOCKFILES.find(([, names]) =>
28
+ names.some((name) => existsSync(join(root, name))),
29
+ );
30
+ return found ? found[0] : "";
31
+ };
32
+
33
+ // [export]
34
+
35
+ module.exports = { detect };
package/src/util/video.js CHANGED
@@ -6,8 +6,8 @@ const path = require("node:path");
6
6
 
7
7
  // [import]
8
8
 
9
- const createFile = require("mp4box").createFile;
10
9
  const fs = require("fs-extra");
10
+ const mp4box = require("mp4box");
11
11
 
12
12
  // [import]
13
13
 
@@ -18,6 +18,7 @@ const phrase = require("../phrase");
18
18
 
19
19
  // [use]
20
20
 
21
+ const createFile = mp4box.createFile;
21
22
  const checkAspect = media.checkAspect;
22
23
  const checkSize = media.checkSize;
23
24
  const extname = path.extname;
@@ -30,6 +31,15 @@ const statSync = fs.statSync; // eslint-disable-line no-sync
30
31
 
31
32
  const NO_TRACK = "no-video-track";
32
33
 
34
+ // [configure]
35
+
36
+ // mp4box prints what it recovers from at its error level, which setLogLevel
37
+ // can't turn down: its colr parser misreads an ICC profile by the box's
38
+ // header, and says so for a video it reads correctly. arc reports a bad video
39
+ // itself, so mp4box only passes on the errors it sends to the file's onError.
40
+ mp4box.Log.error = (mod, msg, isofile) => isofile?.onError?.(mod, msg);
41
+ mp4box.Log.warn = () => {};
42
+
33
43
  // [function]
34
44
 
35
45
  const readMp4 = function (file) {