diff --git a/appview/pages/pipelines_parse_test.go b/appview/pages/pipelines_parse_test.go
new file mode 100644
index 000000000..4bfd0d846
--- /dev/null
+++ b/appview/pages/pipelines_parse_test.go
@@ -0,0 +1,17 @@
+package pages
+
+import (
+ "io"
+ "log/slog"
+ "testing"
+
+ "tangled.org/core/appview/config"
+ "tangled.org/core/idresolver"
+)
+
+func TestPipelinesTemplateParses(t *testing.T) {
+ p := NewPages(&config.Config{}, idresolver.DefaultResolver(""), nil, nil, slog.New(slog.NewTextHandler(io.Discard, nil)))
+ if _, err := p.rawParse("layouts/base", "layouts/repobase", "repo/pipelines/pipelines"); err != nil {
+ t.Fatalf("parse pipelines template: %v", err)
+ }
+}
diff --git a/appview/pages/templates/repo/pipelines/pipelines.html b/appview/pages/templates/repo/pipelines/pipelines.html
index 04f86600b..abbe09b1e 100644
--- a/appview/pages/templates/repo/pipelines/pipelines.html
+++ b/appview/pages/templates/repo/pipelines/pipelines.html
@@ -27,7 +27,13 @@
"Value" "Pull request"
"Icon" "git-pull-request"
"Meta" "") }}
- {{ $values := list $all $push $pr }}
+ {{ $schedule :=
+ (dict
+ "Key" "schedule"
+ "Value" "Schedule"
+ "Icon" "calendar-clock"
+ "Meta" "") }}
+ {{ $values := list $all $push $pr $schedule }}
@@ -121,6 +127,10 @@
{{ else if .Trigger.IsManual }}
{{ i "circle-play" "size-4 text-gray-500 dark:text-gray-400 shrink-0" }}
Manual dispatch
+ {{ else if .Trigger.IsSchedule }}
+ {{ i "calendar-clock" "size-4 text-gray-500 dark:text-gray-400 shrink-0" }}
+ Scheduled run on
+ {{ $target }}
{{ end }}
{{ if .IsResponding }}
diff --git a/appview/pipelines/pipelines.go b/appview/pipelines/pipelines.go
index dc3d328e6..a1d1f870d 100644
--- a/appview/pipelines/pipelines.go
+++ b/appview/pipelines/pipelines.go
@@ -154,7 +154,7 @@ func (p *Pipelines) Index(w http.ResponseWriter, r *http.Request) {
filterKind := r.URL.Query().Get("trigger")
var kinds []string
switch filterKind {
- case "push", "pull_request":
+ case "push", "pull_request", "schedule":
kinds = []string{filterKind}
default:
filterKind = "all"
diff --git a/appview/pipelines/ssh/tui.go b/appview/pipelines/ssh/tui.go
index b06f8441c..10500c852 100644
--- a/appview/pipelines/ssh/tui.go
+++ b/appview/pipelines/ssh/tui.go
@@ -463,6 +463,9 @@ func triggerLine(r *lipgloss.Renderer, t *tangled.CiPipeline_Trigger, sha string
}
return t.CiTrigger_PullRequest.TargetBranch + dim.Render(" <- "+source+"@"+hash) + dim.Render(" (pull-request)")
}
+ if t.CiTrigger_Schedule != nil {
+ return t.CiTrigger_Schedule.Ref + dim.Render("@"+hash) + dim.Render(" (schedule)")
+ }
return dim.Render(hash)
}
diff --git a/types/pipeline.go b/types/pipeline.go
index fa27cca67..437c99535 100644
--- a/types/pipeline.go
+++ b/types/pipeline.go
@@ -119,6 +119,10 @@ func (t Trigger) IsManual() bool {
return t.CiPipeline_Trigger != nil && t.CiPipeline_Trigger.CiTrigger_Manual != nil
}
+func (t Trigger) IsSchedule() bool {
+ return t.CiPipeline_Trigger != nil && t.CiPipeline_Trigger.CiTrigger_Schedule != nil
+}
+
func (t Trigger) TargetRef() string {
if t.CiPipeline_Trigger == nil {
return ""
@@ -136,6 +140,13 @@ func (t Trigger) TargetRef() string {
if t.CiPipeline_Trigger.CiTrigger_PullRequest != nil {
return t.CiPipeline_Trigger.CiTrigger_PullRequest.TargetBranch
}
+ if t.CiPipeline_Trigger.CiTrigger_Schedule != nil {
+ ref := t.CiPipeline_Trigger.CiTrigger_Schedule.Ref
+ if after, ok := strings.CutPrefix(ref, "refs/heads/"); ok {
+ return after
+ }
+ return ref
+ }
return ""
}
diff --git a/types/pipeline_test.go b/types/pipeline_test.go
index dedac1e51..a26a8bd6d 100644
--- a/types/pipeline_test.go
+++ b/types/pipeline_test.go
@@ -28,3 +28,20 @@ func TestPipelinesByCommitKeepsNewestFirstPipeline(t *testing.T) {
t.Fatalf("other commit mapped to pipeline %q, want other", got["other"].Id())
}
}
+
+func TestScheduleTrigger(t *testing.T) {
+ trigger := Trigger{CiPipeline_Trigger: &tangled.CiPipeline_Trigger{
+ CiTrigger_Schedule: &tangled.CiTrigger_Schedule{
+ Ref: "refs/heads/main",
+ Sha: "5c378116b9d0348ef6bd790240dee66f61acaccf",
+ ScheduledAt: "2026-08-10T12:39:00Z",
+ },
+ }}
+
+ if !trigger.IsSchedule() {
+ t.Fatal("schedule trigger was not recognized")
+ }
+ if got := trigger.TargetRef(); got != "main" {
+ t.Fatalf("TargetRef() = %q, want main", got)
+ }
+}