arcadible 0.1.37 → 0.1.39

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
@@ -109,10 +109,10 @@ const handler = function ({ json, pb, quiet, sw }) {
109
109
 
110
110
  // what was deployed, whether workflow.build ran, where it will be once it's
111
111
  // published (the store only shows production), and what happens next. Once
112
- // waited on, it's published, so it says so. `note` is a last block.
112
+ // waited on, it's published, so it says so.
113
113
  const report = function (
114
114
  { built, channel, store, tag, url, version },
115
- { git, note, published },
115
+ { git, published },
116
116
  ) {
117
117
  let heading = git
118
118
  ? phrase.tagPushed(tag)
@@ -124,36 +124,19 @@ const report = function (
124
124
  heading = phrase.releasePublished(version.to, channel, built);
125
125
  next = null;
126
126
  }
127
- log.result(heading, [
128
- store ? ["Store:", store] : null,
129
- ["Play:", url],
130
- next,
131
- note ?? null,
132
- ]);
127
+ log.result(heading, [store ? ["Store:", store] : null, ["Play:", url], next]);
133
128
  };
134
129
 
135
130
  // [function]
136
131
 
137
132
  // the account, or null when there's none or it can't be refreshed: for a git
138
- // deploy, which only needs one to wait.
133
+ // deploy, which needs none.
139
134
  const whoamiIfAny = function (opts) {
140
135
  return whoami(opts).catch(() => null);
141
136
  };
142
137
 
143
138
  // [function]
144
139
 
145
- // whether `current` can wait for a git release: only the game's owner can see
146
- // its releases. A token's account isn't known, so it waits, and so does any
147
- // account when the owner couldn't be looked up.
148
- const canWait = function (current, owner) {
149
- if (!current) {
150
- return false;
151
- }
152
- return Boolean(current.token) || owner === null || owner === current.user?.id;
153
- };
154
-
155
- // [function]
156
-
157
140
  // the release, as `--json` prints it.
158
141
  const releaseJson = function (row) {
159
142
  if (!row) {
@@ -178,7 +161,7 @@ const waitAndReport = async function (
178
161
  current,
179
162
  deployed,
180
163
  result,
181
- { git, json, quiet, root, since, timeout },
164
+ { git, json, owner, quiet, root, since, timeout },
182
165
  ) {
183
166
  let sw = new SpinnerWheel(phrase.releaseWaiting(git));
184
167
  sw.start(json || quiet);
@@ -188,7 +171,7 @@ const waitAndReport = async function (
188
171
  waited = await wait(
189
172
  current,
190
173
  deployed,
191
- { git, root, since, timeout },
174
+ { git, owner, root, since, timeout },
192
175
  {
193
176
  onRelease() {
194
177
  // found: GitHub Actions is done, and Arcadible publishes it now.
@@ -263,8 +246,9 @@ const start = function (argv) {
263
246
  const timeout = Number(argv.timeout ?? TIMEOUT[git ? "git" : "direct"]);
264
247
  const root = resolve(process.cwd(), argv.cwd);
265
248
  let pb = null;
266
- // the note when a git deploy doesn't wait: who can wait for it.
267
- let notWaited = phrase.releaseNotWaited();
249
+ // the game's account, when Arcadible could say: a git deploy's owner waits
250
+ // among their own releases.
251
+ let owner = null;
268
252
  // the project, parsed once and given to each step. A build can write files
269
253
  // the manifest reads, such as art, so it's parsed again after one runs.
270
254
  let parsed = null;
@@ -288,10 +272,9 @@ const start = function (argv) {
288
272
  sw.stop(true);
289
273
  sw = new SpinnerWheel("Checking... %s");
290
274
  // git deploys need no login: anyone who can push to the repository can
291
- // release, and the Arcadible GitHub App publishes the release. The
292
- // account, when there is one, checks the owner's repository is
293
- // connected, and waits for the release, which needs the game's owner,
294
- // so a git deploy that isn't logged in neither checks nor waits.
275
+ // release, and the Arcadible GitHub App publishes the release. Anyone
276
+ // can wait for it too; the game's owner, logged in, waits among their
277
+ // own releases, which also say when the repository isn't connected.
295
278
  if (git) {
296
279
  return whoamiIfAny({ handler: handler({ json, pb, quiet, sw }) });
297
280
  }
@@ -318,15 +301,12 @@ const start = function (argv) {
318
301
  })
319
302
  .then(([current, predeployed]) => {
320
303
  sw.stop(true);
321
- const waits = !git || canWait(current, predeployed.owner ?? null);
322
- if (current && !waits) {
323
- notWaited = phrase.releaseNotWaitedOwner();
324
- }
304
+ owner = predeployed.owner ?? null;
325
305
  sw = new SpinnerWheel("Building... %s");
326
306
  sw.start(git || json || quiet);
327
307
  return Promise.all([
328
- // whoami, kept only to wait with
329
- waits ? current : null,
308
+ // whoami
309
+ current,
330
310
  // build
331
311
  git ? null : build({ project: parsed, root }),
332
312
  ]);
@@ -380,10 +360,11 @@ const start = function (argv) {
380
360
  delete shown.release;
381
361
  const result = git ? shown : { built, ...shown };
382
362
  sw.stop(true);
383
- if (argv.wait && current) {
363
+ if (argv.wait) {
384
364
  return waitAndReport(current, deployed, result, {
385
365
  git,
386
366
  json,
367
+ owner,
387
368
  quiet,
388
369
  root,
389
370
  since,
@@ -393,10 +374,7 @@ const start = function (argv) {
393
374
  if (json) {
394
375
  print(result);
395
376
  } else if (!quiet) {
396
- report(result, {
397
- git,
398
- note: argv.wait ? notWaited : null,
399
- });
377
+ report(result, { git });
400
378
  }
401
379
  return null;
402
380
  })
package/bin/arc-init.js CHANGED
@@ -32,7 +32,6 @@ const spinner = require("../src/util/spinner");
32
32
  const abort = output.abort;
33
33
  const ArcError = error.ArcError;
34
34
  const findRc = rc.findRc;
35
- const join = path.join;
36
35
  const pathExists = fs.pathExists;
37
36
  const print = output.print;
38
37
  const resolve = path.resolve;
@@ -95,7 +94,8 @@ const start = function (argv) {
95
94
  // [function]
96
95
 
97
96
  const patch = function (cwd, project) {
98
- return pathExists(join(resolve(process.cwd(), cwd), project));
97
+ // project may be absolute, as action/init/tool.js resolves it.
98
+ return pathExists(resolve(process.cwd(), cwd, project));
99
99
  };
100
100
 
101
101
  // [function]
package/package.json CHANGED
@@ -75,5 +75,5 @@
75
75
  "scripts": {
76
76
  "test": "node --test \"test/**/*.test.js\""
77
77
  },
78
- "version": "0.1.37"
78
+ "version": "0.1.39"
79
79
  }
@@ -25,9 +25,11 @@ const resolvePath = path.resolve;
25
25
 
26
26
  // [function]
27
27
 
28
+ // a build's output has no limit: exec's default maxBuffer, 1 MB, would kill a
29
+ // verbose build that otherwise succeeds.
28
30
  const execBuild = function (command, { cwdPath }) {
29
31
  return new Promise((resolve, reject) => {
30
- exec(command, { cwd: cwdPath }, (err, stdout, stderr) => {
32
+ exec(command, { cwd: cwdPath, maxBuffer: Infinity }, (err, stdout, stderr) => {
31
33
  if (err) {
32
34
  const { code, signal } = err;
33
35
  reject(
@@ -67,15 +67,16 @@ const findDirect = function (id, headers) {
67
67
  // [function]
68
68
 
69
69
  // a git deploy's release: GitHub's, for this version, made since the tag was
70
- // pushed. Null until GitHub Actions has built it. It's looked for among the
71
- // account's releases, which include one with no game: a repository that
72
- // isn't connected fails that way (repo_not_connected). A token's account
73
- // isn't known, so it looks among the game's.
70
+ // pushed. Null until GitHub Actions has built it. The game's owner looks
71
+ // among their account's releases, which include one with no game: a
72
+ // repository that isn't connected fails that way (repo_not_connected).
73
+ // anyone else, logged in or not, looks among the game's, which anyone can
74
+ // read.
74
75
  const findGit = function ({ account, game, headers, since, version }) {
75
76
  const newest = account
76
77
  ? () => api.accountReleases(account, { limit: 20, select: SELECT }, headers)
77
78
  : () =>
78
- api.gameReleases(game, { limit: 10, select: SELECT, version }, headers);
79
+ api.discoverGameReleases(game, { limit: 10, select: SELECT, version });
79
80
  return () =>
80
81
  newest().then(
81
82
  (rows) =>
@@ -127,17 +128,21 @@ const poll = function (find, timeout, onRelease) {
127
128
 
128
129
  // waits on a deploy's release. `deployed` is what the deploy resolved: a
129
130
  // direct deploy's has the release's id, and a git deploy's its game.
131
+ // `owner` is the game's account, when it's known: only they get Studio's
132
+ // link, since only they can open it. A token's account isn't known.
130
133
  const main = function (
131
134
  current,
132
135
  deployed,
133
- { git: isGit, root, since, timeout },
136
+ { git: isGit, owner, root, since, timeout },
134
137
  { onRelease } = {},
135
138
  ) {
136
139
  const headers = api.auth(current?.session?.access_token);
137
140
  const game = deployed.game;
141
+ const account = current?.token ? null : (current?.user?.id ?? null);
142
+ const owns = !isGit || (account !== null && account === owner);
138
143
  const find = isGit
139
144
  ? findGit({
140
- account: current?.user?.id ?? null,
145
+ account: owns ? account : null,
141
146
  game,
142
147
  headers,
143
148
  since,
@@ -150,7 +155,7 @@ const main = function (
150
155
  ]).then(([{ release, timedOut }, actions]) => ({
151
156
  actions,
152
157
  release,
153
- studio: studioUrl(game),
158
+ studio: owns ? studioUrl(game) : null,
154
159
  timedOut,
155
160
  }));
156
161
  };
@@ -2,12 +2,13 @@
2
2
 
3
3
  // [import]
4
4
 
5
+ const links = require("../../util/links");
5
6
  const project = require("../../parser/project");
6
7
 
7
8
  // [function]
8
9
 
9
10
  const openUrl = function (id) {
10
- return id ? { page: `/play/${id}` } : null;
11
+ return id ? { page: links.storePage(id) } : null;
11
12
  };
12
13
 
13
14
  // [function]
@@ -22,6 +22,13 @@ const HANDLE = cfgproduct.handle;
22
22
  const PREFIX = `${HANDLE}/release/v`;
23
23
  // the release workflow, which builds the release on GitHub Actions.
24
24
  const WORKFLOW = ".github/workflows/arcadible-release.yml";
25
+ // a remote's URL: `scheme://[user@]host/path`, or scp-like
26
+ // `[user@]host:path`. A path on this machine is neither.
27
+ const URL_REMOTE = /^[a-z][a-z0-9+.-]*:\/\/(?:[^@/\s]+@)?[^/\s]+\/(\S+)$/iu;
28
+ const URL_SCP = /^(?:[^@/\s]+@)?[^:/\s]{2,}:(?!\/)(\S+)$/u;
29
+ // the names GitHub allows: an account's, and a repository's.
30
+ const NAME_OWNER = /^[A-Za-z0-9-]{1,39}$/u;
31
+ const NAME_REPO = /^[A-Za-z0-9._-]{1,100}$/u;
25
32
 
26
33
  // [function]
27
34
 
@@ -99,6 +106,40 @@ const ensureWorkflow = function (root) {
99
106
 
100
107
  // [function]
101
108
 
109
+ // a remote URL's repository, as { owner, name }: the last two parts of its
110
+ // path. The host isn't checked, so an SSH alias for GitHub works too. Null
111
+ // for a path on this machine, or one that isn't owner/name.
112
+ const repoOf = function (url) {
113
+ const path = URL_REMOTE.exec(url)?.[1] ?? URL_SCP.exec(url)?.[1] ?? null;
114
+ if (!path) {
115
+ return null;
116
+ }
117
+ const parts = path
118
+ .replace(/\/+$/u, "")
119
+ .replace(/\.git$/u, "")
120
+ .split("/");
121
+ if (parts.length < 2) {
122
+ return null;
123
+ }
124
+ const [owner, name] = parts.slice(-2);
125
+ return NAME_OWNER.test(owner) && NAME_REPO.test(name)
126
+ ? { name, owner }
127
+ : null;
128
+ };
129
+
130
+ // [function]
131
+
132
+ // origin's repository, from its URL as configured (before any `insteadOf`),
133
+ // or null when it has none, or it isn't a repository on a host.
134
+ const originRepo = function (root) {
135
+ return git(root, ["config", "--get", "remote.origin.url"]).then(
136
+ (stdout) => repoOf(stdout.trim()),
137
+ () => null,
138
+ );
139
+ };
140
+
141
+ // [function]
142
+
102
143
  const hasTagLocal = function (root, tag) {
103
144
  return git(root, ["tag", "--list", tag]).then((stdout) =>
104
145
  Boolean(stdout.trim()),
@@ -141,5 +182,7 @@ module.exports = {
141
182
  ensureWorkflow,
142
183
  failWith,
143
184
  git,
185
+ originRepo,
186
+ repoOf,
144
187
  tagOf,
145
188
  };
@@ -21,6 +21,7 @@ const ensureRemote = git.ensureRemote;
21
21
  const ensureRepo = git.ensureRepo;
22
22
  const ensureTagFree = git.ensureTagFree;
23
23
  const ensureWorkflow = git.ensureWorkflow;
24
+ const originRepo = git.originRepo;
24
25
 
25
26
  // [function]
26
27
 
@@ -81,55 +82,55 @@ const ensureProject = function (root) {
81
82
 
82
83
  // [function]
83
84
 
84
- // fails when the game has no repository connected, which Arcadible would
85
- // refuse (repo_not_connected) after GitHub Actions had built it. Only the
86
- // game's owner can see its repository, so it's checked only when they're
87
- // logged in, and skipped when Arcadible can't be reached.
88
- const ensureConnected = function (current, { handle, id }, owner) {
89
- if (!owner || current?.token || current?.user?.id !== owner) {
90
- return Promise.resolve();
91
- }
92
- const headers = api.auth(current.session?.access_token);
93
- return api.gameRepos(id, { select: "id" }, headers).then(
94
- (repos) => {
95
- if (!repos?.length) {
85
+ // fails when origin isn't the game's connected repository, which Arcadible
86
+ // would refuse (repo_not_connected) once GitHub Actions had built it. It's
87
+ // asked by name, which needs no login, so it checks whoever pushes. Skipped
88
+ // when origin is a path on this machine, or Arcadible can't be reached.
89
+ const ensureConnected = function (root, { handle, id }) {
90
+ let repo = null;
91
+ return originRepo(root)
92
+ .then((found) => {
93
+ repo = found;
94
+ return repo
95
+ ? lookup(api.discoverGameRepo(id, repo.owner, repo.name))
96
+ : null;
97
+ })
98
+ .then((connected) => {
99
+ if (connected?.status === 404) {
96
100
  throw new ArcError(
97
101
  cfgcode.REPO_NOT_CONNECTED,
98
102
  phrase.gameRepoMissing(
99
103
  handle ?? id,
104
+ `${repo.owner}/${repo.name}`,
100
105
  `${cfguri.HOST_HTTP_STUDIO}/games/${id}/repository`,
101
106
  ),
102
107
  );
103
108
  }
104
- },
105
- () => null,
106
- );
109
+ });
107
110
  };
108
111
 
109
112
  // [function]
110
113
 
111
- // the release's checks: its tag is free, Arcadible would publish it, and its
112
- // repository is connected. resolves with the version, and the game's owner,
113
- // who alone can wait for the release.
114
- const ensureRelease = function (root, current, game, handler) {
114
+ // the release's checks: its tag is free, Arcadible would publish it, and
115
+ // origin is its connected repository. resolves with the version, and the
116
+ // game's owner, who waits on the release among their own.
117
+ const ensureRelease = function (root, game, handler) {
115
118
  return ensureTagFree(root, game.version)
116
119
  .then(() => ensurePublishable(game, handler))
117
- .then((owner) =>
118
- Promise.all([owner, ensureConnected(current, game, owner)]),
119
- )
120
+ .then((owner) => Promise.all([owner, ensureConnected(root, game)]))
120
121
  .then(([owner]) => ({ owner, to: game.version }));
121
122
  };
122
123
 
123
124
  // [function]
124
125
 
125
- const main = function (current, conf, { onPredeployBegin, ...handler }) {
126
+ const main = function (conf, { onPredeployBegin, ...handler }) {
126
127
  const root = conf.root;
127
128
  if (onPredeployBegin) {
128
129
  onPredeployBegin();
129
130
  }
130
131
  return given(conf)
131
132
  .then(({ game }) => Promise.all([game, ensureProject(root)]))
132
- .then(([game]) => ensureRelease(root, current, game, handler));
133
+ .then(([game]) => ensureRelease(root, game, handler));
133
134
  };
134
135
 
135
136
  // [export]
package/src/api.js CHANGED
@@ -77,35 +77,46 @@ const discoverGameBuild = function (id, version, params, headers) {
77
77
 
78
78
  // [function]
79
79
 
80
- const gameBuild = function (id, version, params, headers) {
80
+ // a game's releases, which anyone can read: what each says about itself.
81
+ const discoverGameReleases = function (id, params, headers) {
81
82
  return request.get(
82
- endpoint(`/games/${id}/builds/${version}`, params),
83
+ endpoint(`/discover/games/${id}/releases`, params),
83
84
  headers,
84
85
  );
85
86
  };
86
87
 
87
88
  // [function]
88
89
 
89
- const release = function (id, params, headers) {
90
- return request.get(endpoint(`/releases/${id}`, params), headers);
90
+ // the game's connected repository, if it's `owner/name`; otherwise 404.
91
+ const discoverGameRepo = function (id, owner, name, params, headers) {
92
+ return request.get(
93
+ endpoint(
94
+ `/discover/games/${id}/repos/${encodeURIComponent(owner)}/${encodeURIComponent(name)}`,
95
+ params,
96
+ ),
97
+ headers,
98
+ );
91
99
  };
92
100
 
93
101
  // [function]
94
102
 
95
- const accountReleases = function (id, params, headers) {
96
- return request.get(endpoint(`/accounts/${id}/releases`, params), headers);
103
+ const gameBuild = function (id, version, params, headers) {
104
+ return request.get(
105
+ endpoint(`/games/${id}/builds/${version}`, params),
106
+ headers,
107
+ );
97
108
  };
98
109
 
99
110
  // [function]
100
111
 
101
- const gameRepos = function (id, params, headers) {
102
- return request.get(endpoint(`/games/${id}/repos`, params), headers);
112
+ const release = function (id, params, headers) {
113
+ return request.get(endpoint(`/releases/${id}`, params), headers);
103
114
  };
104
115
 
105
116
  // [function]
106
117
 
107
- const gameReleases = function (id, params, headers) {
108
- return request.get(endpoint(`/games/${id}/releases`, params), headers);
118
+ const accountReleases = function (id, params, headers) {
119
+ return request.get(endpoint(`/accounts/${id}/releases`, params), headers);
109
120
  };
110
121
 
111
122
  // [export]
@@ -116,10 +127,10 @@ module.exports = {
116
127
  discoverGame,
117
128
  discoverGameBuild,
118
129
  discoverGameBuilds,
130
+ discoverGameReleases,
131
+ discoverGameRepo,
119
132
  flow,
120
133
  gameBuild,
121
- gameReleases,
122
- gameRepos,
123
134
  postAuthRefresh,
124
135
  postFlow,
125
136
  postUpload,
@@ -9,7 +9,7 @@ const predeployToolGit = require("../action/predeploy/tool-git");
9
9
 
10
10
  const main = function ({ current, git, handler, ...conf }) {
11
11
  return git
12
- ? predeployToolGit(current, conf, handler)
12
+ ? predeployToolGit(conf, handler)
13
13
  : predeployTool(current, conf, handler);
14
14
  };
15
15
 
@@ -43,8 +43,8 @@ const CLI = {
43
43
  };
44
44
  // a release's reasons, from the pipeline (the API's
45
45
  // db/type/release_reason.sql). The CLI shares them, so a name means the same
46
- // thing wherever it appears: GAME_UNAVAILABLE, INTERNAL, and VERSION_EXISTS
47
- // are the CLI's too.
46
+ // thing wherever it appears: GAME_UNAVAILABLE, INTERNAL, REPO_NOT_CONNECTED,
47
+ // and VERSION_EXISTS are the CLI's too.
48
48
  const RELEASE = {
49
49
  APP_NOT_INSTALLED: "app_not_installed",
50
50
  EXPIRED: "expired",
package/src/config/url.js CHANGED
@@ -13,7 +13,6 @@ const HANDLE = cfgproduct.handle;
13
13
 
14
14
  module.exports = {
15
15
  ERRORS: `${cfguri.HOST_HTTP_DEVELOPER}/references/errors`,
16
- GETTING_STARTED: `${cfguri.HOST_HTTP_DEVELOPER}/getting-started`,
17
16
  LINK: `${cfguri.HOST_HTTP_ACCOUNT}/link`,
18
17
  MANIFEST: `${cfguri.HOST_HTTP_DEVELOPER}/references/${HANDLE}-manifest`,
19
18
  };
package/src/phrase.js CHANGED
@@ -171,9 +171,10 @@ const gameNotDeployed = function (game) {
171
171
 
172
172
  // [function]
173
173
 
174
- const gameRepoMissing = function (game, url) {
175
- return blocks(`${game} isn't connected to a GitHub repository.`, [
176
- `Connect this repository in ${TITLE} Studio, on the game's Repository tab:`,
174
+ // origin isn't the game's connected repository: none is, or another is.
175
+ const gameRepoMissing = function (game, repo, url) {
176
+ return blocks(`${repo} isn't ${game}'s repository on ${TITLE}.`, [
177
+ `The game's owner connects it in ${TITLE} Studio, on the game's Repository tab, and connects it again if it was renamed on GitHub:`,
177
178
  url,
178
179
  ]);
179
180
  };
@@ -435,30 +436,18 @@ const packageUncompressedTooLarge = function (size) {
435
436
 
436
437
  // a failed release's message, a sentence to a line, then its reason code
437
438
  // and where to see it.
439
+ // `studio` is the game's builds in Studio, for its owner; anyone else can't
440
+ // open it.
438
441
  const releaseFailed = function ({ message, reason }, studio) {
439
442
  return blocks(
440
443
  (message ?? "").split(/(?<=\.)\s+/u).join("\n"),
441
444
  `Reason: ${reason}.`,
442
- [`${TITLE} Studio:`, studio],
445
+ ...(studio ? [[`${TITLE} Studio:`, studio]] : []),
443
446
  );
444
447
  };
445
448
 
446
449
  // [function]
447
450
 
448
- // a git deploy that isn't logged in can't read its release, so it doesn't
449
- // wait for it.
450
- const releaseNotWaited = function () {
451
- return `Log in to wait for it: ${cmdLogin()}.`;
452
- };
453
-
454
- // [function]
455
-
456
- const releaseNotWaitedOwner = function () {
457
- return "Only the game's owner can wait for its release.";
458
- };
459
-
460
- // [function]
461
-
462
451
  // `built` when workflow.build ran first.
463
452
  const releasePublished = function (version, channel, built) {
464
453
  return `${built ? "Built and published" : "Published"} v${version} to ${channel}.`;
@@ -475,12 +464,18 @@ const releaseTimedOut = function (
475
464
  { actions, found, git, studio },
476
465
  ) {
477
466
  const head = `Waited ${waited(seconds)} for v${version}.`;
478
- if (found || !git) {
467
+ if ((found || !git) && studio) {
479
468
  return blocks(head, [
480
469
  `It may still finish. See it in ${TITLE} Studio:`,
481
470
  studio,
482
471
  ]);
483
472
  }
473
+ if (found) {
474
+ return blocks(
475
+ head,
476
+ `It may still finish. The ${TITLE} check on the tagged commit says how it went.`,
477
+ );
478
+ }
484
479
  return blocks(
485
480
  head,
486
481
  [
@@ -789,8 +784,6 @@ module.exports = {
789
784
  packageTooManyFiles,
790
785
  packageUncompressedTooLarge,
791
786
  releaseFailed,
792
- releaseNotWaited,
793
- releaseNotWaitedOwner,
794
787
  releasePublished,
795
788
  releaseTimedOut,
796
789
  releaseWaiting,