@rathnasgala/cli 0.0.21 → 1.0.0
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/README.md +66 -212
- package/package.json +3 -3
- package/src/api/gala.js +126 -0
- package/src/api/github.js +63 -0
- package/src/api/http.js +72 -0
- package/src/auth/gala.js +52 -0
- package/src/auth/github.js +78 -0
- package/src/auth/store.js +88 -0
- package/src/cli/args.js +56 -0
- package/src/cli/terminal.js +104 -0
- package/src/commands/auth.js +20 -0
- package/src/commands/doctor.js +98 -0
- package/src/commands/init.js +197 -0
- package/src/commands/new.js +76 -0
- package/src/commands/preview.js +92 -0
- package/src/commands/publish.js +57 -0
- package/src/commands-manifest.js +58 -0
- package/src/content.js +31 -0
- package/src/git.js +143 -0
- package/src/index.js +44 -297
- package/src/publication.js +37 -0
- package/src/assign-content-ids.js +0 -1
- package/src/auth-command.js +0 -36
- package/src/configure-site.js +0 -102
- package/src/content-files.js +0 -1
- package/src/doctor-command.js +0 -214
- package/src/entitlement-client.js +0 -26
- package/src/entitlement-command.js +0 -74
- package/src/evaluation-date.js +0 -1
- package/src/gala-credential-health.js +0 -34
- package/src/gala-credential-store.js +0 -115
- package/src/gala-device-flow.js +0 -121
- package/src/github-auth-command.js +0 -29
- package/src/github-credential-store.js +0 -65
- package/src/github-device-flow.js +0 -130
- package/src/github-empty-repository.js +0 -76
- package/src/github-identity.js +0 -32
- package/src/github-pages-provisioning.js +0 -107
- package/src/github-repository-secret.js +0 -82
- package/src/github-repository-variable.js +0 -56
- package/src/github-template-repository.js +0 -165
- package/src/hook-command.js +0 -64
- package/src/http-failure.js +0 -55
- package/src/new-command.js +0 -54
- package/src/open-browser.js +0 -40
- package/src/preview-command.js +0 -60
- package/src/publication-creation-client.js +0 -144
- package/src/publication-state.js +0 -7
- package/src/publish-command.js +0 -37
- package/src/record-deployment-command.js +0 -147
- package/src/refresh-command.js +0 -104
- package/src/repository-limits.js +0 -94
- package/src/scaffold-git.js +0 -41
- package/src/scaffold-options.js +0 -58
- package/src/scaffold-preflight.js +0 -147
- package/src/scaffold-site.js +0 -162
- package/src/site-config-registration.js +0 -47
- package/src/site-registration-client.js +0 -138
- package/src/theme-package.js +0 -128
- package/src/topology-client.js +0 -43
- package/src/topology-command.js +0 -70
- package/src/upgrade-command.js +0 -81
- package/src/validate-command.js +0 -5
- package/src/workflow-command.js +0 -87
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
import { chmod, lstat, mkdir, readFile, rename, rm, writeFile } from 'node:fs/promises';
|
|
2
|
-
import os from 'node:os';
|
|
3
|
-
import path from 'node:path';
|
|
4
|
-
|
|
5
|
-
export function githubCredentialPath({ platform = process.platform, environment = process.env, home = os.homedir() } = {}) {
|
|
6
|
-
if (platform === 'win32') {
|
|
7
|
-
if (!environment.APPDATA) throw new Error('APPDATA is required to store GitHub credentials on Windows');
|
|
8
|
-
return path.join(environment.APPDATA, 'Gala', 'github-credentials.json');
|
|
9
|
-
}
|
|
10
|
-
if (platform === 'darwin') return path.join(home, 'Library', 'Application Support', 'Gala', 'github-credentials.json');
|
|
11
|
-
return path.join(environment.XDG_CONFIG_HOME || path.join(home, '.config'), 'gala', 'github-credentials.json');
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
async function regularOrMissing(target) {
|
|
15
|
-
try {
|
|
16
|
-
const metadata = await lstat(target);
|
|
17
|
-
if (metadata.isSymbolicLink() || !metadata.isFile()) throw new TypeError('GitHub credential must be a regular file');
|
|
18
|
-
return true;
|
|
19
|
-
} catch (error) {
|
|
20
|
-
if (error.code === 'ENOENT') return false;
|
|
21
|
-
throw error;
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export async function writeGithubCredential({ accessToken, scopes, target = githubCredentialPath() }) {
|
|
26
|
-
if (typeof accessToken !== 'string' || accessToken === '') throw new TypeError('accessToken is required');
|
|
27
|
-
if (!Array.isArray(scopes) || !scopes.includes('repo') || !scopes.includes('workflow')) {
|
|
28
|
-
throw new TypeError('GitHub credential requires repo and workflow scopes');
|
|
29
|
-
}
|
|
30
|
-
const directory = path.dirname(path.resolve(target));
|
|
31
|
-
await mkdir(directory, { recursive: true, mode: 0o700 });
|
|
32
|
-
const directoryMetadata = await lstat(directory);
|
|
33
|
-
if (directoryMetadata.isSymbolicLink() || !directoryMetadata.isDirectory()) {
|
|
34
|
-
throw new TypeError('GitHub credential directory must be a real directory');
|
|
35
|
-
}
|
|
36
|
-
await chmod(directory, 0o700);
|
|
37
|
-
const exists = await regularOrMissing(target);
|
|
38
|
-
const temporary = `${target}.gala-${process.pid}`;
|
|
39
|
-
const backup = `${target}.gala-backup-${process.pid}`;
|
|
40
|
-
try {
|
|
41
|
-
await writeFile(temporary, `${JSON.stringify({ schemaVersion: 1, accessToken, scopes })}\n`, {
|
|
42
|
-
flag: 'wx', mode: 0o600
|
|
43
|
-
});
|
|
44
|
-
await chmod(temporary, 0o600);
|
|
45
|
-
if (exists) await rename(target, backup);
|
|
46
|
-
try { await rename(temporary, target); }
|
|
47
|
-
catch (error) { if (exists) await rename(backup, target); throw error; }
|
|
48
|
-
await chmod(target, 0o600);
|
|
49
|
-
if (exists) await rm(backup);
|
|
50
|
-
return path.resolve(target);
|
|
51
|
-
} catch (error) {
|
|
52
|
-
await rm(temporary, { force: true });
|
|
53
|
-
throw error;
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
export async function readGithubCredential({ target = githubCredentialPath() } = {}) {
|
|
58
|
-
if (!await regularOrMissing(target)) throw new Error('GitHub authentication is missing; run `gala auth github`');
|
|
59
|
-
const payload = JSON.parse(await readFile(target, 'utf8'));
|
|
60
|
-
if (payload?.schemaVersion !== 1 || typeof payload.accessToken !== 'string'
|
|
61
|
-
|| !Array.isArray(payload.scopes) || !payload.scopes.includes('repo') || !payload.scopes.includes('workflow')) {
|
|
62
|
-
throw new TypeError('GitHub credential file has an unsupported schema or missing scopes');
|
|
63
|
-
}
|
|
64
|
-
return Object.freeze({ accessToken: payload.accessToken, scopes: [...payload.scopes] });
|
|
65
|
-
}
|
|
@@ -1,130 +0,0 @@
|
|
|
1
|
-
const DEVICE_CODE_URL = 'https://github.com/login/device/code';
|
|
2
|
-
const ACCESS_TOKEN_URL = 'https://github.com/login/oauth/access_token';
|
|
3
|
-
const DEVICE_GRANT = 'urn:ietf:params:oauth:grant-type:device_code';
|
|
4
|
-
|
|
5
|
-
function requiredString(value, field) {
|
|
6
|
-
if (typeof value !== 'string' || value.trim() === '') {
|
|
7
|
-
throw new TypeError(`${field} is required`);
|
|
8
|
-
}
|
|
9
|
-
return value.trim();
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
function positiveInteger(value, field) {
|
|
13
|
-
if (!Number.isSafeInteger(value) || value <= 0) {
|
|
14
|
-
throw new TypeError(`${field} must be a positive integer`);
|
|
15
|
-
}
|
|
16
|
-
return value;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
async function postForm(fetchImpl, url, fields) {
|
|
20
|
-
const response = await fetchImpl(url, {
|
|
21
|
-
method: 'POST',
|
|
22
|
-
headers: {
|
|
23
|
-
accept: 'application/json',
|
|
24
|
-
'content-type': 'application/x-www-form-urlencoded'
|
|
25
|
-
},
|
|
26
|
-
body: new URLSearchParams(fields)
|
|
27
|
-
});
|
|
28
|
-
if (!response.ok) {
|
|
29
|
-
throw new Error(`GitHub OAuth request failed with HTTP ${response.status}`);
|
|
30
|
-
}
|
|
31
|
-
const payload = await response.json();
|
|
32
|
-
if (payload == null || Array.isArray(payload) || typeof payload !== 'object') {
|
|
33
|
-
throw new TypeError('GitHub OAuth response must be a JSON object');
|
|
34
|
-
}
|
|
35
|
-
return payload;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export async function requestDeviceCode({ clientId, scopes, fetchImpl = fetch }) {
|
|
39
|
-
const normalizedClientId = requiredString(clientId, 'clientId');
|
|
40
|
-
if (!Array.isArray(scopes) || scopes.length === 0) {
|
|
41
|
-
throw new TypeError('scopes must be a non-empty list');
|
|
42
|
-
}
|
|
43
|
-
const normalizedScopes = scopes.map((scope) => requiredString(scope, 'scope'));
|
|
44
|
-
const payload = await postForm(fetchImpl, DEVICE_CODE_URL, {
|
|
45
|
-
client_id: normalizedClientId,
|
|
46
|
-
scope: normalizedScopes.join(' ')
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
return Object.freeze({
|
|
50
|
-
deviceCode: requiredString(payload.device_code, 'device_code'),
|
|
51
|
-
userCode: requiredString(payload.user_code, 'user_code'),
|
|
52
|
-
verificationUri: requiredString(payload.verification_uri, 'verification_uri'),
|
|
53
|
-
expiresInSeconds: positiveInteger(payload.expires_in, 'expires_in'),
|
|
54
|
-
intervalSeconds: positiveInteger(payload.interval, 'interval')
|
|
55
|
-
});
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
export async function pollForAccessToken({
|
|
59
|
-
clientId,
|
|
60
|
-
deviceCode,
|
|
61
|
-
expiresInSeconds,
|
|
62
|
-
intervalSeconds,
|
|
63
|
-
requiredScopes = [],
|
|
64
|
-
fetchImpl = fetch,
|
|
65
|
-
sleep = (milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds)),
|
|
66
|
-
now = Date.now
|
|
67
|
-
}) {
|
|
68
|
-
const normalizedClientId = requiredString(clientId, 'clientId');
|
|
69
|
-
const normalizedDeviceCode = requiredString(deviceCode, 'deviceCode');
|
|
70
|
-
if (!Array.isArray(requiredScopes)) throw new TypeError('requiredScopes must be a list');
|
|
71
|
-
const normalizedRequiredScopes = requiredScopes.map((scope) =>
|
|
72
|
-
requiredString(scope, 'scope').toLowerCase()
|
|
73
|
-
);
|
|
74
|
-
const lifetime = positiveInteger(expiresInSeconds, 'expiresInSeconds') * 1000;
|
|
75
|
-
let interval = positiveInteger(intervalSeconds, 'intervalSeconds');
|
|
76
|
-
const startedAt = now();
|
|
77
|
-
if (typeof startedAt !== 'number' || !Number.isFinite(startedAt)) {
|
|
78
|
-
throw new TypeError('Clock must return epoch milliseconds');
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
while (true) {
|
|
82
|
-
await sleep(interval * 1000);
|
|
83
|
-
const currentTime = now();
|
|
84
|
-
if (typeof currentTime !== 'number' || !Number.isFinite(currentTime)) {
|
|
85
|
-
throw new TypeError('Clock must return epoch milliseconds');
|
|
86
|
-
}
|
|
87
|
-
if (currentTime - startedAt >= lifetime) {
|
|
88
|
-
throw new Error('GitHub device code expired before authorization completed');
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
const payload = await postForm(fetchImpl, ACCESS_TOKEN_URL, {
|
|
92
|
-
client_id: normalizedClientId,
|
|
93
|
-
device_code: normalizedDeviceCode,
|
|
94
|
-
grant_type: DEVICE_GRANT
|
|
95
|
-
});
|
|
96
|
-
if (typeof payload.access_token === 'string' && payload.access_token !== '') {
|
|
97
|
-
const tokenType = requiredString(payload.token_type, 'token_type');
|
|
98
|
-
if (tokenType.toLowerCase() !== 'bearer') {
|
|
99
|
-
throw new TypeError('GitHub OAuth token_type must be bearer');
|
|
100
|
-
}
|
|
101
|
-
const grantedScopes = typeof payload.scope === 'string'
|
|
102
|
-
? payload.scope.split(/[,\s]+/).map((scope) => scope.trim().toLowerCase()).filter(Boolean)
|
|
103
|
-
: [];
|
|
104
|
-
const missingScopes = normalizedRequiredScopes.filter((scope) => !grantedScopes.includes(scope));
|
|
105
|
-
if (missingScopes.length > 0) {
|
|
106
|
-
throw new Error(`GitHub authorization omitted required scope(s): ${missingScopes.join(', ')}`);
|
|
107
|
-
}
|
|
108
|
-
return Object.freeze({
|
|
109
|
-
accessToken: requiredString(payload.access_token, 'access_token'),
|
|
110
|
-
tokenType: 'bearer',
|
|
111
|
-
scopes: grantedScopes
|
|
112
|
-
});
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
if (payload.error === 'authorization_pending') continue;
|
|
116
|
-
if (payload.error === 'slow_down') {
|
|
117
|
-
interval = Number.isSafeInteger(payload.interval) && payload.interval > interval
|
|
118
|
-
? payload.interval
|
|
119
|
-
: interval + 5;
|
|
120
|
-
continue;
|
|
121
|
-
}
|
|
122
|
-
if (payload.error === 'expired_token' || payload.error === 'token_expired') {
|
|
123
|
-
throw new Error('GitHub device code expired before authorization completed');
|
|
124
|
-
}
|
|
125
|
-
if (typeof payload.error === 'string' && payload.error !== '') {
|
|
126
|
-
throw new Error(`GitHub device authorization failed: ${payload.error}`);
|
|
127
|
-
}
|
|
128
|
-
throw new TypeError('GitHub OAuth response contained neither a token nor an error');
|
|
129
|
-
}
|
|
130
|
-
}
|
|
@@ -1,76 +0,0 @@
|
|
|
1
|
-
import { spawn } from 'node:child_process';
|
|
2
|
-
import { describeHttpFailure } from './http-failure.js';
|
|
3
|
-
|
|
4
|
-
const API_VERSION = '2026-03-10';
|
|
5
|
-
|
|
6
|
-
export async function verifyEmptyRepository({ owner, repository, accessToken, fetchImpl = fetch }) {
|
|
7
|
-
const repositoryUrl = `https://api.github.com/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repository)}`;
|
|
8
|
-
const headers = {
|
|
9
|
-
accept: 'application/vnd.github+json', authorization: `Bearer ${accessToken}`,
|
|
10
|
-
'x-github-api-version': API_VERSION
|
|
11
|
-
};
|
|
12
|
-
const response = await fetchImpl(repositoryUrl, {
|
|
13
|
-
headers
|
|
14
|
-
});
|
|
15
|
-
if (!response.ok) throw new Error(await describeHttpFailure(response, 'GitHub repository lookup'));
|
|
16
|
-
const payload = await response.json();
|
|
17
|
-
if (payload.full_name?.toLowerCase() !== `${owner}/${repository}`.toLowerCase()) {
|
|
18
|
-
throw new TypeError('GitHub returned an unexpected repository');
|
|
19
|
-
}
|
|
20
|
-
const branchesResponse = await fetchImpl(`${repositoryUrl}/branches?per_page=1`, {
|
|
21
|
-
headers: {
|
|
22
|
-
...headers
|
|
23
|
-
}
|
|
24
|
-
});
|
|
25
|
-
if (!branchesResponse.ok) throw new Error(await describeHttpFailure(branchesResponse, 'GitHub branch lookup'));
|
|
26
|
-
const branches = await branchesResponse.json();
|
|
27
|
-
if (payload.size !== 0 || !Array.isArray(branches) || branches.length !== 0) {
|
|
28
|
-
throw new Error('Existing repository is not empty; explicit non-empty adoption is not implemented');
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export function setRepositoryOrigin({ root, owner, repository, spawnProcess = spawn }) {
|
|
33
|
-
const target = `https://github.com/${owner}/${repository}.git`;
|
|
34
|
-
return new Promise((resolve, reject) => {
|
|
35
|
-
const child = spawnProcess('git', ['-C', root, 'remote', 'set-url', 'origin', target], {
|
|
36
|
-
cwd: root, shell: false, stdio: 'inherit'
|
|
37
|
-
});
|
|
38
|
-
child.once('error', reject);
|
|
39
|
-
child.once('exit', (code, signal) => {
|
|
40
|
-
if (signal) reject(new Error(`Git remote update terminated by signal ${signal}`));
|
|
41
|
-
else if (code !== 0) reject(new Error(`Git remote update exited with code ${code}`));
|
|
42
|
-
else resolve();
|
|
43
|
-
});
|
|
44
|
-
});
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
export function verifyRepositoryOrigin({ root, owner, repository, spawnProcess = spawn }) {
|
|
48
|
-
const expected = `https://github.com/${owner}/${repository}.git`;
|
|
49
|
-
const expectedPath = `/${owner}/${repository}.git`;
|
|
50
|
-
return new Promise((resolve, reject) => {
|
|
51
|
-
const child = spawnProcess('git', ['-C', root, 'remote', 'get-url', 'origin'], {
|
|
52
|
-
cwd: root, shell: false, stdio: ['ignore', 'pipe', 'inherit']
|
|
53
|
-
});
|
|
54
|
-
let output = '';
|
|
55
|
-
child.stdout?.setEncoding('utf8');
|
|
56
|
-
child.stdout?.on('data', (chunk) => { output += chunk; });
|
|
57
|
-
child.once('error', reject);
|
|
58
|
-
child.once('exit', (code, signal) => {
|
|
59
|
-
if (signal) reject(new Error(`Git origin verification terminated by signal ${signal}`));
|
|
60
|
-
else if (code !== 0) reject(new Error(`Git origin verification exited with code ${code}`));
|
|
61
|
-
else {
|
|
62
|
-
let origin;
|
|
63
|
-
try {
|
|
64
|
-
origin = new URL(output.trim());
|
|
65
|
-
} catch {
|
|
66
|
-
reject(new Error(`Existing checkout origin must be ${expected}`));
|
|
67
|
-
return;
|
|
68
|
-
}
|
|
69
|
-
if (origin.protocol !== 'https:' || origin.hostname !== 'github.com' || origin.port !== ''
|
|
70
|
-
|| origin.pathname !== expectedPath || origin.search !== '' || origin.hash !== '') {
|
|
71
|
-
reject(new Error(`Existing checkout origin must be ${expected}`));
|
|
72
|
-
} else resolve(root);
|
|
73
|
-
}
|
|
74
|
-
});
|
|
75
|
-
});
|
|
76
|
-
}
|
package/src/github-identity.js
DELETED
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
import { describeHttpFailure } from './http-failure.js';
|
|
2
|
-
const GITHUB_API_VERSION = '2026-03-10';
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* The GitHub account the stored credential belongs to.
|
|
6
|
-
*
|
|
7
|
-
* `scaffold` used to make the writer pass `--owner`, which is a value the token already knows and
|
|
8
|
-
* they can only get wrong. The credential file holds the token and its scopes and nothing else, so
|
|
9
|
-
* this is a live lookup rather than something cached at `auth github` time — a login can be
|
|
10
|
-
* changed, and a stale one would create the repository under a name that no longer exists.
|
|
11
|
-
*/
|
|
12
|
-
export async function resolveGithubLogin({ accessToken, fetchImpl = fetch }) {
|
|
13
|
-
if (typeof accessToken !== 'string' || accessToken === '') {
|
|
14
|
-
throw new TypeError('accessToken is required');
|
|
15
|
-
}
|
|
16
|
-
const response = await fetchImpl('https://api.github.com/user', {
|
|
17
|
-
headers: {
|
|
18
|
-
accept: 'application/vnd.github+json',
|
|
19
|
-
authorization: `Bearer ${accessToken}`,
|
|
20
|
-
'x-github-api-version': GITHUB_API_VERSION
|
|
21
|
-
}
|
|
22
|
-
});
|
|
23
|
-
if (!response.ok) throw new Error(await describeHttpFailure(response, 'GitHub account lookup'));
|
|
24
|
-
const payload = await response.json();
|
|
25
|
-
const login = payload?.login;
|
|
26
|
-
// The same shape `scaffold` demands of `--owner`. Refusing here beats a confusing failure four
|
|
27
|
-
// API calls later.
|
|
28
|
-
if (typeof login !== 'string' || !/^[A-Za-z0-9](?:[A-Za-z0-9-]{0,37}[A-Za-z0-9])?$/.test(login)) {
|
|
29
|
-
throw new TypeError('GitHub returned an unusable account login');
|
|
30
|
-
}
|
|
31
|
-
return login;
|
|
32
|
-
}
|
|
@@ -1,107 +0,0 @@
|
|
|
1
|
-
import { describeHttpFailure } from './http-failure.js';
|
|
2
|
-
const GITHUB_API_VERSION = '2026-03-10';
|
|
3
|
-
const SEGMENT = /^[A-Za-z0-9_.-]+$/;
|
|
4
|
-
const SHA = /^[0-9a-f]{40}$/;
|
|
5
|
-
|
|
6
|
-
function required(value, field, pattern = null) {
|
|
7
|
-
if (typeof value !== 'string' || value.length === 0 || (pattern != null && !pattern.test(value))) {
|
|
8
|
-
throw new TypeError(`${field} is invalid`);
|
|
9
|
-
}
|
|
10
|
-
return value;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
function headers(accessToken) {
|
|
14
|
-
return {
|
|
15
|
-
accept: 'application/vnd.github+json',
|
|
16
|
-
authorization: `Bearer ${accessToken}`,
|
|
17
|
-
'content-type': 'application/json',
|
|
18
|
-
'x-github-api-version': GITHUB_API_VERSION
|
|
19
|
-
};
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
async function json(response, operation) {
|
|
23
|
-
if (!response.ok) throw new Error(await describeHttpFailure(response, `GitHub ${operation}`));
|
|
24
|
-
return response.json();
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export async function provisionGithubPages({
|
|
28
|
-
owner, repository, accessToken, commitSha, fetchImpl = fetch,
|
|
29
|
-
customDomain = null,
|
|
30
|
-
sleep = (milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds)),
|
|
31
|
-
pollIntervalMs = 5_000, maxPolls = 120
|
|
32
|
-
}) {
|
|
33
|
-
const normalizedOwner = required(owner, 'owner', SEGMENT);
|
|
34
|
-
const normalizedRepository = required(repository, 'repository', SEGMENT);
|
|
35
|
-
const token = required(accessToken, 'accessToken');
|
|
36
|
-
const sha = required(commitSha, 'commitSha', SHA);
|
|
37
|
-
if (customDomain != null && (typeof customDomain !== 'string'
|
|
38
|
-
|| !/^(?=.{1,253}$)(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/.test(customDomain))) {
|
|
39
|
-
throw new TypeError('customDomain is invalid');
|
|
40
|
-
}
|
|
41
|
-
if (!Number.isSafeInteger(pollIntervalMs) || pollIntervalMs < 0) {
|
|
42
|
-
throw new TypeError('pollIntervalMs must be a non-negative safe integer');
|
|
43
|
-
}
|
|
44
|
-
if (!Number.isSafeInteger(maxPolls) || maxPolls <= 0) {
|
|
45
|
-
throw new TypeError('maxPolls must be a positive safe integer');
|
|
46
|
-
}
|
|
47
|
-
const requestHeaders = headers(token);
|
|
48
|
-
const repositoryUrl = `https://api.github.com/repos/${encodeURIComponent(normalizedOwner)}/${encodeURIComponent(normalizedRepository)}`;
|
|
49
|
-
const query = new URLSearchParams({ event: 'push', head_sha: sha, per_page: '10' });
|
|
50
|
-
let run = null;
|
|
51
|
-
for (let poll = 0; poll < maxPolls; poll += 1) {
|
|
52
|
-
const response = await fetchImpl(`${repositoryUrl}/actions/workflows/publish.yml/runs?${query}`, {
|
|
53
|
-
method: 'GET', headers: requestHeaders
|
|
54
|
-
});
|
|
55
|
-
const payload = await json(response, 'publish workflow runs request');
|
|
56
|
-
run = payload.workflow_runs?.find((candidate) => candidate.head_sha === sha) ?? null;
|
|
57
|
-
if (run?.status === 'completed') break;
|
|
58
|
-
if (poll + 1 < maxPolls) await sleep(pollIntervalMs);
|
|
59
|
-
}
|
|
60
|
-
if (run == null || run.status !== 'completed') {
|
|
61
|
-
throw new Error(`Timed out waiting for the publish workflow for ${sha}`);
|
|
62
|
-
}
|
|
63
|
-
if (run.conclusion !== 'success') {
|
|
64
|
-
throw new Error(`Initial publish workflow failed: ${run.html_url}`);
|
|
65
|
-
}
|
|
66
|
-
const branch = await fetchImpl(`${repositoryUrl}/branches/gh-pages`, {
|
|
67
|
-
method: 'GET', headers: requestHeaders
|
|
68
|
-
});
|
|
69
|
-
if (!branch.ok) {
|
|
70
|
-
throw new Error(`Successful publish run created no gh-pages branch: ${run.html_url}`);
|
|
71
|
-
}
|
|
72
|
-
const current = await fetchImpl(`${repositoryUrl}/pages`, { method: 'GET', headers: requestHeaders });
|
|
73
|
-
if (current.ok) {
|
|
74
|
-
const configuration = await current.json();
|
|
75
|
-
if (configuration.source?.branch !== 'gh-pages' || configuration.source?.path !== '/') {
|
|
76
|
-
throw new Error('Existing GitHub Pages configuration does not use gh-pages at /');
|
|
77
|
-
}
|
|
78
|
-
if ((configuration.cname ?? null) !== customDomain) {
|
|
79
|
-
const updated = await fetchImpl(`${repositoryUrl}/pages`, {
|
|
80
|
-
method: 'PUT', headers: requestHeaders,
|
|
81
|
-
body: JSON.stringify({ cname: customDomain, source: { branch: 'gh-pages', path: '/' } })
|
|
82
|
-
});
|
|
83
|
-
if (updated.status !== 204) {
|
|
84
|
-
throw new Error(`GitHub Pages custom-domain update failed with HTTP ${updated.status}`);
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
return Object.freeze({ created: false, url: configuration.html_url, runUrl: run.html_url });
|
|
88
|
-
}
|
|
89
|
-
if (current.status !== 404) {
|
|
90
|
-
throw new Error(await describeHttpFailure(current, 'GitHub Pages configuration request'));
|
|
91
|
-
}
|
|
92
|
-
const created = await fetchImpl(`${repositoryUrl}/pages`, {
|
|
93
|
-
method: 'POST', headers: requestHeaders,
|
|
94
|
-
body: JSON.stringify({ source: { branch: 'gh-pages', path: '/' } })
|
|
95
|
-
});
|
|
96
|
-
const configuration = await json(created, 'Pages activation');
|
|
97
|
-
if (customDomain != null) {
|
|
98
|
-
const updated = await fetchImpl(`${repositoryUrl}/pages`, {
|
|
99
|
-
method: 'PUT', headers: requestHeaders,
|
|
100
|
-
body: JSON.stringify({ cname: customDomain, source: { branch: 'gh-pages', path: '/' } })
|
|
101
|
-
});
|
|
102
|
-
if (updated.status !== 204) {
|
|
103
|
-
throw new Error(`GitHub Pages custom-domain update failed with HTTP ${updated.status}`);
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
return Object.freeze({ created: true, url: configuration.html_url, runUrl: run.html_url });
|
|
107
|
-
}
|
|
@@ -1,82 +0,0 @@
|
|
|
1
|
-
import sodium from 'libsodium-wrappers';
|
|
2
|
-
|
|
3
|
-
const GITHUB_API_VERSION = '2026-03-10';
|
|
4
|
-
const OWNER_OR_REPOSITORY = /^[A-Za-z0-9_.-]+$/;
|
|
5
|
-
const SECRET_NAME = /^[A-Z_][A-Z0-9_]*$/;
|
|
6
|
-
|
|
7
|
-
function required(value, field, pattern) {
|
|
8
|
-
if (typeof value !== 'string' || !pattern.test(value)) {
|
|
9
|
-
throw new TypeError(`${field} is invalid`);
|
|
10
|
-
}
|
|
11
|
-
return value;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
function requiredSecret(value, field) {
|
|
15
|
-
if (typeof value !== 'string' || value.length === 0) {
|
|
16
|
-
throw new TypeError(`${field} must not be empty`);
|
|
17
|
-
}
|
|
18
|
-
return value;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
function headers(accessToken) {
|
|
22
|
-
return {
|
|
23
|
-
accept: 'application/vnd.github+json',
|
|
24
|
-
authorization: `Bearer ${accessToken}`,
|
|
25
|
-
'content-type': 'application/json',
|
|
26
|
-
'x-github-api-version': GITHUB_API_VERSION
|
|
27
|
-
};
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
async function requireSuccess(response, operation) {
|
|
31
|
-
if (!response?.ok) {
|
|
32
|
-
const status = Number.isInteger(response?.status) ? response.status : 'unknown';
|
|
33
|
-
throw new Error(`GitHub ${operation} failed with HTTP ${status}`);
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
export async function installRepositorySecret({
|
|
38
|
-
owner,
|
|
39
|
-
repository,
|
|
40
|
-
accessToken,
|
|
41
|
-
secretName,
|
|
42
|
-
secretValue,
|
|
43
|
-
fetchImpl = fetch,
|
|
44
|
-
sodiumImpl = sodium
|
|
45
|
-
}) {
|
|
46
|
-
const normalizedOwner = required(owner, 'owner', OWNER_OR_REPOSITORY);
|
|
47
|
-
const normalizedRepository = required(repository, 'repository', OWNER_OR_REPOSITORY);
|
|
48
|
-
const normalizedSecretName = required(secretName, 'secretName', SECRET_NAME);
|
|
49
|
-
const token = requiredSecret(accessToken, 'accessToken');
|
|
50
|
-
const plaintext = requiredSecret(secretValue, 'secretValue');
|
|
51
|
-
|
|
52
|
-
await sodiumImpl.ready;
|
|
53
|
-
|
|
54
|
-
const baseUrl = `https://api.github.com/repos/${encodeURIComponent(normalizedOwner)}/${encodeURIComponent(normalizedRepository)}/actions/secrets`;
|
|
55
|
-
const publicKeyResponse = await fetchImpl(`${baseUrl}/public-key`, {
|
|
56
|
-
method: 'GET',
|
|
57
|
-
headers: headers(token)
|
|
58
|
-
});
|
|
59
|
-
await requireSuccess(publicKeyResponse, 'repository public-key request');
|
|
60
|
-
const publicKeyPayload = await publicKeyResponse.json();
|
|
61
|
-
const keyId = requiredSecret(publicKeyPayload?.key_id, 'GitHub key_id');
|
|
62
|
-
const publicKey = requiredSecret(publicKeyPayload?.key, 'GitHub public key');
|
|
63
|
-
|
|
64
|
-
const ciphertext = sodiumImpl.crypto_box_seal(
|
|
65
|
-
sodiumImpl.from_string(plaintext),
|
|
66
|
-
sodiumImpl.from_base64(publicKey, sodiumImpl.base64_variants.ORIGINAL)
|
|
67
|
-
);
|
|
68
|
-
const encryptedValue = sodiumImpl.to_base64(
|
|
69
|
-
ciphertext,
|
|
70
|
-
sodiumImpl.base64_variants.ORIGINAL
|
|
71
|
-
);
|
|
72
|
-
|
|
73
|
-
const uploadResponse = await fetchImpl(
|
|
74
|
-
`${baseUrl}/${encodeURIComponent(normalizedSecretName)}`,
|
|
75
|
-
{
|
|
76
|
-
method: 'PUT',
|
|
77
|
-
headers: headers(token),
|
|
78
|
-
body: JSON.stringify({ encrypted_value: encryptedValue, key_id: keyId })
|
|
79
|
-
}
|
|
80
|
-
);
|
|
81
|
-
await requireSuccess(uploadResponse, 'repository secret upload');
|
|
82
|
-
}
|
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
import { describeHttpFailure } from './http-failure.js';
|
|
2
|
-
const GITHUB_API_VERSION = '2026-03-10';
|
|
3
|
-
const OWNER_OR_REPOSITORY = /^[A-Za-z0-9_.-]+$/;
|
|
4
|
-
const VARIABLE_NAME = /^[A-Z_][A-Z0-9_]*$/;
|
|
5
|
-
|
|
6
|
-
function required(value, field, pattern) {
|
|
7
|
-
if (typeof value !== 'string' || !pattern.test(value)) {
|
|
8
|
-
throw new TypeError(`${field} is invalid`);
|
|
9
|
-
}
|
|
10
|
-
return value;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
function requiredValue(value, field) {
|
|
14
|
-
if (typeof value !== 'string' || value.length === 0) {
|
|
15
|
-
throw new TypeError(`${field} must not be empty`);
|
|
16
|
-
}
|
|
17
|
-
return value;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
function headers(accessToken) {
|
|
21
|
-
return {
|
|
22
|
-
accept: 'application/vnd.github+json',
|
|
23
|
-
authorization: `Bearer ${accessToken}`,
|
|
24
|
-
'content-type': 'application/json',
|
|
25
|
-
'x-github-api-version': GITHUB_API_VERSION
|
|
26
|
-
};
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export async function installRepositoryVariable({
|
|
30
|
-
owner, repository, accessToken, variableName, variableValue, fetchImpl = fetch
|
|
31
|
-
}) {
|
|
32
|
-
const normalizedOwner = required(owner, 'owner', OWNER_OR_REPOSITORY);
|
|
33
|
-
const normalizedRepository = required(repository, 'repository', OWNER_OR_REPOSITORY);
|
|
34
|
-
const normalizedName = required(variableName, 'variableName', VARIABLE_NAME);
|
|
35
|
-
const token = requiredValue(accessToken, 'accessToken');
|
|
36
|
-
const value = requiredValue(variableValue, 'variableValue');
|
|
37
|
-
const baseUrl = `https://api.github.com/repos/${encodeURIComponent(normalizedOwner)}/${encodeURIComponent(normalizedRepository)}/actions/variables`;
|
|
38
|
-
const requestHeaders = headers(token);
|
|
39
|
-
const update = await fetchImpl(`${baseUrl}/${encodeURIComponent(normalizedName)}`, {
|
|
40
|
-
method: 'PATCH',
|
|
41
|
-
headers: requestHeaders,
|
|
42
|
-
body: JSON.stringify({ name: normalizedName, value })
|
|
43
|
-
});
|
|
44
|
-
if (update.ok) return;
|
|
45
|
-
if (update.status !== 404) {
|
|
46
|
-
throw new Error(await describeHttpFailure(update, 'GitHub repository variable update'));
|
|
47
|
-
}
|
|
48
|
-
const create = await fetchImpl(baseUrl, {
|
|
49
|
-
method: 'POST',
|
|
50
|
-
headers: requestHeaders,
|
|
51
|
-
body: JSON.stringify({ name: normalizedName, value })
|
|
52
|
-
});
|
|
53
|
-
if (!create.ok) {
|
|
54
|
-
throw new Error(await describeHttpFailure(create, 'GitHub repository variable creation'));
|
|
55
|
-
}
|
|
56
|
-
}
|