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 balance shows what was spent this month and in total | 1 | //! `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. | |
| 4 | ||
| g1t is the stored author of what it opens; the person who asked is requested_by and keeps the author's rights | 5 | use g1t_contracts::{PrincipalKind, User}; |
| The balance shows what was spent this month and in total | 6 | use g1t_contracts::repos::{Commit, Repo}; |
| Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar | 7 | use g1t_contracts::work::{Comment, Issue, MilestoneRef, Pull, PullStatus, State}; |
| The balance shows what was spent this month and in total | 8 | use serde_json::{Value, json}; |
| 9 | ||
| 10 | use crate::{API, SITE}; | |
| 11 | ||
| 12 | pub fn repository(repo: &Repo) -> Value { | |
| 13 | let full_name = format!("{}/{}", repo.namespace, repo.name); | |
| 14 | json!({ | |
| 15 | "id": repo.id, | |
| 16 | "node_id": repo.id, | |
| 17 | "name": repo.name, | |
| 18 | "full_name": full_name, | |
| 19 | "private": repo.is_private, | |
| 20 | "owner": { "login": repo.namespace, "type": "Organization" }, | |
| 21 | "html_url": format!("{SITE}/{full_name}"), | |
| 22 | "url": format!("{API}/repos/{full_name}"), | |
| 23 | "clone_url": format!("{SITE}/{full_name}.git"), | |
| 24 | "description": repo.description, | |
| 25 | "default_branch": repo.default_branch, | |
| 26 | "fork": false, | |
| 27 | "visibility": if repo.is_private { "private" } else { "public" }, | |
| 28 | }) | |
| 29 | } | |
| 30 | ||
| 31 | pub fn user(login: &str) -> Value { | |
| 32 | json!({ "login": login, "type": "User", "html_url": format!("{SITE}/{login}") }) | |
| 33 | } | |
| 34 | ||
| g1t is the stored author of what it opens; the person who asked is requested_by and keeps the author's rights | 35 | /// Someone as GitHub names them. g1t, the author of what it makes and |
| 36 | /// files, is a `Bot`, as an app is there. | |
| The balance shows what was spent this month and in total | 37 | fn 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 rights | 38 | let mut named = self::user(&user.username); |
| 39 | if matches!(user.kind, PrincipalKind::Agent | PrincipalKind::System) { | |
| 40 | named["type"] = json!("Bot"); | |
| 41 | } | |
| 42 | named | |
| The balance shows what was spent this month and in total | 43 | } |
| 44 | ||
| 45 | fn labels(names: &[String]) -> Value { | |
| 46 | Value::Array(names.iter().map(|name| json!({ "name": name })).collect()) | |
| 47 | } | |
| 48 | ||
| Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar | 49 | /// A milestone as an issue or a pull request names it. |
| 50 | fn milestone(milestone: Option<&MilestoneRef>) -> Value { | |
| 51 | milestone.map_or(Value::Null, |milestone| json!({ "number": milestone.number, "title": milestone.title })) | |
| 52 | } | |
| 53 | ||
| The balance shows what was spent this month and in total | 54 | pub fn commit(repo: &Repo, commit: &Commit) -> Value { |
| 55 | json!({ | |
| 56 | "id": commit.hash, | |
| 57 | "tree_id": commit.tree_hash, | |
| 58 | "message": commit.message, | |
| 59 | "timestamp": commit.authored_at, | |
| 60 | "url": format!("{SITE}/{}/{}/commit/{}", repo.namespace, repo.name, commit.hash), | |
| 61 | "author": { "name": commit.author.name, "email": commit.author.email }, | |
| 62 | "committer": { "name": commit.author.name, "email": commit.author.email }, | |
| 63 | "distinct": true, | |
| 64 | }) | |
| 65 | } | |
| 66 | ||
| 67 | pub fn push(repo: &Repo, git_ref: &str, before: Option<&str>, after: &str, commits: &[Commit], pusher: &str) -> Value { | |
| 68 | let zero = "0000000000000000000000000000000000000000"; | |
| 69 | let commits: Vec<Value> = commits.iter().map(|c| commit(repo, c)).collect(); | |
| 70 | json!({ | |
| 71 | "ref": git_ref, | |
| 72 | "before": before.unwrap_or(zero), | |
| 73 | "after": after, | |
| 74 | "created": before.is_none(), | |
| 75 | "deleted": false, | |
| 76 | "forced": false, | |
| 77 | "base_ref": null, | |
| 78 | "compare": format!("{SITE}/{}/{}/commit/{after}", repo.namespace, repo.name), | |
| 79 | "head_commit": commits.first().cloned().unwrap_or(Value::Null), | |
| 80 | "commits": commits, | |
| 81 | "pusher": { "name": pusher, "email": null }, | |
| 82 | "repository": repository(repo), | |
| 83 | "sender": user(pusher), | |
| 84 | }) | |
| 85 | } | |
| 86 | ||
| 87 | pub fn issue(repo: &Repo, issue: &Issue) -> Value { | |
| 88 | let full_name = format!("{}/{}", repo.namespace, repo.name); | |
| 89 | json!({ | |
| 90 | "id": issue.id, | |
| 91 | "number": issue.number, | |
| 92 | "title": issue.title, | |
| 93 | "body": issue.body, | |
| 94 | "state": if issue.state == State::Open { "open" } else { "closed" }, | |
| 95 | "state_reason": issue.reason, | |
| 96 | "labels": labels(&issue.labels), | |
| Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar | 97 | "milestone": milestone(issue.milestone.as_ref()), |
| The balance shows what was spent this month and in total | 98 | "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 rights | 99 | // g1t's own field: for an issue its agent filed, who it worked for. |
| 100 | "requested_by": issue.requested_by.as_ref().map(person), | |
| The balance shows what was spent this month and in total | 101 | "assignees": issue.assignees.iter().map(|a| user(a)).collect::<Vec<_>>(), |
| 102 | "comments": issue.comment_count, | |
| 103 | "created_at": issue.created_at, | |
| 104 | "updated_at": issue.updated_at, | |
| 105 | "closed_at": issue.closed_at, | |
| 106 | "html_url": format!("{SITE}/{full_name}/issues/{}", issue.number), | |
| 107 | "url": format!("{API}/repos/{full_name}/issues/{}", issue.number), | |
| 108 | }) | |
| 109 | } | |
| 110 | ||
| Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar | 111 | /// A pull request, with its own labels. |
| 112 | pub fn pull(repo: &Repo, pull: &Pull) -> Value { | |
| The balance shows what was spent this month and in total | 113 | let full_name = format!("{}/{}", repo.namespace, repo.name); |
| 114 | let head_ref = head_ref(pull); | |
| Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar | 115 | let base_ref = pull.base_branch(&repo.default_branch); |
| The balance shows what was spent this month and in total | 116 | let head_repo = match &pull.fork { |
| 117 | Some(fork) => json!({ "full_name": format!("{}/{}", fork.namespace, fork.name), "fork": true }), | |
| 118 | None => json!({ "full_name": full_name, "fork": false }), | |
| 119 | }; | |
| 120 | json!({ | |
| 121 | "id": pull.id, | |
| 122 | "number": pull.number, | |
| 123 | "title": pull.title, | |
| 124 | "body": pull.body, | |
| 125 | "state": if pull.status.is_active() { "open" } else { "closed" }, | |
| 126 | "draft": pull.status == PullStatus::Draft, | |
| 127 | "merged": pull.status == PullStatus::Merged, | |
| 128 | "merged_at": pull.merged_at, | |
| 129 | "merged_by": pull.merged_by.as_deref().map(user), | |
| 130 | "merge_commit_sha": if pull.status == PullStatus::Merged { pull.head_commit.clone() } else { None }, | |
| Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar | 131 | "labels": labels(&pull.labels), |
| 132 | "milestone": milestone(pull.milestone.as_ref()), | |
| The balance shows what was spent this month and in total | 133 | "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 rights | 134 | // g1t's own field: for a change g1t made, who asked for it. |
| 135 | "requested_by": pull.requested_by.as_ref().map(person), | |
| The balance shows what was spent this month and in total | 136 | "assignees": pull.assignees.iter().map(|a| user(a)).collect::<Vec<_>>(), |
| 137 | "requested_reviewers": pull.reviewers.iter().map(|r| user(r)).collect::<Vec<_>>(), | |
| 138 | "head": { | |
| 139 | "ref": head_ref, | |
| 140 | "sha": pull.head_commit, | |
| 141 | "label": format!("{}:{head_ref}", repo.namespace), | |
| 142 | "repo": head_repo, | |
| 143 | }, | |
| 144 | "base": { | |
| Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar | 145 | "ref": base_ref, |
| The balance shows what was spent this month and in total | 146 | "sha": pull.merge_base, |
| Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar | 147 | "label": format!("{}:{base_ref}", repo.namespace), |
| The balance shows what was spent this month and in total | 148 | "repo": repository(repo), |
| 149 | }, | |
| 150 | "html_url": format!("{SITE}/{full_name}/pull/{}", pull.number), | |
| 151 | "url": format!("{API}/repos/{full_name}/pulls/{}", pull.number), | |
| 152 | "issue_url": pull.issue.map(|n| format!("{API}/repos/{full_name}/issues/{n}")), | |
| 153 | }) | |
| 154 | } | |
| 155 | ||
| Merge branch 'claude/cool-newton-7nm8r9' (#36): real GitHub Actions on g1t, managed deploy workflows, g1t/deploy and the Sysbox runner 0.2.0. The merge keeps both sides' webhook events and API operations, says agent images need tar, guards the job engine's stop to Unix so the runner builds on Windows again, and keeps the moves tests' previews from going idle as the calendar passes their fixture's date. | 156 | /// What a `labeled`, `unlabeled`, `milestoned`, `demilestoned`, |
| 157 | /// `assigned` or base change was about, from the g1t event: `label`, | |
| 158 | /// `milestone`, `assignee` (the first of those newly assigned; g1t sends | |
| 159 | /// one event for them all, GitHub one each), and for a new base, | |
| 160 | /// `changes.base.ref.from` is not known, so only the new base is in | |
| 161 | /// `pull_request.base`. | |
| Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar | 162 | pub fn changed(payload: &mut Value, data: &Value) { |
| Merge branch 'claude/cool-newton-7nm8r9' (#36): real GitHub Actions on g1t, managed deploy workflows, g1t/deploy and the Sysbox runner 0.2.0. The merge keeps both sides' webhook events and API operations, says agent images need tar, guards the job engine's stop to Unix so the runner builds on Windows again, and keeps the moves tests' previews from going idle as the calendar passes their fixture's date. | 163 | if let Some(first) = data["added"].as_array().and_then(|added| added.first()).and_then(Value::as_str) { |
| 164 | payload["assignee"] = user(first); | |
| 165 | } | |
| Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar | 166 | if let Some(label) = data.get("label").filter(|label| label.is_object()) { |
| 167 | payload["label"] = json!({ "name": label["name"], "color": label["color"] }); | |
| 168 | } | |
| 169 | if let Some(milestone) = data.get("milestone").filter(|milestone| milestone.is_object()) { | |
| 170 | payload["milestone"] = json!({ "number": milestone["number"], "title": milestone["title"] }); | |
| 171 | } | |
| 172 | } | |
| 173 | ||
| The balance shows what was spent this month and in total | 174 | /// The branch a pull request comes from, or a name for its fork. |
| 175 | pub fn head_ref(pull: &Pull) -> String { | |
| 176 | pull.branch.clone().unwrap_or_else(|| format!("pull/{}", pull.number)) | |
| 177 | } | |
| 178 | ||
| 179 | pub fn comment(repo: &Repo, number: u32, comment: &Comment, on_pull: bool) -> Value { | |
| 180 | let full_name = format!("{}/{}", repo.namespace, repo.name); | |
| 181 | let page = if on_pull { "pull" } else { "issues" }; | |
| 182 | json!({ | |
| 183 | "id": comment.id, | |
| 184 | "body": comment.body, | |
| 185 | "user": person(&comment.author), | |
| 186 | "created_at": comment.created_at, | |
| 187 | "updated_at": comment.created_at, | |
| 188 | "path": comment.path, | |
| 189 | "line": comment.line, | |
| 190 | "html_url": format!("{SITE}/{full_name}/{page}/{number}#{}", comment.id), | |
| 191 | }) | |
| 192 | } | |
| 193 | ||
| 194 | /// An issue as `issue_comment` gives it for a pull request: the pull | |
| 195 | /// request's number and title, with `pull_request` set. | |
| Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar | 196 | pub fn pull_as_issue(repo: &Repo, pull: &Pull) -> Value { |
| The balance shows what was spent this month and in total | 197 | let full_name = format!("{}/{}", repo.namespace, repo.name); |
| 198 | json!({ | |
| 199 | "id": pull.id, | |
| 200 | "number": pull.number, | |
| 201 | "title": pull.title, | |
| 202 | "body": pull.body, | |
| 203 | "state": if pull.status.is_active() { "open" } else { "closed" }, | |
| Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar | 204 | "labels": labels(&pull.labels), |
| 205 | "milestone": milestone(pull.milestone.as_ref()), | |
| The balance shows what was spent this month and in total | 206 | "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 rights | 207 | "requested_by": pull.requested_by.as_ref().map(person), |
| The balance shows what was spent this month and in total | 208 | "html_url": format!("{SITE}/{full_name}/pull/{}", pull.number), |
| 209 | "pull_request": { | |
| 210 | "url": format!("{API}/repos/{full_name}/pulls/{}", pull.number), | |
| 211 | "html_url": format!("{SITE}/{full_name}/pull/{}", pull.number), | |
| 212 | "merged_at": pull.merged_at, | |
| 213 | }, | |
| 214 | }) | |
| 215 | } | |
| 216 | ||
| Merge Actions: cross-repo workflows and actions, release and deployment triggers, step timeouts | 217 | /// A comment as the event that deleted it kept it (`comment.deleted`'s |
| 218 | /// `comment`), since it can no longer be read. | |
| 219 | pub fn deleted_comment(repo: &Repo, number: u32, kept: &Value, on_pull: bool) -> Value { | |
| 220 | let full_name = format!("{}/{}", repo.namespace, repo.name); | |
| 221 | let page = if on_pull { "pull" } else { "issues" }; | |
| 222 | let id = kept["id"].as_str().unwrap_or_default(); | |
| 223 | let login = kept["author"]["username"].as_str().unwrap_or_default(); | |
| 224 | json!({ | |
| 225 | "id": id, | |
| 226 | "body": kept["body"], | |
| 227 | "user": user(login), | |
| 228 | "created_at": kept["createdAt"], | |
| 229 | "updated_at": kept["createdAt"], | |
| 230 | "path": kept["path"], | |
| 231 | "line": kept["line"], | |
| 232 | "html_url": format!("{SITE}/{full_name}/{page}/{number}#{id}"), | |
| 233 | }) | |
| 234 | } | |
| 235 | ||
| 236 | /// A release, from what a `release.*` event carries | |
| 237 | /// (`g1t_contracts::about::Release`). | |
| 238 | pub fn release(repo: &Repo, release: &Value) -> Value { | |
| 239 | let full_name = format!("{}/{}", repo.namespace, repo.name); | |
| 240 | let tag = release["tagName"].as_str().unwrap_or_default(); | |
| 241 | let id = release["id"].as_str().unwrap_or_default(); | |
| 242 | json!({ | |
| 243 | "id": id, | |
| 244 | "node_id": id, | |
| 245 | "tag_name": tag, | |
| 246 | "target_commitish": release["target"], | |
| 247 | "name": release["name"].as_str().unwrap_or(tag), | |
| 248 | "body": release["body"], | |
| 249 | "draft": release["draft"].as_bool().unwrap_or(false), | |
| 250 | "prerelease": release["prerelease"].as_bool().unwrap_or(false), | |
| 251 | "created_at": release["createdAt"], | |
| 252 | "published_at": release["publishedAt"], | |
| 253 | "author": user(release["author"].as_str().unwrap_or(&repo.namespace)), | |
| 254 | "assets": [], | |
| 255 | "html_url": format!("{SITE}/{full_name}/releases/tag/{tag}"), | |
| 256 | "url": format!("{API}/repos/{full_name}/releases/{id}"), | |
| 257 | }) | |
| 258 | } | |
| 259 | ||
| 260 | /// A deployment, from what `deployment.created` and | |
| 261 | /// `deployment_status.created` carry (`RepoDeployment`, already in GitHub's | |
| 262 | /// spelling). | |
| 263 | pub fn deployment(repo: &Repo, deployment: &Value) -> Value { | |
| 264 | let full_name = format!("{}/{}", repo.namespace, repo.name); | |
| 265 | let id = deployment["id"].as_str().unwrap_or_default(); | |
| 266 | json!({ | |
| 267 | "id": id, | |
| 268 | "node_id": id, | |
| 269 | "sha": deployment["sha"], | |
| 270 | "ref": deployment["ref"], | |
| 271 | "task": deployment["task"].as_str().unwrap_or("deploy"), | |
| 272 | "payload": deployment.get("payload").filter(|p| p.is_object()).cloned().unwrap_or_else(|| json!({})), | |
| 273 | "environment": deployment["environment"], | |
| 274 | "original_environment": deployment["environment"], | |
| 275 | "description": deployment["description"], | |
| 276 | "creator": user(deployment["creator"].as_str().unwrap_or("g1t")), | |
| 277 | "transient_environment": deployment["transient_environment"].as_bool().unwrap_or(false), | |
| 278 | "production_environment": deployment["production_environment"].as_bool().unwrap_or(false), | |
| 279 | "created_at": deployment["created_at"], | |
| 280 | "updated_at": deployment["updated_at"], | |
| 281 | "url": format!("{API}/repos/{full_name}/deployments/{id}"), | |
| 282 | "statuses_url": format!("{API}/repos/{full_name}/deployments/{id}/statuses"), | |
| 283 | }) | |
| 284 | } | |
| 285 | ||
| 286 | /// A deployment's status (`DeploymentStatus`), for `deployment_status`. | |
| 287 | pub fn deployment_status(repo: &Repo, status: &Value, deployment: &Value) -> Value { | |
| 288 | let full_name = format!("{}/{}", repo.namespace, repo.name); | |
| 289 | let deployment_id = deployment["id"].as_str().unwrap_or_default(); | |
| 290 | let id = status["id"].as_str().unwrap_or_default(); | |
| 291 | json!({ | |
| 292 | "id": id, | |
| 293 | "node_id": id, | |
| 294 | "state": status["state"], | |
| 295 | "description": status["description"].as_str().unwrap_or_default(), | |
| 296 | "environment": deployment["environment"], | |
| 297 | "environment_url": status["environment_url"].as_str().unwrap_or_default(), | |
| 298 | "log_url": status["log_url"].as_str().unwrap_or_default(), | |
| 299 | "target_url": status["log_url"].as_str().unwrap_or_default(), | |
| 300 | "creator": user(status["creator"].as_str().unwrap_or("g1t")), | |
| 301 | "created_at": status["created_at"], | |
| 302 | "updated_at": status["created_at"], | |
| 303 | "deployment_url": format!("{API}/repos/{full_name}/deployments/{deployment_id}"), | |
| 304 | "url": format!("{API}/repos/{full_name}/deployments/{deployment_id}/statuses/{id}"), | |
| 305 | }) | |
| 306 | } | |
| 307 | ||
| Merge branch 'claude/cool-newton-7nm8r9' (#36): real GitHub Actions on g1t, managed deploy workflows, g1t/deploy and the Sysbox runner 0.2.0. The merge keeps both sides' webhook events and API operations, says agent images need tar, guards the job engine's stop to Unix so the runner builds on Windows again, and keeps the moves tests' previews from going idle as the calendar passes their fixture's date. | 308 | |
| 309 | /// A review's `state` as GitHub's `pull_request_review` event spells it. | |
| 310 | pub(crate) fn review_state(verdict: g1t_contracts::work::Verdict) -> &'static str { | |
| 311 | match verdict { | |
| 312 | g1t_contracts::work::Verdict::Approve => "approved", | |
| 313 | g1t_contracts::work::Verdict::RequestChanges => "changes_requested", | |
| 314 | } | |
| 315 | } | |
| 316 | ||
| 317 | #[cfg(test)] | |
| 318 | mod review_tests { | |
| 319 | use super::*; | |
| 320 | ||
| 321 | #[test] | |
| 322 | fn review_states_are_spelled_as_githubs() { | |
| 323 | assert_eq!(review_state(g1t_contracts::work::Verdict::Approve), "approved"); | |
| 324 | assert_eq!(review_state(g1t_contracts::work::Verdict::RequestChanges), "changes_requested"); | |
| 325 | } | |
| 326 | } |
This file's history is long; its oldest lines are credited to the oldest commit read.