Skip to content

g1t/services/actions/src/payload.rs

211 lines8,673 bytesCodeBlame
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
5use g1t_contracts::{PrincipalKind, User};
6use g1t_contracts::repos::{Commit, Repo};
7use g1t_contracts::work::{Comment, Issue, MilestoneRef, Pull, PullStatus, State};
8use serde_json::{Value, json};
9
10use crate::{API, SITE};
11
12pub 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
31pub fn user(login: &str) -> Value {
32 json!({ "login": login, "type": "User", "html_url": format!("{SITE}/{login}") })
33}
34
35/// Someone as GitHub names them. g1t, the author of what it makes and
36/// files, is a `Bot`, as an app is there.
37fn person(user: &User) -> Value {
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
43}
44
45fn labels(names: &[String]) -> Value {
46 Value::Array(names.iter().map(|name| json!({ "name": name })).collect())
47}
48
49/// A milestone as an issue or a pull request names it.
50fn milestone(milestone: Option<&MilestoneRef>) -> Value {
51 milestone.map_or(Value::Null, |milestone| json!({ "number": milestone.number, "title": milestone.title }))
52}
53
54pub 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
67pub 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
87pub 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),
97 "milestone": milestone(issue.milestone.as_ref()),
98 "user": person(&issue.author),
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),
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
111/// A pull request, with its own labels.
112pub fn pull(repo: &Repo, pull: &Pull) -> Value {
113 let full_name = format!("{}/{}", repo.namespace, repo.name);
114 let head_ref = head_ref(pull);
115 let base_ref = pull.base_branch(&repo.default_branch);
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 },
131 "labels": labels(&pull.labels),
132 "milestone": milestone(pull.milestone.as_ref()),
133 "user": person(&pull.author),
134 // g1t's own field: for a change g1t made, who asked for it.
135 "requested_by": pull.requested_by.as_ref().map(person),
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": {
145 "ref": base_ref,
146 "sha": pull.merge_base,
147 "label": format!("{}:{base_ref}", repo.namespace),
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
156/// What a `labeled`, `unlabeled`, `milestoned`, `demilestoned` or base
157/// change was about, from the g1t event: `label`, `milestone`, and for a
158/// new base, `changes.base.ref.from` is not known, so only the new base is
159/// in `pull_request.base`.
160pub fn changed(payload: &mut Value, data: &Value) {
161 if let Some(label) = data.get("label").filter(|label| label.is_object()) {
162 payload["label"] = json!({ "name": label["name"], "color": label["color"] });
163 }
164 if let Some(milestone) = data.get("milestone").filter(|milestone| milestone.is_object()) {
165 payload["milestone"] = json!({ "number": milestone["number"], "title": milestone["title"] });
166 }
167}
168
169/// The branch a pull request comes from, or a name for its fork.
170pub fn head_ref(pull: &Pull) -> String {
171 pull.branch.clone().unwrap_or_else(|| format!("pull/{}", pull.number))
172}
173
174pub fn comment(repo: &Repo, number: u32, comment: &Comment, on_pull: bool) -> Value {
175 let full_name = format!("{}/{}", repo.namespace, repo.name);
176 let page = if on_pull { "pull" } else { "issues" };
177 json!({
178 "id": comment.id,
179 "body": comment.body,
180 "user": person(&comment.author),
181 "created_at": comment.created_at,
182 "updated_at": comment.created_at,
183 "path": comment.path,
184 "line": comment.line,
185 "html_url": format!("{SITE}/{full_name}/{page}/{number}#{}", comment.id),
186 })
187}
188
189/// An issue as `issue_comment` gives it for a pull request: the pull
190/// request's number and title, with `pull_request` set.
191pub fn pull_as_issue(repo: &Repo, pull: &Pull) -> Value {
192 let full_name = format!("{}/{}", repo.namespace, repo.name);
193 json!({
194 "id": pull.id,
195 "number": pull.number,
196 "title": pull.title,
197 "body": pull.body,
198 "state": if pull.status.is_active() { "open" } else { "closed" },
199 "labels": labels(&pull.labels),
200 "milestone": milestone(pull.milestone.as_ref()),
201 "user": person(&pull.author),
202 "requested_by": pull.requested_by.as_ref().map(person),
203 "html_url": format!("{SITE}/{full_name}/pull/{}", pull.number),
204 "pull_request": {
205 "url": format!("{API}/repos/{full_name}/pulls/{}", pull.number),
206 "html_url": format!("{SITE}/{full_name}/pull/{}", pull.number),
207 "merged_at": pull.merged_at,
208 },
209 })
210}
211