@rathnasgala/cli 1.1.17 → 1.1.19

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": "1.1.17",
3
+ "version": "1.1.19",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "src"
@@ -1,6 +1,7 @@
1
1
  import path from 'node:path';
2
2
 
3
3
  import { galaApi } from '../api/gala.js';
4
+ import { HttpError } from '../api/http.js';
4
5
  import { accountForCommand } from '../auth/checkout-profile.js';
5
6
  import { authenticatedProfile } from '../auth/profiles.js';
6
7
  import { UsageError } from '../cli/args.js';
@@ -24,7 +25,8 @@ export async function domain({ terminal, options, cwd = process.cwd() }) {
24
25
  if (action !== 'set' && value != null) throw new UsageError(`domain ${action} takes no hostname`);
25
26
 
26
27
  const account = await accountForCommand(options, root, { terminal });
27
- const credential = (await authenticatedProfile({ name: account, terminal })).gala;
28
+ const profile = await authenticatedProfile({ name: account, terminal });
29
+ const credential = profile.gala;
28
30
  const api = galaApi({ baseUrl: credential.apiBaseUrl, token: credential.accessToken });
29
31
 
30
32
  if (action === 'status') {
@@ -41,13 +43,14 @@ export async function domain({ terminal, options, cwd = process.cwd() }) {
41
43
  if (action === 'set') {
42
44
  const checked = customDomain(value);
43
45
  if (checked.error) throw new UsageError(checked.error);
46
+ const site = await ownedSite(api, publication.siteId);
47
+ const owner = site.repository.split('/')[0];
44
48
  const change = await api.prepareTopologyChange(publication.siteId, {
45
49
  canonicalBaseUrl: `https://${checked.host}`,
46
50
  pathPrefix: '/',
47
51
  });
48
52
  terminal.done(`Reserved ${checked.host}`);
49
- terminal.note(`Verify it in the repository owner’s GitHub account, then run: ${cliCommand('domain check')}`);
50
- terminal.openUrl('https://docs.github.com/en/pages/configuring-a-custom-domain-for-your-github-pages-site/verifying-your-custom-domain-for-github-pages');
53
+ showVerificationSteps(terminal, checked.host, owner, profile.metadata.githubLogin);
51
54
  return change;
52
55
  }
53
56
 
@@ -80,14 +83,25 @@ export async function domain({ terminal, options, cwd = process.cwd() }) {
80
83
  }
81
84
 
82
85
  if (action === 'check') {
86
+ const site = await ownedSite(api, publication.siteId);
83
87
  if (pending.cname && pending.state === 'PREPARED') {
84
- const configured = await api.configureTopologyChange(publication.siteId, pending.changeId);
88
+ let configured;
89
+ try {
90
+ configured = await configureWithRetry(api, publication.siteId, pending.changeId, terminal);
91
+ } catch (failure) {
92
+ handleDomainFailure(failure, terminal, pending, site, profile.metadata.githubLogin);
93
+ }
85
94
  terminal.done(`GitHub verified ${configured.cname}`);
86
95
  terminal.note(dnsInstruction(configured.cname, await providerHost(api, publication.siteId)));
87
96
  terminal.note(`After DNS propagates, run: ${cliCommand('domain check')}`);
88
97
  return configured;
89
98
  }
90
- const committed = await api.commitTopologyChange(publication.siteId, pending.changeId);
99
+ let committed;
100
+ try {
101
+ committed = await api.commitTopologyChange(publication.siteId, pending.changeId);
102
+ } catch (failure) {
103
+ handleDomainFailure(failure, terminal, pending, site, profile.metadata.githubLogin);
104
+ }
91
105
  terminal.done(committed.cname
92
106
  ? `${committed.cname} is live with enforced HTTPS`
93
107
  : 'The GitHub Pages address is live again');
@@ -100,6 +114,70 @@ export async function domain({ terminal, options, cwd = process.cwd() }) {
100
114
  throw new UsageError(`Unsupported domain action: ${action}`);
101
115
  }
102
116
 
117
+ async function configureWithRetry(api, siteId, changeId, terminal) {
118
+ const attempts = 5;
119
+ for (let attempt = 1; attempt <= attempts; attempt += 1) {
120
+ try {
121
+ return await api.configureTopologyChange(siteId, changeId);
122
+ } catch (failure) {
123
+ const waiting = failure instanceof HttpError && [
124
+ 'GITHUB_PAGES_DOMAIN_VERIFICATION_REQUIRED',
125
+ 'GITHUB_PAGES_DOMAIN_PROPAGATION_PENDING',
126
+ ].includes(failure.code);
127
+ if (!waiting || attempt === attempts) throw failure;
128
+ if (attempt === 1) terminal.step('Waiting for GitHub Pages to finish applying the domain');
129
+ await new Promise((resolve) => setTimeout(resolve, 500));
130
+ }
131
+ }
132
+ throw new Error('GitHub Pages domain check ended unexpectedly');
133
+ }
134
+
135
+ function handleDomainFailure(failure, terminal, pending, site, login) {
136
+ if (!(failure instanceof HttpError)) throw failure;
137
+ const [owner, repository] = site.repository.split('/');
138
+ const retry = cliCommand('domain check');
139
+ const address = pending.cname ?? 'the GitHub Pages address';
140
+ switch (failure.code) {
141
+ case 'GITHUB_PAGES_DOMAIN_VERIFICATION_REQUIRED':
142
+ showVerificationSteps(terminal, pending.cname, owner, login);
143
+ throw new Error(`GitHub still reports ${pending.cname} as unverified for @${owner}. If GitHub already shows “Verified”, wait for Pages to update, then run: ${retry}`);
144
+ case 'GITHUB_PAGES_DOMAIN_PROPAGATION_PENDING':
145
+ throw new Error(`GitHub accepted ${address} and is still updating Pages. Wait briefly, then run: ${retry}`);
146
+ case 'GITHUB_PAGES_DNS_PENDING':
147
+ terminal.note(dnsInstruction(pending.cname, `${owner.toLowerCase()}.github.io`));
148
+ throw new Error(`GitHub is still checking DNS for ${pending.cname}. After it propagates, run: ${retry}`);
149
+ case 'GITHUB_PAGES_DNS_INVALID':
150
+ terminal.note(dnsInstruction(pending.cname, `${owner.toLowerCase()}.github.io`));
151
+ throw new Error(`GitHub rejected the DNS records for ${pending.cname}. Correct them, then run: ${retry}`);
152
+ case 'GITHUB_PAGES_PERMISSION_REQUIRED':
153
+ terminal.openUrl(owner.toLowerCase() === login.toLowerCase()
154
+ ? 'https://github.com/settings/installations'
155
+ : `https://github.com/organizations/${encodeURIComponent(owner)}/settings/installations`);
156
+ throw new Error(`The Gala GitHub App needs Pages and Administration access to ${owner}/${repository}. Grant it, then run: ${retry}`);
157
+ case 'GITHUB_PAGES_DOMAIN_REJECTED':
158
+ terminal.openUrl(`https://github.com/${encodeURIComponent(owner)}/${encodeURIComponent(repository)}/settings/pages`);
159
+ throw new Error(`GitHub rejected ${address}. Review the repository’s Pages settings, then run: ${retry}`);
160
+ case 'GITHUB_PAGES_TOPOLOGY_NOT_READY':
161
+ throw new Error(`GitHub Pages has not finished applying ${address}. Wait briefly, then run: ${retry}`);
162
+ case 'GITHUB_PAGES_VERIFICATION_UNAVAILABLE':
163
+ throw new Error(`GitHub Pages is temporarily unavailable. Your pending change is preserved; run later: ${retry}`);
164
+ case 'SITE_TOPOLOGY_STATE_CONFLICT':
165
+ throw new Error(`This domain change is no longer in the expected state. Inspect it with: ${cliCommand('domain status')}`);
166
+ default:
167
+ throw failure;
168
+ }
169
+ }
170
+
171
+ function showVerificationSteps(terminal, host, owner, login) {
172
+ terminal.note(`GitHub owner @${owner} must verify ${host} once under Settings → Pages → Verified domains.`);
173
+ terminal.openUrl(owner.toLowerCase() === login.toLowerCase()
174
+ ? 'https://github.com/settings/pages'
175
+ : `https://github.com/organizations/${encodeURIComponent(owner)}/settings/pages`);
176
+ terminal.note(`Choose “Add a domain”, enter ${host}, and add GitHub’s TXT record at your DNS provider.`);
177
+ terminal.note(`The TXT record name will be _github-pages-challenge-${owner}.${host}; GitHub supplies its value.`);
178
+ terminal.note(`When GitHub shows “Verified”, run: ${cliCommand('domain check')}`);
179
+ }
180
+
103
181
  async function ownedSite(api, siteId) {
104
182
  const sites = await api.listPublications();
105
183
  const site = Array.isArray(sites) ? sites.find((candidate) => candidate.siteId === siteId) : null;