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