CiHistory.tsx
Raw
1import type { RepositoryRow } from "../../db/index.ts";
2import { formatDateTime } from "../../lib/formatDate.ts";
3import type { SessionUser } from "../../middleware/session.ts";
4import { Layout } from "../layout.tsx";
5import { Pagination, type PaginationInfo } from "../Pagination.tsx";
6import { RepoHeader } from "../repos/RepoHeader.tsx";
7import { RepoNav } from "../repos/RepoNav.tsx";
8import { CiStatusPill } from "./CiStatusPill.tsx";
9
10interface RunSummary {
11 id: number;
12 repo_run_id: number | null;
13 status: string;
14 trigger_source: string;
15 commit_sha: string | null;
16 commit_branch: string | null;
17 commit_tag: string | null;
18 started_at: string | null;
19 finished_at: string | null;
20 created_at: string;
21 triggered_by_username: string | null;
22 artifact_count: number;
23}
24
25interface CiHistoryProps {
26 user: SessionUser | null;
27 repo: RepositoryRow;
28 runs: RunSummary[];
29 pagination: PaginationInfo;
30 manualTriggerDisabledReason: string | null;
31}
32
33function duration(start: string | null, end: string | null): string {
34 if (!start || !end) return "";
35 const ms = new Date(end).getTime() - new Date(start).getTime();
36 if (ms < 0) return "";
37 const s = Math.floor(ms / 1000);
38 if (s < 60) return `${s}s`;
39 const m = Math.floor(s / 60);
40 const rem = s % 60;
41 return rem > 0 ? `${m}m ${rem}s` : `${m}m`;
42}
43
44const CI_VARIABLES = [
45 ["CI", "always true"],
46 ["CI_PIPELINE_ID", "numeric run ID"],
47 ["CI_COMMIT_SHA", "full commit hash"],
48 ["CI_COMMIT_SHORT_SHA", "first 8 chars"],
49 ["CI_COMMIT_BRANCH", "branch name (empty for tags)"],
50 ["CI_COMMIT_TAG", "tag name (empty for branches)"],
51 ["CI_COMMIT_REF_NAME", "branch or tag name"],
52 ["CI_TRIGGER_SOURCE", "push · tag · manual"],
53 ["CI_REPO_NAME", "repository name"],
54 ["CI_SERVER_URL", "HearthForge base URL"],
55];
56
57function CiHelp({ repo }: { repo: RepositoryRow }) {
58 return (
59 <details class="form-card ci-help">
60 <summary class="ci-help-summary">
61 <span>How to define a pipeline file</span>
62 <a
63 href="/assets/hearthforge-ci-template.toml"
64 download=".hearthforge-ci.toml"
65 class="btn btn-sm btn-secondary ci-help-download"
66 >
67 Download template
68 </a>
69 </summary>
70 <div class="ci-help-body">
71 <p class="ci-help-desc">
72 Add <code>.hearthforge-ci.toml</code> to your repository
73 root. Each <code>[section]</code> is a step executed in file
74 order. Reserved tables: <code>[on]</code> (triggers) and{" "}
75 <code>[variables]</code> (user-overridable inputs).
76 </p>
77 <div class="ci-help-sections">
78 <div class="ci-help-section">
79 <h4 class="ci-help-section-title">
80 Predefined variables
81 </h4>
82 <dl class="ci-help-vars">
83 {CI_VARIABLES.map(([name, desc]) => (
84 <>
85 <dt>
86 <code>{name}</code>
87 </dt>
88 <dd>{desc}</dd>
89 </>
90 ))}
91 </dl>
92 </div>
93 <div class="ci-help-section">
94 <h4 class="ci-help-section-title">Status badge</h4>
95 <p class="ci-help-badge-desc">Embed in your README:</p>
96 <code class="ci-help-badge-code">{`![pipeline](/${repo.name}/ci/badge.svg)`}</code>
97 <h4
98 class="ci-help-section-title"
99 style="margin-top: var(--space-4)"
100 >
101 Artifact types
102 </h4>
103 <dl class="ci-help-vars">
104 {[
105 ["publish_file", "copy file as-is"],
106 ["publish_tar", ".tar archive"],
107 ["publish_gzip", ".tar.gz archive"],
108 ["publish_zstd", ".tar.zst archive"],
109 ["publish_zip", ".zip archive"],
110 ].map(([k, v]) => (
111 <>
112 <dt>
113 <code>{k}</code>
114 </dt>
115 <dd>{v}</dd>
116 </>
117 ))}
118 </dl>
119 </div>
120 </div>
121 </div>
122 </details>
123 );
124}
125
126export function CiHistory({
127 user,
128 repo,
129 runs,
130 pagination,
131 manualTriggerDisabledReason,
132}: CiHistoryProps) {
133 const isRunning = runs.some(
134 (r) => r.status === "pending" || r.status === "running",
135 );
136 return (
137 <Layout user={user} title={`Pipelines — ${repo.name}`}>
138 {
139 (isRunning ? (
140 <meta http-equiv="refresh" content="4" />
141 ) : (
142 ""
143 )) as unknown as JSX.Element
144 }
145 <div class="container">
146 <RepoHeader repo={repo} />
147 <RepoNav repo={repo} active="ci" user={user} />
148 <div class="list-header">
149 <h2 class="list-heading">Pipelines</h2>
150 {user?.isAdmin && (
151 <div class="ci-history-actions">
152 <form
153 method="POST"
154 action={`/${repo.name}/ci/purge-cache`}
155 class="inline-form"
156 >
157 <button
158 type="submit"
159 class="btn btn-secondary btn-sm"
160 title="Delete all Docker cache volumes for this repository"
161 >
162 Purge caches
163 </button>
164 </form>
165 <form
166 method="POST"
167 action={`/${repo.name}/ci/run`}
168 class="inline-form"
169 >
170 <button
171 type="submit"
172 class="btn btn-primary btn-sm"
173 disabled={
174 manualTriggerDisabledReason
175 ? true
176 : undefined
177 }
178 title={
179 manualTriggerDisabledReason ?? undefined
180 }
181 >
182 Run pipeline
183 </button>
184 </form>
185 </div>
186 )}
187 </div>
188 {runs.length === 0 ? (
189 <div class="empty-state">
190 <p>No pipeline runs yet.</p>
191 <p class="text-muted">
192 Push a <code>.hearthforge-ci.toml</code> to your
193 repository to get started.
194 </p>
195 </div>
196 ) : (
197 <ul class="issue-list">
198 {runs.map((run) => (
199 <li class="issue-item">
200 <div class="release-item-header">
201 <div class="release-item-main">
202 <a
203 href={`/${repo.name}/ci/${run.id}`}
204 class="release-item-title"
205 >
206 <CiStatusPill status={run.status} />
207 <span class="ci-run-id">
208 #{run.repo_run_id ?? run.id}
209 </span>
210 </a>
211 <div class="release-item-meta">
212 {run.commit_sha && (
213 <code class="ci-sha">
214 {run.commit_sha.slice(0, 8)}
215 </code>
216 )}
217 {run.commit_branch && (
218 <span class="badge">
219 {run.commit_branch}
220 </span>
221 )}
222 {run.commit_tag && (
223 <span class="badge">
224 {run.commit_tag}
225 </span>
226 )}
227 <span class="text-muted">
228 {run.trigger_source}
229 </span>
230 {run.triggered_by_username && (
231 <span class="text-muted">
232 by{" "}
233 {run.triggered_by_username}
234 </span>
235 )}
236 {run.artifact_count > 0 && (
237 <span>
238 {run.artifact_count}{" "}
239 artifact
240 {run.artifact_count !== 1
241 ? "s"
242 : ""}
243 </span>
244 )}
245 {run.started_at &&
246 run.finished_at && (
247 <span class="text-muted">
248 {duration(
249 run.started_at,
250 run.finished_at,
251 )}
252 </span>
253 )}
254 </div>
255 </div>
256 <div class="release-item-date">
257 <time datetime={run.created_at}>
258 {formatDateTime(run.created_at)}
259 </time>
260 </div>
261 </div>
262 </li>
263 ))}
264 </ul>
265 )}
266 <Pagination {...pagination} />
267 <CiHelp repo={repo} />
268 </div>
269 </Layout>
270 );
271}
272