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.

Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1//! `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.
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put12
Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9'13use g1t_actions::events::{account_id, github_id};
g1t is the stored author of what it opens; the person who asked is requested_by and keeps the author's rights14use 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};
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put18use 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
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put38pub 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),
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put42 "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),
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put47 "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,
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put53 "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,
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put57 "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
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put94pub 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 })
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put102}
103
g1t is the stored author of what it opens; the person who asked is requested_by and keeps the author's rights104/// Someone as GitHub names them. g1t, the author of what it makes and
105/// files, is a `Bot`, as an app is there.
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put106fn person(user: &User) -> Value {
g1t is the stored author of what it opens; the person who asked is requested_by and keeps the author's rights107 let mut named = self::user(&user.username);
Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9'108 named["node_id"] = json!(user.id);
g1t is the stored author of what it opens; the person who asked is requested_by and keeps the author's rights109 if matches!(user.kind, PrincipalKind::Agent | PrincipalKind::System) {
110 named["type"] = json!("Bot");
111 }
112 named
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put113}
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 }
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put166}
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
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put182pub 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": [],
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put195 })
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;
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put235 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 };
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put242 json!({
243 "ref": git_ref,
Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9'244 "before": before,
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put245 "after": after,
Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9'246 "created": created,
247 "deleted": deleted,
248 "forced": forced && !created && !deleted,
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put249 "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),
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put253 "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 },
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put255 "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,
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put265 "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()),
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put273 "user": person(&issue.author),
g1t is the stored author of what it opens; the person who asked is requested_by and keeps the author's rights274 // 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),
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put278 "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}"),
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put285 })
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 {
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put292 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()),
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put298 };
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 };
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put305 json!({
Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9'306 "id": github_id(&pull.id),
307 "node_id": pull.id,
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put308 "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,
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put313 "draft": pull.status == PullStatus::Draft,
Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9'314 "merged": merged,
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put315 "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()),
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put322 "user": person(&pull.author),
g1t is the stored author of what it opens; the person who asked is requested_by and keeps the author's rights323 // 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),
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put328 "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),
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put333 "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,
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put337 "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),
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put340 "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,
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put348 "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),
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put351 })
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
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put375/// 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.
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put382pub 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,
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put388 "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),
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put392 "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),
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put417 })
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 {
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put424 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,
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put428 "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()),
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put436 "user": person(&pull.author),
g1t is the stored author of what it opens; the person who asked is requested_by and keeps the author's rights437 "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,
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put442 "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}"),
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put445 "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.