Add CI/CD tab and pipeline summary page to repository UI

Seto Elkahfi committed Apr 15, 2026 at 22:38 UTC 9e42334b54ba445c5186c626633b06b5ebfe6793
9 files changed +306 -6
app/controllers/blobs_controller.rb
+1
@@ -17,6 +17,7 @@ class BlobsController < ApplicationController
17 @raw_content = content
18 @filename = File.basename(@file_path)
19 @extension = File.extname(@filename).delete_prefix(".")
20 + @commit_count = GitRepositoryService.commit_count(@repository.disk_path, @branch)
21 @is_binary = content.encoding != Encoding::UTF_8 || !content.valid_encoding? || binary_content?(content)
22 @file_size = content.bytesize
23
app/controllers/commits_controller.rb
+1
@@ -19,6 +19,7 @@ class CommitsController < ApplicationController
19 limit: @per_page, offset: @offset
20 )
21 @total = GitRepositoryService.commit_count(@repository.disk_path, @branch)
22 + @commit_count = @total
23 @total_pages = (@total.to_f / @per_page).ceil
24 end
25
app/controllers/repositories_controller.rb
+36 -3
@@ -1,8 +1,8 @@
1 class RepositoriesController < ApplicationController
2 before_action :require_sign_in!, only: [ :new, :create ]
3 - before_action :load_owner, only: [ :show, :tree ]
4 - before_action :load_repository, only: [ :show, :tree ]
5 - before_action :ensure_can_read!, only: [ :show, :tree ]
3 + before_action :load_owner, only: [ :show, :tree, :cicd ]
4 + before_action :load_repository, only: [ :show, :tree, :cicd ]
5 + before_action :ensure_can_read!, only: [ :show, :tree, :cicd ]
6
7 def new
8 @repository = Repository.new
@@ -64,6 +64,7 @@ class RepositoriesController < ApplicationController
64 @tree = GitRepositoryService.list_tree(@repository.disk_path, branch, tree_path)
65 @current_path = tree_path
66 @path_parts = tree_path.to_s.split("/").reject(&:blank?)
67 + @commit_count = GitRepositoryService.commit_count(@repository.disk_path, branch)
68
69 # If it's actually a file, redirect to the blob view
70 if @tree.empty?
@@ -71,6 +72,38 @@ class RepositoriesController < ApplicationController
72 end
73 end
74
75 + def cicd
76 + @branch = @repository.default_branch
77 + @commit_count = if @repository.initialized? && GitRepositoryService.branch_exists?(@repository.disk_path, @branch)
78 + GitRepositoryService.commit_count(@repository.disk_path, @branch)
79 + else
80 + 0
81 + end
82 + @pipeline_summary = {
83 + success_rate: "98.4%",
84 + avg_duration: "6m 12s",
85 + weekly_runs: 27,
86 + last_deploy: "18 minutes ago"
87 + }
88 + @pipeline_runs = [
89 + { name: "Deploy production", ref: "main", sha: "8f3c1b2", status: "passed", duration: "5m 48s", started_at: "18 minutes ago", actor: "sigit" },
90 + { name: "PR validation", ref: "feature/repo-tabs", sha: "7bd91aa", status: "running", duration: "2m 14s", started_at: "now", actor: "naufal" },
91 + { name: "Nightly security scan", ref: "main", sha: "4c72e16", status: "passed", duration: "9m 03s", started_at: "7 hours ago", actor: "automation" },
92 + { name: "Deploy staging", ref: "release/apr", sha: "cb194ef", status: "failed", duration: "4m 51s", started_at: "yesterday", actor: "sigit" }
93 + ]
94 + @environments = [
95 + { name: "Production", url: "app.sigitsi.com", status: "healthy", version: "v1.24.0", updated_at: "18 minutes ago" },
96 + { name: "Staging", url: "staging.sigitsi.com", status: "degraded", version: "v1.25.0-rc.2", updated_at: "42 minutes ago" },
97 + { name: "Preview", url: "preview-482.sigitsi.com", status: "building", version: "feature/repo-tabs", updated_at: "2 minutes ago" }
98 + ]
99 + @checks = [
100 + { name: "RSpec", status: "passed", detail: "248 examples in 32s" },
101 + { name: "Brakeman", status: "passed", detail: "0 warnings" },
102 + { name: "Lint", status: "running", detail: "RuboCop on 84 files" },
103 + { name: "Deploy smoke test", status: "failed", detail: "Health endpoint timed out once" }
104 + ]
105 + end
106 +
107 private
108
109 def repository_params
app/views/blobs/show.html.erb
+23 -1
@@ -18,7 +18,29 @@
18 <span class="font-medium text-gray-100"><%= @filename %></span>
19 </nav>
20 <div class="flex items-center gap-0 -mb-px">
21 - <div class="tab-item active">Code</div>
21 + <%= link_to repository_path(@owner.username, @repository.name), class: "tab-item active flex items-center gap-1.5" do %>
22 + <svg class="w-4 h-4" viewBox="0 0 16 16" fill="currentColor">
23 + <path d="M2 2.5A2.5 2.5 0 0 1 4.5 0h8.75a.75.75 0 0 1 .75.75v12.5a.75.75 0 0 1-.75.75h-2.5a.75.75 0 0 1 0-1.5h1.75v-2h-8a1 1 0 0 0-.714 1.7.75.75 0 1 1-1.072 1.05A2.495 2.495 0 0 1 2 11.5Zm10.5-1h-8a1 1 0 0 0-1 1v6.708A2.486 2.486 0 0 1 4.5 9h8Z"/>
24 + </svg>
25 + Code
26 + <% end %>
27 + <%= link_to repository_commits_path(@owner.username, @repository.name, @branch),
28 + class: "tab-item flex items-center gap-1.5" do %>
29 + <svg class="w-4 h-4" viewBox="0 0 16 16" fill="currentColor">
30 + <path d="M11.93 8.5a4.002 4.002 0 0 1-7.86 0H.75a.75.75 0 0 1 0-1.5h3.32a4.002 4.002 0 0 1 7.86 0h3.32a.75.75 0 0 1 0 1.5Zm-1.43-.75a2.5 2.5 0 1 0-5 0 2.5 2.5 0 0 0 5 0Z"/>
31 + </svg>
32 + Commits
33 + <% end %>
34 + <%= link_to repository_cicd_path(@owner.username, @repository.name),
35 + class: "tab-item flex items-center gap-1.5" do %>
36 + <svg class="w-4 h-4" viewBox="0 0 16 16" fill="currentColor">
37 + <path d="M4.75 4.5C2.679 4.5 1 5.958 1 7.75S2.679 11 4.75 11c1.028 0 1.948-.36 2.75-1.242l.652-.716.696.672C10.023 10.849 10.956 11 11.25 11 13.321 11 15 9.542 15 7.75S13.321 4.5 11.25 4.5c-1.028 0-1.948.36-2.75 1.242l-.652.716-.696-.672C5.977 4.651 5.044 4.5 4.75 4.5Zm0 1.5c.483 0 .943.152 1.36.556l.562.541-.562.618C5.693 8.119 5.233 8.5 4.75 8.5c-1.241 0-2.25-.672-2.25-1.5S3.509 6 4.75 6Zm6.5 0c1.241 0 2.25.672 2.25 1.5S12.491 9 11.25 9c-.483 0-.943-.152-1.36-.556l-.562-.541.562-.618c.417-.404.877-.785 1.36-.785Z"/>
38 + </svg>
39 + CI/CD
40 + <% if @commit_count&.> 0 %>
41 + <span class="badge-gray text-xs"><%= number_with_delimiter(@commit_count) %></span>
42 + <% end %>
43 + <% end %>
44 </div>
45 </div>
46 </div>
app/views/commits/index.html.erb
+32 -2
@@ -3,16 +3,46 @@
3 <div class="border-b border-surface-600 bg-surface-800">
4 <div class="max-w-6xl mx-auto px-4 sm:px-6 pt-6 pb-0">
5 <div class="flex items-center gap-2 text-sm mb-4">
6 + <svg class="w-4 h-4 text-gray-500" viewBox="0 0 16 16" fill="currentColor">
7 + <path d="M2 2.5A2.5 2.5 0 0 1 4.5 0h8.75a.75.75 0 0 1 .75.75v12.5a.75.75 0 0 1-.75.75h-2.5a.75.75 0 0 1 0-1.5h1.75v-2h-8a1 1 0 0 0-.714 1.7.75.75 0 1 1-1.072 1.05A2.495 2.495 0 0 1 2 11.5Zm10.5-1h-8a1 1 0 0 0-1 1v6.708A2.486 2.486 0 0 1 4.5 9h8Z"/>
8 + </svg>
9 <%= link_to "@#{@owner.username}", user_profile_path(@owner.username), class: "text-brand-500 hover:underline font-medium" %>
10 <span class="text-gray-500">/</span>
11 <%= link_to @repository.name, repository_path(@owner.username, @repository.name), class: "text-brand-500 hover:underline font-semibold" %>
12 + <% if @repository.is_private %>
13 + <span class="badge-gray ml-1">Private</span>
14 + <% end %>
15 </div>
16 +
17 + <% if @repository.description.present? %>
18 + <p class="text-sm text-gray-400 mb-4"><%= @repository.description %></p>
19 + <% end %>
20 +
21 <div class="flex items-center gap-0 -mb-px">
11 - <%= link_to repository_path(@owner.username, @repository.name), class: "tab-item flex items-center gap-1.5" do %>Code<% end %>
22 + <%= link_to repository_path(@owner.username, @repository.name),
23 + class: "tab-item flex items-center gap-1.5" do %>
24 + <svg class="w-4 h-4" viewBox="0 0 16 16" fill="currentColor">
25 + <path d="M2 2.5A2.5 2.5 0 0 1 4.5 0h8.75a.75.75 0 0 1 .75.75v12.5a.75.75 0 0 1-.75.75h-2.5a.75.75 0 0 1 0-1.5h1.75v-2h-8a1 1 0 0 0-.714 1.7.75.75 0 1 1-1.072 1.05A2.495 2.495 0 0 1 2 11.5Zm10.5-1h-8a1 1 0 0 0-1 1v6.708A2.486 2.486 0 0 1 4.5 9h8Z"/>
26 + </svg>
27 + Code
28 + <% end %>
29 <div class="tab-item active flex items-center gap-1.5">
30 + <svg class="w-4 h-4" viewBox="0 0 16 16" fill="currentColor">
31 + <path d="M11.93 8.5a4.002 4.002 0 0 1-7.86 0H.75a.75.75 0 0 1 0-1.5h3.32a4.002 4.002 0 0 1 7.86 0h3.32a.75.75 0 0 1 0 1.5Zm-1.43-.75a2.5 2.5 0 1 0-5 0 2.5 2.5 0 0 0 5 0Z"/>
32 + </svg>
33 Commits
14 - <span class="badge-gray"><%= number_with_delimiter(@total) %></span>
34 + <span class="badge-gray text-xs"><%= number_with_delimiter(@total) %></span>
35 </div>
36 + <%= link_to repository_cicd_path(@owner.username, @repository.name),
37 + class: "tab-item flex items-center gap-1.5" do %>
38 + <svg class="w-4 h-4" viewBox="0 0 16 16" fill="currentColor">
39 + <path d="M4.75 4.5C2.679 4.5 1 5.958 1 7.75S2.679 11 4.75 11c1.028 0 1.948-.36 2.75-1.242l.652-.716.696.672C10.023 10.849 10.956 11 11.25 11 13.321 11 15 9.542 15 7.75S13.321 4.5 11.25 4.5c-1.028 0-1.948.36-2.75 1.242l-.652.716-.696-.672C5.977 4.651 5.044 4.5 4.75 4.5Zm0 1.5c.483 0 .943.152 1.36.556l.562.541-.562.618C5.693 8.119 5.233 8.5 4.75 8.5c-1.241 0-2.25-.672-2.25-1.5S3.509 6 4.75 6Zm6.5 0c1.241 0 2.25.672 2.25 1.5S12.491 9 11.25 9c-.483 0-.943-.152-1.36-.556l-.562-.541.562-.618c.417-.404.877-.785 1.36-.785Z"/>
40 + </svg>
41 + CI/CD
42 + <% if @commit_count&.> 0 %>
43 + <span class="badge-gray text-xs"><%= number_with_delimiter(@commit_count) %></span>
44 + <% end %>
45 + <% end %>
46 </div>
47 </div>
48 </div>
app/views/repositories/cicd.html.erb new
+185
@@ -0,0 +1,185 @@
1 +<% content_for :title, "CI/CD · #{@repository.full_name}" %>
2 +
3 +<div class="border-b border-surface-600 bg-surface-800">
4 + <div class="max-w-6xl mx-auto px-4 sm:px-6 pt-6 pb-0">
5 + <div class="flex items-center gap-2 text-sm mb-4">
6 + <svg class="w-4 h-4 text-gray-500" viewBox="0 0 16 16" fill="currentColor">
7 + <path d="M2 2.5A2.5 2.5 0 0 1 4.5 0h8.75a.75.75 0 0 1 .75.75v12.5a.75.75 0 0 1-.75.75h-2.5a.75.75 0 0 1 0-1.5h1.75v-2h-8a1 1 0 0 0-.714 1.7.75.75 0 1 1-1.072 1.05A2.495 2.495 0 0 1 2 11.5Zm10.5-1h-8a1 1 0 0 0-1 1v6.708A2.486 2.486 0 0 1 4.5 9h8Z"/>
8 + </svg>
9 + <%= link_to "@#{@owner.username}", user_profile_path(@owner.username),
10 + class: "text-brand-500 hover:underline font-medium" %>
11 + <span class="text-gray-500">/</span>
12 + <%= link_to @repository.name, repository_path(@owner.username, @repository.name),
13 + class: "text-brand-500 hover:underline font-semibold" %>
14 + <% if @repository.is_private %>
15 + <span class="badge-gray ml-1">Private</span>
16 + <% end %>
17 + </div>
18 +
19 + <% if @repository.description.present? %>
20 + <p class="text-sm text-gray-400 mb-4"><%= @repository.description %></p>
21 + <% end %>
22 +
23 + <div class="flex items-center gap-0 -mb-px">
24 + <%= link_to repository_path(@owner.username, @repository.name),
25 + class: "tab-item flex items-center gap-1.5" do %>
26 + <svg class="w-4 h-4" viewBox="0 0 16 16" fill="currentColor">
27 + <path d="M2 2.5A2.5 2.5 0 0 1 4.5 0h8.75a.75.75 0 0 1 .75.75v12.5a.75.75 0 0 1-.75.75h-2.5a.75.75 0 0 1 0-1.5h1.75v-2h-8a1 1 0 0 0-.714 1.7.75.75 0 1 1-1.072 1.05A2.495 2.495 0 0 1 2 11.5Zm10.5-1h-8a1 1 0 0 0-1 1v6.708A2.486 2.486 0 0 1 4.5 9h8Z"/>
28 + </svg>
29 + Code
30 + <% end %>
31 + <% if @repository.initialized? %>
32 + <%= link_to repository_commits_path(@owner.username, @repository.name, @repository.default_branch),
33 + class: "tab-item flex items-center gap-1.5" do %>
34 + <svg class="w-4 h-4" viewBox="0 0 16 16" fill="currentColor">
35 + <path d="M11.93 8.5a4.002 4.002 0 0 1-7.86 0H.75a.75.75 0 0 1 0-1.5h3.32a4.002 4.002 0 0 1 7.86 0h3.32a.75.75 0 0 1 0 1.5Zm-1.43-.75a2.5 2.5 0 1 0-5 0 2.5 2.5 0 0 0 5 0Z"/>
36 + </svg>
37 + Commits
38 + <% if @commit_count&.> 0 %>
39 + <span class="badge-gray text-xs"><%= number_with_delimiter(@commit_count) %></span>
40 + <% end %>
41 + <% end %>
42 + <% end %>
43 + <div class="tab-item active flex items-center gap-1.5">
44 + <svg class="w-4 h-4" viewBox="0 0 16 16" fill="currentColor">
45 + <path d="M4.75 4.5C2.679 4.5 1 5.958 1 7.75S2.679 11 4.75 11c1.028 0 1.948-.36 2.75-1.242l.652-.716.696.672C10.023 10.849 10.956 11 11.25 11 13.321 11 15 9.542 15 7.75S13.321 4.5 11.25 4.5c-1.028 0-1.948.36-2.75 1.242l-.652.716-.696-.672C5.977 4.651 5.044 4.5 4.75 4.5Zm0 1.5c.483 0 .943.152 1.36.556l.562.541-.562.618C5.693 8.119 5.233 8.5 4.75 8.5c-1.241 0-2.25-.672-2.25-1.5S3.509 6 4.75 6Zm6.5 0c1.241 0 2.25.672 2.25 1.5S12.491 9 11.25 9c-.483 0-.943-.152-1.36-.556l-.562-.541.562-.618c.417-.404.877-.785 1.36-.785Z"/>
46 + </svg>
47 + CI/CD
48 + <% if @commit_count&.> 0 %>
49 + <span class="badge-gray text-xs"><%= number_with_delimiter(@commit_count) %></span>
50 + <% end %>
51 + </div>
52 + </div>
53 + </div>
54 +</div>
55 +
56 +<div class="max-w-6xl mx-auto px-4 sm:px-6 py-6">
57 + <div class="grid gap-6 lg:grid-cols-[1.35fr_0.95fr]">
58 + <div class="space-y-6">
59 + <div class="grid gap-4 sm:grid-cols-2 xl:grid-cols-4">
60 + <div class="card p-5">
61 + <p class="text-xs uppercase tracking-[0.18em] text-gray-500">Success Rate</p>
62 + <p class="mt-3 text-2xl font-semibold text-gray-100"><%= @pipeline_summary[:success_rate] %></p>
63 + <p class="mt-1 text-xs text-emerald-400">+1.2% in the last 7 days</p>
64 + </div>
65 + <div class="card p-5">
66 + <p class="text-xs uppercase tracking-[0.18em] text-gray-500">Avg Duration</p>
67 + <p class="mt-3 text-2xl font-semibold text-gray-100"><%= @pipeline_summary[:avg_duration] %></p>
68 + <p class="mt-1 text-xs text-gray-400">From build to deploy</p>
69 + </div>
70 + <div class="card p-5">
71 + <p class="text-xs uppercase tracking-[0.18em] text-gray-500">Runs This Week</p>
72 + <p class="mt-3 text-2xl font-semibold text-gray-100"><%= @pipeline_summary[:weekly_runs] %></p>
73 + <p class="mt-1 text-xs text-gray-400">Across all branches</p>
74 + </div>
75 + <div class="card p-5">
76 + <p class="text-xs uppercase tracking-[0.18em] text-gray-500">Last Deploy</p>
77 + <p class="mt-3 text-2xl font-semibold text-gray-100"><%= @pipeline_summary[:last_deploy] %></p>
78 + <p class="mt-1 text-xs text-brand-400">Production is live</p>
79 + </div>
80 + </div>
81 +
82 + <div class="card overflow-hidden">
83 + <div class="flex items-center justify-between gap-4 border-b border-surface-600 px-5 py-4 bg-surface-600">
84 + <div>
85 + <h2 class="text-sm font-semibold text-gray-100">Recent Pipeline Runs</h2>
86 + <p class="mt-1 text-xs text-gray-400">Latest activity for <span class="text-gray-200"><%= @repository.full_name %></span></p>
87 + </div>
88 + <span class="badge-gray text-xs"><%= @branch %></span>
89 + </div>
90 +
91 + <div>
92 + <% @pipeline_runs.each do |run| %>
93 + <% status_classes = case run[:status]
94 + when "passed" then "bg-emerald-500/12 text-emerald-300 ring-1 ring-inset ring-emerald-500/20"
95 + when "running" then "bg-sky-500/12 text-sky-300 ring-1 ring-inset ring-sky-500/20"
96 + else "bg-rose-500/12 text-rose-300 ring-1 ring-inset ring-rose-500/20"
97 + end %>
98 + <div class="flex items-center justify-between gap-4 border-b border-surface-600/80 px-5 py-4 last:border-b-0">
99 + <div class="min-w-0">
100 + <div class="flex items-center gap-3">
101 + <p class="text-sm font-medium text-gray-100"><%= run[:name] %></p>
102 + <span class="rounded-full px-2 py-0.5 text-[11px] font-medium <%= status_classes %>"><%= run[:status].capitalize %></span>
103 + </div>
104 + <p class="mt-1 text-xs text-gray-400">
105 + <span class="font-mono text-brand-400"><%= run[:sha] %></span>
106 + on <span class="text-gray-200"><%= run[:ref] %></span>
107 + by <span class="text-gray-200"><%= run[:actor] %></span>
108 + </p>
109 + </div>
110 + <div class="shrink-0 text-right text-xs text-gray-400">
111 + <p><%= run[:duration] %></p>
112 + <p class="mt-1 text-gray-500"><%= run[:started_at] %></p>
113 + </div>
114 + </div>
115 + <% end %>
116 + </div>
117 + </div>
118 + </div>
119 +
120 + <div class="space-y-6">
121 + <div class="card overflow-hidden">
122 + <div class="border-b border-surface-600 px-5 py-4 bg-surface-600">
123 + <h2 class="text-sm font-semibold text-gray-100">Environments</h2>
124 + </div>
125 + <div class="space-y-0">
126 + <% @environments.each do |environment| %>
127 + <% env_classes = case environment[:status]
128 + when "healthy" then "bg-emerald-400"
129 + when "building" then "bg-sky-400"
130 + else "bg-amber-400"
131 + end %>
132 + <div class="border-b border-surface-600/80 px-5 py-4 last:border-b-0">
133 + <div class="flex items-center justify-between gap-3">
134 + <div class="flex items-center gap-3">
135 + <span class="h-2.5 w-2.5 rounded-full <%= env_classes %>"></span>
136 + <div>
137 + <p class="text-sm font-medium text-gray-100"><%= environment[:name] %></p>
138 + <p class="text-xs text-gray-400"><%= environment[:url] %></p>
139 + </div>
140 + </div>
141 + <span class="badge-gray text-xs"><%= environment[:version] %></span>
142 + </div>
143 + <p class="mt-3 text-xs text-gray-500">Updated <%= environment[:updated_at] %></p>
144 + </div>
145 + <% end %>
146 + </div>
147 + </div>
148 +
149 + <div class="card overflow-hidden">
150 + <div class="border-b border-surface-600 px-5 py-4 bg-surface-600">
151 + <h2 class="text-sm font-semibold text-gray-100">Checks</h2>
152 + </div>
153 + <div class="space-y-0">
154 + <% @checks.each do |check| %>
155 + <% icon = case check[:status]
156 + when "passed" then "text-emerald-400"
157 + when "running" then "text-sky-400"
158 + else "text-rose-400"
159 + end %>
160 + <div class="flex items-start gap-3 border-b border-surface-600/80 px-5 py-4 last:border-b-0">
161 + <svg class="w-4 h-4 mt-0.5 shrink-0 <%= icon %>" viewBox="0 0 16 16" fill="currentColor">
162 + <path d="M8 1.5a6.5 6.5 0 1 0 6.5 6.5A6.507 6.507 0 0 0 8 1.5Zm3.03 5.28-3.5 3.75a.75.75 0 0 1-1.08.02L4.72 8.81a.75.75 0 0 1 1.06-1.06l1.18 1.18 2.95-3.17a.75.75 0 1 1 1.12 1Z"/>
163 + </svg>
164 + <div class="min-w-0">
165 + <div class="flex items-center gap-2">
166 + <p class="text-sm font-medium text-gray-100"><%= check[:name] %></p>
167 + <span class="text-xs text-gray-500"><%= check[:status] %></span>
168 + </div>
169 + <p class="mt-1 text-xs text-gray-400"><%= check[:detail] %></p>
170 + </div>
171 + </div>
172 + <% end %>
173 + </div>
174 + </div>
175 +
176 + <div class="card p-5">
177 + <h2 class="text-sm font-semibold text-gray-100">Deployment Policy</h2>
178 + <p class="mt-2 text-sm text-gray-400">
179 + Pushes to <span class="font-mono text-gray-200">main</span> deploy automatically after all required checks pass.
180 + Release branches require one approval before staging promotion.
181 + </p>
182 + </div>
183 + </div>
184 + </div>
185 +</div>
app/views/repositories/show.html.erb
+10
@@ -42,6 +42,16 @@
42 <% end %>
43 <% end %>
44 <% end %>
45 + <%= link_to repository_cicd_path(@owner.username, @repository.name),
46 + class: "tab-item flex items-center gap-1.5" do %>
47 + <svg class="w-4 h-4" viewBox="0 0 16 16" fill="currentColor">
48 + <path d="M4.75 4.5C2.679 4.5 1 5.958 1 7.75S2.679 11 4.75 11c1.028 0 1.948-.36 2.75-1.242l.652-.716.696.672C10.023 10.849 10.956 11 11.25 11 13.321 11 15 9.542 15 7.75S13.321 4.5 11.25 4.5c-1.028 0-1.948.36-2.75 1.242l-.652.716-.696-.672C5.977 4.651 5.044 4.5 4.75 4.5Zm0 1.5c.483 0 .943.152 1.36.556l.562.541-.562.618C5.693 8.119 5.233 8.5 4.75 8.5c-1.241 0-2.25-.672-2.25-1.5S3.509 6 4.75 6Zm6.5 0c1.241 0 2.25.672 2.25 1.5S12.491 9 11.25 9c-.483 0-.943-.152-1.36-.556l-.562-.541.562-.618c.417-.404.877-.785 1.36-.785Z"/>
49 + </svg>
50 + CI/CD
51 + <% if @commit_count&.> 0 %>
52 + <span class="badge-gray text-xs"><%= number_with_delimiter(@commit_count) %></span>
53 + <% end %>
54 + <% end %>
55 </div>
56 </div>
57 </div>
app/views/repositories/tree.html.erb
+17
@@ -36,6 +36,23 @@
36 </svg>
37 Code
38 <% end %>
39 + <%= link_to repository_commits_path(@owner.username, @repository.name, @branch),
40 + class: "tab-item flex items-center gap-1.5" do %>
41 + <svg class="w-4 h-4" viewBox="0 0 16 16" fill="currentColor">
42 + <path d="M11.93 8.5a4.002 4.002 0 0 1-7.86 0H.75a.75.75 0 0 1 0-1.5h3.32a4.002 4.002 0 0 1 7.86 0h3.32a.75.75 0 0 1 0 1.5Zm-1.43-.75a2.5 2.5 0 1 0-5 0 2.5 2.5 0 0 0 5 0Z"/>
43 + </svg>
44 + Commits
45 + <% end %>
46 + <%= link_to repository_cicd_path(@owner.username, @repository.name),
47 + class: "tab-item flex items-center gap-1.5" do %>
48 + <svg class="w-4 h-4" viewBox="0 0 16 16" fill="currentColor">
49 + <path d="M4.75 4.5C2.679 4.5 1 5.958 1 7.75S2.679 11 4.75 11c1.028 0 1.948-.36 2.75-1.242l.652-.716.696.672C10.023 10.849 10.956 11 11.25 11 13.321 11 15 9.542 15 7.75S13.321 4.5 11.25 4.5c-1.028 0-1.948.36-2.75 1.242l-.652.716-.696-.672C5.977 4.651 5.044 4.5 4.75 4.5Zm0 1.5c.483 0 .943.152 1.36.556l.562.541-.562.618C5.693 8.119 5.233 8.5 4.75 8.5c-1.241 0-2.25-.672-2.25-1.5S3.509 6 4.75 6Zm6.5 0c1.241 0 2.25.672 2.25 1.5S12.491 9 11.25 9c-.483 0-.943-.152-1.36-.556l-.562-.541.562-.618c.417-.404.877-.785 1.36-.785Z"/>
50 + </svg>
51 + CI/CD
52 + <% if @commit_count&.> 0 %>
53 + <span class="badge-gray text-xs"><%= number_with_delimiter(@commit_count) %></span>
54 + <% end %>
55 + <% end %>
56 </div>
57 </div>
58 </div>
config/routes.rb
+1
@@ -33,6 +33,7 @@ Rails.application.routes.draw do
33 # Repository routes (in order of specificity)
34 get "/:username/:repository/commits/:branch", to: "commits#index", as: :repository_commits,
35 constraints: { branch: /[^\/]+/, repository: /[^\/.][^\/]*/ }
36 + get "/:username/:repository/ci-cd", to: "repositories#cicd", as: :repository_cicd
37 get "/:username/:repository/commit/:sha", to: "commits#show", as: :repository_commit
38 get "/:username/:repository/blob/:branch/*path", to: "blobs#show", as: :repository_blob, format: false
39 get "/:username/:repository/raw/:branch/*path", to: "blobs#raw", as: :repository_blob_raw, format: false