vouchington-tooling 0.5.1 โ†’ 0.6.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.
@@ -0,0 +1,58 @@
1
+ export declare const CHECKPOINT_MARKER = "pr-checkpoint:v1";
2
+ export type CheckpointStatus = 'queued' | 'running' | 'awaiting_verification' | 'failed' | 'deadline' | 'unresumable' | 'complete';
3
+ export type Checkpoint = {
4
+ marker: string;
5
+ repository: string;
6
+ pr: number;
7
+ /** Absent on checkpoints posted before trigger ids were recorded; keep optional so resume can still parse them. */
8
+ triggerCommentId?: number;
9
+ headRef: string;
10
+ startSha: string;
11
+ sessionStartSha: string;
12
+ runId: string;
13
+ runUrl: string;
14
+ actor: string;
15
+ sessionId: string;
16
+ /** Absent from v1 checkpoints created before session URLs were exposed. */
17
+ sessionUrl?: string;
18
+ resumeSourceRunId: string;
19
+ status: CheckpointStatus;
20
+ createdAt: string;
21
+ updatedAt: string;
22
+ };
23
+ export type GitHubComment = {
24
+ id: number;
25
+ user?: {
26
+ login?: string;
27
+ type?: string;
28
+ };
29
+ performed_via_github_app?: {
30
+ slug?: string;
31
+ } | null;
32
+ body?: string;
33
+ created_at?: string;
34
+ };
35
+ export interface CheckpointCodecOptions {
36
+ marker?: string;
37
+ sessionIdPattern?: RegExp;
38
+ }
39
+ export interface TrustedCheckpointActor {
40
+ actor: string;
41
+ userType?: string;
42
+ appSlug?: string;
43
+ }
44
+ /**
45
+ * Anti-forgery predicate: a checkpoint HTML comment is plain text any PR commenter can paste,
46
+ * so `parseCheckpoint` alone proves shape, not provenance. This proves the comment itself was
47
+ * authored by the expected App identity.
48
+ */
49
+ export declare function isTrustedCheckpointComment(comment: GitHubComment, context: TrustedCheckpointActor): boolean;
50
+ export declare function renderCheckpoint(checkpoint: Checkpoint, options?: {
51
+ marker?: string;
52
+ }): string;
53
+ export declare function parseCheckpoint(body: string, options?: CheckpointCodecOptions): Checkpoint | undefined;
54
+ export declare function validateCheckpoint(value: unknown, options?: CheckpointCodecOptions): Checkpoint | undefined;
55
+ export declare function sortedCheckpointCandidates(comments: GitHubComment[], options?: CheckpointCodecOptions): {
56
+ comment: GitHubComment;
57
+ checkpoint: Checkpoint;
58
+ }[];
@@ -0,0 +1,100 @@
1
+ export const CHECKPOINT_MARKER = 'pr-checkpoint:v1';
2
+ /**
3
+ * Anti-forgery predicate: a checkpoint HTML comment is plain text any PR commenter can paste,
4
+ * so `parseCheckpoint` alone proves shape, not provenance. This proves the comment itself was
5
+ * authored by the expected App identity.
6
+ */
7
+ export function isTrustedCheckpointComment(comment, context) {
8
+ return (comment.user?.login === context.actor &&
9
+ comment.user?.type === (context.userType ?? 'Bot') &&
10
+ comment.performed_via_github_app?.slug === (context.appSlug ?? 'github-actions'));
11
+ }
12
+ export function renderCheckpoint(checkpoint, options = {}) {
13
+ const marker = options.marker ?? checkpoint.marker;
14
+ const payload = Buffer.from(JSON.stringify({ ...checkpoint, marker }), 'utf8').toString('base64url');
15
+ const session = checkpoint.sessionUrl
16
+ ? `[${checkpoint.sessionId}](${checkpoint.sessionUrl})`
17
+ : `\`${checkpoint.sessionId || 'pending'}\``;
18
+ return [
19
+ `๐Ÿ‘€ [View the automation workflow run](${checkpoint.runUrl}).`,
20
+ '',
21
+ `Status: \`${checkpoint.status}\` ยท Session: ${session}`,
22
+ '',
23
+ `<!-- ${marker} ${payload} -->`,
24
+ ].join('\n');
25
+ }
26
+ export function parseCheckpoint(body, options = {}) {
27
+ const marker = options.marker ?? CHECKPOINT_MARKER;
28
+ const match = body.match(new RegExp(`<!-- ${escapeRegExp(marker)} ([A-Za-z0-9_-]+) -->`, 'u'));
29
+ if (!match?.[1])
30
+ return undefined;
31
+ try {
32
+ return validateCheckpoint(JSON.parse(Buffer.from(match[1], 'base64url').toString('utf8')), options);
33
+ }
34
+ catch {
35
+ return undefined;
36
+ }
37
+ }
38
+ export function validateCheckpoint(value, options = {}) {
39
+ if (!value || typeof value !== 'object')
40
+ return undefined;
41
+ const checkpoint = value;
42
+ const marker = options.marker ?? CHECKPOINT_MARKER;
43
+ if (checkpoint.marker !== marker ||
44
+ !Number.isInteger(checkpoint.pr) ||
45
+ (checkpoint.triggerCommentId !== undefined && !Number.isInteger(checkpoint.triggerCommentId)) ||
46
+ typeof checkpoint.startSha !== 'string' ||
47
+ !/^[0-9a-f]{40}$/u.test(checkpoint.startSha) ||
48
+ typeof checkpoint.sessionStartSha !== 'string' ||
49
+ !/^[0-9a-f]{40}$/u.test(checkpoint.sessionStartSha) ||
50
+ typeof checkpoint.runId !== 'string' ||
51
+ !/^[0-9]+$/u.test(checkpoint.runId) ||
52
+ typeof checkpoint.resumeSourceRunId !== 'string' ||
53
+ (checkpoint.resumeSourceRunId !== '' && !/^[0-9]+$/u.test(checkpoint.resumeSourceRunId)) ||
54
+ typeof checkpoint.repository !== 'string' ||
55
+ typeof checkpoint.headRef !== 'string' ||
56
+ typeof checkpoint.runUrl !== 'string' ||
57
+ typeof checkpoint.actor !== 'string' ||
58
+ typeof checkpoint.sessionId !== 'string' ||
59
+ typeof checkpoint.createdAt !== 'string' ||
60
+ !Number.isFinite(Date.parse(checkpoint.createdAt)) ||
61
+ typeof checkpoint.updatedAt !== 'string' ||
62
+ !Number.isFinite(Date.parse(checkpoint.updatedAt)) ||
63
+ typeof checkpoint.status !== 'string' ||
64
+ ![
65
+ 'queued',
66
+ 'running',
67
+ 'awaiting_verification',
68
+ 'failed',
69
+ 'deadline',
70
+ 'unresumable',
71
+ 'complete',
72
+ ].includes(checkpoint.status)) {
73
+ return undefined;
74
+ }
75
+ if (checkpoint.sessionId &&
76
+ options.sessionIdPattern &&
77
+ !matchesPattern(options.sessionIdPattern, checkpoint.sessionId)) {
78
+ return undefined;
79
+ }
80
+ if (checkpoint.sessionUrl !== undefined &&
81
+ (!URL.canParse(checkpoint.sessionUrl) || !checkpoint.sessionUrl.startsWith('https:'))) {
82
+ return undefined;
83
+ }
84
+ return checkpoint;
85
+ }
86
+ export function sortedCheckpointCandidates(comments, options = {}) {
87
+ return comments
88
+ .map((comment) => ({ comment, checkpoint: parseCheckpoint(comment.body ?? '', options) }))
89
+ .filter((candidate) => candidate.checkpoint !== undefined)
90
+ .toSorted((left, right) => {
91
+ const timestampOrder = String(right.comment.created_at ?? '').localeCompare(String(left.comment.created_at ?? ''));
92
+ return timestampOrder || right.comment.id - left.comment.id;
93
+ });
94
+ }
95
+ function matchesPattern(pattern, value) {
96
+ return new RegExp(pattern.source, pattern.flags.replaceAll(/[gy]/g, '')).test(value);
97
+ }
98
+ function escapeRegExp(value) {
99
+ return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
100
+ }
@@ -1,58 +1,3 @@
1
- export declare const CHECKPOINT_MARKER = "pr-checkpoint:v1";
2
- export type CheckpointStatus = 'queued' | 'running' | 'awaiting_verification' | 'failed' | 'deadline' | 'unresumable' | 'complete';
3
- export type Checkpoint = {
4
- marker: string;
5
- repository: string;
6
- pr: number;
7
- /** Absent on checkpoints posted before trigger ids were recorded; keep optional so resume can still parse them. */
8
- triggerCommentId?: number;
9
- headRef: string;
10
- startSha: string;
11
- sessionStartSha: string;
12
- runId: string;
13
- runUrl: string;
14
- actor: string;
15
- sessionId: string;
16
- /** Absent from v1 checkpoints created before session URLs were exposed. */
17
- sessionUrl?: string;
18
- resumeSourceRunId: string;
19
- status: CheckpointStatus;
20
- createdAt: string;
21
- updatedAt: string;
22
- };
23
- export type GitHubComment = {
24
- id: number;
25
- user?: {
26
- login?: string;
27
- type?: string;
28
- };
29
- performed_via_github_app?: {
30
- slug?: string;
31
- } | null;
32
- body?: string;
33
- created_at?: string;
34
- };
35
- export interface CheckpointCodecOptions {
36
- marker?: string;
37
- sessionIdPattern?: RegExp;
38
- }
39
- export interface TrustedCheckpointActor {
40
- actor: string;
41
- userType?: string;
42
- appSlug?: string;
43
- }
44
- /**
45
- * Anti-forgery predicate: a checkpoint HTML comment is plain text any PR commenter can paste,
46
- * so `parseCheckpoint` alone proves shape, not provenance. This proves the comment itself was
47
- * authored by the expected App identity.
48
- */
49
- export declare function isTrustedCheckpointComment(comment: GitHubComment, context: TrustedCheckpointActor): boolean;
50
- export declare function renderCheckpoint(checkpoint: Checkpoint, options?: {
51
- marker?: string;
52
- }): string;
53
- export declare function parseCheckpoint(body: string, options?: CheckpointCodecOptions): Checkpoint | undefined;
54
- export declare function validateCheckpoint(value: unknown, options?: CheckpointCodecOptions): Checkpoint | undefined;
55
- export declare function sortedCheckpointCandidates(comments: GitHubComment[], options?: CheckpointCodecOptions): {
56
- comment: GitHubComment;
57
- checkpoint: Checkpoint;
58
- }[];
1
+ export * from './codec.mts';
2
+ export { selectResumeCheckpoint, type CheckpointSelectionContext } from './resume.mts';
3
+ export { updateExactCheckpoint, type CheckpointUpdateContext } from './update.mts';
@@ -1,100 +1,3 @@
1
- export const CHECKPOINT_MARKER = 'pr-checkpoint:v1';
2
- /**
3
- * Anti-forgery predicate: a checkpoint HTML comment is plain text any PR commenter can paste,
4
- * so `parseCheckpoint` alone proves shape, not provenance. This proves the comment itself was
5
- * authored by the expected App identity.
6
- */
7
- export function isTrustedCheckpointComment(comment, context) {
8
- return (comment.user?.login === context.actor &&
9
- comment.user?.type === (context.userType ?? 'Bot') &&
10
- comment.performed_via_github_app?.slug === (context.appSlug ?? 'github-actions'));
11
- }
12
- export function renderCheckpoint(checkpoint, options = {}) {
13
- const marker = options.marker ?? checkpoint.marker;
14
- const payload = Buffer.from(JSON.stringify({ ...checkpoint, marker }), 'utf8').toString('base64url');
15
- const session = checkpoint.sessionUrl
16
- ? `[${checkpoint.sessionId}](${checkpoint.sessionUrl})`
17
- : `\`${checkpoint.sessionId || 'pending'}\``;
18
- return [
19
- `๐Ÿ‘€ [View the automation workflow run](${checkpoint.runUrl}).`,
20
- '',
21
- `Status: \`${checkpoint.status}\` ยท Session: ${session}`,
22
- '',
23
- `<!-- ${marker} ${payload} -->`,
24
- ].join('\n');
25
- }
26
- export function parseCheckpoint(body, options = {}) {
27
- const marker = options.marker ?? CHECKPOINT_MARKER;
28
- const match = body.match(new RegExp(`<!-- ${escapeRegExp(marker)} ([A-Za-z0-9_-]+) -->`, 'u'));
29
- if (!match?.[1])
30
- return undefined;
31
- try {
32
- return validateCheckpoint(JSON.parse(Buffer.from(match[1], 'base64url').toString('utf8')), options);
33
- }
34
- catch {
35
- return undefined;
36
- }
37
- }
38
- export function validateCheckpoint(value, options = {}) {
39
- if (!value || typeof value !== 'object')
40
- return undefined;
41
- const checkpoint = value;
42
- const marker = options.marker ?? CHECKPOINT_MARKER;
43
- if (checkpoint.marker !== marker ||
44
- !Number.isInteger(checkpoint.pr) ||
45
- (checkpoint.triggerCommentId !== undefined && !Number.isInteger(checkpoint.triggerCommentId)) ||
46
- typeof checkpoint.startSha !== 'string' ||
47
- !/^[0-9a-f]{40}$/u.test(checkpoint.startSha) ||
48
- typeof checkpoint.sessionStartSha !== 'string' ||
49
- !/^[0-9a-f]{40}$/u.test(checkpoint.sessionStartSha) ||
50
- typeof checkpoint.runId !== 'string' ||
51
- !/^[0-9]+$/u.test(checkpoint.runId) ||
52
- typeof checkpoint.resumeSourceRunId !== 'string' ||
53
- (checkpoint.resumeSourceRunId !== '' && !/^[0-9]+$/u.test(checkpoint.resumeSourceRunId)) ||
54
- typeof checkpoint.repository !== 'string' ||
55
- typeof checkpoint.headRef !== 'string' ||
56
- typeof checkpoint.runUrl !== 'string' ||
57
- typeof checkpoint.actor !== 'string' ||
58
- typeof checkpoint.sessionId !== 'string' ||
59
- typeof checkpoint.createdAt !== 'string' ||
60
- !Number.isFinite(Date.parse(checkpoint.createdAt)) ||
61
- typeof checkpoint.updatedAt !== 'string' ||
62
- !Number.isFinite(Date.parse(checkpoint.updatedAt)) ||
63
- typeof checkpoint.status !== 'string' ||
64
- ![
65
- 'queued',
66
- 'running',
67
- 'awaiting_verification',
68
- 'failed',
69
- 'deadline',
70
- 'unresumable',
71
- 'complete',
72
- ].includes(checkpoint.status)) {
73
- return undefined;
74
- }
75
- if (checkpoint.sessionId &&
76
- options.sessionIdPattern &&
77
- !matchesPattern(options.sessionIdPattern, checkpoint.sessionId)) {
78
- return undefined;
79
- }
80
- if (checkpoint.sessionUrl !== undefined &&
81
- (!URL.canParse(checkpoint.sessionUrl) || !checkpoint.sessionUrl.startsWith('https:'))) {
82
- return undefined;
83
- }
84
- return checkpoint;
85
- }
86
- export function sortedCheckpointCandidates(comments, options = {}) {
87
- return comments
88
- .map((comment) => ({ comment, checkpoint: parseCheckpoint(comment.body ?? '', options) }))
89
- .filter((candidate) => candidate.checkpoint !== undefined)
90
- .toSorted((left, right) => {
91
- const timestampOrder = String(right.comment.created_at ?? '').localeCompare(String(left.comment.created_at ?? ''));
92
- return timestampOrder || right.comment.id - left.comment.id;
93
- });
94
- }
95
- function matchesPattern(pattern, value) {
96
- return new RegExp(pattern.source, pattern.flags.replaceAll(/[gy]/g, '')).test(value);
97
- }
98
- function escapeRegExp(value) {
99
- return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
100
- }
1
+ export * from './codec.mjs';
2
+ export { selectResumeCheckpoint } from './resume.mjs';
3
+ export { updateExactCheckpoint } from './update.mjs';
@@ -0,0 +1,14 @@
1
+ import { type Checkpoint, type GitHubComment } from './codec.mts';
2
+ export type CheckpointSelectionContext = {
3
+ repository: string;
4
+ pr: number;
5
+ headRef: string;
6
+ headSha: string;
7
+ actor: string;
8
+ isAncestor: (candidate: string, head: string) => boolean;
9
+ isShepherdRun: (runId: string) => boolean;
10
+ };
11
+ export declare function selectResumeCheckpoint(comments: GitHubComment[], context: CheckpointSelectionContext): {
12
+ checkpoint: Checkpoint;
13
+ commentId: number;
14
+ } | undefined;
@@ -0,0 +1,21 @@
1
+ import { isTrustedCheckpointComment, sortedCheckpointCandidates, } from './codec.mjs';
2
+ export function selectResumeCheckpoint(comments, context) {
3
+ const candidates = sortedCheckpointCandidates(comments);
4
+ for (const { comment, checkpoint } of candidates) {
5
+ if (!isTrustedCheckpointComment(comment, context) ||
6
+ checkpoint.actor !== context.actor ||
7
+ checkpoint.repository !== context.repository ||
8
+ checkpoint.pr !== context.pr ||
9
+ checkpoint.headRef !== context.headRef ||
10
+ !checkpoint.sessionId ||
11
+ !context.isShepherdRun(checkpoint.runId) ||
12
+ !context.isAncestor(checkpoint.startSha, context.headSha) ||
13
+ !context.isAncestor(checkpoint.sessionStartSha, context.headSha)) {
14
+ continue;
15
+ }
16
+ if (checkpoint.status === 'complete' || checkpoint.status === 'unresumable')
17
+ return undefined;
18
+ return { checkpoint, commentId: comment.id };
19
+ }
20
+ return undefined;
21
+ }
@@ -0,0 +1,15 @@
1
+ import { type CheckpointStatus, type GitHubComment } from './codec.mts';
2
+ export type CheckpointUpdateContext = {
3
+ actor: string;
4
+ commentId: number;
5
+ headRef: string;
6
+ headSha: string;
7
+ pr: number;
8
+ repository: string;
9
+ runId: string;
10
+ triggerCommentId: number;
11
+ };
12
+ export declare function updateExactCheckpoint(comment: GitHubComment, context: CheckpointUpdateContext, status: Extract<CheckpointStatus, 'failed' | 'running'>, session: {
13
+ id?: string;
14
+ url?: string;
15
+ }): string;
@@ -0,0 +1,30 @@
1
+ import { isTrustedCheckpointComment, parseCheckpoint, renderCheckpoint, } from './codec.mjs';
2
+ export function updateExactCheckpoint(comment, context, status, session) {
3
+ const checkpoint = parseCheckpoint(comment.body ?? '');
4
+ if (!checkpoint ||
5
+ comment.id !== context.commentId ||
6
+ !isTrustedCheckpointComment(comment, context) ||
7
+ checkpoint.actor !== context.actor ||
8
+ checkpoint.repository !== context.repository ||
9
+ checkpoint.pr !== context.pr ||
10
+ checkpoint.triggerCommentId !== context.triggerCommentId ||
11
+ checkpoint.runId !== context.runId ||
12
+ checkpoint.headRef !== context.headRef ||
13
+ checkpoint.startSha !== context.headSha) {
14
+ throw new Error('Checkpoint comment does not match the active run binding');
15
+ }
16
+ if (status === 'running' && (!session.id || !session.url)) {
17
+ throw new Error('Running checkpoint requires a session id and URL');
18
+ }
19
+ if (session.url && (!URL.canParse(session.url) || !session.url.startsWith('https:'))) {
20
+ throw new Error('Session URL must be a parseable https URL');
21
+ }
22
+ const next = {
23
+ ...checkpoint,
24
+ ...(session.id ? { sessionId: session.id } : {}),
25
+ ...(session.url ? { sessionUrl: session.url } : {}),
26
+ status,
27
+ updatedAt: new Date().toISOString(),
28
+ };
29
+ return renderCheckpoint(next);
30
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vouchington-tooling",
3
- "version": "0.5.1",
3
+ "version": "0.6.0",
4
4
  "description": "Vouchington CLI and extractable tooling libraries.",
5
5
  "homepage": "https://github.com/vouchington/vouchington-tooling/tree/main/packages/vouchington-tooling#readme",
6
6
  "bugs": {