Skip to content
1,032 linesCodeBlameRaw

Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.

The sidebar asks billing whether it is on, not for the account, so no page waits on the card processor1//! `github.event`: the webhook-shaped payload GitHub gives a workflow,
2//! built from g1t's own records so `github.event.pull_request.number`,
3//! `github.event.issue.labels.*.name` and the like read as they do there.
Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9'4//!
5//! Ids are as GitHub's are: `id` a whole number, the same for an object
6//! wherever it appears ([`github_id`] of its g1t id, [`account_id`] of a
7//! person's or workspace's name), and `node_id` g1t's own id. What g1t's
8//! API addresses by its own id keeps that id as `id`, so a payload's id
9//! can be passed straight back to the API: a comment (`PATCH
10//! /repos/{owner}/{repo}/issues/comments/{comment_id}`), a release, a
11//! deployment and its statuses, and a workflow run.
The sidebar asks billing whether it is on, not for the account, so no page waits on the card processor12
Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9'13use g1t_actions::events::{account_id, github_id};
The sidebar asks billing whether it is on, not for the account, so no page waits on the card processor14use g1t_contracts::{PrincipalKind, User};
Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9'15use g1t_contracts::repos::{Commit, Repo, RepoPath};
Merge GitHub pull request #8 (claude/cool-newton-7nm8r9): Docker deployments on g1t.page behind a switch that stays off until Cloudflare's container APIs are checked live, workflows a workspace manages in its repositories, tarball and zipball downloads, refs/pull/* kept to g1t alone, framework detection for g1t.page builds, and a workflow's event payload matching GitHub's.16use g1t_contracts::repos::FileStatus;
17use g1t_contracts::work::{Comment, DEFAULT_LABELS, Issue, Label, MilestoneRef, Pull, PullStatus, State, label_color_for};
The sidebar asks billing whether it is on, not for the account, so no page waits on the card processor18use serde_json::{Value, json};
19
20use crate::{API, SITE};
21
Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9'22/// The commit hash GitHub gives for no commit: a branch made or deleted.
23pub const ZERO: &str = "0000000000000000000000000000000000000000";
24
25/// A repository's owner, as a workspace (an `Organization`). A personal
26/// workspace's repositories are owned by a `User` instead: see
27/// [`owned_by_person`].
28fn owner(login: &str) -> Value {
29 json!({
30 "login": login,
31 "id": account_id(login),
32 "type": "Organization",
33 "html_url": format!("{SITE}/{login}"),
34 "url": format!("{API}/users/{login}"),
35 })
36}
37
The sidebar asks billing whether it is on, not for the account, so no page waits on the card processor38pub fn repository(repo: &Repo) -> Value {
39 let full_name = format!("{}/{}", repo.namespace, repo.name);
40 json!({
Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9'41 "id": github_id(&repo.id),
The sidebar asks billing whether it is on, not for the account, so no page waits on the card processor42 "node_id": repo.id,
43 "name": repo.name,
44 "full_name": full_name,
45 "private": repo.is_private,
Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9'46 "owner": owner(&repo.namespace),
The sidebar asks billing whether it is on, not for the account, so no page waits on the card processor47 "html_url": format!("{SITE}/{full_name}"),
48 "url": format!("{API}/repos/{full_name}"),
49 "clone_url": format!("{SITE}/{full_name}.git"),
50 "description": repo.description,
Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9'51 "homepage": repo.website,
52 "topics": repo.topics,
The sidebar asks billing whether it is on, not for the account, so no page waits on the card processor53 "default_branch": repo.default_branch,
54 "fork": false,
Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9'55 "archived": repo.archived(),
56 "created_at": repo.created_at,
The sidebar asks billing whether it is on, not for the account, so no page waits on the card processor57 "visibility": if repo.is_private { "private" } else { "public" },
58 })
59}
60
Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9'61/// A pull request's fork, from what the pull request knows of it.
62fn fork_repository(fork: &RepoPath, id: Option<&str>) -> Value {
63 let full_name = format!("{}/{}", fork.namespace, fork.name);
64 json!({
65 "id": id.map(github_id),
66 "node_id": id,
67 "name": fork.name,
68 "full_name": full_name,
69 "owner": owner(&fork.namespace),
70 "html_url": format!("{SITE}/{full_name}"),
71 "url": format!("{API}/repos/{full_name}"),
72 "clone_url": format!("{SITE}/{full_name}.git"),
73 "fork": true,
74 })
75}
76
77/// Marks the owner of `namespace`'s repositories as a `User` everywhere
78/// in a payload: for a personal workspace, whose name is its person's.
79pub fn owned_by_person(payload: &mut Value, namespace: &str) {
80 match payload {
81 Value::Object(map) => {
82 if map.get("login").and_then(Value::as_str).is_some_and(|login| login.eq_ignore_ascii_case(namespace))
83 && map.get("type").and_then(Value::as_str) == Some("Organization")
84 {
85 map.insert("type".into(), json!("User"));
86 }
87 map.values_mut().for_each(|value| owned_by_person(value, namespace));
88 }
89 Value::Array(items) => items.iter_mut().for_each(|value| owned_by_person(value, namespace)),
90 _ => {}
91 }
92}
93
The sidebar asks billing whether it is on, not for the account, so no page waits on the card processor94pub fn user(login: &str) -> Value {
Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9'95 json!({
96 "login": login,
97 "id": account_id(login),
98 "type": "User",
99 "html_url": format!("{SITE}/{login}"),
100 "url": format!("{API}/users/{login}"),
101 })
The sidebar asks billing whether it is on, not for the account, so no page waits on the card processor102}
103
104/// Someone as GitHub names them. g1t, the author of what it makes and
105/// files, is a `Bot`, as an app is there.
106fn person(user: &User) -> Value {
107 let mut named = self::user(&user.username);
Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9'108 named["node_id"] = json!(user.id);
The sidebar asks billing whether it is on, not for the account, so no page waits on the card processor109 if matches!(user.kind, PrincipalKind::Agent | PrincipalKind::System) {
110 named["type"] = json!("Bot");
111 }
112 named
113}
114
Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9'115fn people(logins: &[String]) -> Vec<Value> {
116 logins.iter().map(|login| user(login)).collect()
117}
118
119fn labels(repo: &Repo, names: &[String]) -> Value {
120 Value::Array(names.iter().map(|name| label(repo, name, None)).collect())
121}
122
Merge GitHub pull request #8 (claude/cool-newton-7nm8r9): Docker deployments on g1t.page behind a switch that stays off until Cloudflare's container APIs are checked live, workflows a workspace manages in its repositories, tarball and zipball downloads, refs/pull/* kept to g1t alone, framework detection for g1t.page builds, and a workflow's event payload matching GitHub's.123/// A label, by its name in `repo`. Its `color` is the one g1t gives a
124/// label of that name until [`color_labels`] sets the repository's own;
125/// `default` is whether it is one of the labels a repository starts with.
Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9'126fn label(repo: &Repo, name: &str, color: Option<&Value>) -> Value {
Merge GitHub pull request #8 (claude/cool-newton-7nm8r9): Docker deployments on g1t.page behind a switch that stays off until Cloudflare's container APIs are checked live, workflows a workspace manages in its repositories, tarball and zipball downloads, refs/pull/* kept to g1t alone, framework detection for g1t.page builds, and a workflow's event payload matching GitHub's.127 json!({
128 "id": github_id(&format!("{}:label:{name}", repo.id)),
129 "node_id": format!("{}:label:{name}", repo.id),
130 "name": name,
131 "color": color.filter(|c| c.is_string()).cloned().unwrap_or_else(|| json!(label_color_for(name))),
132 "description": DEFAULT_LABELS.iter().find(|(known, _, _)| *known == name).map(|(_, _, about)| *about),
133 "default": DEFAULT_LABELS.iter().any(|(known, _, _)| *known == name),
134 })
135}
136
137/// Gives every label in a payload (`labels[]` and `label`) the color and
138/// description the repository has for it (work's `list_labels`).
139pub fn color_labels(payload: &mut Value, known: &[Label]) {
140 fn set(label: &mut Value, known: &[Label]) {
141 let Some(found) = label["name"].as_str().and_then(|name| known.iter().find(|k| k.name == name)) else { return };
142 label["color"] = json!(found.color);
143 label["description"] = if found.description.is_empty() { Value::Null } else { json!(found.description) };
144 }
145 if let Value::Object(map) = payload {
146 for (key, value) in map.iter_mut() {
147 match (key.as_str(), value) {
148 ("labels", Value::Array(labels)) => labels.iter_mut().for_each(|label| set(label, known)),
149 ("label", label @ Value::Object(_)) => set(label, known),
150 (_, value) => color_labels(value, known),
151 }
152 }
153 }
154}
155
156/// Whether a payload has any label, so its colors are worth reading.
157pub fn has_labels(payload: &Value) -> bool {
158 match payload {
159 Value::Object(map) => map.iter().any(|(key, value)| match (key.as_str(), value) {
160 ("labels", Value::Array(labels)) => !labels.is_empty(),
161 ("label", Value::Object(_)) => true,
162 (_, value) => has_labels(value),
163 }),
164 _ => false,
Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9'165 }
The sidebar asks billing whether it is on, not for the account, so no page waits on the card processor166}
167
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar168/// A milestone as an issue or a pull request names it.
Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9'169fn milestone(repo: &Repo, milestone: Option<&MilestoneRef>) -> Value {
170 milestone.map_or(Value::Null, |milestone| milestone_of(repo, milestone.number, &json!(milestone.title)))
171}
172
173fn milestone_of(repo: &Repo, number: u32, title: &Value) -> Value {
174 json!({
175 "id": github_id(&format!("{}:milestone:{number}", repo.id)),
176 "number": number,
177 "title": title,
178 "html_url": format!("{SITE}/{}/{}/milestones/{number}", repo.namespace, repo.name),
179 })
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar180}
181
The sidebar asks billing whether it is on, not for the account, so no page waits on the card processor182pub fn commit(repo: &Repo, commit: &Commit) -> Value {
183 json!({
184 "id": commit.hash,
185 "tree_id": commit.tree_hash,
186 "message": commit.message,
187 "timestamp": commit.authored_at,
188 "url": format!("{SITE}/{}/{}/commit/{}", repo.namespace, repo.name, commit.hash),
189 "author": { "name": commit.author.name, "email": commit.author.email },
190 "committer": { "name": commit.author.name, "email": commit.author.email },
191 "distinct": true,
Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9'192 "added": [],
193 "removed": [],
194 "modified": [],
The sidebar asks billing whether it is on, not for the account, so no page waits on the card processor195 })
196}
197
Merge GitHub pull request #8 (claude/cool-newton-7nm8r9): Docker deployments on g1t.page behind a switch that stays off until Cloudflare's container APIs are checked live, workflows a workspace manages in its repositories, tarball and zipball downloads, refs/pull/* kept to g1t alone, framework detection for g1t.page builds, and a workflow's event payload matching GitHub's.198/// Sets a one-commit push's `added`, `removed` and `modified` (on its
199/// commit and `head_commit`) from what the push changed, compared as its
200/// `paths` filters are. A push of several commits keeps them empty: the
201/// comparison is of the whole push, not of each commit.
202pub fn push_changes(payload: &mut Value, changes: &[(String, FileStatus)]) {
203 if payload["commits"].as_array().map(Vec::len) != Some(1) {
204 return;
205 }
206 let of = |status: FileStatus| -> Value {
207 changes.iter().filter(|(_, s)| *s == status).map(|(path, _)| json!(path)).collect::<Vec<_>>().into()
208 };
209 for commit in ["commits", "head_commit"] {
210 let commit = if commit == "commits" { &mut payload["commits"][0] } else { &mut payload["head_commit"] };
211 commit["added"] = of(FileStatus::Added);
212 commit["removed"] = of(FileStatus::Deleted);
213 commit["modified"] = of(FileStatus::Modified);
214 }
215}
216
Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9'217/// A push. `forced` is whether `before` is not in `after`'s history, when
218/// that is known; a push whose history is too long to read says `false`.
Merge GitHub pull request #8 (claude/cool-newton-7nm8r9): Docker deployments on g1t.page behind a switch that stays off until Cloudflare's container APIs are checked live, workflows a workspace manages in its repositories, tarball and zipball downloads, refs/pull/* kept to g1t alone, framework detection for g1t.page builds, and a workflow's event payload matching GitHub's.219/// `pusher_email` is the pusher's noreply address, when the push was a
220/// person's.
221#[allow(clippy::too_many_arguments)]
222pub fn push(
223 repo: &Repo,
224 git_ref: &str,
225 before: Option<&str>,
226 after: &str,
227 commits: &[Commit],
228 pusher: &str,
229 pusher_email: Option<&str>,
230 forced: bool,
231) -> Value {
Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9'232 let before = before.unwrap_or(ZERO);
233 let created = before == ZERO;
234 let deleted = after == ZERO;
The sidebar asks billing whether it is on, not for the account, so no page waits on the card processor235 let commits: Vec<Value> = commits.iter().map(|c| commit(repo, c)).collect();
Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9'236 let full_name = format!("{}/{}", repo.namespace, repo.name);
237 let compare = if created {
238 format!("{SITE}/{full_name}/commit/{after}")
239 } else {
240 format!("{SITE}/{full_name}/compare/{before}...{after}")
241 };
The sidebar asks billing whether it is on, not for the account, so no page waits on the card processor242 json!({
243 "ref": git_ref,
Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9'244 "before": before,
The sidebar asks billing whether it is on, not for the account, so no page waits on the card processor245 "after": after,
Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9'246 "created": created,
247 "deleted": deleted,
248 "forced": forced && !created && !deleted,
The sidebar asks billing whether it is on, not for the account, so no page waits on the card processor249 "base_ref": null,
Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9'250 "compare": compare,
251 // GitHub's head commit is the one pushed: the last.
252 "head_commit": commits.last().cloned().unwrap_or(Value::Null),
The sidebar asks billing whether it is on, not for the account, so no page waits on the card processor253 "commits": commits,
Merge GitHub pull request #8 (claude/cool-newton-7nm8r9): Docker deployments on g1t.page behind a switch that stays off until Cloudflare's container APIs are checked live, workflows a workspace manages in its repositories, tarball and zipball downloads, refs/pull/* kept to g1t alone, framework detection for g1t.page builds, and a workflow's event payload matching GitHub's.254 "pusher": { "name": pusher, "email": pusher_email },
The sidebar asks billing whether it is on, not for the account, so no page waits on the card processor255 "repository": repository(repo),
256 "sender": user(pusher),
257 })
258}
259
260pub fn issue(repo: &Repo, issue: &Issue) -> Value {
261 let full_name = format!("{}/{}", repo.namespace, repo.name);
262 json!({
Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9'263 "id": github_id(&issue.id),
264 "node_id": issue.id,
The sidebar asks billing whether it is on, not for the account, so no page waits on the card processor265 "number": issue.number,
266 "title": issue.title,
267 "body": issue.body,
268 "state": if issue.state == State::Open { "open" } else { "closed" },
269 "state_reason": issue.reason,
Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9'270 "locked": false,
271 "labels": labels(repo, &issue.labels),
272 "milestone": milestone(repo, issue.milestone.as_ref()),
The sidebar asks billing whether it is on, not for the account, so no page waits on the card processor273 "user": person(&issue.author),
274 // g1t's own field: for an issue its agent filed, who it worked for.
275 "requested_by": issue.requested_by.as_ref().map(person),
Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9'276 "assignee": issue.assignees.first().map(|a| user(a)),
277 "assignees": people(&issue.assignees),
The sidebar asks billing whether it is on, not for the account, so no page waits on the card processor278 "comments": issue.comment_count,
279 "created_at": issue.created_at,
280 "updated_at": issue.updated_at,
281 "closed_at": issue.closed_at,
282 "html_url": format!("{SITE}/{full_name}/issues/{}", issue.number),
283 "url": format!("{API}/repos/{full_name}/issues/{}", issue.number),
Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9'284 "repository_url": format!("{API}/repos/{full_name}"),
The sidebar asks billing whether it is on, not for the account, so no page waits on the card processor285 })
286}
287
Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9'288/// A pull request, with its own labels. `base.sha` is the pull request's
289/// merge base until the trigger sets the base branch's tip, as GitHub
290/// gives it (trigger.rs).
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar291pub fn pull(repo: &Repo, pull: &Pull) -> Value {
The sidebar asks billing whether it is on, not for the account, so no page waits on the card processor292 let full_name = format!("{}/{}", repo.namespace, repo.name);
293 let head_ref = head_ref(pull);
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar294 let base_ref = pull.base_branch(&repo.default_branch);
Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9'295 let (head_repo, head_owner) = match &pull.fork {
296 Some(fork) => (fork_repository(fork, pull.fork_repo_id.as_deref()), fork.namespace.as_str()),
297 None => (repository(repo), repo.namespace.as_str()),
The sidebar asks billing whether it is on, not for the account, so no page waits on the card processor298 };
Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9'299 let merged = pull.status == PullStatus::Merged;
300 let closed_at = match pull.status {
301 PullStatus::Merged => pull.merged_at.clone(),
302 PullStatus::Closed => Some(pull.updated_at.clone()),
303 _ => None,
304 };
The sidebar asks billing whether it is on, not for the account, so no page waits on the card processor305 json!({
Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9'306 "id": github_id(&pull.id),
307 "node_id": pull.id,
The sidebar asks billing whether it is on, not for the account, so no page waits on the card processor308 "number": pull.number,
309 "title": pull.title,
310 "body": pull.body,
311 "state": if pull.status.is_active() { "open" } else { "closed" },
Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9'312 "locked": false,
The sidebar asks billing whether it is on, not for the account, so no page waits on the card processor313 "draft": pull.status == PullStatus::Draft,
Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9'314 "merged": merged,
The sidebar asks billing whether it is on, not for the account, so no page waits on the card processor315 "merged_at": pull.merged_at,
316 "merged_by": pull.merged_by.as_deref().map(user),
Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9'317 // Only once merged, as on GitHub; the trigger sets the commit it
318 // landed as when the event says.
319 "merge_commit_sha": if merged { pull.head_commit.clone() } else { None },
320 "labels": labels(repo, &pull.labels),
321 "milestone": milestone(repo, pull.milestone.as_ref()),
The sidebar asks billing whether it is on, not for the account, so no page waits on the card processor322 "user": person(&pull.author),
323 // g1t's own field: for a change g1t made, who asked for it.
324 "requested_by": pull.requested_by.as_ref().map(person),
Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9'325 "assignee": pull.assignees.first().map(|a| user(a)),
326 "assignees": people(&pull.assignees),
327 "requested_reviewers": people(&pull.reviewers),
The sidebar asks billing whether it is on, not for the account, so no page waits on the card processor328 "head": {
329 "ref": head_ref,
330 "sha": pull.head_commit,
Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9'331 "label": format!("{head_owner}:{head_ref}"),
332 "user": owner(head_owner),
The sidebar asks billing whether it is on, not for the account, so no page waits on the card processor333 "repo": head_repo,
334 },
335 "base": {
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar336 "ref": base_ref,
The sidebar asks billing whether it is on, not for the account, so no page waits on the card processor337 "sha": pull.merge_base,
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar338 "label": format!("{}:{base_ref}", repo.namespace),
Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9'339 "user": owner(&repo.namespace),
The sidebar asks billing whether it is on, not for the account, so no page waits on the card processor340 "repo": repository(repo),
341 },
Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9'342 "changed_files": pull.files.len(),
343 "additions": pull.files.iter().map(|f| u64::from(f.additions)).sum::<u64>(),
344 "deletions": pull.files.iter().map(|f| u64::from(f.deletions)).sum::<u64>(),
345 "created_at": pull.created_at,
346 "updated_at": pull.updated_at,
347 "closed_at": closed_at,
The sidebar asks billing whether it is on, not for the account, so no page waits on the card processor348 "html_url": format!("{SITE}/{full_name}/pull/{}", pull.number),
349 "url": format!("{API}/repos/{full_name}/pulls/{}", pull.number),
Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9'350 "issue_url": format!("{API}/repos/{full_name}/issues/{}", pull.number),
The sidebar asks billing whether it is on, not for the account, so no page waits on the card processor351 })
352}
353
Merge branch 'worktree-agent-a8b78c62188153aa3' into claude/cool-newton-7nm8r9354/// What a `labeled`, `unlabeled`, `milestoned`, `demilestoned`,
355/// `assigned` or base change was about, from the g1t event: `label`,
356/// `milestone`, `assignee` (the first of those newly assigned; g1t sends
357/// one event for them all, GitHub one each), and for a new base,
358/// `changes.base.ref.from` is not known, so only the new base is in
359/// `pull_request.base`.
Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9'360pub fn changed(repo: &Repo, payload: &mut Value, data: &Value) {
Merge branch 'worktree-agent-a8b78c62188153aa3' into claude/cool-newton-7nm8r9361 if let Some(first) = data["added"].as_array().and_then(|added| added.first()).and_then(Value::as_str) {
362 payload["assignee"] = user(first);
363 }
Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9'364 if let Some(found) = data.get("label").filter(|label| label.is_object()) {
365 payload["label"] = label(repo, found["name"].as_str().unwrap_or_default(), Some(&found["color"]));
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar366 }
Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9'367 if let Some(found) = data.get("milestone").filter(|milestone| milestone.is_object()) {
368 payload["milestone"] = match found["number"].as_u64() {
369 Some(number) => milestone_of(repo, number as u32, &found["title"]),
370 None => json!({ "number": found["number"], "title": found["title"] }),
371 };
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar372 }
373}
374
The sidebar asks billing whether it is on, not for the account, so no page waits on the card processor375/// The branch a pull request comes from, or a name for its fork.
376pub fn head_ref(pull: &Pull) -> String {
377 pull.branch.clone().unwrap_or_else(|| format!("pull/{}", pull.number))
378}
379
Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9'380/// A comment. Its `id` is g1t's (`cmt_…`), as the API takes it back to
381/// edit or delete it.
The sidebar asks billing whether it is on, not for the account, so no page waits on the card processor382pub fn comment(repo: &Repo, number: u32, comment: &Comment, on_pull: bool) -> Value {
383 let full_name = format!("{}/{}", repo.namespace, repo.name);
384 let page = if on_pull { "pull" } else { "issues" };
385 json!({
386 "id": comment.id,
Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9'387 "node_id": comment.id,
The sidebar asks billing whether it is on, not for the account, so no page waits on the card processor388 "body": comment.body,
389 "user": person(&comment.author),
390 "created_at": comment.created_at,
Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9'391 "updated_at": comment.edited_at.as_deref().unwrap_or(&comment.created_at),
The sidebar asks billing whether it is on, not for the account, so no page waits on the card processor392 "path": comment.path,
393 "line": comment.line,
394 "html_url": format!("{SITE}/{full_name}/{page}/{number}#{}", comment.id),
Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9'395 "url": format!("{API}/repos/{full_name}/issues/comments/{}", comment.id),
396 "issue_url": format!("{API}/repos/{full_name}/issues/{number}"),
397 })
398}
399
400/// A review, for `pull_request_review`: g1t keeps a review as a comment
401/// with a verdict, and its id is that comment's.
402pub fn review(repo: &Repo, pull: &Pull, review: Option<&Comment>) -> Value {
403 let full_name = format!("{}/{}", repo.namespace, repo.name);
404 let Some(review) = review else {
405 return json!({ "state": null, "body": null, "user": null });
406 };
407 json!({
408 "id": github_id(&review.id),
409 "node_id": review.id,
410 "state": review.verdict.map(review_state),
411 "body": review.body,
412 "user": person(&review.author),
413 "commit_id": pull.head_commit,
414 "submitted_at": review.created_at,
415 "html_url": format!("{SITE}/{full_name}/pull/{}#{}", pull.number, review.id),
416 "pull_request_url": format!("{API}/repos/{full_name}/pulls/{}", pull.number),
The sidebar asks billing whether it is on, not for the account, so no page waits on the card processor417 })
418}
419
420/// An issue as `issue_comment` gives it for a pull request: the pull
Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9'421/// request's number and title, with `pull_request` set. Its id is not the
422/// pull request's, as on GitHub, where the two are different objects.
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar423pub fn pull_as_issue(repo: &Repo, pull: &Pull) -> Value {
The sidebar asks billing whether it is on, not for the account, so no page waits on the card processor424 let full_name = format!("{}/{}", repo.namespace, repo.name);
425 json!({
Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9'426 "id": github_id(&format!("issue:{}", pull.id)),
427 "node_id": pull.id,
The sidebar asks billing whether it is on, not for the account, so no page waits on the card processor428 "number": pull.number,
429 "title": pull.title,
430 "body": pull.body,
431 "state": if pull.status.is_active() { "open" } else { "closed" },
Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9'432 "locked": false,
433 "draft": pull.status == PullStatus::Draft,
434 "labels": labels(repo, &pull.labels),
435 "milestone": milestone(repo, pull.milestone.as_ref()),
The sidebar asks billing whether it is on, not for the account, so no page waits on the card processor436 "user": person(&pull.author),
437 "requested_by": pull.requested_by.as_ref().map(person),
Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9'438 "assignee": pull.assignees.first().map(|a| user(a)),
439 "assignees": people(&pull.assignees),
440 "created_at": pull.created_at,
441 "updated_at": pull.updated_at,
The sidebar asks billing whether it is on, not for the account, so no page waits on the card processor442 "html_url": format!("{SITE}/{full_name}/pull/{}", pull.number),
Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9'443 "url": format!("{API}/repos/{full_name}/issues/{}", pull.number),
444 "repository_url": format!("{API}/repos/{full_name}"),
The sidebar asks billing whether it is on, not for the account, so no page waits on the card processor445 "pull_request": {
446 "url": format!("{API}/repos/{full_name}/pulls/{}", pull.number),
447 "html_url": format!("{SITE}/{full_name}/pull/{}", pull.number),
448 "merged_at": pull.merged_at,
449 },
450 })
451}
452
Merge Actions: cross-repo workflows and actions, release and deployment triggers, step timeouts453/// A comment as the event that deleted it kept it (`comment.deleted`'s
454/// `comment`), since it can no longer be read.
455pub fn deleted_comment(repo: &Repo, number: u32, kept: &Value, on_pull: bool) -> Value {
456 let full_name = format!("{}/{}", repo.namespace, repo.name);
457 let page = if on_pull { "pull" } else { "issues" };
458 let id = kept["id"].as_str().unwrap_or_default();
459 let login = kept["author"]["username"].as_str().unwrap_or_default();
460 json!({
461 "id": id,
Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9'462 "node_id": id,
Merge Actions: cross-repo workflows and actions, release and deployment triggers, step timeouts463 "body": kept["body"],
464 "user": user(login),
465 "created_at": kept["createdAt"],
Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9'466 "updated_at": kept["editedAt"].as_str().map_or(kept["createdAt"].clone(), |at| json!(at)),
Merge Actions: cross-repo workflows and actions, release and deployment triggers, step timeouts467 "path": kept["path"],
468 "line": kept["line"],
469 "html_url": format!("{SITE}/{full_name}/{page}/{number}#{id}"),
Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9'470 "url": format!("{API}/repos/{full_name}/issues/comments/{id}"),
471 "issue_url": format!("{API}/repos/{full_name}/issues/{number}"),
Merge Actions: cross-repo workflows and actions, release and deployment triggers, step timeouts472 })
473}
474
Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9'475/// What `workflow_run` is told of the run it follows: the
476/// `workflow.completed` event's data, and the run's row when it is still
477/// there. Its `id` and `workflow_id` are g1t's (`run_…`, `wfl_…`), which
478/// `GITHUB_RUN_ID` and the API use too, so `actions/download-artifact`
479/// with `run-id: ${{ github.event.workflow_run.id }}` finds its artifacts.
480pub struct FollowedRun<'a> {
481 pub data: &'a Value,
482 pub workflow_id: Option<&'a str>,
483 pub attempt: Option<u64>,
484 pub created_at: Option<&'a str>,
485 pub started_at: Option<&'a str>,
486 pub updated_at: Option<&'a str>,
487 pub actor: Option<&'a str>,
488 pub triggering_actor: Option<&'a str>,
489 pub title: Option<&'a str>,
490 pub head_commit: Option<&'a Commit>,
Merge GitHub pull request #8 (claude/cool-newton-7nm8r9): Docker deployments on g1t.page behind a switch that stays off until Cloudflare's container APIs are checked live, workflows a workspace manages in its repositories, tarball and zipball downloads, refs/pull/* kept to g1t alone, framework detection for g1t.page builds, and a workflow's event payload matching GitHub's.491 /// The pull request the run was for (`data.pull`), when it can be
492 /// read, and its base branch's latest commit.
493 pub pull: Option<&'a Pull>,
494 pub base_sha: Option<&'a str>,
Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9'495}
496
Merge GitHub pull request #8 (claude/cool-newton-7nm8r9): Docker deployments on g1t.page behind a switch that stays off until Cloudflare's container APIs are checked live, workflows a workspace manages in its repositories, tarball and zipball downloads, refs/pull/* kept to g1t alone, framework detection for g1t.page builds, and a workflow's event payload matching GitHub's.497/// The run's pull request as `workflow_run.pull_requests[]` gives it. One
498/// from a fork is left out, as on GitHub, where only pull requests whose
499/// head is in the repository are listed.
500fn run_pulls(repo: &Repo, run: &FollowedRun) -> Vec<Value> {
501 let full_name = format!("{}/{}", repo.namespace, repo.name);
502 let Some(number) = run.data["pull"].as_u64() else { return Vec::new() };
503 let url = format!("{API}/repos/{full_name}/pulls/{number}");
504 let Some(pull) = run.pull else {
505 return vec![json!({ "number": number, "url": url })];
506 };
507 if pull.fork.is_some() {
508 return Vec::new();
509 }
510 let short = json!({ "id": github_id(&repo.id), "url": format!("{API}/repos/{full_name}"), "name": repo.name });
511 vec![json!({
512 "id": github_id(&pull.id),
513 "number": number,
514 "url": url,
515 "head": { "ref": head_ref(pull), "sha": pull.head_commit, "repo": short },
516 "base": {
517 "ref": pull.base_branch(&repo.default_branch),
518 "sha": run.base_sha.map(str::to_owned).or_else(|| pull.merge_base.clone()),
519 "repo": short,
520 },
521 })]
522}
523
Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9'524pub fn workflow_run(repo: &Repo, run: &FollowedRun) -> Value {
525 let full_name = format!("{}/{}", repo.namespace, repo.name);
526 let data = run.data;
527 let id = data["runId"].as_str().unwrap_or_default();
528 let name = data["workflow"].as_str().unwrap_or_default();
529 let head_branch = data["ref"].as_str().unwrap_or_default().trim_start_matches("refs/heads/").to_owned();
530 let url = format!("{API}/repos/{full_name}/actions/runs/{id}");
531 let actor = run.actor.map(user);
532 json!({
533 "id": id,
534 "node_id": id,
535 "name": name,
536 "display_title": run.title.unwrap_or(name),
537 "path": data["path"],
538 "event": data["event"],
539 "status": "completed",
540 "conclusion": data["conclusion"],
541 "workflow_id": run.workflow_id,
542 "head_sha": data["sha"],
543 "head_branch": head_branch,
544 "head_commit": run.head_commit.map(|c| json!({
545 "id": c.hash,
546 "tree_id": c.tree_hash,
547 "message": c.message,
548 "timestamp": c.authored_at,
549 "author": { "name": c.author.name, "email": c.author.email },
550 "committer": { "name": c.author.name, "email": c.author.email },
551 })),
552 "run_number": data["number"],
553 "run_attempt": run.attempt.unwrap_or(1),
554 "created_at": run.created_at,
555 "run_started_at": run.started_at.or(run.created_at),
556 "updated_at": run.updated_at.or(run.created_at),
557 "actor": actor,
558 "triggering_actor": run.triggering_actor.map(user).or_else(|| run.actor.map(user)),
559 "html_url": format!("{SITE}/{full_name}/actions/runs/{id}"),
560 "url": url,
561 "jobs_url": format!("{url}/jobs"),
562 "logs_url": format!("{url}/logs"),
563 "artifacts_url": format!("{url}/artifacts"),
564 "workflow_url": run.workflow_id.map(|w| format!("{API}/repos/{full_name}/actions/workflows/{w}")),
Merge GitHub pull request #8 (claude/cool-newton-7nm8r9): Docker deployments on g1t.page behind a switch that stays off until Cloudflare's container APIs are checked live, workflows a workspace manages in its repositories, tarball and zipball downloads, refs/pull/* kept to g1t alone, framework detection for g1t.page builds, and a workflow's event payload matching GitHub's.565 "pull_requests": run_pulls(repo, run),
Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9'566 "repository": repository(repo),
567 "head_repository": repository(repo),
568 })
569}
570
Merge Actions: cross-repo workflows and actions, release and deployment triggers, step timeouts571/// A release, from what a `release.*` event carries
Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9'572/// (`g1t_contracts::about::Release`). Its `id` is g1t's, as
573/// `/repos/{owner}/{repo}/releases/{id}` takes it.
Merge Actions: cross-repo workflows and actions, release and deployment triggers, step timeouts574pub fn release(repo: &Repo, release: &Value) -> Value {
575 let full_name = format!("{}/{}", repo.namespace, repo.name);
576 let tag = release["tagName"].as_str().unwrap_or_default();
577 let id = release["id"].as_str().unwrap_or_default();
578 json!({
579 "id": id,
580 "node_id": id,
581 "tag_name": tag,
582 "target_commitish": release["target"],
583 "name": release["name"].as_str().unwrap_or(tag),
584 "body": release["body"],
585 "draft": release["draft"].as_bool().unwrap_or(false),
586 "prerelease": release["prerelease"].as_bool().unwrap_or(false),
587 "created_at": release["createdAt"],
588 "published_at": release["publishedAt"],
589 "author": user(release["author"].as_str().unwrap_or(&repo.namespace)),
590 "assets": [],
591 "html_url": format!("{SITE}/{full_name}/releases/tag/{tag}"),
592 "url": format!("{API}/repos/{full_name}/releases/{id}"),
Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9'593 "zipball_url": format!("{SITE}/{full_name}/archive/{tag}.zip"),
Merge Actions: cross-repo workflows and actions, release and deployment triggers, step timeouts594 })
595}
596
597/// A deployment, from what `deployment.created` and
598/// `deployment_status.created` carry (`RepoDeployment`, already in GitHub's
599/// spelling).
600pub fn deployment(repo: &Repo, deployment: &Value) -> Value {
601 let full_name = format!("{}/{}", repo.namespace, repo.name);
602 let id = deployment["id"].as_str().unwrap_or_default();
603 json!({
604 "id": id,
605 "node_id": id,
606 "sha": deployment["sha"],
607 "ref": deployment["ref"],
608 "task": deployment["task"].as_str().unwrap_or("deploy"),
609 "payload": deployment.get("payload").filter(|p| p.is_object()).cloned().unwrap_or_else(|| json!({})),
610 "environment": deployment["environment"],
611 "original_environment": deployment["environment"],
612 "description": deployment["description"],
613 "creator": user(deployment["creator"].as_str().unwrap_or("g1t")),
614 "transient_environment": deployment["transient_environment"].as_bool().unwrap_or(false),
615 "production_environment": deployment["production_environment"].as_bool().unwrap_or(false),
616 "created_at": deployment["created_at"],
617 "updated_at": deployment["updated_at"],
618 "url": format!("{API}/repos/{full_name}/deployments/{id}"),
619 "statuses_url": format!("{API}/repos/{full_name}/deployments/{id}/statuses"),
620 })
621}
622
623/// A deployment's status (`DeploymentStatus`), for `deployment_status`.
624pub fn deployment_status(repo: &Repo, status: &Value, deployment: &Value) -> Value {
625 let full_name = format!("{}/{}", repo.namespace, repo.name);
626 let deployment_id = deployment["id"].as_str().unwrap_or_default();
627 let id = status["id"].as_str().unwrap_or_default();
628 json!({
629 "id": id,
630 "node_id": id,
631 "state": status["state"],
632 "description": status["description"].as_str().unwrap_or_default(),
633 "environment": deployment["environment"],
634 "environment_url": status["environment_url"].as_str().unwrap_or_default(),
635 "log_url": status["log_url"].as_str().unwrap_or_default(),
636 "target_url": status["log_url"].as_str().unwrap_or_default(),
637 "creator": user(status["creator"].as_str().unwrap_or("g1t")),
638 "created_at": status["created_at"],
639 "updated_at": status["created_at"],
640 "deployment_url": format!("{API}/repos/{full_name}/deployments/{deployment_id}"),
641 "url": format!("{API}/repos/{full_name}/deployments/{deployment_id}/statuses/{id}"),
642 })
643}
644
What runs differently on g1t's machines is said before it surprises anyone, and four things now behave as on GitHub. A commit whose message says `[skip ci]`, `[ci skip]`, `[no ci]`, `[skip actions]` or `[actions skip]`, or ends with `skip-checks: true`, starts no `push` or `pull_request` workflows. A `pull_request_review` event's `review.state` is `approved` or `changes_requested`, not `approve` or `requestchanges`. A self-hosted job that names no `timeout-minutes` runs for 360 minutes, not 60. A JavaScript action's `::debug::` lines show on a debug run, as `run:` steps' do. The workflow's page now warns when a job asks for more than the 60 minutes g1t's machines give it, when an Arm label will run on x64, and when a matrix on a job that calls a reusable workflow will run it once; it no longer says artifacts are kept up to 60 MB (they are kept up to 5 GiB), nor that `setup-node` with `cache:` runs without its cache (the toolkit's cache works). The desktop release's two hosted jobs ask for the 60 minutes they get. The Actions and self-hosted runners guides say so.645
646/// A review's `state` as GitHub's `pull_request_review` event spells it.
647pub(crate) fn review_state(verdict: g1t_contracts::work::Verdict) -> &'static str {
648 match verdict {
649 g1t_contracts::work::Verdict::Approve => "approved",
650 g1t_contracts::work::Verdict::RequestChanges => "changes_requested",
651 }
652}
653
654#[cfg(test)]
655mod review_tests {
656 use super::*;
657
658 #[test]
659 fn review_states_are_spelled_as_githubs() {
660 assert_eq!(review_state(g1t_contracts::work::Verdict::Approve), "approved");
661 assert_eq!(review_state(g1t_contracts::work::Verdict::RequestChanges), "changes_requested");
662 }
663}
Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9'664
665/// g1t's payloads against GitHub's: each fixture in `tests/fixtures` is a
666/// trimmed webhook payload in GitHub's documented shape, and every key it
667/// has must be in g1t's payload for the same event with the same JSON type
668/// (a whole number where GitHub's is one). `null` in a fixture takes any
669/// type. Where g1t differs on purpose, `KNOWN` says how and why.
670#[cfg(test)]
671mod github_shapes {
672 use super::*;
673 use g1t_contracts::work::Verdict;
674
675 /// Paths where g1t's payload differs from GitHub's on purpose, and
676 /// what g1t has there: a JSON type, or `absent`.
677 const KNOWN: &[(&str, &str, &str)] = &[
678 // The API addresses these by g1t's own id, so the payload gives
679 // that id, to be passed straight back.
680 ("issue_comment", "comment.id", "string"),
681 ("release", "release.id", "string"),
682 ("workflow_run", "workflow_run.id", "string"),
683 ("workflow_run", "workflow_run.workflow_id", "string"),
684 ("workflow_run", "workflow.id", "string"),
685 ];
686
687 fn kind(value: &Value) -> &'static str {
688 match value {
689 Value::Null => "null",
690 Value::Bool(_) => "boolean",
691 Value::Number(_) => "number",
692 Value::String(_) => "string",
693 Value::Array(_) => "array",
694 Value::Object(_) => "object",
695 }
696 }
697
698 fn compare(event: &str, github: &Value, ours: Option<&Value>, path: &str, problems: &mut Vec<String>) {
699 if let Some((_, _, known)) = KNOWN.iter().find(|(e, p, _)| *e == event && *p == path) {
700 let had = ours.map_or("absent", kind);
701 if had != *known {
702 problems.push(format!("{path}: g1t is known to have {known} here, but has {had}"));
703 }
704 return;
705 }
706 let Some(ours) = ours else {
707 problems.push(format!("{path}: missing"));
708 return;
709 };
710 if github.is_null() {
711 return;
712 }
713 if kind(github) != kind(ours) {
714 problems.push(format!("{path}: GitHub's is {}, g1t's {}", kind(github), kind(ours)));
715 return;
716 }
717 match github {
718 Value::Number(n) if n.is_u64() => {
719 if !ours.as_u64().is_some_and(|n| n < 1 << 53) {
720 problems.push(format!("{path}: GitHub's is a whole number, g1t's {ours}"));
721 }
722 }
723 Value::Object(map) => {
724 for (key, inner) in map {
725 let at = if path.is_empty() { key.clone() } else { format!("{path}.{key}") };
726 compare(event, inner, ours.get(key), &at, problems);
727 }
728 }
729 Value::Array(items) => {
730 if let (Some(first), Some(our_first)) = (items.first(), ours.as_array().and_then(|o| o.first())) {
731 compare(event, first, Some(our_first), &format!("{path}[]"), problems);
732 }
733 }
734 _ => {}
735 }
736 }
737
738 fn repo() -> Repo {
739 serde_json::from_value(json!({
740 "id": "rep_01kpw0aaaaaaaaaaaaaaaaaaaa", "namespace": "acme", "name": "web", "description": null, "isPrivate": false,
741 "ownerId": "wsp_1", "defaultBranch": "main", "forkOf": null, "createdAt": "2026-05-15T15:19:25Z"
742 }))
743 .unwrap()
744 }
745
746 fn someone(id: &str, username: &str) -> User {
747 User { id: id.into(), username: username.into(), kind: PrincipalKind::User, ..User::default() }
748 }
749
750 fn a_pull() -> Pull {
751 serde_json::from_value(json!({
752 "id": "pr_01kq000000000000000000000a", "repoId": "rep_1", "number": 2, "issue": null,
753 "title": "Update the README with new information.", "body": "A simple change.",
754 "agent": "", "runtime": "external", "status": "open",
755 "fork": null, "forkRepoId": null, "branch": "changes",
756 "headCommit": "ec26c3e57ca3a959ca5aad62de7213c562f8c821", "mergeBase": "f95f852bd8fca8fcc58a9a2d6c842781e32a215e",
757 "mergedBy": null, "mergedAt": null, "supersededBy": null, "checkStatus": null,
758 "files": [{ "path": "README.md", "additions": 1, "deletions": 1 }],
759 "assignees": ["bo"], "reviewers": ["cy"], "labels": ["bug"], "milestone": { "number": 1, "title": "v1.0" },
760 "author": someone("usr_1", "ada"), "requestedBy": null,
761 "createdAt": "2026-05-15T15:20:33Z", "updatedAt": "2026-05-15T15:20:33Z"
762 }))
763 .unwrap()
764 }
765
766 fn an_issue() -> Issue {
767 serde_json::from_value(json!({
768 "id": "iss_01kq000000000000000000000a", "repoId": "rep_1", "number": 1,
769 "title": "Spelling error in the README file", "body": "Two t's.", "labels": ["bug"], "state": "open",
770 "reason": null, "resolvedBy": null, "author": someone("usr_1", "ada"), "requestedBy": null,
771 "createdAt": "2026-05-15T15:20:18Z", "updatedAt": "2026-05-15T15:20:18Z", "closedAt": null,
772 "pullCount": 0, "commentCount": 0, "assignees": ["bo"], "milestone": { "number": 1, "title": "v1.0" }
773 }))
774 .unwrap()
775 }
776
777 fn a_comment(verdict: Option<Verdict>) -> Comment {
778 serde_json::from_value(json!({
779 "id": "cmt_01kq000000000000000000000a", "author": someone("usr_3", "cy"), "body": "Looks good.",
780 "path": null, "line": null, "verdict": verdict, "createdAt": "2026-05-15T15:20:38Z"
781 }))
782 .unwrap()
783 }
784
785 fn a_commit() -> Commit {
786 serde_json::from_value(json!({
787 "hash": "0d1a26e67d8f5eaf1f6ba5c57fc3c7d91ac0fd1c", "treeHash": "f9d2a07e9488b91af2641b26b9407fe22a451433",
788 "message": "Update README.md", "author": { "name": "Ada", "email": "ada@example.com" },
789 "parents": ["6113728f27ae82c7b1a177c8d03f9e96e0adf246"], "authoredAt": "2026-05-15T15:20:30Z"
790 }))
791 .unwrap()
792 }
793
794 /// g1t's payload for each event, put together as trigger.rs does.
795 fn payloads() -> Vec<(&'static str, Value)> {
796 let repo = repo();
797 let pull = a_pull();
Merge GitHub pull request #8 (claude/cool-newton-7nm8r9): Docker deployments on g1t.page behind a switch that stays off until Cloudflare's container APIs are checked live, workflows a workspace manages in its repositories, tarball and zipball downloads, refs/pull/* kept to g1t alone, framework detection for g1t.page builds, and a workflow's event payload matching GitHub's.798 let mut push = push(&repo, "refs/heads/main", Some("6113728f27ae82c7b1a177c8d03f9e96e0adf246"), &a_commit().hash, &[a_commit()], "ada", Some("0d0efg+ada@users.noreply.g1t.sh"), false);
799 push_changes(&mut push, &[("README.md".into(), FileStatus::Modified)]);
800 let known = [Label { name: "bug".into(), color: "d73a4a".into(), description: "Something isn't working".into(), issues: 1, pulls: 1 }];
801 let mut pull_request = json!({
Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9'802 "action": "opened", "number": 2, "pull_request": self::pull(&repo, &pull),
803 "repository": repository(&repo), "sender": user("ada"),
804 });
Merge GitHub pull request #8 (claude/cool-newton-7nm8r9): Docker deployments on g1t.page behind a switch that stays off until Cloudflare's container APIs are checked live, workflows a workspace manages in its repositories, tarball and zipball downloads, refs/pull/* kept to g1t alone, framework detection for g1t.page builds, and a workflow's event payload matching GitHub's.805 color_labels(&mut pull_request, &known);
Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9'806 let review = json!({
807 "action": "submitted", "review": self::review(&repo, &pull, Some(&a_comment(Some(Verdict::Approve)))),
808 "pull_request": self::pull(&repo, &pull), "repository": repository(&repo), "sender": user("cy"),
809 });
810 let mut issues = json!({ "action": "labeled", "issue": issue(&repo, &an_issue()), "repository": repository(&repo), "sender": user("ada") });
811 changed(&repo, &mut issues, &json!({ "label": { "name": "bug", "color": "d73a4a" } }));
Merge GitHub pull request #8 (claude/cool-newton-7nm8r9): Docker deployments on g1t.page behind a switch that stays off until Cloudflare's container APIs are checked live, workflows a workspace manages in its repositories, tarball and zipball downloads, refs/pull/* kept to g1t alone, framework detection for g1t.page builds, and a workflow's event payload matching GitHub's.812 color_labels(&mut issues, &known);
Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9'813 let mut on_pull = pull.clone();
814 on_pull.labels.clear();
815 on_pull.assignees.clear();
816 on_pull.milestone = None;
817 let mut comment = a_comment(None);
818 comment.author = someone("usr_1", "ada");
819 let issue_comment = json!({
820 "action": "created", "issue": pull_as_issue(&repo, &on_pull), "comment": self::comment(&repo, 2, &comment, true),
821 "repository": repository(&repo), "sender": user("ada"),
822 });
823 let data = json!({
824 "runId": "run_01kq000000000000000000000a", "workflow": "Build", "path": ".g1t/workflows/build.yml", "number": 562,
825 "event": "push", "conclusion": "success", "ref": "refs/heads/main", "sha": "acb5820ced9479c074f688cc328bf03f341a511d", "pull": 2,
826 });
827 let head = Commit { hash: "acb5820ced9479c074f688cc328bf03f341a511d".into(), ..a_commit() };
828 let followed = FollowedRun {
829 data: &data,
830 workflow_id: Some("wfl_1"),
831 attempt: Some(1),
832 created_at: Some("2026-05-15T15:20:35Z"),
833 started_at: Some("2026-05-15T15:20:35Z"),
834 updated_at: Some("2026-05-15T15:21:10Z"),
835 actor: Some("ada"),
836 triggering_actor: None,
837 title: Some("Update README.md"),
838 head_commit: Some(&head),
Merge GitHub pull request #8 (claude/cool-newton-7nm8r9): Docker deployments on g1t.page behind a switch that stays off until Cloudflare's container APIs are checked live, workflows a workspace manages in its repositories, tarball and zipball downloads, refs/pull/* kept to g1t alone, framework detection for g1t.page builds, and a workflow's event payload matching GitHub's.839 pull: Some(&pull),
840 base_sha: Some("f95f852bd8fca8fcc58a9a2d6c842781e32a215e"),
Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9'841 };
842 // As trigger.rs gives it.
843 let workflow_run = json!({
844 "action": "completed",
845 "workflow_run": self::workflow_run(&repo, &followed),
846 "workflow": { "id": "wfl_1", "node_id": "wfl_1", "name": "Build", "path": data["path"], "state": "active", "html_url": "https://g1t.sh/acme/web/actions" },
847 "repository": repository(&repo),
848 "sender": user("ada"),
849 });
850 let release = json!({
851 "action": "published",
852 "release": self::release(&repo, &json!({ "id": "rel_1", "tagName": "v1.2.0", "target": "main", "name": "v1.2.0", "body": "Notes",
853 "draft": false, "prerelease": false, "author": "ada", "createdAt": "2026-05-15T15:19:53Z", "publishedAt": "2026-05-15T15:20:05Z" })),
854 "repository": repository(&repo),
855 "sender": user("ada"),
856 });
857 vec![
858 ("push", push),
859 ("pull_request", pull_request),
860 ("pull_request_review", review),
861 ("issues", issues),
862 ("issue_comment", issue_comment),
863 ("workflow_run", workflow_run),
864 ("release", release),
865 ]
866 }
867
868 fn fixture(event: &str) -> Value {
869 let text = match event {
870 "push" => include_str!("../tests/fixtures/push.json"),
871 "pull_request" => include_str!("../tests/fixtures/pull_request.json"),
872 "pull_request_review" => include_str!("../tests/fixtures/pull_request_review.json"),
873 "issues" => include_str!("../tests/fixtures/issues.json"),
874 "issue_comment" => include_str!("../tests/fixtures/issue_comment.json"),
875 "workflow_run" => include_str!("../tests/fixtures/workflow_run.json"),
876 "release" => include_str!("../tests/fixtures/release.json"),
877 other => panic!("no fixture for {other}"),
878 };
879 serde_json::from_str(text).unwrap()
880 }
881
882 #[test]
883 fn payloads_have_githubs_keys_and_types() {
884 let mut problems = Vec::new();
885 for (event, ours) in payloads() {
886 let mut found = Vec::new();
887 compare(event, &fixture(event), Some(&ours), "", &mut found);
888 problems.extend(found.into_iter().map(|problem| format!("{event}: {problem}")));
889 }
890 assert!(problems.is_empty(), "g1t's payloads differ from GitHub's:\n{}", problems.join("\n"));
891 }
892
893 #[test]
894 fn the_comparison_finds_what_differs() {
895 let mut found = Vec::new();
896 let github = json!({ "id": 1, "user": { "login": "a" }, "labels": [{ "name": "x" }], "body": null });
897 compare("x", &github, Some(&json!({ "id": "pr_1", "labels": [{}], "body": 3 })), "", &mut found);
898 assert_eq!(found, ["id: GitHub's is number, g1t's string", "user: missing", "labels[].name: missing"]);
899 found.clear();
900 compare("x", &json!({ "id": 1 }), Some(&json!({ "id": 1.5 })), "", &mut found);
901 assert_eq!(found, ["id: GitHub's is a whole number, g1t's 1.5"]);
902 }
903
904 #[test]
905 fn the_same_object_has_the_same_number_everywhere() {
906 let all = payloads();
907 let by = |event: &str| all.iter().find(|(e, _)| *e == event).map(|(_, v)| v.clone()).unwrap();
908 let (pr, issues, run) = (by("pull_request"), by("issues"), by("workflow_run"));
909 let repo_id = &pr["repository"]["id"];
910 assert_eq!(repo_id, &json!(github_id(&repo().id)), "github.repository_id is the same number");
911 assert_eq!(&pr["pull_request"]["base"]["repo"]["id"], repo_id);
912 assert_eq!(&run["workflow_run"]["repository"]["id"], repo_id);
913 // ada as sender, author and run actor; acme as owner and head user.
914 assert_eq!(pr["sender"]["id"], pr["pull_request"]["user"]["id"]);
915 assert_eq!(pr["sender"]["id"], run["workflow_run"]["actor"]["id"]);
916 assert_eq!(pr["sender"]["id"], json!(account_id("ada")), "github.actor_id is the same number");
917 assert_eq!(pr["repository"]["owner"]["id"], pr["pull_request"]["head"]["user"]["id"]);
918 assert_eq!(pr["repository"]["owner"]["id"], json!(account_id("acme")));
919 assert_eq!(pr["pull_request"]["labels"][0]["id"], issues["label"]["id"]);
920 // A pull request's own id and its issue's are different objects.
921 assert_ne!(pull_as_issue(&repo(), &a_pull())["id"], pr["pull_request"]["id"]);
922 assert_eq!(pr["pull_request"]["node_id"], a_pull().id);
923 }
924
925 #[test]
926 fn a_personal_workspaces_repositories_are_a_users() {
927 let mut event = json!({ "pull_request": pull(&repo(), &a_pull()), "repository": repository(&repo()), "sender": user("ada") });
928 assert_eq!(event["repository"]["owner"]["type"], "Organization");
929 owned_by_person(&mut event, "Acme");
930 assert_eq!(event["repository"]["owner"]["type"], "User");
931 assert_eq!(event["pull_request"]["base"]["repo"]["owner"]["type"], "User");
932 assert_eq!(event["pull_request"]["head"]["user"]["type"], "User");
933 // Nobody else changes, a bot least of all.
934 assert_eq!(event["sender"]["type"], "User");
935 let mut bot = json!({ "user": { "login": "acme", "type": "Bot" } });
936 owned_by_person(&mut bot, "acme");
937 assert_eq!(bot["user"]["type"], "Bot");
938 }
939
940 #[test]
941 fn a_push_says_whether_it_made_deleted_or_forced_its_ref() {
942 let repo = repo();
943 let after = "0d1a26e67d8f5eaf1f6ba5c57fc3c7d91ac0fd1c";
Merge GitHub pull request #8 (claude/cool-newton-7nm8r9): Docker deployments on g1t.page behind a switch that stays off until Cloudflare's container APIs are checked live, workflows a workspace manages in its repositories, tarball and zipball downloads, refs/pull/* kept to g1t alone, framework detection for g1t.page builds, and a workflow's event payload matching GitHub's.944 let made = push(&repo, "refs/heads/new", None, after, &[a_commit()], "ada", None, false);
Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9'945 assert_eq!((made["created"].clone(), made["deleted"].clone(), made["forced"].clone()), (json!(true), json!(false), json!(false)));
946 assert_eq!(made["before"], ZERO);
Merge GitHub pull request #8 (claude/cool-newton-7nm8r9): Docker deployments on g1t.page behind a switch that stays off until Cloudflare's container APIs are checked live, workflows a workspace manages in its repositories, tarball and zipball downloads, refs/pull/* kept to g1t alone, framework detection for g1t.page builds, and a workflow's event payload matching GitHub's.947 let forced = push(&repo, "refs/heads/main", Some("6113728f27ae82c7b1a177c8d03f9e96e0adf246"), after, &[a_commit()], "ada", None, true);
Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9'948 assert_eq!(forced["forced"], true);
949 assert_eq!(forced["compare"], format!("https://g1t.sh/acme/web/compare/6113728f27ae82c7b1a177c8d03f9e96e0adf246...{after}"));
950 assert_eq!(forced["head_commit"]["id"], after);
Merge GitHub pull request #8 (claude/cool-newton-7nm8r9): Docker deployments on g1t.page behind a switch that stays off until Cloudflare's container APIs are checked live, workflows a workspace manages in its repositories, tarball and zipball downloads, refs/pull/* kept to g1t alone, framework detection for g1t.page builds, and a workflow's event payload matching GitHub's.951 let gone = push(&repo, "refs/heads/old", Some("6113728f27ae82c7b1a177c8d03f9e96e0adf246"), ZERO, &[], "ada", None, true);
Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9'952 assert_eq!((gone["deleted"].clone(), gone["forced"].clone()), (json!(true), json!(false)));
953 }
954
955 #[test]
Merge GitHub pull request #8 (claude/cool-newton-7nm8r9): Docker deployments on g1t.page behind a switch that stays off until Cloudflare's container APIs are checked live, workflows a workspace manages in its repositories, tarball and zipball downloads, refs/pull/* kept to g1t alone, framework detection for g1t.page builds, and a workflow's event payload matching GitHub's.956 fn a_one_commit_push_lists_what_it_changed() {
957 let repo = repo();
958 let changes = [
959 ("new.txt".to_owned(), FileStatus::Added),
960 ("README.md".to_owned(), FileStatus::Modified),
961 ("old.txt".to_owned(), FileStatus::Deleted),
962 ];
963 let mut one = push(&repo, "refs/heads/main", None, &a_commit().hash, &[a_commit()], "ada", None, false);
964 push_changes(&mut one, &changes);
965 for commit in [&one["commits"][0], &one["head_commit"]] {
966 assert_eq!(commit["added"], json!(["new.txt"]));
967 assert_eq!(commit["modified"], json!(["README.md"]));
968 assert_eq!(commit["removed"], json!(["old.txt"]));
969 }
970 // Several commits: the comparison is not any one commit's.
971 let mut two = push(&repo, "refs/heads/main", None, &a_commit().hash, &[a_commit(), a_commit()], "ada", None, false);
972 push_changes(&mut two, &changes);
973 assert_eq!(two["head_commit"]["modified"], json!([]));
974 }
975
976 #[test]
977 fn labels_have_the_repositorys_colors() {
978 let repo = repo();
979 let mut event = json!({ "issue": issue(&repo, &an_issue()), "label": label(&repo, "bug", None) });
980 // Before the repository's are known, g1t's default for the name.
981 assert_eq!(event["issue"]["labels"][0]["color"], "d73a4a");
982 assert_eq!(event["issue"]["labels"][0]["default"], true);
983 assert!(has_labels(&event));
984 let known = [Label { name: "bug".into(), color: "123456".into(), description: String::new(), issues: 1, pulls: 0 }];
985 color_labels(&mut event, &known);
986 assert_eq!(event["issue"]["labels"][0]["color"], "123456");
987 assert_eq!(event["issue"]["labels"][0]["description"], Value::Null);
988 assert_eq!(event["label"]["color"], "123456");
989 assert!(!has_labels(&json!({ "issue": { "labels": [] } })));
990 }
991
992 #[test]
993 fn a_run_names_its_pull_request_unless_it_is_from_a_fork() {
994 let repo = repo();
995 let data = json!({ "runId": "run_1", "pull": 2 });
996 let pull = a_pull();
997 let run = |pull: Option<&Pull>| {
998 workflow_run(&repo, &FollowedRun {
999 data: &data, workflow_id: None, attempt: None, created_at: None, started_at: None, updated_at: None,
1000 actor: None, triggering_actor: None, title: None, head_commit: None, pull, base_sha: Some("b4se"),
1001 })
1002 };
1003 let named = run(Some(&pull));
1004 let first = &named["pull_requests"][0];
1005 assert_eq!(first["number"], 2);
1006 assert_eq!(first["id"], json!(github_id(&pull.id)));
1007 assert_eq!(first["head"]["ref"], "changes");
1008 assert_eq!(first["head"]["sha"], "ec26c3e57ca3a959ca5aad62de7213c562f8c821");
1009 assert_eq!(first["base"]["ref"], "main");
1010 assert_eq!(first["base"]["sha"], "b4se");
1011 assert_eq!(first["base"]["repo"]["id"], json!(github_id(&repo.id)));
1012 let mut forked = a_pull();
1013 forked.fork = Some(RepoPath { namespace: "ana".into(), name: "web".into() });
1014 assert_eq!(run(Some(&forked))["pull_requests"], json!([]));
1015 assert_eq!(run(None)["pull_requests"][0]["number"], 2, "unread, still numbered");
1016 }
1017
1018 #[test]
Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9'1019 fn a_pull_request_from_a_fork_names_the_fork() {
1020 let mut pull = a_pull();
1021 pull.fork = Some(RepoPath { namespace: "ana".into(), name: "web".into() });
1022 pull.fork_repo_id = Some("rep_f".into());
1023 pull.branch = None;
1024 let event = self::pull(&repo(), &pull);
1025 assert_eq!(event["head"]["repo"]["full_name"], "ana/web");
1026 assert_eq!(event["head"]["repo"]["fork"], true);
1027 assert_eq!(event["head"]["repo"]["id"], json!(github_id("rep_f")));
1028 assert_eq!(event["head"]["label"], "ana:pull/2");
1029 assert_eq!(event["head"]["user"]["login"], "ana");
1030 assert_eq!(event["merge_commit_sha"], Value::Null, "only once merged");
1031 }
1032}

This file's history is long; its oldest lines are credited to the oldest commit read.