g1t/services/actions/src/payload.rs

189 lines7,613 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, 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
49pub fn commit(repo: &Repo, commit: &Commit) -> Value {
50 json!({
51 "id": commit.hash,
52 "tree_id": commit.tree_hash,
53 "message": commit.message,
54 "timestamp": commit.authored_at,
55 "url": format!("{SITE}/{}/{}/commit/{}", repo.namespace, repo.name, commit.hash),
56 "author": { "name": commit.author.name, "email": commit.author.email },
57 "committer": { "name": commit.author.name, "email": commit.author.email },
58 "distinct": true,
59 })
60}
61
62pub fn push(repo: &Repo, git_ref: &str, before: Option<&str>, after: &str, commits: &[Commit], pusher: &str) -> Value {
63 let zero = "0000000000000000000000000000000000000000";
64 let commits: Vec<Value> = commits.iter().map(|c| commit(repo, c)).collect();
65 json!({
66 "ref": git_ref,
67 "before": before.unwrap_or(zero),
68 "after": after,
69 "created": before.is_none(),
70 "deleted": false,
71 "forced": false,
72 "base_ref": null,
73 "compare": format!("{SITE}/{}/{}/commit/{after}", repo.namespace, repo.name),
74 "head_commit": commits.first().cloned().unwrap_or(Value::Null),
75 "commits": commits,
76 "pusher": { "name": pusher, "email": null },
77 "repository": repository(repo),
78 "sender": user(pusher),
79 })
80}
81
82pub fn issue(repo: &Repo, issue: &Issue) -> Value {
83 let full_name = format!("{}/{}", repo.namespace, repo.name);
84 json!({
85 "id": issue.id,
86 "number": issue.number,
87 "title": issue.title,
88 "body": issue.body,
89 "state": if issue.state == State::Open { "open" } else { "closed" },
90 "state_reason": issue.reason,
91 "labels": labels(&issue.labels),
92 "user": person(&issue.author),
93 // g1t's own field: for an issue its agent filed, who it worked for.
94 "requested_by": issue.requested_by.as_ref().map(person),
95 "assignees": issue.assignees.iter().map(|a| user(a)).collect::<Vec<_>>(),
96 "comments": issue.comment_count,
97 "created_at": issue.created_at,
98 "updated_at": issue.updated_at,
99 "closed_at": issue.closed_at,
100 "html_url": format!("{SITE}/{full_name}/issues/{}", issue.number),
101 "url": format!("{API}/repos/{full_name}/issues/{}", issue.number),
102 })
103}
104
105/// A pull request. `labels` are its issue's, since g1t labels issues.
106pub fn pull(repo: &Repo, pull: &Pull, labels_of: &[String]) -> Value {
107 let full_name = format!("{}/{}", repo.namespace, repo.name);
108 let head_ref = head_ref(pull);
109 let head_repo = match &pull.fork {
110 Some(fork) => json!({ "full_name": format!("{}/{}", fork.namespace, fork.name), "fork": true }),
111 None => json!({ "full_name": full_name, "fork": false }),
112 };
113 json!({
114 "id": pull.id,
115 "number": pull.number,
116 "title": pull.title,
117 "body": pull.body,
118 "state": if pull.status.is_active() { "open" } else { "closed" },
119 "draft": pull.status == PullStatus::Draft,
120 "merged": pull.status == PullStatus::Merged,
121 "merged_at": pull.merged_at,
122 "merged_by": pull.merged_by.as_deref().map(user),
123 "merge_commit_sha": if pull.status == PullStatus::Merged { pull.head_commit.clone() } else { None },
124 "labels": labels(labels_of),
125 "user": person(&pull.author),
126 // g1t's own field: for a change g1t made, who asked for it.
127 "requested_by": pull.requested_by.as_ref().map(person),
128 "assignees": pull.assignees.iter().map(|a| user(a)).collect::<Vec<_>>(),
129 "requested_reviewers": pull.reviewers.iter().map(|r| user(r)).collect::<Vec<_>>(),
130 "head": {
131 "ref": head_ref,
132 "sha": pull.head_commit,
133 "label": format!("{}:{head_ref}", repo.namespace),
134 "repo": head_repo,
135 },
136 "base": {
137 "ref": repo.default_branch,
138 "sha": pull.merge_base,
139 "label": format!("{}:{}", repo.namespace, repo.default_branch),
140 "repo": repository(repo),
141 },
142 "html_url": format!("{SITE}/{full_name}/pull/{}", pull.number),
143 "url": format!("{API}/repos/{full_name}/pulls/{}", pull.number),
144 "issue_url": pull.issue.map(|n| format!("{API}/repos/{full_name}/issues/{n}")),
145 })
146}
147
148/// The branch a pull request comes from, or a name for its fork.
149pub fn head_ref(pull: &Pull) -> String {
150 pull.branch.clone().unwrap_or_else(|| format!("pull/{}", pull.number))
151}
152
153pub fn comment(repo: &Repo, number: u32, comment: &Comment, on_pull: bool) -> Value {
154 let full_name = format!("{}/{}", repo.namespace, repo.name);
155 let page = if on_pull { "pull" } else { "issues" };
156 json!({
157 "id": comment.id,
158 "body": comment.body,
159 "user": person(&comment.author),
160 "created_at": comment.created_at,
161 "updated_at": comment.created_at,
162 "path": comment.path,
163 "line": comment.line,
164 "html_url": format!("{SITE}/{full_name}/{page}/{number}#{}", comment.id),
165 })
166}
167
168/// An issue as `issue_comment` gives it for a pull request: the pull
169/// request's number and title, with `pull_request` set.
170pub fn pull_as_issue(repo: &Repo, pull: &Pull, labels_of: &[String]) -> Value {
171 let full_name = format!("{}/{}", repo.namespace, repo.name);
172 json!({
173 "id": pull.id,
174 "number": pull.number,
175 "title": pull.title,
176 "body": pull.body,
177 "state": if pull.status.is_active() { "open" } else { "closed" },
178 "labels": labels(labels_of),
179 "user": person(&pull.author),
180 "requested_by": pull.requested_by.as_ref().map(person),
181 "html_url": format!("{SITE}/{full_name}/pull/{}", pull.number),
182 "pull_request": {
183 "url": format!("{API}/repos/{full_name}/pulls/{}", pull.number),
184 "html_url": format!("{SITE}/{full_name}/pull/{}", pull.number),
185 "merged_at": pull.merged_at,
186 },
187 })
188}
189