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 statement is a month at a time, a line per kind of charge | 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 statement is a month at a time, a line per kind of charge | 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 statement is a month at a time, a line per kind of charge | 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 statement is a month at a time, a line per kind of charge | 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 statement is a month at a time, a line per kind of charge | 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 statement is a month at a time, a line per kind of charge | 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 statement is a month at a time, a line per kind of charge | 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 statement is a month at a time, a line per kind of charge | 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 statement is a month at a time, a line per kind of charge | 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 statement is a month at a time, a line per kind of charge | 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 statement is a month at a time, a line per kind of charge | 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 statement is a month at a time, a line per kind of charge | 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 statement is a month at a time, a line per kind of charge | 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 statement is a month at a time, a line per kind of charge | 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 statement is a month at a time, a line per kind of charge | 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 statement is a month at a time, a line per kind of charge | 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 statement is a month at a time, a line per kind of charge | 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 statement is a month at a time, a line per kind of charge | 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.