create-interview-cockpit 0.27.0 → 0.28.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/package.json +1 -1
- package/template/client/src/codeowners.ts +792 -0
- package/template/client/src/components/CodeContextPanel.tsx +44 -0
- package/template/client/src/components/DiagramsModal.tsx +839 -0
- package/template/client/src/components/GithubActionsLabModal.tsx +291 -264
- package/template/client/src/components/LabsPanel.tsx +3 -3
- package/template/client/src/components/PullRequestPanel.tsx +1142 -0
- package/template/client/src/components/SettingsPanel.tsx +1395 -0
- package/template/client/src/githubActionsLab.ts +461 -3
- package/template/client/src/types.ts +219 -0
- package/template/client/tsconfig.tsbuildinfo +1 -1
- package/template/cockpit.json +1 -1
- package/template/server/src/gha-runner.ts +1 -1
|
@@ -1,5 +1,80 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
|
|
1
|
+
import type {
|
|
2
|
+
GithubActionsLabWorkspace,
|
|
3
|
+
GithubActionsLabEnvironment,
|
|
4
|
+
GithubLabOrg,
|
|
5
|
+
GithubLabBranchProtection,
|
|
6
|
+
GithubLabPullRequest,
|
|
7
|
+
GithubLabReview,
|
|
8
|
+
GithubLabCheckRun,
|
|
9
|
+
GithubLabCheckRunJob,
|
|
10
|
+
GithubLabRuleset,
|
|
11
|
+
GithubLabRulesetEnforcement,
|
|
12
|
+
GithubLabRulesetRules,
|
|
13
|
+
} from "./types";
|
|
14
|
+
import { rulesetFromLegacyProtection } from "./codeowners";
|
|
15
|
+
|
|
16
|
+
// ─── Default GitHub Lab "org" roster ────────────────────────────────────
|
|
17
|
+
//
|
|
18
|
+
// The lab simulates an org called `acme` so CODEOWNERS handles like
|
|
19
|
+
// `@acme/frontend` resolve to real people for the PR sim. Users can edit
|
|
20
|
+
// this in the Pull Request tab.
|
|
21
|
+
export const DEFAULT_GH_LAB_ORG: GithubLabOrg = {
|
|
22
|
+
slug: "acme",
|
|
23
|
+
viewerLogin: "octocat",
|
|
24
|
+
users: ["octocat", "alice", "bob", "carol"],
|
|
25
|
+
teams: [
|
|
26
|
+
{
|
|
27
|
+
slug: "frontend",
|
|
28
|
+
name: "Frontend",
|
|
29
|
+
members: ["alice"],
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
slug: "platform",
|
|
33
|
+
name: "Platform / Infra",
|
|
34
|
+
members: ["bob"],
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
slug: "docs",
|
|
38
|
+
name: "Docs",
|
|
39
|
+
members: ["carol"],
|
|
40
|
+
},
|
|
41
|
+
],
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export const DEFAULT_GH_LAB_BRANCH_PROTECTION: GithubLabBranchProtection = {
|
|
45
|
+
requireCodeOwnerReview: true,
|
|
46
|
+
requiredApprovingReviews: 1,
|
|
47
|
+
requiredStatusChecks: [],
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
// Default ruleset — mirrors the most common GitHub repo template:
|
|
51
|
+
// "protect the default branch with a PR + review + green CI before merge".
|
|
52
|
+
export const DEFAULT_GH_LAB_RULESETS: GithubLabRuleset[] = [
|
|
53
|
+
{
|
|
54
|
+
id: "default-branch-protection",
|
|
55
|
+
name: "Default branch protection",
|
|
56
|
+
enforcement: "active",
|
|
57
|
+
targetInclude: ["~DEFAULT_BRANCH"],
|
|
58
|
+
targetExclude: [],
|
|
59
|
+
bypass: [],
|
|
60
|
+
rules: {
|
|
61
|
+
pullRequest: {
|
|
62
|
+
requiredApprovingReviewCount: 1,
|
|
63
|
+
requireCodeOwnerReview: true,
|
|
64
|
+
dismissStaleReviewsOnPush: false,
|
|
65
|
+
requireLastPushApproval: false,
|
|
66
|
+
},
|
|
67
|
+
blockForcePushes: true,
|
|
68
|
+
restrictDeletions: true,
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
];
|
|
72
|
+
|
|
73
|
+
export const DEFAULT_GH_LAB_PULL_REQUEST: GithubLabPullRequest = {
|
|
74
|
+
changedFiles: [],
|
|
75
|
+
reviews: [],
|
|
76
|
+
title: "Draft pull request",
|
|
77
|
+
};
|
|
3
78
|
|
|
4
79
|
// ─── Default Lab Template ────────────────────────────────────────────────
|
|
5
80
|
//
|
|
@@ -162,6 +237,38 @@ require("fs").readdirSync(".").forEach((f) => console.log(" -", f));
|
|
|
162
237
|
# Pinning the runner image keeps installs reproducible across machines.
|
|
163
238
|
-P ubuntu-latest=catthehacker/ubuntu:act-latest
|
|
164
239
|
--container-architecture linux/amd64
|
|
240
|
+
`,
|
|
241
|
+
|
|
242
|
+
// The CODEOWNERS file lives in .github/ (one of the three locations
|
|
243
|
+
// GitHub looks at). Open the "Pull Request" tab to see who gets
|
|
244
|
+
// auto-requested when these patterns match files in your PR.
|
|
245
|
+
".github/CODEOWNERS": `# CODEOWNERS — auto-request reviewers when files match a pattern.
|
|
246
|
+
#
|
|
247
|
+
# Syntax: <pattern> <@user> | <@org/team> | <email>
|
|
248
|
+
# Rules are evaluated top-to-bottom, and the LAST matching rule wins.
|
|
249
|
+
# A line with no owners explicitly clears ownership for that path.
|
|
250
|
+
#
|
|
251
|
+
# This lab ships with a fake "acme" org you can edit in the Pull Request
|
|
252
|
+
# tab. The default users are:
|
|
253
|
+
# - octocat (you, the PR author — cannot self-approve)
|
|
254
|
+
# - alice (frontend lead)
|
|
255
|
+
# - bob (platform/infra)
|
|
256
|
+
# - carol (docs writer)
|
|
257
|
+
# Teams: @acme/frontend, @acme/platform, @acme/docs
|
|
258
|
+
|
|
259
|
+
# Default owner for everything not matched by a more specific rule.
|
|
260
|
+
* @octocat
|
|
261
|
+
|
|
262
|
+
# Frontend code is owned by the frontend team.
|
|
263
|
+
/scripts/ @acme/frontend
|
|
264
|
+
*.js @acme/frontend
|
|
265
|
+
|
|
266
|
+
# CI / actions config is owned by the platform team.
|
|
267
|
+
/.github/ @acme/platform
|
|
268
|
+
/.github/workflows/ @acme/platform @bob
|
|
269
|
+
|
|
270
|
+
# Docs are reviewed by the docs writer.
|
|
271
|
+
*.md @acme/docs @carol
|
|
165
272
|
`,
|
|
166
273
|
};
|
|
167
274
|
|
|
@@ -384,11 +491,14 @@ li {
|
|
|
384
491
|
|
|
385
492
|
export const DEFAULT_GHA_LAB: GithubActionsLabWorkspace = {
|
|
386
493
|
version: 1,
|
|
387
|
-
label: "GitHub
|
|
494
|
+
label: "GitHub Lab Playground",
|
|
388
495
|
activeFile: ".github/workflows/ci.yml",
|
|
389
496
|
defaultEvent: "push",
|
|
390
497
|
defaultWorkflow: ".github/workflows/ci.yml",
|
|
391
498
|
files: DEFAULT_FILES,
|
|
499
|
+
ghOrg: DEFAULT_GH_LAB_ORG,
|
|
500
|
+
rulesets: DEFAULT_GH_LAB_RULESETS,
|
|
501
|
+
pullRequest: DEFAULT_GH_LAB_PULL_REQUEST,
|
|
392
502
|
};
|
|
393
503
|
|
|
394
504
|
export const REACT_VITE_TYPESCRIPT_GHA_LAB: GithubActionsLabWorkspace = {
|
|
@@ -452,6 +562,28 @@ export function cloneGhaLabWorkspace(
|
|
|
452
562
|
? source.activeFile
|
|
453
563
|
: (Object.keys(sourceFiles)[0] ?? ".github/workflows/ci.yml");
|
|
454
564
|
const environment = cloneGhaLabEnvironment(source.environment);
|
|
565
|
+
const ghOrg = cloneGhLabOrg(source.ghOrg);
|
|
566
|
+
const pullRequest = cloneGhLabPullRequest(source.pullRequest, sourceFiles);
|
|
567
|
+
// Branches default to ["main"]; we always keep the default branch in the
|
|
568
|
+
// list, and dedupe/sanitize any user-added names so a saved lab can't
|
|
569
|
+
// smuggle in empty strings or non-string junk.
|
|
570
|
+
const rawBranches = Array.isArray(source.branches)
|
|
571
|
+
? source.branches.filter(
|
|
572
|
+
(b): b is string => typeof b === "string" && !!b.trim(),
|
|
573
|
+
)
|
|
574
|
+
: [];
|
|
575
|
+
const defaultBranch =
|
|
576
|
+
typeof source.defaultBranch === "string" && source.defaultBranch.trim()
|
|
577
|
+
? source.defaultBranch.trim()
|
|
578
|
+
: "main";
|
|
579
|
+
const branches = Array.from(new Set([defaultBranch, ...rawBranches]));
|
|
580
|
+
// Migrate legacy branchProtection → a synthetic ruleset so saved labs
|
|
581
|
+
// from before the rulesets era keep their merge gating.
|
|
582
|
+
const rulesets = cloneGhLabRulesets(
|
|
583
|
+
source.rulesets,
|
|
584
|
+
source.branchProtection,
|
|
585
|
+
defaultBranch,
|
|
586
|
+
);
|
|
455
587
|
|
|
456
588
|
return {
|
|
457
589
|
version: 1,
|
|
@@ -471,6 +603,313 @@ export function cloneGhaLabWorkspace(
|
|
|
471
603
|
? { includeRunHistoryInContext: true }
|
|
472
604
|
: {}),
|
|
473
605
|
...(environment ? { environment } : {}),
|
|
606
|
+
...(ghOrg ? { ghOrg } : {}),
|
|
607
|
+
...(pullRequest ? { pullRequest } : {}),
|
|
608
|
+
branches,
|
|
609
|
+
defaultBranch,
|
|
610
|
+
rulesets,
|
|
611
|
+
};
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
// ─── GitHub Lab roster / PR helpers ────────────────────────────────────
|
|
615
|
+
|
|
616
|
+
function cloneGhLabOrg(org?: GithubLabOrg): GithubLabOrg | undefined {
|
|
617
|
+
if (!org || typeof org !== "object") return undefined;
|
|
618
|
+
const users = Array.isArray(org.users)
|
|
619
|
+
? Array.from(
|
|
620
|
+
new Set(
|
|
621
|
+
org.users.filter(
|
|
622
|
+
(u): u is string => typeof u === "string" && !!u.trim(),
|
|
623
|
+
),
|
|
624
|
+
),
|
|
625
|
+
)
|
|
626
|
+
: [];
|
|
627
|
+
const teams = Array.isArray(org.teams)
|
|
628
|
+
? org.teams
|
|
629
|
+
.filter((t) => t && typeof t.slug === "string" && t.slug.trim())
|
|
630
|
+
.map((t) => ({
|
|
631
|
+
slug: t.slug,
|
|
632
|
+
...(typeof t.name === "string" && t.name ? { name: t.name } : {}),
|
|
633
|
+
members: Array.isArray(t.members)
|
|
634
|
+
? Array.from(
|
|
635
|
+
new Set(
|
|
636
|
+
t.members.filter(
|
|
637
|
+
(m): m is string => typeof m === "string" && !!m.trim(),
|
|
638
|
+
),
|
|
639
|
+
),
|
|
640
|
+
)
|
|
641
|
+
: [],
|
|
642
|
+
}))
|
|
643
|
+
: [];
|
|
644
|
+
return {
|
|
645
|
+
slug: typeof org.slug === "string" && org.slug.trim() ? org.slug : "acme",
|
|
646
|
+
viewerLogin:
|
|
647
|
+
typeof org.viewerLogin === "string" && org.viewerLogin.trim()
|
|
648
|
+
? org.viewerLogin
|
|
649
|
+
: users[0] || "octocat",
|
|
650
|
+
users,
|
|
651
|
+
teams,
|
|
652
|
+
};
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
function cloneGhLabBranchProtection(
|
|
656
|
+
bp?: GithubLabBranchProtection,
|
|
657
|
+
): GithubLabBranchProtection | undefined {
|
|
658
|
+
if (!bp || typeof bp !== "object") return undefined;
|
|
659
|
+
const requiredStatusChecks = Array.isArray(bp.requiredStatusChecks)
|
|
660
|
+
? Array.from(
|
|
661
|
+
new Set(
|
|
662
|
+
bp.requiredStatusChecks.filter(
|
|
663
|
+
(n): n is string => typeof n === "string" && !!n.trim(),
|
|
664
|
+
),
|
|
665
|
+
),
|
|
666
|
+
)
|
|
667
|
+
: [];
|
|
668
|
+
return {
|
|
669
|
+
requireCodeOwnerReview: !!bp.requireCodeOwnerReview,
|
|
670
|
+
requiredApprovingReviews: Math.max(
|
|
671
|
+
0,
|
|
672
|
+
Math.floor(Number(bp.requiredApprovingReviews) || 0),
|
|
673
|
+
),
|
|
674
|
+
...(requiredStatusChecks.length ? { requiredStatusChecks } : {}),
|
|
675
|
+
};
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
// ─── Rulesets clone + migration ────────────────────────────────────────
|
|
679
|
+
//
|
|
680
|
+
// Saved labs may carry: (a) the modern `rulesets` array, (b) the legacy
|
|
681
|
+
// `branchProtection` object, or (c) neither. We preserve user-authored
|
|
682
|
+
// rulesets when present, otherwise synthesize one from branchProtection
|
|
683
|
+
// (so labs from before the rulesets era keep their merge gating), and
|
|
684
|
+
// fall back to the default ruleset otherwise.
|
|
685
|
+
|
|
686
|
+
const RULESET_ENFORCEMENTS: ReadonlyArray<GithubLabRulesetEnforcement> = [
|
|
687
|
+
"active",
|
|
688
|
+
"evaluate",
|
|
689
|
+
"disabled",
|
|
690
|
+
];
|
|
691
|
+
|
|
692
|
+
function cloneStringArray(value: unknown): string[] {
|
|
693
|
+
if (!Array.isArray(value)) return [];
|
|
694
|
+
return Array.from(
|
|
695
|
+
new Set(
|
|
696
|
+
value.filter((v): v is string => typeof v === "string" && !!v.trim()),
|
|
697
|
+
),
|
|
698
|
+
);
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
function cloneGhLabRulesetRules(rules: unknown): GithubLabRulesetRules {
|
|
702
|
+
if (!rules || typeof rules !== "object") return {};
|
|
703
|
+
const r = rules as Partial<GithubLabRulesetRules>;
|
|
704
|
+
const out: GithubLabRulesetRules = {};
|
|
705
|
+
if (r.restrictCreations) out.restrictCreations = true;
|
|
706
|
+
if (r.restrictUpdates) out.restrictUpdates = true;
|
|
707
|
+
if (r.restrictDeletions) out.restrictDeletions = true;
|
|
708
|
+
if (r.requireLinearHistory) out.requireLinearHistory = true;
|
|
709
|
+
if (r.requireSignedCommits) out.requireSignedCommits = true;
|
|
710
|
+
if (r.blockForcePushes) out.blockForcePushes = true;
|
|
711
|
+
if (r.requireCodeQuality) out.requireCodeQuality = true;
|
|
712
|
+
if (r.pullRequest && typeof r.pullRequest === "object") {
|
|
713
|
+
out.pullRequest = {
|
|
714
|
+
requiredApprovingReviewCount: Math.max(
|
|
715
|
+
0,
|
|
716
|
+
Math.floor(Number(r.pullRequest.requiredApprovingReviewCount) || 0),
|
|
717
|
+
),
|
|
718
|
+
requireCodeOwnerReview: !!r.pullRequest.requireCodeOwnerReview,
|
|
719
|
+
dismissStaleReviewsOnPush: !!r.pullRequest.dismissStaleReviewsOnPush,
|
|
720
|
+
requireLastPushApproval: !!r.pullRequest.requireLastPushApproval,
|
|
721
|
+
};
|
|
722
|
+
}
|
|
723
|
+
if (r.statusChecks && typeof r.statusChecks === "object") {
|
|
724
|
+
const checks = cloneStringArray(r.statusChecks.checks);
|
|
725
|
+
if (checks.length || r.statusChecks.strict) {
|
|
726
|
+
out.statusChecks = { checks, strict: !!r.statusChecks.strict };
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
if (r.requireDeployments && typeof r.requireDeployments === "object") {
|
|
730
|
+
const environments = cloneStringArray(r.requireDeployments.environments);
|
|
731
|
+
if (environments.length) {
|
|
732
|
+
out.requireDeployments = { environments };
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
if (r.requireCodeScanning && typeof r.requireCodeScanning === "object") {
|
|
736
|
+
out.requireCodeScanning = {
|
|
737
|
+
tools: cloneStringArray(r.requireCodeScanning.tools),
|
|
738
|
+
};
|
|
739
|
+
}
|
|
740
|
+
return out;
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
function cloneGhLabRuleset(input: unknown): GithubLabRuleset | undefined {
|
|
744
|
+
if (!input || typeof input !== "object") return undefined;
|
|
745
|
+
const obj = input as Partial<GithubLabRuleset>;
|
|
746
|
+
const targetInclude = cloneStringArray(obj.targetInclude);
|
|
747
|
+
if (targetInclude.length === 0) return undefined;
|
|
748
|
+
const enforcement = RULESET_ENFORCEMENTS.includes(
|
|
749
|
+
obj.enforcement as GithubLabRulesetEnforcement,
|
|
750
|
+
)
|
|
751
|
+
? (obj.enforcement as GithubLabRulesetEnforcement)
|
|
752
|
+
: "active";
|
|
753
|
+
return {
|
|
754
|
+
id:
|
|
755
|
+
typeof obj.id === "string" && obj.id
|
|
756
|
+
? obj.id
|
|
757
|
+
: `rs_${Math.random().toString(36).slice(2, 10)}`,
|
|
758
|
+
name:
|
|
759
|
+
typeof obj.name === "string" && obj.name.trim()
|
|
760
|
+
? obj.name.trim()
|
|
761
|
+
: "Untitled ruleset",
|
|
762
|
+
enforcement,
|
|
763
|
+
targetInclude,
|
|
764
|
+
targetExclude: cloneStringArray(obj.targetExclude),
|
|
765
|
+
bypass: cloneStringArray(obj.bypass),
|
|
766
|
+
rules: cloneGhLabRulesetRules(obj.rules),
|
|
767
|
+
};
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
function cloneGhLabRulesets(
|
|
771
|
+
rulesets: unknown,
|
|
772
|
+
legacyProtection: GithubLabBranchProtection | undefined,
|
|
773
|
+
defaultBranch: string,
|
|
774
|
+
): GithubLabRuleset[] {
|
|
775
|
+
if (Array.isArray(rulesets)) {
|
|
776
|
+
const cleaned = rulesets
|
|
777
|
+
.map(cloneGhLabRuleset)
|
|
778
|
+
.filter((r): r is GithubLabRuleset => !!r);
|
|
779
|
+
if (cleaned.length > 0) return cleaned;
|
|
780
|
+
}
|
|
781
|
+
// No rulesets persisted — try migrating classic branchProtection.
|
|
782
|
+
const migrated = rulesetFromLegacyProtection(legacyProtection, defaultBranch);
|
|
783
|
+
if (migrated) return [migrated];
|
|
784
|
+
// Brand-new lab with no protection at all → use the template ruleset.
|
|
785
|
+
return DEFAULT_GH_LAB_RULESETS.map((r) => ({
|
|
786
|
+
...r,
|
|
787
|
+
targetInclude: [...r.targetInclude],
|
|
788
|
+
targetExclude: [...r.targetExclude],
|
|
789
|
+
bypass: [...r.bypass],
|
|
790
|
+
rules: cloneGhLabRulesetRules(r.rules),
|
|
791
|
+
}));
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
const REVIEW_STATES: ReadonlyArray<GithubLabReview["state"]> = [
|
|
795
|
+
"approved",
|
|
796
|
+
"changes_requested",
|
|
797
|
+
"commented",
|
|
798
|
+
];
|
|
799
|
+
|
|
800
|
+
function cloneGhLabReview(r: unknown): GithubLabReview | undefined {
|
|
801
|
+
if (!r || typeof r !== "object") return undefined;
|
|
802
|
+
const obj = r as Partial<GithubLabReview>;
|
|
803
|
+
if (typeof obj.author !== "string" || !obj.author.trim()) return undefined;
|
|
804
|
+
if (!REVIEW_STATES.includes(obj.state as GithubLabReview["state"])) {
|
|
805
|
+
return undefined;
|
|
806
|
+
}
|
|
807
|
+
return {
|
|
808
|
+
id:
|
|
809
|
+
typeof obj.id === "string" && obj.id
|
|
810
|
+
? obj.id
|
|
811
|
+
: `rev_${Math.random().toString(36).slice(2, 10)}`,
|
|
812
|
+
author: obj.author,
|
|
813
|
+
state: obj.state as GithubLabReview["state"],
|
|
814
|
+
createdAt:
|
|
815
|
+
typeof obj.createdAt === "string" && obj.createdAt
|
|
816
|
+
? obj.createdAt
|
|
817
|
+
: new Date().toISOString(),
|
|
818
|
+
...(typeof obj.body === "string" && obj.body.trim()
|
|
819
|
+
? { body: obj.body }
|
|
820
|
+
: {}),
|
|
821
|
+
};
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
const CHECK_STATUSES: ReadonlyArray<GithubLabCheckRunJob["status"]> = [
|
|
825
|
+
"queued",
|
|
826
|
+
"running",
|
|
827
|
+
"success",
|
|
828
|
+
"failed",
|
|
829
|
+
"cancelled",
|
|
830
|
+
"skipped",
|
|
831
|
+
];
|
|
832
|
+
|
|
833
|
+
function cloneGhLabCheckRun(
|
|
834
|
+
run: GithubLabCheckRun | undefined,
|
|
835
|
+
): GithubLabCheckRun | undefined {
|
|
836
|
+
if (!run || typeof run !== "object") return undefined;
|
|
837
|
+
const jobs = Array.isArray(run.jobs)
|
|
838
|
+
? run.jobs.flatMap((j) => {
|
|
839
|
+
if (!j || typeof j !== "object") return [];
|
|
840
|
+
const name = (j as Partial<GithubLabCheckRunJob>).name;
|
|
841
|
+
const status = (j as Partial<GithubLabCheckRunJob>).status;
|
|
842
|
+
if (typeof name !== "string" || !name.trim()) return [];
|
|
843
|
+
if (
|
|
844
|
+
!CHECK_STATUSES.includes(status as GithubLabCheckRunJob["status"])
|
|
845
|
+
) {
|
|
846
|
+
return [];
|
|
847
|
+
}
|
|
848
|
+
const durationMs = (j as Partial<GithubLabCheckRunJob>).durationMs;
|
|
849
|
+
return [
|
|
850
|
+
{
|
|
851
|
+
name,
|
|
852
|
+
status: status as GithubLabCheckRunJob["status"],
|
|
853
|
+
...(typeof durationMs === "number" && Number.isFinite(durationMs)
|
|
854
|
+
? { durationMs }
|
|
855
|
+
: {}),
|
|
856
|
+
} satisfies GithubLabCheckRunJob,
|
|
857
|
+
];
|
|
858
|
+
})
|
|
859
|
+
: [];
|
|
860
|
+
if (jobs.length === 0) return undefined;
|
|
861
|
+
return {
|
|
862
|
+
completedAt:
|
|
863
|
+
typeof run.completedAt === "string" && run.completedAt
|
|
864
|
+
? run.completedAt
|
|
865
|
+
: new Date().toISOString(),
|
|
866
|
+
...(typeof run.workflow === "string" && run.workflow
|
|
867
|
+
? { workflow: run.workflow }
|
|
868
|
+
: {}),
|
|
869
|
+
jobs,
|
|
870
|
+
};
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
function cloneGhLabPullRequest(
|
|
874
|
+
pr: GithubLabPullRequest | undefined,
|
|
875
|
+
files: Record<string, string>,
|
|
876
|
+
): GithubLabPullRequest | undefined {
|
|
877
|
+
if (!pr || typeof pr !== "object") return undefined;
|
|
878
|
+
const changedFiles = Array.isArray(pr.changedFiles)
|
|
879
|
+
? Array.from(
|
|
880
|
+
new Set(
|
|
881
|
+
pr.changedFiles.filter(
|
|
882
|
+
(p): p is string =>
|
|
883
|
+
typeof p === "string" &&
|
|
884
|
+
Object.prototype.hasOwnProperty.call(files, p),
|
|
885
|
+
),
|
|
886
|
+
),
|
|
887
|
+
)
|
|
888
|
+
: [];
|
|
889
|
+
const reviews: GithubLabReview[] = Array.isArray(pr.reviews)
|
|
890
|
+
? pr.reviews
|
|
891
|
+
.map((r) => cloneGhLabReview(r))
|
|
892
|
+
.filter((r): r is GithubLabReview => !!r)
|
|
893
|
+
: [];
|
|
894
|
+
// Back-compat: pre-reviews labs only stored `approvals: string[]`. Promote
|
|
895
|
+
// those into synthetic "approved" reviews so they keep blocking merge.
|
|
896
|
+
if (reviews.length === 0 && Array.isArray(pr.approvals)) {
|
|
897
|
+
for (const login of pr.approvals) {
|
|
898
|
+
if (typeof login !== "string" || !login.trim()) continue;
|
|
899
|
+
reviews.push({
|
|
900
|
+
id: `legacy_${login}`,
|
|
901
|
+
author: login,
|
|
902
|
+
state: "approved",
|
|
903
|
+
createdAt: new Date(0).toISOString(),
|
|
904
|
+
});
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
const lastCheckRun = cloneGhLabCheckRun(pr.lastCheckRun);
|
|
908
|
+
return {
|
|
909
|
+
changedFiles,
|
|
910
|
+
reviews,
|
|
911
|
+
...(lastCheckRun ? { lastCheckRun } : {}),
|
|
912
|
+
...(typeof pr.title === "string" && pr.title ? { title: pr.title } : {}),
|
|
474
913
|
};
|
|
475
914
|
}
|
|
476
915
|
|
|
@@ -558,6 +997,25 @@ export function parseGhaLabWorkspace(
|
|
|
558
997
|
...(parsed.environment && typeof parsed.environment === "object"
|
|
559
998
|
? { environment: parsed.environment as GithubActionsLabEnvironment }
|
|
560
999
|
: {}),
|
|
1000
|
+
...(parsed.ghOrg && typeof parsed.ghOrg === "object"
|
|
1001
|
+
? { ghOrg: parsed.ghOrg as GithubLabOrg }
|
|
1002
|
+
: {}),
|
|
1003
|
+
...(parsed.branchProtection && typeof parsed.branchProtection === "object"
|
|
1004
|
+
? {
|
|
1005
|
+
branchProtection:
|
|
1006
|
+
parsed.branchProtection as GithubLabBranchProtection,
|
|
1007
|
+
}
|
|
1008
|
+
: {}),
|
|
1009
|
+
...(Array.isArray(parsed.rulesets)
|
|
1010
|
+
? { rulesets: parsed.rulesets as GithubLabRuleset[] }
|
|
1011
|
+
: {}),
|
|
1012
|
+
...(parsed.pullRequest && typeof parsed.pullRequest === "object"
|
|
1013
|
+
? { pullRequest: parsed.pullRequest as GithubLabPullRequest }
|
|
1014
|
+
: {}),
|
|
1015
|
+
...(Array.isArray(parsed.branches) ? { branches: parsed.branches } : {}),
|
|
1016
|
+
...(typeof parsed.defaultBranch === "string"
|
|
1017
|
+
? { defaultBranch: parsed.defaultBranch }
|
|
1018
|
+
: {}),
|
|
561
1019
|
});
|
|
562
1020
|
} catch {
|
|
563
1021
|
return null;
|