Skip to content
Clone

feat(web): add the actions pipeline view

ZhenYi committed
parent b200b73 commit 4568665 Browse files

8 files changed

+464 −75

This change is very large, so some diffs are not shown. Clone the repository to see the full change.

File tree 8
Modified web/src/entities/pipeline/model/pipeline.ts View web/src/entities/pipeline/model/pipeline.ts
@@ -7,7 +7,9 @@
77 AppResponseData_LogPageDto,
88 AppResponseData_RunDetailDto,
99 AppResponseData_RunPageDto,
10 AppResponseData_Vec_ApprovalDto,
1011 AppResponseData_WorkflowSummaryList,
12 ReviewCmd,
1113 TriggerRunCmd,
1214} from '@/shared/api'
1315
@@ -17,6 +19,7 @@
1719export type RunJob = RunDetail['jobs'][number]
1820export type RunStep = RunJob['steps'][number]
1921export type RunArtifact = RunDetail['artifacts'][number]
22export type EnvironmentApproval = NonNullable<AppResponseData_Vec_ApprovalDto['data']>[number]
2023export type LogPage = NonNullable<AppResponseData_LogPageDto['data']>
2124export type WorkflowList = NonNullable<AppResponseData_WorkflowSummaryList['data']>
2225
@@ -57,6 +60,32 @@
5760 }),
5861 )
5962
63export const fetchApprovals = (space: string, repo: string, runId: string) =>
64 unwrapApi<EnvironmentApproval[]>(
65 Apis.pipelines.approvals({
66 pathParams: { space_name: space, repo_name: repo, run_id: runId },
67 }),
68 )
69
70export const reviewApproval = (
71 space: string,
72 repo: string,
73 runId: string,
74 reviewId: string,
75 cmd: ReviewCmd,
76) =>
77 unwrapApi<string>(
78 Apis.pipelines.review({
79 pathParams: {
80 space_name: space,
81 repo_name: repo,
82 run_id: runId,
83 review_id: reviewId,
84 },
85 data: cmd,
86 }),
87 )
88
6089export const fetchWorkflows = (space: string, repo: string, revision?: string) =>
6190 unwrapApi<WorkflowList>(
6291 Apis.pipelines.list_workflows({
Modified web/src/features/repo-pipelines/ui/pipelines-view.tsx View web/src/features/repo-pipelines/ui/pipelines-view.tsx
@@ -5,15 +5,18 @@
55import { fmtTime, useAsync } from '@/shared/lib'
66import {
77 cancelRun,
8 fetchApprovals,
89 fetchLogs,
910 fetchRun,
1011 fetchRuns,
1112 fetchWorkflows,
1213 rerun,
14 reviewApproval,
1315 triggerRun,
1416 type RunDetail,
1517 type RunJob,
1618 type RunSummary,
19 type WorkflowList,
1720} from '@/entities/pipeline'
1821
1922const STATE_LABEL: Record<string, string> = {
@@ -65,6 +68,38 @@
6568 return sha.slice(0, 8)
6669}
6770
71type InputValue = string | number | boolean
72type InputValues = Record<string, InputValue>
73type JsonRecord = Record<string, unknown>
74
75function jsonEntries(value: unknown): Array<[string, unknown]> {
76 return value && typeof value === 'object' && !Array.isArray(value)
77 ? Object.entries(value as JsonRecord)
78 : []
79}
80
81function formatValue(value: unknown): string {
82 if (value === null || value === undefined) return ''
83 if (typeof value === 'boolean') return value ? 'true' : 'false'
84 if (typeof value === 'object') return JSON.stringify(value)
85 return String(value)
86}
87
88function inputDefault(kind: string | null | undefined, value: unknown): InputValue {
89 if (value !== null && value !== undefined && ['string', 'number', 'boolean'].includes(typeof value)) {
90 return value as InputValue
91 }
92 if (kind === 'boolean') return false
93 if (kind === 'number') return 0
94 return ''
95}
96
97function requiredMissing(required: boolean | undefined, value: InputValue | undefined) {
98 if (!required) return false
99 if (value === undefined) return true
100 return typeof value === 'string' && value.trim() === ''
101}
102
68103export function PipelinesView({
69104 space,
70105 repo,
@@ -157,7 +192,12 @@
157192 >
158193 <div className="flex flex-wrap items-center gap-3">
159194 <StateBadge state={run.state} />
160 <span className="text-[13.5px] font-medium">{run.workflow_name}</span>
195 {run.run_number ? (
196 <span className="text-[12.5px] text-shell-ink-muted">
197 {t('repo.pipeline.runNumber', { number: run.run_number })}
198 </span>
199 ) : null}
200 <span className="text-[13.5px] font-medium">{run.display_name ?? run.workflow_name}</span>
161201 <span className="text-[12.5px] text-shell-ink-muted">
162202 {run.event} · {run.branch ?? run.tag ?? run.git_ref} · {shortSha(run.commit_sha)}
163203 </span>
@@ -190,6 +230,8 @@
190230 const { t } = useT()
191231 const [gitRef, setGitRef] = useState(defaultRef)
192232 const [workflowInput, setWorkflowInput] = useState('')
233 const [inputValues, setInputValues] = useState<InputValues>({})
234 const [inputTouched, setInputTouched] = useState(false)
193235 const [busy, setBusy] = useState(false)
194236 const [error, setError] = useState<string | null>(null)
195237 const workflows = useAsync(
@@ -201,14 +243,37 @@
201243 () => (workflows.data?.workflows ?? []).filter((w) => w.valid && w.triggers.workflow_dispatch),
202244 [workflows.data],
203245 )
246 const workflow = workflowInput || dispatchable[0]?.file || ''
247 const selected = dispatchable.find((item) => item.file === workflow) ?? dispatchable[0]
248 const definitions = selected?.inputs ?? {}
249 const effectiveInputs: InputValues = inputTouched
250 ? inputValues
251 : Object.fromEntries(
252 Object.entries(definitions).map(([name, definition]) => [
253 name,
254 inputDefault(definition.type, definition.default),
255 ]),
256 )
257 const missingInputs = Object.entries(definitions).filter(([name, definition]) =>
258 requiredMissing(definition.required, effectiveInputs[name]),
259 )
260
261 const changeWorkflow = (file: string) => {
262 setWorkflowInput(file)
263 setInputValues({})
264 setInputTouched(false)
265 }
204266
205 const workflow = workflowInput || dispatchable[0]?.file || ''
267 const changeInput = (name: string, value: InputValue) => {
268 setInputValues({ ...effectiveInputs, [name]: value })
269 setInputTouched(true)
270 }
206271
207272 const submit = async () => {
208273 setBusy(true)
209274 setError(null)
210275 try {
211 const detail = await triggerRun(space, repo, { workflow, git_ref: gitRef.trim() })
276 const detail = await triggerRun(space, repo, { workflow, git_ref: gitRef.trim(), inputs: effectiveInputs })
212277 onTriggered(detail.run)
213278 } catch (err) {
214279 setError(err instanceof Error ? err.message : t('repo.pipeline.triggerFailed'))
@@ -234,7 +299,7 @@
234299 <select
235300 className={modalInputClass}
236301 value={workflow}
237 onChange={(event) => setWorkflowInput(event.target.value)}
302 onChange={(event) => changeWorkflow(event.target.value)}
238303 >
239304 {dispatchable.map((item) => (
240305 <option key={item.file} value={item.file}>
@@ -243,6 +308,20 @@
243308 ))}
244309 </select>
245310 </label>
311 {Object.keys(definitions).length > 0 ? (
312 <div className="mb-3 space-y-3">
313 <span className={modalLabelClass}>{t('repo.pipeline.inputs')}</span>
314 {Object.entries(definitions).map(([name, definition]) => (
315 <WorkflowInput
316 key={name}
317 name={name}
318 definition={definition}
319 value={effectiveInputs[name]}
320 onChange={(value) => changeInput(name, value)}
321 />
322 ))}
323 </div>
324 ) : null}
246325 {workflows.error ? (
247326 <Alert tone="error">
248327 {workflows.error instanceof Error ? workflows.error.message : t('repo.pipeline.workflowsFailed')}
@@ -251,12 +330,21 @@
251330 {!workflows.loading && dispatchable.length === 0 ? (
252331 <PanelNote text={t('repo.pipeline.noDispatchable')} />
253332 ) : null}
333 {missingInputs.length > 0 ? (
334 <Alert tone="error">
335 {missingInputs.map(([name]) => `${name}: ${t('repo.pipeline.inputRequired')}`).join('; ')}
336 </Alert>
337 ) : null}
254338 {error ? <Alert tone="error">{error}</Alert> : null}
255339 <div className="mt-3 flex justify-end gap-2">
256340 <Button size="default" variant="secondary" onClick={onClose}>
257341 {t('common.action.cancel')}
258342 </Button>
259 <Button size="default" disabled={busy || !workflow || !gitRef.trim()} onClick={submit}>
343 <Button
344 size="default"
345 disabled={busy || !workflow || !gitRef.trim() || missingInputs.length > 0}
346 onClick={submit}
347 >
260348 {busy ? t('repo.pipeline.triggering') : t('repo.pipeline.triggerConfirm')}
261349 </Button>
262350 </div>
@@ -264,6 +352,75 @@
264352 )
265353}
266354
355type WorkflowInputDefinition = NonNullable<WorkflowList['workflows'][number]['inputs']>[string]
356
357function WorkflowInput({
358 name,
359 definition,
360 value,
361 onChange,
362}: {
363 name: string
364 definition: WorkflowInputDefinition
365 value: InputValue | undefined
366 onChange: (value: InputValue) => void
367}) {
368 const { t } = useT()
369 const kind = definition.type ?? 'string'
370 const label = (
371 <span className={modalLabelClass}>
372 {name}
373 {definition.required ? ` · ${t('repo.pipeline.inputRequired')}` : ''}
374 </span>
375 )
376
377 if (kind === 'boolean') {
378 return (
379 <label className="flex items-center gap-2">
380 <input
381 type="checkbox"
382 checked={value === true}
383 onChange={(event) => onChange(event.target.checked)}
384 />
385 {label}
386 </label>
387 )
388 }
389
390 if (kind === 'choice' && definition.options?.length) {
391 return (
392 <label className="block">
393 {label}
394 <select
395 className={modalInputClass}
396 value={typeof value === 'string' ? value : ''}
397 onChange={(event) => onChange(event.target.value)}
398 >
399 {definition.options.map((option) => (
400 <option key={option} value={option}>{option}</option>
401 ))}
402 </select>
403 </label>
404 )
405 }
406
407 return (
408 <label className="block">
409 {label}
410 {definition.description ? (
411 <span className="mb-1 block text-[11.5px] text-shell-ink-muted">{definition.description}</span>
412 ) : null}
413 <input
414 className={modalInputClass}
415 type={kind === 'number' ? 'number' : 'text'}
416 value={value === undefined ? '' : String(value)}
417 onChange={(event) =>
418 onChange(kind === 'number' ? Number(event.target.value || 0) : event.target.value)
419 }
420 />
421 </label>
422 )
423}
267424function RunDetailView({
268425 space,
269426 repo,
@@ -285,7 +442,7 @@
285442 const next = await fetchRun(space, repo, runId)
286443 setDetail(next)
287444 setError(null)
288 setSelectedJob((current) => current || next.jobs[0]?.id || '')
445 setSelectedJob((current) => current || next.jobs.find((job) => !job.is_template)?.id || '')
289446 } catch (err) {
290447 setError(err instanceof Error ? err.message : t('repo.error.loadFailed'))
291448 }
@@ -320,6 +477,7 @@
320477 const finished = detail
321478 ? ['success', 'failure', 'cancelled'].includes(detail.run.state)
322479 : false
480 const jobs = detail ? detail.jobs.filter((job) => !job.is_template) : []
323481
324482 return (
325483 <div className="space-y-4 pt-2">
@@ -330,7 +488,14 @@
330488 {detail ? (
331489 <>
332490 <StateBadge state={detail.run.state} />
333 <span className="text-[14px] font-semibold">{detail.run.workflow_name}</span>
491 {detail.run.run_number ? (
492 <span className="text-[12.5px] text-shell-ink-muted">
493 {t('repo.pipeline.runNumber', { number: detail.run.run_number })}
494 </span>
495 ) : null}
496 <span className="text-[14px] font-semibold">
497 {detail.run.display_name ?? detail.run.workflow_name}
498 </span>
334499 <span className="text-[12.5px] text-shell-ink-muted">
335500 {detail.run.event} · {detail.run.branch ?? detail.run.tag ?? detail.run.git_ref} ·{' '}
336501 {shortSha(detail.run.commit_sha)}
@@ -358,70 +523,82 @@
358523 {!detail ? <PanelNote text={t('repo.pipeline.detailLoading')} /> : null}
359524
360525 {detail ? (
361 <div className="grid gap-4 lg:grid-cols-[260px_minmax(0,1fr)]">
526 <div className="grid gap-4 lg:grid-cols-[280px_minmax(0,1fr)]">
362527 <div className="space-y-2">
363528 <h4 className="text-[13px] font-semibold text-shell-ink-muted">{t('repo.pipeline.jobs')}</h4>
364529 <ul className="space-y-1.5">
365 {detail.jobs.map((job) => (
366 <li key={job.id}>
367 <button
368 type="button"
369 onClick={() => setSelectedJob(job.id)}
370 className={`w-full rounded-lg border px-3 py-2 text-left text-[13px] transition-colors ${
371 selectedJob === job.id
372 ? 'border-shell-tile bg-shell-field'
373 : 'border-shell-hairline hover:border-shell-tile'
374 }`}
375 >
376 <div className="flex items-center justify-between gap-2">
377 <span className="truncate font-medium">{job.name}</span>
378 <StateBadge state={job.state} />
379 </div>
380 <div className="mt-1 text-[11.5px] text-shell-ink-muted">
381 {job.runs_on.join(', ')}
382 {job.environment ? ` · ${job.environment}` : ''}
383 </div>
384 </button>
385 </li>
386 ))}
530 {jobs.map((job) => {
531 const matrix = jsonEntries(job.matrix)
532 return (
533 <li key={job.id}>
534 <button
535 type="button"
536 onClick={() => setSelectedJob(job.id)}
537 className={`w-full rounded-lg border px-3 py-2 text-left text-[13px] transition-colors ${
538 selectedJob === job.id
539 ? 'border-shell-tile bg-shell-field'
540 : 'border-shell-hairline hover:border-shell-tile'
541 }`}
542 >
543 <div className="flex items-center justify-between gap-2">
544 <span className="truncate font-medium">{job.name}</span>
545 <StateBadge state={job.state} />
546 </div>
547 <div className="mt-1 text-[11.5px] text-shell-ink-muted">
548 {job.runs_on.join(', ')}
549 {job.environment ? ` · ${job.environment}` : ''}
550 </div>
551 {matrix.length > 0 ? (
552 <div className="mt-1 flex flex-wrap gap-1">
553 {matrix.map(([name, value]) => (
554 <span key={name} className="rounded bg-shell-field px-1.5 py-0.5 text-[11px]">
555 {name}: {formatValue(value)}
556 </span>
557 ))}
558 </div>
559 ) : null}
560 </button>
561 </li>
562 )
563 })}
387564 </ul>
388565
389566 {detail.artifacts.length > 0 ? (
390567 <>
391568 <h4 className="pt-2 text-[13px] font-semibold text-shell-ink-muted">{t('repo.pipeline.artifacts')}</h4>
392569 <ul className="space-y-1.5">
393 {detail.artifacts.map((artifact) => (
394 <li
395 key={artifact.id}
396 className="flex items-center justify-between gap-2 rounded-lg border border-shell-hairline px-3 py-2 text-[12.5px]"
397 >
398 <span className="truncate">{artifact.name}</span>
399 {artifact.state === 'ready' ? (
400 <button
401 type="button"
402 className="flex items-center gap-1 text-sky-400 hover:underline"
403 onClick={() =>
404 window.open(
405 `/api/v1/repos/${space}/${repo}/pipelines/artifacts/${artifact.id}/download`,
406 '_blank',
407 'noopener,noreferrer',
408 )
409 }
410 >
411 <Download className="h-3.5 w-3.5" /> {t('common.action.download')}
412 </button>
413 ) : (
414 <span className="text-shell-ink-muted">{STATE_LABEL[artifact.state] ?? artifact.state}</span>
415 )}
416 </li>
417 ))}
570 {detail.artifacts.map((artifact) => {
571 const href = artifact.actions_id
572 ? `/api/v1/repos/${space}/${repo}/pipelines/actions-artifacts/${artifact.actions_id}/download`
573 : `/api/v1/repos/${space}/${repo}/pipelines/artifacts/${artifact.id}/download`
574 return (
575 <li
576 key={artifact.id}
577 className="flex items-center justify-between gap-2 rounded-lg border border-shell-hairline px-3 py-2 text-[12.5px]"
578 >
579 <span className="truncate">{artifact.name}</span>
580 {artifact.state === 'ready' ? (
581 <button
582 type="button"
583 className="flex items-center gap-1 text-sky-400 hover:underline"
584 onClick={() => window.open(href, '_blank', 'noopener,noreferrer')}
585 >
586 <Download className="h-3.5 w-3.5" /> {t('common.action.download')}
587 </button>
588 ) : (
589 <span className="text-shell-ink-muted">{STATE_LABEL[artifact.state] ?? artifact.state}</span>
590 )}
591 </li>
592 )
593 })}
418594 </ul>
419595 </>
420596 ) : null}
421597 </div>
422598
423599 <div className="min-w-0 space-y-3">
424 {detail.jobs
600 <ApprovalsPanel space={space} repo={repo} runId={runId} />
601 {jobs
425602 .filter((job) => job.id === selectedJob)
426603 .map((job) => (
427604 <JobPanel key={job.id} space={space} repo={repo} runId={runId} job={job} />
@@ -433,6 +610,87 @@
433610 )
434611}
435612
613function ApprovalsPanel({ space, repo, runId }: { space: string; repo: string; runId: string }) {
614 const { t } = useT()
615 const [tick, setTick] = useState(0)
616 const [busy, setBusy] = useState<string | null>(null)
617 const [error, setError] = useState<string | null>(null)
618 const approvals = useAsync(
619 useCallback(() => fetchApprovals(space, repo, runId), [space, repo, runId]),
620 [space, repo, runId, tick],
621 )
622
623 const submit = async (reviewId: string, approved: boolean) => {
624 setBusy(reviewId)
625 setError(null)
626 try {
627 await reviewApproval(space, repo, runId, reviewId, { approved })
628 setTick((value) => value + 1)
629 } catch (err) {
630 setError(err instanceof Error ? err.message : t('repo.pipeline.approvalFailed'))
631 } finally {
632 setBusy(null)
633 }
634 }
635
636 return (
637 <section className="rounded-xl border border-shell-hairline bg-shell-surface p-3">
638 <h4 className="mb-2 text-[13px] font-semibold">{t('repo.pipeline.approvals')}</h4>
639 {approvals.error ? (
640 <Alert tone="error">
641 {approvals.error instanceof Error ? approvals.error.message : t('repo.pipeline.approvalFailed')}
642 </Alert>
643 ) : null}
644 {approvals.loading && !approvals.data ? <PanelNote text={t('repo.pipeline.detailLoading')} /> : null}
645 {approvals.data?.length === 0 ? <PanelNote text={t('repo.pipeline.noApprovals')} /> : null}
646 {approvals.data?.length ? (
647 <ul className="space-y-2">
648 {approvals.data.map((approval) => (
649 <li key={approval.id} className="flex flex-wrap items-center gap-2 text-[12.5px]">
650 <span className="font-medium">{approval.environment}</span>
651 <span
652 className={`rounded-md px-2 py-0.5 text-[12px] ${
653 approval.state === 'approved'
654 ? 'bg-emerald-500/12 text-emerald-500'
655 : approval.state === 'rejected'
656 ? 'bg-red-500/12 text-red-400'
657 : 'bg-amber-500/12 text-amber-500'
658 }`}
659 >
660 {approval.state === 'approved'
661 ? t('repo.pipeline.approvalApproved')
662 : approval.state === 'rejected'
663 ? t('repo.pipeline.approvalRejected')
664 : t('repo.pipeline.approvalPending')}
665 </span>
666 <span className="text-shell-ink-muted">{fmtTime(approval.eligible_at)}</span>
667 {approval.state === 'pending' ? (
668 <span className="ml-auto flex gap-2">
669 <Button
670 size="default"
671 disabled={busy === approval.id}
672 onClick={() => void submit(approval.id, true)}
673 >
674 {t('repo.pipeline.approve')}
675 </Button>
676 <Button
677 size="default"
678 variant="secondary"
679 disabled={busy === approval.id}
680 onClick={() => void submit(approval.id, false)}
681 >
682 {t('repo.pipeline.reject')}
683 </Button>
684 </span>
685 ) : null}
686 </li>
687 ))}
688 </ul>
689 ) : null}
690 {error ? <Alert tone="error">{error}</Alert> : null}
691 </section>
692 )
693}
436694function JobPanel({
437695 space,
438696 repo,
@@ -484,15 +742,28 @@
484742 {job.error ? <span className="text-[12px] text-red-400">{job.error}</span> : null}
485743 </div>
486744
487 <ol className="space-y-1.5">
745 <KeyValueBlock title={t('repo.pipeline.outputs')} value={job.outputs} />
746
747 <ol className="space-y-2">
488748 {job.steps.map((step) => (
489 <li
490 key={step.index}
491 className="flex items-center gap-3 rounded-lg border border-shell-hairline px-3 py-2 text-[12.5px]"
492 >
493 <span className="w-6 text-shell-ink-muted">{step.index + 1}</span>
494 <span className="min-w-0 flex-1 truncate">{step.name}</span>
495 <StateBadge state={step.state} />
749 <li key={step.index} className="space-y-2 rounded-lg border border-shell-hairline px-3 py-2 text-[12.5px]">
750 <div className="flex items-center gap-3">
751 <span className="w-6 text-shell-ink-muted">{step.index + 1}</span>
752 <span className="min-w-0 flex-1 truncate">{step.name}</span>
753 <StateBadge state={step.state} />
754 </div>
755 <KeyValueBlock title={t('repo.pipeline.outputs')} value={step.outputs} />
756 <AnnotationList value={step.annotations} />
757 {step.summary ? (
758 <div>
759 <span className="text-[11.5px] font-medium text-shell-ink-muted">
760 {t('repo.pipeline.summary')}
761 </span>
762 <pre className="mt-1 max-h-48 overflow-auto whitespace-pre-wrap rounded-lg bg-black/25 px-2 py-2 text-[12px] leading-5">
763 {step.summary}
764 </pre>
765 </div>
766 ) : null}
496767 </li>
497768 ))}
498769 </ol>
@@ -507,4 +778,43 @@
507778 )
508779}
509780
781function KeyValueBlock({ title, value }: { title: string; value: unknown }) {
782 const entries = jsonEntries(value)
783 if (entries.length === 0) return null
784 return (
785 <div>
786 <span className="text-[11.5px] font-medium text-shell-ink-muted">{title}</span>
787 <dl className="mt-1 flex flex-wrap gap-1.5">
788 {entries.map(([name, item]) => (
789 <div key={name} className="rounded bg-shell-field px-2 py-1 text-[11.5px]">
790 <dt className="inline text-shell-ink-muted">{name}: </dt>
791 <dd className="inline font-medium">{formatValue(item)}</dd>
792 </div>
793 ))}
794 </dl>
795 </div>
796 )
797}
510798
799function AnnotationList({ value }: { value: unknown }) {
800 const { t } = useT()
801 const items = Array.isArray(value) ? value.filter((item) => item && typeof item === 'object') : []
802 if (items.length === 0) return null
803 return (
804 <div>
805 <span className="text-[11.5px] font-medium text-shell-ink-muted">{t('repo.pipeline.annotations')}</span>
806 <ul className="mt-1 space-y-1">
807 {items.map((item, index) => {
808 const record = item as JsonRecord
809 const title = formatValue(record.title ?? 'annotation')
810 const text = formatValue(record.text ?? record.message ?? record.description)
811 return (
812 <li key={index} className="rounded bg-amber-500/10 px-2 py-1 text-[11.5px] text-amber-500">
813 {text ? `${title}: ${text}` : title}
814 </li>
815 )
816 })}
817 </ul>
818 </div>
819 )
820}
No newline at end of file
Modified web/src/features/repo-settings/ui/sections/ci-settings.tsx View web/src/features/repo-settings/ui/sections/ci-settings.tsx
@@ -13,7 +13,7 @@
1313import { Field, ListRow, Notice, SectionHead, inputCls } from '../parts'
1414
1515/**
16 * CI/CD settings: workflow definitions found in `.workflows/` and repo-level
16 * CI/CD settings: workflow definitions found in `.github/workflows/` and repo-level
1717 * secrets. Secret values are write-only — the API never returns them.
1818 */
1919export function CiSettings({ space, repo, rev }: { space: string; repo: string; rev: string }) {
Modified web/src/shared/api/apiDefinitions.ts View web/src/shared/api/apiDefinitions.ts
@@ -430,6 +430,8 @@
430430 'diffs.diff_for_commit': ['GET', '/repos/{space_name}/{repo_name}/diff/for-commit/{commit}'],
431431 'diffs.range_diff': ['GET', '/repos/{space_name}/{repo_name}/diff/range'],
432432 'diffs.diff_raw': ['GET', '/repos/{space_name}/{repo_name}/diff/raw'],
433 'pipelines.environments': ['GET', '/repos/{space_name}/{repo_name}/environments'],
434 'pipelines.upsert_environment': ['PUT', '/repos/{space_name}/{repo_name}/environments/{name}'],
433435 'hooks.execute_script': ['POST', '/repos/{space_name}/{repo_name}/hooks/{script}'],
434436 'blobs.list_lfs_pointers': ['GET', '/repos/{space_name}/{repo_name}/lfs-pointers'],
435437 'lfs.list_locks': ['GET', '/repos/{space_name}/{repo_name}/lfs/locks'],
@@ -440,10 +442,17 @@
440442 'patches.patch_generate': ['GET', '/repos/{space_name}/{repo_name}/patch/generate'],
441443 'logs.path_log': ['GET', '/repos/{space_name}/{repo_name}/path-log'],
442444 'pipelines.list_runs': ['GET', '/repos/{space_name}/{repo_name}/pipelines'],
445 'pipelines.download_actions_artifact': [
446 'GET',
447 '/repos/{space_name}/{repo_name}/pipelines/actions-artifacts/{artifact_id}/download'
448 ],
443449 'pipelines.download_artifact': ['GET', '/repos/{space_name}/{repo_name}/pipelines/artifacts/{artifact_id}/download'],
450 'pipelines.dispatch': ['POST', '/repos/{space_name}/{repo_name}/pipelines/dispatches'],
444451 'pipelines.trigger': ['POST', '/repos/{space_name}/{repo_name}/pipelines/trigger'],
445452 'pipelines.list_workflows': ['GET', '/repos/{space_name}/{repo_name}/pipelines/workflows'],
446453 'pipelines.get_run': ['GET', '/repos/{space_name}/{repo_name}/pipelines/{run_id}'],
454 'pipelines.approvals': ['GET', '/repos/{space_name}/{repo_name}/pipelines/{run_id}/approvals'],
455 'pipelines.review': ['POST', '/repos/{space_name}/{repo_name}/pipelines/{run_id}/approvals/{review_id}'],
447456 'pipelines.list_artifacts': ['GET', '/repos/{space_name}/{repo_name}/pipelines/{run_id}/artifacts'],
448457 'pipelines.cancel_run': ['POST', '/repos/{space_name}/{repo_name}/pipelines/{run_id}/cancel'],
449458 'pipelines.read_logs': ['GET', '/repos/{space_name}/{repo_name}/pipelines/{run_id}/jobs/{job_id}/logs'],
@@ -547,6 +556,9 @@
547556 'repos.remove_topic_from_repo': ['DELETE', '/repos/{space_name}/{repo_name}/topics/{topic_name}'],
548557 'trees.get_tree': ['GET', '/repos/{space_name}/{repo_name}/tree/{revision}'],
549558 'repos.unarchive_repo': ['POST', '/repos/{space_name}/{repo_name}/unarchive'],
559 'pipelines.variables': ['GET', '/repos/{space_name}/{repo_name}/variables'],
560 'pipelines.upsert_variable': ['PUT', '/repos/{space_name}/{repo_name}/variables/{name}'],
561 'pipelines.delete_variable': ['DELETE', '/repos/{space_name}/{repo_name}/variables/{name}'],
550562 'repos.upsert_watch': ['PUT', '/repos/{space_name}/{repo_name}/watch'],
551563 'repos.list_watchers': ['GET', '/repos/{space_name}/{repo_name}/watchers'],
552564 'search.global_search': ['GET', '/search'],
Modified web/src/shared/api/globals.d.ts View web/src/shared/api/globals.d.ts

Diff too large to display.

Modified web/src/shared/i18n/messages/en/repo.ts View web/src/shared/i18n/messages/en/repo.ts
@@ -323,7 +323,7 @@
323323 'repo.release.titleOptional': 'Title (optional)',
324324 'repo.release.saveAsDraft': 'Save as a draft first',
325325 'repo.pipeline.heading': 'Pipelines',
326 'repo.pipeline.subtitle': 'Driven by definitions in .workflows/; runners pick up jobs through the scheduler.',
326 'repo.pipeline.subtitle': 'Driven by GitHub Actions-compatible workflows in .github/workflows/; runners pick up jobs through the scheduler.',
327327 'repo.pipeline.state.pending': 'Waiting on dependencies',
328328 'repo.pipeline.state.claimed': 'Claimed',
329329 'repo.pipeline.state.timedOut': 'Timed out',
@@ -331,7 +331,7 @@
331331 'repo.pipeline.state.ready': 'Ready',
332332 'repo.pipeline.state.uploading': 'Uploading',
333333 'repo.pipeline.runsLoading': 'Loading runs…',
334 'repo.pipeline.runsEmpty': 'No runs yet. Push a commit containing .workflows/*.yml, or trigger one here.',
334 'repo.pipeline.runsEmpty': 'No runs yet. Push a commit containing .github/workflows/*.yml, or trigger one here.',
335335 'repo.pipeline.triggerFailed': 'Failed to trigger',
336336 'repo.pipeline.triggerTitle': 'Manually trigger workflow_dispatch',
337337 'repo.pipeline.ref': 'Branch or tag',
@@ -343,6 +343,25 @@
343343 'repo.pipeline.detailLoading': 'Loading run details…',
344344 'repo.pipeline.attempt': 'Attempt {count}',
345345 'repo.pipeline.noLogs': '(no log output yet)',
346 'repo.pipeline.runNumber': '#{number}',
347 'repo.pipeline.matrix': 'Matrix',
348 'repo.pipeline.outputs': 'Outputs',
349 'repo.pipeline.annotations': 'Annotations',
350 'repo.pipeline.summary': 'Summary',
351 'repo.pipeline.emptyOutputs': 'No outputs',
352 'repo.pipeline.emptyAnnotations': 'No annotations',
353 'repo.pipeline.inputs': 'Inputs',
354 'repo.pipeline.inputRequired': 'Required',
355 'repo.pipeline.approvals': 'Environment approvals',
356 'repo.pipeline.noApprovals': 'No environment approvals for this run',
357 'repo.pipeline.approvalPending': 'Pending',
358 'repo.pipeline.approvalApproved': 'Approved',
359 'repo.pipeline.approvalRejected': 'Rejected',
360 'repo.pipeline.approve': 'Approve',
361 'repo.pipeline.reject': 'Reject',
362 'repo.pipeline.approvalFailed': 'Approval action failed',
363 'repo.pipeline.booleanTrue': 'Yes',
364 'repo.pipeline.booleanFalse': 'No',
346365 'repo.ai.triggerSummaryFailed': 'Failed to trigger the AI summary',
347366 'repo.ai.queued': 'Summary job submitted; click “Load results” once it finishes.',
348367 'repo.ai.loading': 'Loading the AI summary…',
@@ -601,10 +620,10 @@
601620 'repo.ci.secretSaved': 'Secret saved',
602621 'repo.ci.secretDeleted': 'Secret deleted',
603622 'repo.ci.deleteFailed': 'Failed to delete',
604 'repo.ci.sub': 'Workflow definitions come from the .workflows/ directory of the default branch (or the current revision); runners pick up jobs through the scheduler.',
623 'repo.ci.sub': 'Workflow definitions come from .github/workflows/ on the default branch (or current revision); runners pick up jobs through the scheduler.',
605624 'repo.ci.workflows': 'Workflows',
606 'repo.ci.workflowsLoading': 'Reading .workflows/…',
607 'repo.ci.workflowsEmpty': 'This revision has no .workflows/*.yml definitions.',
625 'repo.ci.workflowsLoading': 'Reading .github/workflows/…',
626 'repo.ci.workflowsEmpty': 'This revision has no .github/workflows/*.yml definitions.',
608627 'repo.ci.valid': 'Valid',
609628 'repo.ci.invalid': 'Parse failed',
610629 'repo.ci.dispatchable': 'Manually triggerable',
Modified web/src/shared/i18n/messages/zh-CN/repo.ts View web/src/shared/i18n/messages/zh-CN/repo.ts
@@ -323,7 +323,7 @@
323323 'repo.release.titleOptional': '标题(可选)',
324324 'repo.release.saveAsDraft': '先保存为草稿',
325325 'repo.pipeline.heading': '流水线',
326 'repo.pipeline.subtitle': '由 .workflows/ 中的定义驱动,runner 通过调度中心领取作业。',
326 'repo.pipeline.subtitle': '由 .github/workflows/ 中的 GitHub Actions 兼容工作流驱动,runner 通过调度中心领取作业。',
327327 'repo.pipeline.state.pending': '等待依赖',
328328 'repo.pipeline.state.claimed': '已领取',
329329 'repo.pipeline.state.timedOut': '超时',
@@ -331,7 +331,7 @@
331331 'repo.pipeline.state.ready': '就绪',
332332 'repo.pipeline.state.uploading': '上传中',
333333 'repo.pipeline.runsLoading': '正在加载运行记录…',
334 'repo.pipeline.runsEmpty': '还没有运行记录。推送包含 .workflows/*.yml 的提交,或在此手动触发。',
334 'repo.pipeline.runsEmpty': '还没有运行记录。推送包含 .github/workflows/*.yml 的提交,或在此手动触发。',
335335 'repo.pipeline.triggerFailed': '触发失败',
336336 'repo.pipeline.triggerTitle': '手动触发 workflow_dispatch',
337337 'repo.pipeline.ref': '分支或标签',
@@ -343,6 +343,25 @@
343343 'repo.pipeline.detailLoading': '正在加载运行详情…',
344344 'repo.pipeline.attempt': '第 {count} 次尝试',
345345 'repo.pipeline.noLogs': '(暂无日志输出)',
346 'repo.pipeline.runNumber': '#{number}',
347 'repo.pipeline.matrix': 'Matrix',
348 'repo.pipeline.outputs': 'Outputs',
349 'repo.pipeline.annotations': 'Annotations',
350 'repo.pipeline.summary': 'Summary',
351 'repo.pipeline.emptyOutputs': '暂无 outputs',
352 'repo.pipeline.emptyAnnotations': '暂无 annotations',
353 'repo.pipeline.inputs': '输入参数',
354 'repo.pipeline.inputRequired': '必填',
355 'repo.pipeline.approvals': '环境审批',
356 'repo.pipeline.noApprovals': '当前运行没有环境审批记录',
357 'repo.pipeline.approvalPending': '等待审批',
358 'repo.pipeline.approvalApproved': '已批准',
359 'repo.pipeline.approvalRejected': '已拒绝',
360 'repo.pipeline.approve': '批准',
361 'repo.pipeline.reject': '拒绝',
362 'repo.pipeline.approvalFailed': '审批操作失败',
363 'repo.pipeline.booleanTrue': '是',
364 'repo.pipeline.booleanFalse': '否',
346365 'repo.ai.triggerSummaryFailed': '触发 AI 总结失败',
347366 'repo.ai.queued': '已提交总结任务;完成后点击“载入结果”查看。',
348367 'repo.ai.loading': '加载 AI 总结…',
@@ -601,10 +620,10 @@
601620 'repo.ci.secretSaved': 'secret 已保存',
602621 'repo.ci.secretDeleted': 'secret 已删除',
603622 'repo.ci.deleteFailed': '删除失败',
604 'repo.ci.sub': '工作流定义来自默认分支(或当前修订)的 .workflows/ 目录;runner 通过调度中心领取作业。',
623 'repo.ci.sub': '工作流定义来自默认分支(或当前修订)的 .github/workflows/ 目录;runner 通过调度中心领取作业。',
605624 'repo.ci.workflows': '工作流',
606 'repo.ci.workflowsLoading': '正在读取 .workflows/…',
607 'repo.ci.workflowsEmpty': '该修订下没有 .workflows/*.yml 定义。',
625 'repo.ci.workflowsLoading': '正在读取 .github/workflows/…',
626 'repo.ci.workflowsEmpty': '该修订下没有 .github/workflows/*.yml 定义。',
608627 'repo.ci.valid': '有效',
609628 'repo.ci.invalid': '解析失败',
610629 'repo.ci.dispatchable': '手动触发可用',
Modified web/openapi.json View web/openapi.json

Diff too large to display.