@rathnasgala/cli 0.0.19 → 0.0.21

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": "@rathnasgala/cli",
3
- "version": "0.0.19",
3
+ "version": "0.0.21",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "src"
package/src/index.js CHANGED
@@ -130,6 +130,9 @@ if (command === 'auth') {
130
130
  ask
131
131
  });
132
132
  const result = await scaffoldSite({
133
+ notify: (message) => process.stdout.write(`${message}\n`),
134
+ ask,
135
+ openUrl: openInBrowser,
133
136
  owner: prepared.owner,
134
137
  repository: prepared.repository,
135
138
  target: prepared.target,
@@ -22,7 +22,73 @@ export async function createPublication({
22
22
  githubAccessToken,
23
23
  name,
24
24
  fetchImpl = fetch,
25
- authorize = exchangeGithubAuthorization
25
+ authorize = exchangeGithubAuthorization,
26
+ notify = () => {},
27
+ ask,
28
+ openUrl = () => false,
29
+ shareAttempts = 3,
30
+ selfLogin
31
+ }) {
32
+ let created = false;
33
+ let shareUrl = 'https://github.com/settings/installations';
34
+ let repositoryOwner = selfLogin ?? '';
35
+ let repositoryName = name;
36
+ for (let attempt = 0; attempt < Math.max(1, shareAttempts); attempt += 1) {
37
+ const result = await requestPublication({
38
+ apiBaseUrl, galaAccessToken, githubAccessToken, name, fetchImpl, authorize
39
+ });
40
+ if (result.ready) return result.publication;
41
+
42
+ /*
43
+ * The repository exists with the right content; the App installation simply cannot see it,
44
+ * because it is scoped to selected repositories rather than all of them. Sharing it is a click,
45
+ * and asking again then returns READY — the server short-circuits on a repository it can
46
+ * already see. The browser editor recovers the same way; without this the CLI dead-ends on a
47
+ * state that is one click from working.
48
+ *
49
+ * After the first attempt the repository exists, so a further refusal reports UNSUPPORTED
50
+ * rather than NEEDS_SHARING — the same situation under a different name.
51
+ */
52
+ const shareable = result.status === 'NEEDS_SHARING' || created;
53
+ if (result.status === 'NEEDS_SHARING') {
54
+ created = true;
55
+ shareUrl = installationSettingsUrl(result.installationId, result.owner, selfLogin);
56
+ repositoryName = result.repository ?? repositoryName;
57
+ repositoryOwner = result.owner ?? repositoryOwner;
58
+ }
59
+ if (!shareable || typeof ask !== 'function') throw result.failure;
60
+
61
+ notify(`${repositoryOwner}/${repositoryName} was created, but the Gala GitHub App cannot reach `
62
+ + 'it yet — its installation covers only selected repositories, which is the right way to '
63
+ + 'have it. Add this one repository to the installation; nothing else needs granting.');
64
+ notify(`${openUrl(shareUrl) ? 'Opened' : 'Open'} ${shareUrl}`);
65
+ await ask('Press enter once the App can access that repository. ');
66
+ }
67
+ throw new Error(
68
+ `The Gala GitHub App still cannot reach ${repositoryOwner}/${repositoryName}. Add that `
69
+ + `repository to the installation at ${shareUrl}, then run scaffold again.`
70
+ );
71
+ }
72
+
73
+ /**
74
+ * The page that grants one repository, rather than the list of every app ever installed.
75
+ *
76
+ * GitHub keeps user and organisation installation settings on different paths, and only the caller
77
+ * knows which this is: the created owner differing from the token's own account means the
78
+ * installation lives on an organisation.
79
+ */
80
+ export function installationSettingsUrl(installationId, owner, selfLogin) {
81
+ const generic = 'https://github.com/settings/installations';
82
+ if (!Number.isSafeInteger(Number(installationId)) || Number(installationId) <= 0) return generic;
83
+ const isOrganization = typeof owner === 'string' && typeof selfLogin === 'string'
84
+ && owner.toLowerCase() !== selfLogin.toLowerCase();
85
+ return isOrganization
86
+ ? `https://github.com/organizations/${encodeURIComponent(owner)}/settings/installations/${installationId}`
87
+ : `https://github.com/settings/installations/${installationId}`;
88
+ }
89
+
90
+ async function requestPublication({
91
+ apiBaseUrl, galaAccessToken, githubAccessToken, name, fetchImpl, authorize
26
92
  }) {
27
93
  const authorization = await authorize({
28
94
  apiBaseUrl, galaAccessToken, githubAccessToken, fetchImpl
@@ -44,19 +110,19 @@ export async function createPublication({
44
110
  const owner = payload?.owner;
45
111
  const repository = payload?.name;
46
112
 
47
- if (status === 'NEEDS_SHARING') {
48
- throw new Error(
49
- `${owner}/${repository} was created from the template, but the Gala GitHub App cannot reach `
50
- + 'it yet. Open https://github.com/settings/installations, give the App access to that '
51
- + 'repository, then run scaffold again with --resume.'
52
- );
53
- }
54
113
  if (status !== 'READY') {
55
- throw new Error(
56
- `Gala could not create the publication repository (${payload?.outcome ?? status}). `
57
- + 'Create it from https://github.com/rathnasgala/site-template yourself, then run scaffold '
58
- + 'with --empty-existing-repository.'
59
- );
114
+ return {
115
+ ready: false, status, owner, repository,
116
+ // Carried even though the repository is not in the installation yet: it is what makes the
117
+ // grant a deep link rather than a hunt.
118
+ installationId: payload?.installationId,
119
+ failure: new Error(
120
+ `Gala could not create the publication repository (${payload?.outcome ?? status}). `
121
+ + 'Give the Gala GitHub App access to it at https://github.com/settings/installations, or '
122
+ + 'create it from https://github.com/rathnasgala/site-template yourself and run scaffold '
123
+ + 'with --empty-existing-repository.'
124
+ )
125
+ };
60
126
  }
61
127
  if (typeof owner !== 'string' || typeof repository !== 'string') {
62
128
  throw new TypeError('Gala publication creation returned no repository identity');
@@ -66,7 +132,7 @@ export async function createPublication({
66
132
  throw new TypeError('Gala publication creation returned no installation');
67
133
  }
68
134
 
69
- return Object.freeze({
135
+ return { ready: true, status, owner, repository, publication: Object.freeze({
70
136
  owner,
71
137
  repository,
72
138
  installationId,
@@ -74,5 +140,5 @@ export async function createPublication({
74
140
  // The server names the repository it actually made, which may differ from what was asked for.
75
141
  fullName: `${owner}/${repository}`,
76
142
  cloneUrl: `https://github.com/${owner}/${repository}.git`
77
- });
143
+ }) };
78
144
  }
@@ -48,6 +48,7 @@ function registrationLocation(owner, topology, canonicalBaseUrl) {
48
48
 
49
49
  export async function scaffoldSite({
50
50
  owner, repository, target, githubInstallationId, siteOptions, emptyExistingRepository = false,
51
+ notify = (message) => process.stdout.write(`${message}\n`), ask, openUrl,
51
52
  resumeExistingCheckout = false, topology = 'provider-default', canonicalBaseUrl, actionRef,
52
53
  buildMode = 'build-and-deploy', templateOwner = 'rathnasgala',
53
54
  templateRepository = 'site-template',
@@ -110,7 +111,8 @@ export async function scaffoldSite({
110
111
  */
111
112
  const created = await createRepository({
112
113
  apiBaseUrl: gala.apiBaseUrl, galaAccessToken: gala.accessToken,
113
- githubAccessToken: github.accessToken, name: repositoryName
114
+ githubAccessToken: github.accessToken, name: repositoryName,
115
+ notify, ask, openUrl, selfLogin: requestedOwner
114
116
  });
115
117
  repositoryOwner = segment(created.owner, 'owner');
116
118
  repositoryName = segment(created.repository, 'repository');