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
|
@@ -5,8 +5,7 @@ import {
|
|
|
5
5
|
Copy,
|
|
6
6
|
FilePlus,
|
|
7
7
|
Folder,
|
|
8
|
-
|
|
9
|
-
KeyRound,
|
|
8
|
+
GitPullRequest,
|
|
10
9
|
ListChecks,
|
|
11
10
|
Loader2,
|
|
12
11
|
Maximize2,
|
|
@@ -18,6 +17,7 @@ import {
|
|
|
18
17
|
Pencil,
|
|
19
18
|
Play,
|
|
20
19
|
Save,
|
|
20
|
+
Settings,
|
|
21
21
|
StopCircle,
|
|
22
22
|
Terminal,
|
|
23
23
|
Trash2,
|
|
@@ -39,12 +39,19 @@ import {
|
|
|
39
39
|
serializeGhaLabWorkspace,
|
|
40
40
|
} from "../githubActionsLab";
|
|
41
41
|
import type { GithubActionsLabWorkspace } from "../types";
|
|
42
|
-
import type { GithubActionsLabEnvironmentEntry } from "../types";
|
|
43
42
|
import * as api from "../api";
|
|
44
43
|
import type { GhaJobSnapshot, GhaStreamMessage } from "../api";
|
|
45
44
|
import GhaJobsPanel from "./GhaJobsPanel";
|
|
46
45
|
import GhaHistoryPanel from "./GhaHistoryPanel";
|
|
47
|
-
import
|
|
46
|
+
import PullRequestPanel from "./PullRequestPanel";
|
|
47
|
+
import SettingsPanel from "./SettingsPanel";
|
|
48
|
+
import {
|
|
49
|
+
evaluateCodeOwners,
|
|
50
|
+
findCodeOwnersPath,
|
|
51
|
+
isCodeOwnersPath,
|
|
52
|
+
lintCodeOwnersAgainstOrg,
|
|
53
|
+
parseCodeOwners,
|
|
54
|
+
} from "../codeowners";
|
|
48
55
|
import {
|
|
49
56
|
defaultContextForEvent,
|
|
50
57
|
evaluateConcurrencyFor,
|
|
@@ -215,6 +222,16 @@ function getGhaLabModelPath(filePath: string): string {
|
|
|
215
222
|
.join("/")}`;
|
|
216
223
|
}
|
|
217
224
|
|
|
225
|
+
// Translate the act runner's GhaJobStatus into the PR-side "check run"
|
|
226
|
+
// vocabulary used by codeowners.ts. "pending" is a pre-run state we
|
|
227
|
+
// don't get from act in practice, so we treat it as "queued" for parity.
|
|
228
|
+
function mapJobStatusToCheck(
|
|
229
|
+
s: GhaJobSnapshot["status"],
|
|
230
|
+
): "queued" | "running" | "success" | "failed" | "cancelled" | "skipped" {
|
|
231
|
+
if (s === "pending") return "queued";
|
|
232
|
+
return s;
|
|
233
|
+
}
|
|
234
|
+
|
|
218
235
|
// Tiny grouped-by-folder list to keep the modal lean.
|
|
219
236
|
function groupByFolder(paths: string[]): { folder: string; files: string[] }[] {
|
|
220
237
|
const map = new Map<string, string[]>();
|
|
@@ -239,41 +256,9 @@ interface ConsoleLine {
|
|
|
239
256
|
|
|
240
257
|
type GhaEnvironmentKind = "variables" | "secrets" | "env";
|
|
241
258
|
|
|
242
|
-
const GHA_ENV_NAME_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
243
|
-
|
|
244
259
|
const GHA_ENVIRONMENT_SECTIONS: Array<{
|
|
245
260
|
kind: GhaEnvironmentKind;
|
|
246
|
-
|
|
247
|
-
addLabel: string;
|
|
248
|
-
namePlaceholder: string;
|
|
249
|
-
valuePlaceholder: string;
|
|
250
|
-
help: string;
|
|
251
|
-
}> = [
|
|
252
|
-
{
|
|
253
|
-
kind: "variables",
|
|
254
|
-
title: "Repository variables",
|
|
255
|
-
addLabel: "Add variable",
|
|
256
|
-
namePlaceholder: "API_URL",
|
|
257
|
-
valuePlaceholder: "https://example.test",
|
|
258
|
-
help: "Available in workflows as `${{ vars.NAME }}` through act's --var-file.",
|
|
259
|
-
},
|
|
260
|
-
{
|
|
261
|
-
kind: "secrets",
|
|
262
|
-
title: "Repository secrets",
|
|
263
|
-
addLabel: "Add secret",
|
|
264
|
-
namePlaceholder: "NPM_TOKEN",
|
|
265
|
-
valuePlaceholder: "secret value",
|
|
266
|
-
help: "Available in workflows as `${{ secrets.NAME }}` through act's --secret-file.",
|
|
267
|
-
},
|
|
268
|
-
{
|
|
269
|
-
kind: "env",
|
|
270
|
-
title: "Runner environment",
|
|
271
|
-
addLabel: "Add env",
|
|
272
|
-
namePlaceholder: "NODE_ENV",
|
|
273
|
-
valuePlaceholder: "test",
|
|
274
|
-
help: "Available to shell steps as environment variables, e.g. `$NAME`, through act's --env-file.",
|
|
275
|
-
},
|
|
276
|
-
];
|
|
261
|
+
}> = [{ kind: "variables" }, { kind: "secrets" }, { kind: "env" }];
|
|
277
262
|
|
|
278
263
|
export default function GithubActionsLabModal() {
|
|
279
264
|
const {
|
|
@@ -328,10 +313,23 @@ export default function GithubActionsLabModal() {
|
|
|
328
313
|
// Live job snapshots reported by the server during the active run.
|
|
329
314
|
// Reset every time the user kicks off a new run.
|
|
330
315
|
const [liveJobs, setLiveJobs] = useState<GhaJobSnapshot[]>([]);
|
|
331
|
-
//
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
316
|
+
// Mirror into a ref so the run-complete handler can read the freshest
|
|
317
|
+
// job set without racing the React state queue.
|
|
318
|
+
const liveJobsRef = useRef<GhaJobSnapshot[]>([]);
|
|
319
|
+
useEffect(() => {
|
|
320
|
+
liveJobsRef.current = liveJobs;
|
|
321
|
+
}, [liveJobs]);
|
|
322
|
+
// ── Right-pane tab state ───────────────────────────────────────
|
|
323
|
+
// The right pane mimics github.com's repo nav: three top-level tabs
|
|
324
|
+
// (Actions / Pull request / Settings) and Actions has its own sub-
|
|
325
|
+
// tabs (Console / Jobs / History). Splitting state keeps the sub-tab
|
|
326
|
+
// sticky as the user bounces between top tabs.
|
|
327
|
+
const [topTab, setTopTab] = useState<"actions" | "pr" | "settings">(
|
|
328
|
+
"actions",
|
|
329
|
+
);
|
|
330
|
+
const [rightTab, setRightTab] = useState<"console" | "jobs" | "history">(
|
|
331
|
+
"console",
|
|
332
|
+
);
|
|
335
333
|
const environmentEntryCount = useMemo(
|
|
336
334
|
() =>
|
|
337
335
|
GHA_ENVIRONMENT_SECTIONS.reduce((total, section) => {
|
|
@@ -964,46 +962,9 @@ export default function GithubActionsLabModal() {
|
|
|
964
962
|
};
|
|
965
963
|
|
|
966
964
|
// ── GitHub Actions environment inputs ─────────────────────────────
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
const setEnvironmentRows = (
|
|
971
|
-
kind: GhaEnvironmentKind,
|
|
972
|
-
rows: GithubActionsLabEnvironmentEntry[],
|
|
973
|
-
) => {
|
|
974
|
-
setWorkspace((prev) => ({
|
|
975
|
-
...prev,
|
|
976
|
-
environment: {
|
|
977
|
-
...(prev.environment ?? {}),
|
|
978
|
-
[kind]: rows,
|
|
979
|
-
},
|
|
980
|
-
}));
|
|
981
|
-
};
|
|
982
|
-
|
|
983
|
-
const addEnvironmentEntry = (kind: GhaEnvironmentKind) => {
|
|
984
|
-
const rows = getEnvironmentRows(kind);
|
|
985
|
-
setEnvironmentRows(kind, [...rows, { name: "", value: "", enabled: true }]);
|
|
986
|
-
setRightTab("env");
|
|
987
|
-
};
|
|
988
|
-
|
|
989
|
-
const updateEnvironmentEntry = (
|
|
990
|
-
kind: GhaEnvironmentKind,
|
|
991
|
-
index: number,
|
|
992
|
-
patch: Partial<GithubActionsLabEnvironmentEntry>,
|
|
993
|
-
) => {
|
|
994
|
-
const rows = getEnvironmentRows(kind).slice();
|
|
995
|
-
const current = rows[index];
|
|
996
|
-
if (!current) return;
|
|
997
|
-
rows[index] = { ...current, ...patch };
|
|
998
|
-
setEnvironmentRows(kind, rows);
|
|
999
|
-
};
|
|
1000
|
-
|
|
1001
|
-
const removeEnvironmentEntry = (kind: GhaEnvironmentKind, index: number) => {
|
|
1002
|
-
setEnvironmentRows(
|
|
1003
|
-
kind,
|
|
1004
|
-
getEnvironmentRows(kind).filter((_, i) => i !== index),
|
|
1005
|
-
);
|
|
1006
|
-
};
|
|
965
|
+
// The Environments table itself lives inside SettingsPanel; we keep
|
|
966
|
+
// only the bits the modal still needs to build act --var/secret/env
|
|
967
|
+
// files on every run.
|
|
1007
968
|
|
|
1008
969
|
// ── Save lab as context file ──────────────────────────────────────
|
|
1009
970
|
const handleSave = useCallback(async () => {
|
|
@@ -1119,9 +1080,58 @@ export default function GithubActionsLabModal() {
|
|
|
1119
1080
|
// success/failed" lifecycle for this invocation. Switch tabs to Jobs
|
|
1120
1081
|
// so the visualisation is immediately visible.
|
|
1121
1082
|
setLiveJobs([]);
|
|
1083
|
+
setTopTab("actions");
|
|
1122
1084
|
setRightTab("jobs");
|
|
1123
1085
|
appendConsole({ kind: "input", text: `$ ${run.command}\n` });
|
|
1124
1086
|
|
|
1087
|
+
// For push / pull_request events, surface a quick CODEOWNERS dry-run
|
|
1088
|
+
// so the user sees who would be auto-requested as a reviewer, just
|
|
1089
|
+
// like GitHub posts on a real PR. We do this client-side because
|
|
1090
|
+
// act has no concept of CODEOWNERS — it's a github.com feature.
|
|
1091
|
+
try {
|
|
1092
|
+
const eventName = run.command.match(/^act\s+([a-z_]+)/i)?.[1];
|
|
1093
|
+
if (
|
|
1094
|
+
(eventName === "push" || eventName === "pull_request") &&
|
|
1095
|
+
workspace.pullRequest?.changedFiles?.length
|
|
1096
|
+
) {
|
|
1097
|
+
const codeownersPath = findCodeOwnersPath(workspace.files);
|
|
1098
|
+
if (codeownersPath) {
|
|
1099
|
+
const parsed = parseCodeOwners(
|
|
1100
|
+
workspace.files[codeownersPath] ?? "",
|
|
1101
|
+
);
|
|
1102
|
+
const evalResult = evaluateCodeOwners(
|
|
1103
|
+
parsed.rules,
|
|
1104
|
+
workspace.pullRequest.changedFiles,
|
|
1105
|
+
workspace.ghOrg,
|
|
1106
|
+
);
|
|
1107
|
+
appendConsole({
|
|
1108
|
+
kind: "info",
|
|
1109
|
+
text: `[CODEOWNERS] ${codeownersPath} \u2192 ${evalResult.ownedFiles.length}/${workspace.pullRequest.changedFiles.length} files owned`,
|
|
1110
|
+
});
|
|
1111
|
+
for (const match of evalResult.perFile) {
|
|
1112
|
+
if (match.rule && match.owners.length > 0) {
|
|
1113
|
+
appendConsole({
|
|
1114
|
+
kind: "info",
|
|
1115
|
+
text: `[CODEOWNERS] ${match.path} \u2192 ${match.owners
|
|
1116
|
+
.map((o) => `@${o}`)
|
|
1117
|
+
.join(" ")}`,
|
|
1118
|
+
});
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1121
|
+
if (evalResult.requestedHandles.length > 0) {
|
|
1122
|
+
appendConsole({
|
|
1123
|
+
kind: "info",
|
|
1124
|
+
text: `[CODEOWNERS] Reviewers requested: ${evalResult.requestedHandles
|
|
1125
|
+
.map((h) => `@${h}`)
|
|
1126
|
+
.join(", ")}`,
|
|
1127
|
+
});
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
} catch {
|
|
1132
|
+
// CODEOWNERS preview is informational only \u2014 never block the run.
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1125
1135
|
// Flip this run's record to running so the Concurrency panel ticks.
|
|
1126
1136
|
setConcurrencyRuns((rs) =>
|
|
1127
1137
|
rs.map((r) =>
|
|
@@ -1175,6 +1185,46 @@ export default function GithubActionsLabModal() {
|
|
|
1175
1185
|
kind: "info",
|
|
1176
1186
|
text: `\n[done] exit=${message.exitCode} • ${message.durationMs}ms\n`,
|
|
1177
1187
|
});
|
|
1188
|
+
// Persist this run as the PR's "last check run" so the Pull
|
|
1189
|
+
// Request tab can grade required status checks. We pull the
|
|
1190
|
+
// freshest job snapshots from the ref, not the closure-captured
|
|
1191
|
+
// value, because `setLiveJobs` updates above may not have
|
|
1192
|
+
// flushed by the time `complete` arrives.
|
|
1193
|
+
const completedAt = new Date().toISOString();
|
|
1194
|
+
setWorkspace((prev) => {
|
|
1195
|
+
const jobs = liveJobsRef.current.map((j) => {
|
|
1196
|
+
const baseJob = {
|
|
1197
|
+
name: j.name,
|
|
1198
|
+
status: mapJobStatusToCheck(j.status),
|
|
1199
|
+
} as {
|
|
1200
|
+
name: string;
|
|
1201
|
+
status:
|
|
1202
|
+
| "queued"
|
|
1203
|
+
| "running"
|
|
1204
|
+
| "success"
|
|
1205
|
+
| "failed"
|
|
1206
|
+
| "cancelled"
|
|
1207
|
+
| "skipped";
|
|
1208
|
+
durationMs?: number;
|
|
1209
|
+
};
|
|
1210
|
+
if (typeof j.durationMs === "number") {
|
|
1211
|
+
baseJob.durationMs = j.durationMs;
|
|
1212
|
+
}
|
|
1213
|
+
return baseJob;
|
|
1214
|
+
});
|
|
1215
|
+
if (jobs.length === 0) return prev;
|
|
1216
|
+
return {
|
|
1217
|
+
...prev,
|
|
1218
|
+
pullRequest: {
|
|
1219
|
+
...(prev.pullRequest ?? { changedFiles: [] }),
|
|
1220
|
+
lastCheckRun: {
|
|
1221
|
+
completedAt,
|
|
1222
|
+
...(workflow ? { workflow } : {}),
|
|
1223
|
+
jobs,
|
|
1224
|
+
},
|
|
1225
|
+
},
|
|
1226
|
+
};
|
|
1227
|
+
});
|
|
1178
1228
|
// Refresh History so the just-completed run shows up.
|
|
1179
1229
|
setHistoryNonce((n) => n + 1);
|
|
1180
1230
|
}
|
|
@@ -1595,6 +1645,46 @@ interface ImportMeta {
|
|
|
1595
1645
|
[activeFile],
|
|
1596
1646
|
);
|
|
1597
1647
|
|
|
1648
|
+
// ── CODEOWNERS live validation ───────────────────────────────────
|
|
1649
|
+
// When the CODEOWNERS file is open, push diagnostics into Monaco so
|
|
1650
|
+
// the user sees red squiggles for bad owner handles (just like the
|
|
1651
|
+
// GitHub UI flags unknown users in the CODEOWNERS settings page).
|
|
1652
|
+
useEffect(() => {
|
|
1653
|
+
const monaco = monacoRef.current;
|
|
1654
|
+
if (!monaco) return;
|
|
1655
|
+
// Clear our markers on every editor change; we only set them when
|
|
1656
|
+
// the active file is a CODEOWNERS file.
|
|
1657
|
+
const codeownersOwner = "gha-lab-codeowners";
|
|
1658
|
+
if (!isCodeOwnersPath(activeFile)) {
|
|
1659
|
+
// Clear markers on whatever model is currently active.
|
|
1660
|
+
const uri = monaco.Uri.parse(getGhaLabModelPath(activeFile));
|
|
1661
|
+
const model = monaco.editor.getModel(uri);
|
|
1662
|
+
if (model) monaco.editor.setModelMarkers(model, codeownersOwner, []);
|
|
1663
|
+
return;
|
|
1664
|
+
}
|
|
1665
|
+
const body = workspace.files[activeFile] ?? "";
|
|
1666
|
+
const parsed = parseCodeOwners(body);
|
|
1667
|
+
const issues = lintCodeOwnersAgainstOrg(parsed, workspace.ghOrg);
|
|
1668
|
+
const uri = monaco.Uri.parse(getGhaLabModelPath(activeFile));
|
|
1669
|
+
const model = monaco.editor.getModel(uri);
|
|
1670
|
+
if (!model) return;
|
|
1671
|
+
monaco.editor.setModelMarkers(
|
|
1672
|
+
model,
|
|
1673
|
+
codeownersOwner,
|
|
1674
|
+
issues.map((issue) => ({
|
|
1675
|
+
severity:
|
|
1676
|
+
issue.severity === "error"
|
|
1677
|
+
? monaco.MarkerSeverity.Error
|
|
1678
|
+
: monaco.MarkerSeverity.Warning,
|
|
1679
|
+
message: issue.message,
|
|
1680
|
+
startLineNumber: issue.line,
|
|
1681
|
+
endLineNumber: issue.line,
|
|
1682
|
+
startColumn: 1,
|
|
1683
|
+
endColumn: (model.getLineContent(issue.line)?.length ?? 0) + 1,
|
|
1684
|
+
})),
|
|
1685
|
+
);
|
|
1686
|
+
}, [activeFile, workspace.files, workspace.ghOrg]);
|
|
1687
|
+
|
|
1598
1688
|
// ── Render ────────────────────────────────────────────────────────
|
|
1599
1689
|
const containerStyle: React.CSSProperties = maximized
|
|
1600
1690
|
? { top: 0, left: 0, width: "100vw", height: "100vh" }
|
|
@@ -1659,7 +1749,7 @@ interface ImportMeta {
|
|
|
1659
1749
|
<div className="flex items-center gap-2">
|
|
1660
1750
|
<span className="inline-block h-2 w-2 rounded-full bg-amber-400" />
|
|
1661
1751
|
<span className="text-xs font-semibold tracking-widest text-amber-300">
|
|
1662
|
-
GITHUB
|
|
1752
|
+
GITHUB LAB
|
|
1663
1753
|
</span>
|
|
1664
1754
|
</div>
|
|
1665
1755
|
<input
|
|
@@ -2143,7 +2233,7 @@ interface ImportMeta {
|
|
|
2143
2233
|
</div>
|
|
2144
2234
|
</div>
|
|
2145
2235
|
|
|
2146
|
-
{/* Right pane:
|
|
2236
|
+
{/* Right pane: GitHub-style top tabs (Actions / PR / Settings) */}
|
|
2147
2237
|
<div
|
|
2148
2238
|
className="min-h-0 flex flex-col bg-slate-950 overflow-hidden"
|
|
2149
2239
|
style={{
|
|
@@ -2151,46 +2241,60 @@ interface ImportMeta {
|
|
|
2151
2241
|
visibility: rightCollapsed ? "hidden" : undefined,
|
|
2152
2242
|
}}
|
|
2153
2243
|
>
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
<
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2244
|
+
{/* Top-level nav mimicking github.com's repo header. We keep
|
|
2245
|
+
three tabs only — the saturated 6-tab bar moved its CI
|
|
2246
|
+
config items (Env, Concurrency) into a real Settings page. */}
|
|
2247
|
+
<div className="flex items-center justify-between border-b border-slate-800/60 bg-slate-950 px-2">
|
|
2248
|
+
<div className="flex items-center gap-2 text-[12px]">
|
|
2249
|
+
{[
|
|
2250
|
+
{
|
|
2251
|
+
id: "actions" as const,
|
|
2252
|
+
label: "Actions",
|
|
2253
|
+
icon: Play,
|
|
2254
|
+
},
|
|
2255
|
+
{
|
|
2256
|
+
id: "pr" as const,
|
|
2257
|
+
label: "Pull request",
|
|
2258
|
+
icon: GitPullRequest,
|
|
2259
|
+
},
|
|
2260
|
+
{
|
|
2261
|
+
id: "settings" as const,
|
|
2262
|
+
label: "Settings",
|
|
2263
|
+
icon: Settings,
|
|
2264
|
+
},
|
|
2265
|
+
].map((t) => {
|
|
2266
|
+
const Icon = t.icon;
|
|
2267
|
+
const isActive = topTab === t.id;
|
|
2268
|
+
return (
|
|
2269
|
+
<button
|
|
2270
|
+
key={t.id}
|
|
2271
|
+
onClick={() => setTopTab(t.id)}
|
|
2272
|
+
className={`-mb-px flex items-center gap-1.5 border-b-2 px-1.5 py-2 ${
|
|
2273
|
+
isActive
|
|
2274
|
+
? "border-amber-500 text-amber-200"
|
|
2275
|
+
: "border-transparent text-slate-400 hover:text-slate-200"
|
|
2276
|
+
}`}
|
|
2277
|
+
>
|
|
2278
|
+
<Icon className="h-3.5 w-3.5" />
|
|
2279
|
+
{t.label}
|
|
2280
|
+
{t.id === "actions" && running && (
|
|
2281
|
+
<span className="ml-0.5 inline-block h-1.5 w-1.5 rounded-full bg-amber-400 animate-pulse" />
|
|
2282
|
+
)}
|
|
2283
|
+
{t.id === "pr" &&
|
|
2284
|
+
(workspace.pullRequest?.changedFiles.length ?? 0) >
|
|
2285
|
+
0 && (
|
|
2286
|
+
<span className="ml-0.5 rounded bg-emerald-500/15 px-1 text-[10px] text-emerald-200">
|
|
2287
|
+
{workspace.pullRequest?.changedFiles.length}
|
|
2288
|
+
</span>
|
|
2289
|
+
)}
|
|
2290
|
+
{t.id === "settings" && environmentEntryCount > 0 && (
|
|
2291
|
+
<span className="ml-0.5 rounded bg-slate-700/60 px-1 text-[10px] text-slate-300">
|
|
2292
|
+
{environmentEntryCount}
|
|
2293
|
+
</span>
|
|
2294
|
+
)}
|
|
2295
|
+
</button>
|
|
2296
|
+
);
|
|
2297
|
+
})}
|
|
2194
2298
|
</div>
|
|
2195
2299
|
<div className="flex items-center gap-1">
|
|
2196
2300
|
{running && (
|
|
@@ -2202,7 +2306,7 @@ interface ImportMeta {
|
|
|
2202
2306
|
<StopCircle className="w-3.5 h-3.5" />
|
|
2203
2307
|
</button>
|
|
2204
2308
|
)}
|
|
2205
|
-
{rightTab === "console" && (
|
|
2309
|
+
{topTab === "actions" && rightTab === "console" && (
|
|
2206
2310
|
<button
|
|
2207
2311
|
onClick={clearConsole}
|
|
2208
2312
|
className="p-1 rounded text-slate-500 hover:text-slate-200 hover:bg-slate-800/60"
|
|
@@ -2214,7 +2318,47 @@ interface ImportMeta {
|
|
|
2214
2318
|
</div>
|
|
2215
2319
|
</div>
|
|
2216
2320
|
|
|
2217
|
-
{
|
|
2321
|
+
{/* Actions sub-tabs (Workflow runs view): only render when the
|
|
2322
|
+
user is on the Actions top tab. Console + Jobs + History
|
|
2323
|
+
are sub-views of the same "runs" workflow on github.com. */}
|
|
2324
|
+
{topTab === "actions" && (
|
|
2325
|
+
<div className="flex items-center gap-0.5 border-b border-slate-800/40 bg-slate-900/30 px-2 py-1 text-[11px]">
|
|
2326
|
+
{[
|
|
2327
|
+
{
|
|
2328
|
+
id: "console" as const,
|
|
2329
|
+
label: "Workflow runs",
|
|
2330
|
+
icon: Terminal,
|
|
2331
|
+
},
|
|
2332
|
+
{ id: "jobs" as const, label: "Jobs", icon: ListChecks },
|
|
2333
|
+
{
|
|
2334
|
+
id: "history" as const,
|
|
2335
|
+
label: "History",
|
|
2336
|
+
icon: ListChecks,
|
|
2337
|
+
},
|
|
2338
|
+
].map((t) => {
|
|
2339
|
+
const Icon = t.icon;
|
|
2340
|
+
return (
|
|
2341
|
+
<button
|
|
2342
|
+
key={t.id}
|
|
2343
|
+
onClick={() => setRightTab(t.id)}
|
|
2344
|
+
className={`flex items-center gap-1 rounded px-2 py-1 ${
|
|
2345
|
+
rightTab === t.id
|
|
2346
|
+
? "bg-slate-800/70 text-amber-200"
|
|
2347
|
+
: "text-slate-400 hover:text-slate-200 hover:bg-slate-800/40"
|
|
2348
|
+
}`}
|
|
2349
|
+
>
|
|
2350
|
+
<Icon className="h-3 w-3" />
|
|
2351
|
+
{t.label}
|
|
2352
|
+
{t.id === "jobs" && running && (
|
|
2353
|
+
<span className="ml-1 inline-block w-1.5 h-1.5 rounded-full bg-amber-400 animate-pulse" />
|
|
2354
|
+
)}
|
|
2355
|
+
</button>
|
|
2356
|
+
);
|
|
2357
|
+
})}
|
|
2358
|
+
</div>
|
|
2359
|
+
)}
|
|
2360
|
+
|
|
2361
|
+
{topTab === "actions" && rightTab === "console" && (
|
|
2218
2362
|
<>
|
|
2219
2363
|
<div className="flex-1 min-h-0 overflow-auto px-3 py-2 text-[11px] font-mono whitespace-pre-wrap break-words">
|
|
2220
2364
|
{consoleLines.length === 0 ? (
|
|
@@ -2275,7 +2419,7 @@ interface ImportMeta {
|
|
|
2275
2419
|
</>
|
|
2276
2420
|
)}
|
|
2277
2421
|
|
|
2278
|
-
{rightTab === "jobs" && (
|
|
2422
|
+
{topTab === "actions" && rightTab === "jobs" && (
|
|
2279
2423
|
<GhaJobsPanel
|
|
2280
2424
|
workflowYaml={workflow ? workspace.files[workflow] : undefined}
|
|
2281
2425
|
jobs={liveJobs}
|
|
@@ -2289,154 +2433,37 @@ interface ImportMeta {
|
|
|
2289
2433
|
/>
|
|
2290
2434
|
)}
|
|
2291
2435
|
|
|
2292
|
-
{
|
|
2293
|
-
<
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
<KeyRound className="h-4 w-4" />
|
|
2297
|
-
GitHub-style act inputs
|
|
2298
|
-
</div>
|
|
2299
|
-
<p className="leading-5 text-slate-400">
|
|
2300
|
-
These are written to temporary act files for each run:
|
|
2301
|
-
variables become{" "}
|
|
2302
|
-
<span className="text-amber-200">vars.*</span>, secrets
|
|
2303
|
-
become <span className="text-amber-200">secrets.*</span>,
|
|
2304
|
-
and runner env values become shell environment variables.
|
|
2305
|
-
Use fake or local-only values; saved lab snapshots store
|
|
2306
|
-
these values as plain text.
|
|
2307
|
-
</p>
|
|
2308
|
-
</div>
|
|
2309
|
-
|
|
2310
|
-
<div className="space-y-3">
|
|
2311
|
-
{GHA_ENVIRONMENT_SECTIONS.map((section) => {
|
|
2312
|
-
const rows = getEnvironmentRows(section.kind);
|
|
2313
|
-
return (
|
|
2314
|
-
<section
|
|
2315
|
-
key={section.kind}
|
|
2316
|
-
className="rounded-xl border border-slate-800 bg-slate-900/40"
|
|
2317
|
-
>
|
|
2318
|
-
<div className="flex items-start justify-between gap-2 border-b border-slate-800/70 px-3 py-2">
|
|
2319
|
-
<div>
|
|
2320
|
-
<h3 className="text-[11px] font-semibold uppercase tracking-wider text-slate-200">
|
|
2321
|
-
{section.title}
|
|
2322
|
-
</h3>
|
|
2323
|
-
<p className="mt-0.5 text-[11px] leading-4 text-slate-500">
|
|
2324
|
-
{section.help}
|
|
2325
|
-
</p>
|
|
2326
|
-
</div>
|
|
2327
|
-
<button
|
|
2328
|
-
onClick={() => addEnvironmentEntry(section.kind)}
|
|
2329
|
-
className="shrink-0 rounded border border-slate-700 px-2 py-1 text-[11px] text-slate-300 hover:border-amber-500/40 hover:text-amber-200"
|
|
2330
|
-
>
|
|
2331
|
-
{section.addLabel}
|
|
2332
|
-
</button>
|
|
2333
|
-
</div>
|
|
2334
|
-
|
|
2335
|
-
<div className="space-y-2 p-3">
|
|
2336
|
-
{rows.length === 0 ? (
|
|
2337
|
-
<button
|
|
2338
|
-
onClick={() => addEnvironmentEntry(section.kind)}
|
|
2339
|
-
className="w-full rounded-lg border border-dashed border-slate-700 px-3 py-3 text-left text-[11px] text-slate-500 hover:border-amber-500/40 hover:text-amber-200"
|
|
2340
|
-
>
|
|
2341
|
-
No entries yet — click to add one.
|
|
2342
|
-
</button>
|
|
2343
|
-
) : (
|
|
2344
|
-
rows.map((row, index) => {
|
|
2345
|
-
const nameIsValid =
|
|
2346
|
-
!row.name.trim() ||
|
|
2347
|
-
GHA_ENV_NAME_RE.test(row.name.trim());
|
|
2348
|
-
return (
|
|
2349
|
-
<div
|
|
2350
|
-
key={`${section.kind}-${index}`}
|
|
2351
|
-
className="rounded-lg border border-slate-800 bg-slate-950/60 p-2"
|
|
2352
|
-
>
|
|
2353
|
-
<div className="grid grid-cols-[minmax(0,0.85fr)_minmax(0,1.15fr)_auto] gap-2">
|
|
2354
|
-
<input
|
|
2355
|
-
value={row.name}
|
|
2356
|
-
onChange={(e) =>
|
|
2357
|
-
updateEnvironmentEntry(
|
|
2358
|
-
section.kind,
|
|
2359
|
-
index,
|
|
2360
|
-
{ name: e.target.value },
|
|
2361
|
-
)
|
|
2362
|
-
}
|
|
2363
|
-
placeholder={section.namePlaceholder}
|
|
2364
|
-
className={`min-w-0 rounded border bg-slate-900 px-2 py-1.5 font-mono text-[11px] text-slate-100 outline-none placeholder:text-slate-600 ${
|
|
2365
|
-
nameIsValid
|
|
2366
|
-
? "border-slate-700 focus:border-amber-500/60"
|
|
2367
|
-
: "border-red-500/60 focus:border-red-400"
|
|
2368
|
-
}`}
|
|
2369
|
-
/>
|
|
2370
|
-
<input
|
|
2371
|
-
type={
|
|
2372
|
-
section.kind === "secrets"
|
|
2373
|
-
? "password"
|
|
2374
|
-
: "text"
|
|
2375
|
-
}
|
|
2376
|
-
value={row.value}
|
|
2377
|
-
onChange={(e) =>
|
|
2378
|
-
updateEnvironmentEntry(
|
|
2379
|
-
section.kind,
|
|
2380
|
-
index,
|
|
2381
|
-
{ value: e.target.value },
|
|
2382
|
-
)
|
|
2383
|
-
}
|
|
2384
|
-
placeholder={section.valuePlaceholder}
|
|
2385
|
-
className="min-w-0 rounded border border-slate-700 bg-slate-900 px-2 py-1.5 font-mono text-[11px] text-slate-100 outline-none placeholder:text-slate-600 focus:border-amber-500/60"
|
|
2386
|
-
/>
|
|
2387
|
-
<button
|
|
2388
|
-
onClick={() =>
|
|
2389
|
-
removeEnvironmentEntry(
|
|
2390
|
-
section.kind,
|
|
2391
|
-
index,
|
|
2392
|
-
)
|
|
2393
|
-
}
|
|
2394
|
-
className="rounded px-2 text-slate-500 hover:bg-red-500/10 hover:text-red-300"
|
|
2395
|
-
title="Remove entry"
|
|
2396
|
-
>
|
|
2397
|
-
<Trash2 className="h-3.5 w-3.5" />
|
|
2398
|
-
</button>
|
|
2399
|
-
</div>
|
|
2400
|
-
{!nameIsValid && (
|
|
2401
|
-
<div className="mt-1 text-[10px] text-red-300">
|
|
2402
|
-
Use letters, numbers, and underscores; do
|
|
2403
|
-
not start with a number.
|
|
2404
|
-
</div>
|
|
2405
|
-
)}
|
|
2406
|
-
</div>
|
|
2407
|
-
);
|
|
2408
|
-
})
|
|
2409
|
-
)}
|
|
2410
|
-
</div>
|
|
2411
|
-
</section>
|
|
2412
|
-
);
|
|
2413
|
-
})}
|
|
2414
|
-
</div>
|
|
2415
|
-
</div>
|
|
2416
|
-
)}
|
|
2417
|
-
|
|
2418
|
-
{rightTab === "concurrency" && (
|
|
2419
|
-
<GhaConcurrencyPanel
|
|
2420
|
-
parsed={parseConcurrencyBlock(
|
|
2421
|
-
workflow ? workspace.files[workflow] : undefined,
|
|
2422
|
-
)}
|
|
2436
|
+
{topTab === "settings" && (
|
|
2437
|
+
<SettingsPanel
|
|
2438
|
+
workspace={workspace}
|
|
2439
|
+
onChange={(updater) => setWorkspace(updater)}
|
|
2423
2440
|
workflowPath={workflow}
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
// ones because the queue depends on them.
|
|
2441
|
+
workflowYaml={workflow ? workspace.files[workflow] : undefined}
|
|
2442
|
+
concurrencyRuns={concurrencyRuns}
|
|
2443
|
+
concurrencyContext={concurrencyContext}
|
|
2444
|
+
onConcurrencyContextChange={setConcurrencyContext}
|
|
2445
|
+
onClearConcurrencyRuns={() => {
|
|
2430
2446
|
setConcurrencyRuns((rs) =>
|
|
2431
2447
|
rs.filter(
|
|
2432
2448
|
(r) => r.status === "running" || r.status === "pending",
|
|
2433
2449
|
),
|
|
2434
2450
|
);
|
|
2435
2451
|
}}
|
|
2452
|
+
onResetWorkspace={() => {
|
|
2453
|
+
setWorkspace(cloneGhaLabWorkspace(DEFAULT_GHA_LAB));
|
|
2454
|
+
setTopTab("actions");
|
|
2455
|
+
}}
|
|
2456
|
+
/>
|
|
2457
|
+
)}
|
|
2458
|
+
|
|
2459
|
+
{topTab === "pr" && (
|
|
2460
|
+
<PullRequestPanel
|
|
2461
|
+
workspace={workspace}
|
|
2462
|
+
onChange={(updater) => setWorkspace(updater)}
|
|
2436
2463
|
/>
|
|
2437
2464
|
)}
|
|
2438
2465
|
|
|
2439
|
-
{rightTab === "history" && (
|
|
2466
|
+
{topTab === "actions" && rightTab === "history" && (
|
|
2440
2467
|
<GhaHistoryPanel
|
|
2441
2468
|
{...(currentQuestion ? { questionId: currentQuestion.id } : {})}
|
|
2442
2469
|
{...(activeGhaId ? { fileId: activeGhaId } : {})}
|