@rathnasgala/cli 0.0.17 → 0.0.18
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 +1 -1
- package/src/github-empty-repository.js +3 -2
- package/src/github-identity.js +2 -1
- package/src/github-pages-provisioning.js +3 -2
- package/src/github-repository-variable.js +3 -2
- package/src/github-template-repository.js +2 -1
- package/src/http-failure.js +55 -0
- package/src/site-registration-client.js +3 -2
package/package.json
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
|
+
import { describeHttpFailure } from './http-failure.js';
|
|
2
3
|
|
|
3
4
|
const API_VERSION = '2026-03-10';
|
|
4
5
|
|
|
@@ -11,7 +12,7 @@ export async function verifyEmptyRepository({ owner, repository, accessToken, fe
|
|
|
11
12
|
const response = await fetchImpl(repositoryUrl, {
|
|
12
13
|
headers
|
|
13
14
|
});
|
|
14
|
-
if (!response.ok) throw new Error(
|
|
15
|
+
if (!response.ok) throw new Error(await describeHttpFailure(response, 'GitHub repository lookup'));
|
|
15
16
|
const payload = await response.json();
|
|
16
17
|
if (payload.full_name?.toLowerCase() !== `${owner}/${repository}`.toLowerCase()) {
|
|
17
18
|
throw new TypeError('GitHub returned an unexpected repository');
|
|
@@ -21,7 +22,7 @@ export async function verifyEmptyRepository({ owner, repository, accessToken, fe
|
|
|
21
22
|
...headers
|
|
22
23
|
}
|
|
23
24
|
});
|
|
24
|
-
if (!branchesResponse.ok) throw new Error(
|
|
25
|
+
if (!branchesResponse.ok) throw new Error(await describeHttpFailure(branchesResponse, 'GitHub branch lookup'));
|
|
25
26
|
const branches = await branchesResponse.json();
|
|
26
27
|
if (payload.size !== 0 || !Array.isArray(branches) || branches.length !== 0) {
|
|
27
28
|
throw new Error('Existing repository is not empty; explicit non-empty adoption is not implemented');
|
package/src/github-identity.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { describeHttpFailure } from './http-failure.js';
|
|
1
2
|
const GITHUB_API_VERSION = '2026-03-10';
|
|
2
3
|
|
|
3
4
|
/**
|
|
@@ -19,7 +20,7 @@ export async function resolveGithubLogin({ accessToken, fetchImpl = fetch }) {
|
|
|
19
20
|
'x-github-api-version': GITHUB_API_VERSION
|
|
20
21
|
}
|
|
21
22
|
});
|
|
22
|
-
if (!response.ok) throw new Error(
|
|
23
|
+
if (!response.ok) throw new Error(await describeHttpFailure(response, 'GitHub account lookup'));
|
|
23
24
|
const payload = await response.json();
|
|
24
25
|
const login = payload?.login;
|
|
25
26
|
// The same shape `scaffold` demands of `--owner`. Refusing here beats a confusing failure four
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { describeHttpFailure } from './http-failure.js';
|
|
1
2
|
const GITHUB_API_VERSION = '2026-03-10';
|
|
2
3
|
const SEGMENT = /^[A-Za-z0-9_.-]+$/;
|
|
3
4
|
const SHA = /^[0-9a-f]{40}$/;
|
|
@@ -19,7 +20,7 @@ function headers(accessToken) {
|
|
|
19
20
|
}
|
|
20
21
|
|
|
21
22
|
async function json(response, operation) {
|
|
22
|
-
if (!response.ok) throw new Error(`GitHub ${operation}
|
|
23
|
+
if (!response.ok) throw new Error(await describeHttpFailure(response, `GitHub ${operation}`));
|
|
23
24
|
return response.json();
|
|
24
25
|
}
|
|
25
26
|
|
|
@@ -86,7 +87,7 @@ export async function provisionGithubPages({
|
|
|
86
87
|
return Object.freeze({ created: false, url: configuration.html_url, runUrl: run.html_url });
|
|
87
88
|
}
|
|
88
89
|
if (current.status !== 404) {
|
|
89
|
-
throw new Error(
|
|
90
|
+
throw new Error(await describeHttpFailure(current, 'GitHub Pages configuration request'));
|
|
90
91
|
}
|
|
91
92
|
const created = await fetchImpl(`${repositoryUrl}/pages`, {
|
|
92
93
|
method: 'POST', headers: requestHeaders,
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { describeHttpFailure } from './http-failure.js';
|
|
1
2
|
const GITHUB_API_VERSION = '2026-03-10';
|
|
2
3
|
const OWNER_OR_REPOSITORY = /^[A-Za-z0-9_.-]+$/;
|
|
3
4
|
const VARIABLE_NAME = /^[A-Z_][A-Z0-9_]*$/;
|
|
@@ -42,7 +43,7 @@ export async function installRepositoryVariable({
|
|
|
42
43
|
});
|
|
43
44
|
if (update.ok) return;
|
|
44
45
|
if (update.status !== 404) {
|
|
45
|
-
throw new Error(
|
|
46
|
+
throw new Error(await describeHttpFailure(update, 'GitHub repository variable update'));
|
|
46
47
|
}
|
|
47
48
|
const create = await fetchImpl(baseUrl, {
|
|
48
49
|
method: 'POST',
|
|
@@ -50,6 +51,6 @@ export async function installRepositoryVariable({
|
|
|
50
51
|
body: JSON.stringify({ name: normalizedName, value })
|
|
51
52
|
});
|
|
52
53
|
if (!create.ok) {
|
|
53
|
-
throw new Error(
|
|
54
|
+
throw new Error(await describeHttpFailure(create, 'GitHub repository variable creation'));
|
|
54
55
|
}
|
|
55
56
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
+
import { describeHttpFailure } from './http-failure.js';
|
|
3
4
|
|
|
4
5
|
const GITHUB_API_VERSION = '2026-03-10';
|
|
5
6
|
const REPOSITORY_IDENTITY = /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/;
|
|
@@ -57,7 +58,7 @@ export async function generateRepositoryFromTemplate({
|
|
|
57
58
|
}
|
|
58
59
|
);
|
|
59
60
|
if (response.status !== 201) {
|
|
60
|
-
throw new Error(
|
|
61
|
+
throw new Error(await describeHttpFailure(response, 'GitHub template generation'));
|
|
61
62
|
}
|
|
62
63
|
const payload = await response.json();
|
|
63
64
|
if (payload == null || Array.isArray(payload) || typeof payload !== 'object') {
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the server actually said, instead of only what it scored.
|
|
3
|
+
*
|
|
4
|
+
* Every failure in this CLI reported `failed with HTTP 403` and discarded the response body. GitHub
|
|
5
|
+
* puts the reason there and nowhere else — an organisation's OAuth App restrictions, a missing
|
|
6
|
+
* scope, a rename, a rate limit all arrive as 403 with a sentence explaining which — so the one
|
|
7
|
+
* fact worth having was the one being thrown away. Diagnosing anything meant guessing between
|
|
8
|
+
* causes the server had already distinguished.
|
|
9
|
+
*
|
|
10
|
+
* Only the response is read. Request bodies, tokens and secrets never pass through here.
|
|
11
|
+
*/
|
|
12
|
+
const MAX_DETAIL = 400;
|
|
13
|
+
|
|
14
|
+
export async function describeHttpFailure(response, action) {
|
|
15
|
+
const status = response?.status ?? 0;
|
|
16
|
+
let detail = '';
|
|
17
|
+
try {
|
|
18
|
+
// Cloned so a caller that also reads the body still can; a response whose body is already
|
|
19
|
+
// consumed simply yields no detail rather than a second failure on top of the first.
|
|
20
|
+
const text = await (typeof response.clone === 'function' ? response.clone() : response).text();
|
|
21
|
+
detail = extract(text);
|
|
22
|
+
} catch {
|
|
23
|
+
detail = '';
|
|
24
|
+
}
|
|
25
|
+
return `${action} failed with HTTP ${status}${detail === '' ? '' : `: ${detail}`}`;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function extract(text) {
|
|
29
|
+
if (typeof text !== 'string' || text.trim() === '') return '';
|
|
30
|
+
let payload;
|
|
31
|
+
try {
|
|
32
|
+
payload = JSON.parse(text);
|
|
33
|
+
} catch {
|
|
34
|
+
return truncate(text);
|
|
35
|
+
}
|
|
36
|
+
if (payload == null || typeof payload !== 'object') return truncate(text);
|
|
37
|
+
const parts = [];
|
|
38
|
+
if (typeof payload.message === 'string' && payload.message.trim() !== '') parts.push(payload.message.trim());
|
|
39
|
+
// GitHub's `errors` array carries the specific field or reason behind a generic message.
|
|
40
|
+
if (Array.isArray(payload.errors)) {
|
|
41
|
+
for (const error of payload.errors) {
|
|
42
|
+
const reason = typeof error === 'string' ? error : error?.message ?? error?.code;
|
|
43
|
+
if (typeof reason === 'string' && reason.trim() !== '') parts.push(reason.trim());
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
if (typeof payload.error_description === 'string') parts.push(payload.error_description.trim());
|
|
47
|
+
if (typeof payload.code === 'string' && parts.length === 0) parts.push(payload.code);
|
|
48
|
+
if (typeof payload.documentation_url === 'string') parts.push(`See ${payload.documentation_url}`);
|
|
49
|
+
return truncate(parts.length === 0 ? text : parts.join(' — '));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function truncate(value) {
|
|
53
|
+
const flattened = value.replace(/\s+/g, ' ').trim();
|
|
54
|
+
return flattened.length > MAX_DETAIL ? `${flattened.slice(0, MAX_DETAIL)}…` : flattened;
|
|
55
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { describeHttpFailure } from './http-failure.js';
|
|
1
2
|
const ULID = /^[0-7][0-9A-HJKMNP-TV-Z]{25}$/;
|
|
2
3
|
const REPOSITORY_PART = /^[A-Za-z0-9_.-]+$/;
|
|
3
4
|
const IDEMPOTENCY_KEY = /^[A-Za-z0-9._:-]{16,128}$/;
|
|
@@ -34,7 +35,7 @@ async function authorizeGitHub({ apiBaseUrl, galaAccessToken, githubAccessToken,
|
|
|
34
35
|
throw new Error('GitHub or Gala authentication expired; run `gala auth` again');
|
|
35
36
|
}
|
|
36
37
|
if (response.status !== 200) {
|
|
37
|
-
throw new Error(
|
|
38
|
+
throw new Error(await describeHttpFailure(response, 'GitHub repository authorization'));
|
|
38
39
|
}
|
|
39
40
|
const payload = await response.json();
|
|
40
41
|
return required(payload?.authorization, 'GitHub authorization', /^[A-Za-z0-9_-]{43}$/);
|
|
@@ -103,7 +104,7 @@ export async function registerSite({
|
|
|
103
104
|
}
|
|
104
105
|
throw new Error('Site registration conflicts with existing protected state; use the recovery command');
|
|
105
106
|
}
|
|
106
|
-
if (response.status !== 201) throw new Error(
|
|
107
|
+
if (response.status !== 201) throw new Error(await describeHttpFailure(response, 'Gala site registration'));
|
|
107
108
|
const payload = await response.json();
|
|
108
109
|
if (!ULID.test(payload?.siteId) || typeof payload.siteSecret !== 'string' || payload.siteSecret === '') {
|
|
109
110
|
throw new TypeError('Gala site registration response is invalid');
|