ras-stack 0.26.0 β 0.27.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 +13 -0
- package/dist/preview/github.d.ts +20 -0
- package/dist/preview/github.js +112 -0
- package/dist/preview/github.js.map +1 -0
- package/package.json +5 -1
package/README.md
CHANGED
|
@@ -425,6 +425,19 @@ The loaded image tag is also available to the command as `RAS_STACK_TEST_IMAGE`.
|
|
|
425
425
|
|
|
426
426
|
Dokploy previews can share the application/domain/image/environment/deploy/health/delete/prune lifecycle through `ras-stack/preview/dokploy`. Product-specific Stripe, storage, seed, and verification work stays around the manager's configure and cleanup hooks.
|
|
427
427
|
|
|
428
|
+
Preview comments and commit checks can use the same state transition without carrying a GitHub API client in every repository:
|
|
429
|
+
|
|
430
|
+
```ts
|
|
431
|
+
import { reportPreviewStatus } from 'ras-stack/preview/github'
|
|
432
|
+
|
|
433
|
+
await reportPreviewStatus(
|
|
434
|
+
{ repository, token, marker: '<!-- app-preview -->', note: 'Preview data is disposable.' },
|
|
435
|
+
{ state: 'ready', prNumber, sha, previewUrl, runUrl },
|
|
436
|
+
)
|
|
437
|
+
```
|
|
438
|
+
|
|
439
|
+
The reporter keeps one marked comment and one named check run, preserves the last ready commit while a replacement builds, bounds comment pagination, and validates repository, pull request, commit, marker, and URL inputs. Applications retain their preview hostname, access note, seed credentials, and product cleanup hooks.
|
|
440
|
+
|
|
428
441
|
The reusable `build-preview-image.yml` workflow publishes same-repository pull requests directly but turns fork builds into one-day artifacts without exposing a token or secret. A trusted `workflow_run` job can publish that artifact with `actions/publish-preview-image` before running its repository-owned deployment command. The event wrapper and secret-to-environment mapping remain in each application so the trust boundary is visible locally.
|
|
429
442
|
|
|
430
443
|
Self-hosted images that run the app, Centrifugo, and Caddy together can share the lifecycle without sharing a Dockerfile:
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export type PreviewStatusState = 'awaiting' | 'building' | 'ready' | 'failed' | 'deleted';
|
|
2
|
+
export type GitHubPreviewOptions = {
|
|
3
|
+
repository: string;
|
|
4
|
+
token: string;
|
|
5
|
+
marker: string;
|
|
6
|
+
note?: string;
|
|
7
|
+
checkName?: string;
|
|
8
|
+
fetch?: typeof fetch;
|
|
9
|
+
};
|
|
10
|
+
export type PreviewStatus = {
|
|
11
|
+
state: 'deleted';
|
|
12
|
+
prNumber: string;
|
|
13
|
+
} | {
|
|
14
|
+
state: Exclude<PreviewStatusState, 'deleted'>;
|
|
15
|
+
prNumber: string;
|
|
16
|
+
sha: string;
|
|
17
|
+
previewUrl?: string;
|
|
18
|
+
runUrl?: string;
|
|
19
|
+
};
|
|
20
|
+
export declare function reportPreviewStatus(options: GitHubPreviewOptions, status: PreviewStatus): Promise<void>;
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { pullRequestNumber } from './dokploy.js';
|
|
2
|
+
export async function reportPreviewStatus(options, status) {
|
|
3
|
+
const repository = githubRepository(options.repository);
|
|
4
|
+
const marker = commentMarker(options.marker);
|
|
5
|
+
if (!options.token)
|
|
6
|
+
throw new Error('GitHub token is required');
|
|
7
|
+
const prNumber = pullRequestNumber(status.prNumber);
|
|
8
|
+
const request = options.fetch ?? fetch;
|
|
9
|
+
const api = (path, init) => github(request, options.token, `/repos/${repository}${path}`, init);
|
|
10
|
+
if (status.state !== 'deleted') {
|
|
11
|
+
await updateCheck(api, options.checkName ?? 'PR preview deploy', commitSha(status.sha), status);
|
|
12
|
+
}
|
|
13
|
+
const comments = await issueComments(api, prNumber);
|
|
14
|
+
const existing = comments.find((comment) => comment.body?.includes(marker));
|
|
15
|
+
const body = previewComment({ ...options, marker }, status, existing?.body);
|
|
16
|
+
if (existing)
|
|
17
|
+
await api(`/issues/comments/${existing.id}`, jsonRequest('PATCH', { body }));
|
|
18
|
+
else
|
|
19
|
+
await api(`/issues/${prNumber}/comments`, jsonRequest('POST', { body }));
|
|
20
|
+
}
|
|
21
|
+
async function updateCheck(api, name, sha, status) {
|
|
22
|
+
const checks = await api(`/commits/${sha}/check-runs?check_name=${encodeURIComponent(name)}&filter=latest`);
|
|
23
|
+
const state = checkState(status.state);
|
|
24
|
+
const body = {
|
|
25
|
+
name,
|
|
26
|
+
head_sha: sha,
|
|
27
|
+
status: state.status,
|
|
28
|
+
...(state.conclusion ? { conclusion: state.conclusion } : {}),
|
|
29
|
+
...(status.runUrl ? { details_url: webUrl(status.runUrl, 'workflow run URL') } : {}),
|
|
30
|
+
output: { title: name, summary: state.summary },
|
|
31
|
+
};
|
|
32
|
+
const existing = checks.check_runs[0];
|
|
33
|
+
if (existing)
|
|
34
|
+
await api(`/check-runs/${existing.id}`, jsonRequest('PATCH', body));
|
|
35
|
+
else
|
|
36
|
+
await api('/check-runs', jsonRequest('POST', body));
|
|
37
|
+
}
|
|
38
|
+
async function issueComments(api, prNumber, page = 1) {
|
|
39
|
+
if (page > 10)
|
|
40
|
+
throw new Error('preview comment lookup exceeded 1,000 comments');
|
|
41
|
+
const comments = await api(`/issues/${prNumber}/comments?per_page=100&page=${page}`);
|
|
42
|
+
return comments.length < 100 ? comments : [...comments, ...(await issueComments(api, prNumber, page + 1))];
|
|
43
|
+
}
|
|
44
|
+
function previewComment(options, status, previous) {
|
|
45
|
+
if (status.state === 'deleted')
|
|
46
|
+
return `${options.marker}\nποΈ Preview deleted because this pull request was closed.`;
|
|
47
|
+
const sha = commitSha(status.sha).slice(0, 7);
|
|
48
|
+
const standingSha = previous?.match(/up to date with commit `([0-9a-f]{7})`/)?.[1];
|
|
49
|
+
const standing = standingSha ? ` The preview of \`${standingSha}\` stays up until it does.` : '';
|
|
50
|
+
const heading = {
|
|
51
|
+
awaiting: `βΈοΈ The preview of \`${sha}\` is waiting for a maintainer to approve its build.${standing}`,
|
|
52
|
+
building: `π Deploying \`${sha}\`.${standing}`,
|
|
53
|
+
ready: `β
Preview is up to date with commit \`${sha}\`.`,
|
|
54
|
+
failed: `β Deploying commit \`${sha}\` failed${status.runUrl ? ` ([workflow run](${webUrl(status.runUrl, 'workflow run URL')}))` : ''}. The preview below may be stale or unavailable.`,
|
|
55
|
+
}[status.state];
|
|
56
|
+
if (!status.previewUrl)
|
|
57
|
+
throw new Error('preview URL is required for active preview status');
|
|
58
|
+
return [
|
|
59
|
+
options.marker,
|
|
60
|
+
heading,
|
|
61
|
+
'',
|
|
62
|
+
`Preview: ${webUrl(status.previewUrl, 'preview URL')}`,
|
|
63
|
+
...(options.note ? ['', options.note] : []),
|
|
64
|
+
].join('\n');
|
|
65
|
+
}
|
|
66
|
+
function checkState(state) {
|
|
67
|
+
return {
|
|
68
|
+
awaiting: { status: 'queued', summary: 'The preview build is waiting for workflow approval.' },
|
|
69
|
+
building: { status: 'in_progress', summary: 'A new preview version is deploying.' },
|
|
70
|
+
ready: { status: 'completed', conclusion: 'success', summary: 'The preview is up to date.' },
|
|
71
|
+
failed: { status: 'completed', conclusion: 'failure', summary: 'The preview deployment failed.' },
|
|
72
|
+
}[state];
|
|
73
|
+
}
|
|
74
|
+
async function github(request, token, path, init) {
|
|
75
|
+
const headers = new Headers(init?.headers);
|
|
76
|
+
headers.set('accept', 'application/vnd.github+json');
|
|
77
|
+
headers.set('authorization', `Bearer ${token}`);
|
|
78
|
+
headers.set('x-github-api-version', '2022-11-28');
|
|
79
|
+
const response = await request(`https://api.github.com${path}`, {
|
|
80
|
+
...init,
|
|
81
|
+
headers,
|
|
82
|
+
});
|
|
83
|
+
const text = await response.text();
|
|
84
|
+
if (!response.ok)
|
|
85
|
+
throw new Error(`GitHub ${path.split('?')[0]} failed with ${response.status}: ${text.slice(0, 500)}`);
|
|
86
|
+
return text ? JSON.parse(text) : undefined;
|
|
87
|
+
}
|
|
88
|
+
function jsonRequest(method, body) {
|
|
89
|
+
return { method, headers: { 'content-type': 'application/json' }, body: JSON.stringify(body) };
|
|
90
|
+
}
|
|
91
|
+
function githubRepository(value) {
|
|
92
|
+
if (!/^[a-z\d][\w.-]*\/[a-z\d][\w.-]*$/i.test(value))
|
|
93
|
+
throw new Error('repository must use an owner/name identifier');
|
|
94
|
+
return value;
|
|
95
|
+
}
|
|
96
|
+
function commentMarker(value) {
|
|
97
|
+
if (!/^<!-- [a-z\d-]+ -->$/i.test(value))
|
|
98
|
+
throw new Error('preview marker must be a named HTML comment');
|
|
99
|
+
return value;
|
|
100
|
+
}
|
|
101
|
+
function webUrl(value, name) {
|
|
102
|
+
const url = new URL(value);
|
|
103
|
+
if (url.protocol !== 'https:' && url.protocol !== 'http:')
|
|
104
|
+
throw new Error(`${name} must use HTTP or HTTPS`);
|
|
105
|
+
return url.toString().replace(/\/$/, '');
|
|
106
|
+
}
|
|
107
|
+
function commitSha(value) {
|
|
108
|
+
if (!value || !/^[0-9a-f]{40}$/i.test(value))
|
|
109
|
+
throw new Error('commit SHA must contain 40 hexadecimal characters');
|
|
110
|
+
return value;
|
|
111
|
+
}
|
|
112
|
+
//# sourceMappingURL=github.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"github.js","sourceRoot":"","sources":["../../src/preview/github.ts"],"names":[],"mappings":"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","sourcesContent":["import { pullRequestNumber } from './dokploy.js'\n\nexport type PreviewStatusState = 'awaiting' | 'building' | 'ready' | 'failed' | 'deleted'\n\nexport type GitHubPreviewOptions = {\n repository: string\n token: string\n marker: string\n note?: string\n checkName?: string\n fetch?: typeof fetch\n}\n\nexport type PreviewStatus =\n | { state: 'deleted'; prNumber: string }\n | {\n state: Exclude<PreviewStatusState, 'deleted'>\n prNumber: string\n sha: string\n previewUrl?: string\n runUrl?: string\n }\n\ntype Check = { id: number }\ntype Comment = { id: number; body?: string }\n\nexport async function reportPreviewStatus(options: GitHubPreviewOptions, status: PreviewStatus) {\n const repository = githubRepository(options.repository)\n const marker = commentMarker(options.marker)\n if (!options.token) throw new Error('GitHub token is required')\n const prNumber = pullRequestNumber(status.prNumber)\n const request = options.fetch ?? fetch\n const api = <T>(path: string, init?: RequestInit) => github<T>(request, options.token, `/repos/${repository}${path}`, init)\n\n if (status.state !== 'deleted') {\n await updateCheck(api, options.checkName ?? 'PR preview deploy', commitSha(status.sha), status)\n }\n const comments = await issueComments(api, prNumber)\n const existing = comments.find((comment) => comment.body?.includes(marker))\n const body = previewComment({ ...options, marker }, status, existing?.body)\n if (existing) await api(`/issues/comments/${existing.id}`, jsonRequest('PATCH', { body }))\n else await api(`/issues/${prNumber}/comments`, jsonRequest('POST', { body }))\n}\n\nasync function updateCheck(\n api: <T>(path: string, init?: RequestInit) => Promise<T>,\n name: string,\n sha: string,\n status: Extract<PreviewStatus, { state: Exclude<PreviewStatusState, 'deleted'> }>,\n) {\n const checks = await api<{ check_runs: Check[] }>(`/commits/${sha}/check-runs?check_name=${encodeURIComponent(name)}&filter=latest`)\n const state = checkState(status.state)\n const body = {\n name,\n head_sha: sha,\n status: state.status,\n ...(state.conclusion ? { conclusion: state.conclusion } : {}),\n ...(status.runUrl ? { details_url: webUrl(status.runUrl, 'workflow run URL') } : {}),\n output: { title: name, summary: state.summary },\n }\n const existing = checks.check_runs[0]\n if (existing) await api(`/check-runs/${existing.id}`, jsonRequest('PATCH', body))\n else await api('/check-runs', jsonRequest('POST', body))\n}\n\nasync function issueComments(api: <T>(path: string, init?: RequestInit) => Promise<T>, prNumber: string, page = 1): Promise<Comment[]> {\n if (page > 10) throw new Error('preview comment lookup exceeded 1,000 comments')\n const comments = await api<Comment[]>(`/issues/${prNumber}/comments?per_page=100&page=${page}`)\n return comments.length < 100 ? comments : [...comments, ...(await issueComments(api, prNumber, page + 1))]\n}\n\nfunction previewComment(options: GitHubPreviewOptions, status: PreviewStatus, previous?: string) {\n if (status.state === 'deleted') return `${options.marker}\\nποΈ Preview deleted because this pull request was closed.`\n const sha = commitSha(status.sha).slice(0, 7)\n const standingSha = previous?.match(/up to date with commit `([0-9a-f]{7})`/)?.[1]\n const standing = standingSha ? ` The preview of \\`${standingSha}\\` stays up until it does.` : ''\n const heading = {\n awaiting: `βΈοΈ The preview of \\`${sha}\\` is waiting for a maintainer to approve its build.${standing}`,\n building: `π Deploying \\`${sha}\\`.${standing}`,\n ready: `β
Preview is up to date with commit \\`${sha}\\`.`,\n failed: `β Deploying commit \\`${sha}\\` failed${status.runUrl ? ` ([workflow run](${webUrl(status.runUrl, 'workflow run URL')}))` : ''}. The preview below may be stale or unavailable.`,\n }[status.state]\n if (!status.previewUrl) throw new Error('preview URL is required for active preview status')\n return [\n options.marker,\n heading,\n '',\n `Preview: ${webUrl(status.previewUrl, 'preview URL')}`,\n ...(options.note ? ['', options.note] : []),\n ].join('\\n')\n}\n\nfunction checkState(state: Exclude<PreviewStatusState, 'deleted'>) {\n return {\n awaiting: { status: 'queued', summary: 'The preview build is waiting for workflow approval.' },\n building: { status: 'in_progress', summary: 'A new preview version is deploying.' },\n ready: { status: 'completed', conclusion: 'success', summary: 'The preview is up to date.' },\n failed: { status: 'completed', conclusion: 'failure', summary: 'The preview deployment failed.' },\n }[state]\n}\n\nasync function github<T>(request: typeof fetch, token: string, path: string, init?: RequestInit): Promise<T> {\n const headers = new Headers(init?.headers)\n headers.set('accept', 'application/vnd.github+json')\n headers.set('authorization', `Bearer ${token}`)\n headers.set('x-github-api-version', '2022-11-28')\n const response = await request(`https://api.github.com${path}`, {\n ...init,\n headers,\n })\n const text = await response.text()\n if (!response.ok) throw new Error(`GitHub ${path.split('?')[0]} failed with ${response.status}: ${text.slice(0, 500)}`)\n return text ? (JSON.parse(text) as T) : (undefined as T)\n}\n\nfunction jsonRequest(method: 'POST' | 'PATCH', body: unknown): RequestInit {\n return { method, headers: { 'content-type': 'application/json' }, body: JSON.stringify(body) }\n}\n\nfunction githubRepository(value: string) {\n if (!/^[a-z\\d][\\w.-]*\\/[a-z\\d][\\w.-]*$/i.test(value)) throw new Error('repository must use an owner/name identifier')\n return value\n}\n\nfunction commentMarker(value: string) {\n if (!/^<!-- [a-z\\d-]+ -->$/i.test(value)) throw new Error('preview marker must be a named HTML comment')\n return value\n}\n\nfunction webUrl(value: string, name: string) {\n const url = new URL(value)\n if (url.protocol !== 'https:' && url.protocol !== 'http:') throw new Error(`${name} must use HTTP or HTTPS`)\n return url.toString().replace(/\\/$/, '')\n}\n\nfunction commitSha(value: string | undefined) {\n if (!value || !/^[0-9a-f]{40}$/i.test(value)) throw new Error('commit SHA must contain 40 hexadecimal characters')\n return value\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ras-stack",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.27.0",
|
|
4
4
|
"description": "Composable full-stack primitives shared across Richard Solomou's applications.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"authentication",
|
|
@@ -71,6 +71,10 @@
|
|
|
71
71
|
"types": "./dist/preview/dokploy.d.ts",
|
|
72
72
|
"default": "./dist/preview/dokploy.js"
|
|
73
73
|
},
|
|
74
|
+
"./preview/github": {
|
|
75
|
+
"types": "./dist/preview/github.d.ts",
|
|
76
|
+
"default": "./dist/preview/github.js"
|
|
77
|
+
},
|
|
74
78
|
"./realtime": {
|
|
75
79
|
"types": "./dist/realtime/index.d.ts",
|
|
76
80
|
"default": "./dist/realtime/index.js"
|