Skip to content

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

g1t.sh

Commit

A workflow's event payload fills the last of the gaps it had against GitHub's. Labels on issues and pull requests carry their `color`, `description` and `default` as the repository has them (one read of its labels, only when the event has any); a push's `pusher.email` is the pusher's noreply address, or g1t's for g1t; a one-commit push lists the paths it `added`, `removed` and `modified` when a `paths` filter has the push compared anyway, which now happens before any of its runs start so every run sees the same payload; and `workflow_run.pull_requests[]` has each pull request's `id`, `number`, `url` and its `head` and `base` with `ref`, `sha` and `repo`, leaving out one from a fork as GitHub does. The fixtures carry GitHub's shapes for these, the known differences are down to the ids the API takes back, and the Actions guide says what each field holds.

Claude-Session:
https://claude.ai/code/session_01PnwTRSNaC7vEHTAuBzjhrn
Claudeagent and Claudeagentcommitted Parentc5c643bBrowse files
6 files+292−410/6 viewed
+15−5
14921492 is true when the commit before the push is not in the new commit's
14931493 history. g1t reads the 20 latest commits to tell, so a forced push onto
14941494 a longer history says `false`. `head_commit` is the commit pushed and
1495− `commits` lists at most 20, oldest first.
1495+ `commits` lists at most 20, oldest first. `pusher.email` is the
1496+ pusher's noreply address (`<id>+<username>@users.noreply.g1t.sh`), or
1497+ `g1t@users.noreply.g1t.sh` for g1t.
1498+- **A commit's `added`, `removed` and `modified`** list the paths the
1499+ push changed when it pushed one commit and a workflow on `push` has a
1500+ `paths` or `paths-ignore` filter, which makes g1t compare the push.
1501+ Otherwise, and for a push of several commits, they are empty lists.
14961502 - **On pull request events,** `pull_request.base.sha` is the tip of the
14971503 branch it merges into when the event happened, and
14981504 `pull_request.merge_commit_sha` is set only once it is merged, to the
15001506 - **On `workflow_run`,** `workflow_run` has the followed run's `name`,
15011507 `path`, `event`, `status`, `conclusion`, `head_branch`, `head_sha`,
15021508 `head_commit`, `run_number`, `run_attempt`, `actor`, `created_at`,
1503− `updated_at`, `html_url`, `repository` and `pull_requests` (each with
1504− its `number` and `url`).
1509+ `updated_at`, `html_url`, `repository` and `pull_requests`. Each pull
1510+ request there has its `id`, `number`, `url`, and `head` and `base`
1511+ with their `ref`, `sha` and `repo` (`id`, `url`, `name`). As on
1512+ GitHub, a pull request from a fork is not listed.
15051513
1506−Labels on `issue` and `pull_request` have their `id` and `name` but no
1507−`color`; a `labeled` event's own `label` has its color.
1514+Labels on `issue`, `pull_request` and a `labeled` event's `label` have
1515+their `id`, `name`, `color` and `description` as the repository has
1516+them, and `default`, which is true for the labels a repository starts
1517+with.
15081518
15091519 ## Checks
15101520
+182−23
1313 use g1t_actions::events::{account_id, github_id};
1414 use g1t_contracts::{PrincipalKind, User};
1515 use g1t_contracts::repos::{Commit, Repo, RepoPath};
16−use g1t_contracts::work::{Comment, Issue, MilestoneRef, Pull, PullStatus, State};
16+use g1t_contracts::repos::FileStatus;
17+use g1t_contracts::work::{Comment, DEFAULT_LABELS, Issue, Label, MilestoneRef, Pull, PullStatus, State, label_color_for};
1718 use serde_json::{Value, json};
1819
1920 use crate::{API, SITE};
119120 Value::Array(names.iter().map(|name| label(repo, name, None)).collect())
120121 }
121122
122−/// A label, by its name in `repo`.
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.
123126 fn label(repo: &Repo, name: &str, color: Option<&Value>) -> Value {
124− let mut label = json!({ "id": github_id(&format!("{}:label:{name}", repo.id)), "name": name });
125− if let Some(color) = color {
126− label["color"] = color.clone();
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`).
139+pub 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.
157+pub 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,
127165 }
128− label
129166 }
130167
131168 /// A milestone as an issue or a pull request names it.
158195 })
159196 }
160197
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.
202+pub 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+
161217 /// A push. `forced` is whether `before` is not in `after`'s history, when
162218 /// that is known; a push whose history is too long to read says `false`.
163−pub fn push(repo: &Repo, git_ref: &str, before: Option<&str>, after: &str, commits: &[Commit], pusher: &str, forced: bool) -> Value {
219+/// `pusher_email` is the pusher's noreply address, when the push was a
220+/// person's.
221+#[allow(clippy::too_many_arguments)]
222+pub 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 {
164232 let before = before.unwrap_or(ZERO);
165233 let created = before == ZERO;
166234 let deleted = after == ZERO;
183251 // GitHub's head commit is the one pushed: the last.
184252 "head_commit": commits.last().cloned().unwrap_or(Value::Null),
185253 "commits": commits,
186− "pusher": { "name": pusher, "email": null },
254+ "pusher": { "name": pusher, "email": pusher_email },
187255 "repository": repository(repo),
188256 "sender": user(pusher),
189257 })
420488 pub triggering_actor: Option<&'a str>,
421489 pub title: Option<&'a str>,
422490 pub head_commit: Option<&'a Commit>,
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>,
423495 }
424496
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.
500+fn 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+
425524 pub fn workflow_run(repo: &Repo, run: &FollowedRun) -> Value {
426525 let full_name = format!("{}/{}", repo.namespace, repo.name);
427526 let data = run.data;
463562 "logs_url": format!("{url}/logs"),
464563 "artifacts_url": format!("{url}/artifacts"),
465564 "workflow_url": run.workflow_id.map(|w| format!("{API}/repos/{full_name}/actions/workflows/{w}")),
466− "pull_requests": data["pull"].as_u64().map(|n| vec![json!({
467− "number": n,
468− "url": format!("{API}/repos/{full_name}/pulls/{n}"),
469− })]).unwrap_or_default(),
565+ "pull_requests": run_pulls(repo, run),
470566 "repository": repository(repo),
471567 "head_repository": repository(repo),
472568 })
586682 ("workflow_run", "workflow_run.id", "string"),
587683 ("workflow_run", "workflow_run.workflow_id", "string"),
588684 ("workflow_run", "workflow.id", "string"),
589− // g1t keeps labels by name on issues and pull requests; only a
590− // `labeled` event's own `label` has its color.
591− ("pull_request", "pull_request.labels[].color", "absent"),
592− ("issues", "issue.labels[].color", "absent"),
593− // A push names who pushed, not their address.
594− ("push", "pusher.email", "null"),
595685 ];
596686
597687 fn kind(value: &Value) -> &'static str {
705795 fn payloads() -> Vec<(&'static str, Value)> {
706796 let repo = repo();
707797 let pull = a_pull();
708− let push = push(&repo, "refs/heads/main", Some("6113728f27ae82c7b1a177c8d03f9e96e0adf246"), &a_commit().hash, &[a_commit()], "ada", false);
709− let pull_request = json!({
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!({
710802 "action": "opened", "number": 2, "pull_request": self::pull(&repo, &pull),
711803 "repository": repository(&repo), "sender": user("ada"),
712804 });
805+ color_labels(&mut pull_request, &known);
713806 let review = json!({
714807 "action": "submitted", "review": self::review(&repo, &pull, Some(&a_comment(Some(Verdict::Approve)))),
715808 "pull_request": self::pull(&repo, &pull), "repository": repository(&repo), "sender": user("cy"),
716809 });
717810 let mut issues = json!({ "action": "labeled", "issue": issue(&repo, &an_issue()), "repository": repository(&repo), "sender": user("ada") });
718811 changed(&repo, &mut issues, &json!({ "label": { "name": "bug", "color": "d73a4a" } }));
812+ color_labels(&mut issues, &known);
719813 let mut on_pull = pull.clone();
720814 on_pull.labels.clear();
721815 on_pull.assignees.clear();
742836 triggering_actor: None,
743837 title: Some("Update README.md"),
744838 head_commit: Some(&head),
839+ pull: Some(&pull),
840+ base_sha: Some("f95f852bd8fca8fcc58a9a2d6c842781e32a215e"),
745841 };
746842 // As trigger.rs gives it.
747843 let workflow_run = json!({
845941 fn a_push_says_whether_it_made_deleted_or_forced_its_ref() {
846942 let repo = repo();
847943 let after = "0d1a26e67d8f5eaf1f6ba5c57fc3c7d91ac0fd1c";
848− let made = push(&repo, "refs/heads/new", None, after, &[a_commit()], "ada", false);
944+ let made = push(&repo, "refs/heads/new", None, after, &[a_commit()], "ada", None, false);
849945 assert_eq!((made["created"].clone(), made["deleted"].clone(), made["forced"].clone()), (json!(true), json!(false), json!(false)));
850946 assert_eq!(made["before"], ZERO);
851− let forced = push(&repo, "refs/heads/main", Some("6113728f27ae82c7b1a177c8d03f9e96e0adf246"), after, &[a_commit()], "ada", true);
947+ let forced = push(&repo, "refs/heads/main", Some("6113728f27ae82c7b1a177c8d03f9e96e0adf246"), after, &[a_commit()], "ada", None, true);
852948 assert_eq!(forced["forced"], true);
853949 assert_eq!(forced["compare"], format!("https://g1t.sh/acme/web/compare/6113728f27ae82c7b1a177c8d03f9e96e0adf246...{after}"));
854950 assert_eq!(forced["head_commit"]["id"], after);
855− let gone = push(&repo, "refs/heads/old", Some("6113728f27ae82c7b1a177c8d03f9e96e0adf246"), ZERO, &[], "ada", true);
951+ let gone = push(&repo, "refs/heads/old", Some("6113728f27ae82c7b1a177c8d03f9e96e0adf246"), ZERO, &[], "ada", None, true);
856952 assert_eq!((gone["deleted"].clone(), gone["forced"].clone()), (json!(true), json!(false)));
857953 }
858954
859955 #[test]
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]
8601019 fn a_pull_request_from_a_fork_names_the_fork() {
8611020 let mut pull = a_pull();
8621021 pull.fork = Some(RepoPath { namespace: "ana".into(), name: "web".into() });
+77−9
88 use g1t_contracts::actions::{DispatchArgs, RepositoryDispatchArgs, WorkflowRun};
99 use g1t_contracts::events::{Event, caused_by_job};
1010 use g1t_contracts::identity::{AGENT_ID, AGENT_NAME, UsernamesArgs};
11−use g1t_contracts::repos::{Commit, CompareArgs, Comparison, LogArgs, PullMergeRef, PullMergeRefArgs, Repo, RepoPath};
12−use g1t_contracts::work::{IssueDetail, PullDetail, ViewArgs};
11+use g1t_contracts::repos::{Commit, CompareArgs, Comparison, FileStatus, LogArgs, PullMergeRef, PullMergeRefArgs, Repo, RepoPath};
12+use g1t_contracts::work::{IssueDetail, Label, PullDetail, ViewArgs};
1313 use g1t_contracts::{FailureCode, Outcome, User, new_id};
1414 use g1t_contracts::time::rfc3339;
1515 use g1t_kit::now_ms;
5353 merge: Option<String>,
5454 }
5555
56+impl Subject {
57+ /// What the event changes, once compared: the paths for `paths`
58+ /// filters, and a push's commit's `added`, `removed` and `modified`.
59+ fn changed(&mut self, changes: Vec<(String, FileStatus)>) {
60+ payload::push_changes(&mut self.payload, &changes);
61+ self.paths = Some(changes.into_iter().map(|(path, _)| path).collect());
62+ }
63+}
64+
65+/// The address a push's `pusher.email` gives: g1t's for its agent and
66+/// itself, a person's noreply address (which needs no lookup) for them,
67+/// and none for anyone else.
68+fn pusher_email(actor: Option<&str>, username: &str) -> Option<String> {
69+ match actor? {
70+ id if id == AGENT_ID || id == g1t_contracts::system::ID => Some(g1t_contracts::system::EMAIL.to_owned()),
71+ id if id.starts_with("usr_") => Some(g1t_contracts::accounts::noreply_address(id, username)),
72+ _ => None,
73+ }
74+}
75+
5676 /// Whether a deployment's `ref` is a commit's full hash rather than a
5777 /// branch or tag.
5878 fn is_commit(name: &str) -> bool {
206226 Ok(log.into_result().ok().and_then(|commits| commits.into_iter().next()))
207227 }
208228
209− async fn changed_paths(&self, repo: &Repo, actor: &User, base: Option<String>, head: String) -> Result<Vec<String>> {
229+ /// What `head` changes from `base`: each path and how.
230+ async fn changed_paths(&self, repo: &Repo, actor: &User, base: Option<String>, head: String) -> Result<Vec<(String, FileStatus)>> {
210231 let compared: Outcome<Comparison> = g1t_kit::call(
211232 &self.repos,
212233 "compare",
219240 },
220241 )
221242 .await?;
222− Ok(compared.into_result().map(|c| c.files.into_iter().map(|f| f.path).collect()).unwrap_or_default())
243+ Ok(compared.into_result().map(|c| c.files.into_iter().map(|f| (f.path, f.status)).collect()).unwrap_or_default())
223244 }
224245
246+ /// The repository's labels, for their colors, when `payload` has any.
247+ async fn label_colors(&self, repo: &Repo, ws: &User, payload: &mut Value) -> Result<()> {
248+ if !payload::has_labels(payload) {
249+ return Ok(());
250+ }
251+ let args = ViewArgs { repo: Self::repo_path(repo), number: 0, viewer: Some(ws.clone()), after_seq: 0 };
252+ let labels: Outcome<Vec<Label>> = g1t_kit::call(&self.work, "list_labels", &args).await?;
253+ if let Outcome::Ok(labels) = labels {
254+ payload::color_labels(payload, &labels);
255+ }
256+ Ok(())
257+ }
258+
225259 async fn default_head(&self, repo: &Repo) -> Result<Option<String>> {
226260 self.branch_head(repo, &repo.default_branch).await
227261 }
318352 let before = data["before"].as_str();
319353 let (commits, forced) = self.pushed_commits(repo, ws, after, before).await?;
320354 let title = commits.last().map(|c| c.message.lines().next().unwrap_or_default().to_owned()).unwrap_or_default();
321− let payload = payload::push(repo, git_ref, before, after, &commits, sender, forced);
355+ let email = pusher_email(event.actor.as_deref(), sender);
356+ let payload = payload::push(repo, git_ref, before, after, &commits, sender, email.as_deref(), forced);
322357 Some(Subject {
323358 source: path.clone(),
324359 source_ref: Some(after.to_owned()),
408443 "sender": payload::user(sender),
409444 });
410445 payload::changed(repo, &mut payload, data);
446+ self.label_colors(repo, ws, &mut payload).await?;
411447 // `base.sha` is the base branch's tip now, as GitHub gives
412448 // it, not where the two last agreed.
413449 if let Some(tip) = self.branch_head(repo, &base_ref).await? {
521557 Some(head) => self.commit_at(&path, ws, head).await?.filter(|commit| commit.hash == head),
522558 None => None,
523559 };
560+ // Its pull request, for `pull_requests[]`.
561+ let pull = match data["pull"].as_u64() {
562+ Some(number) => match g1t_kit::call::<_, Outcome<PullDetail>>(&self.work, "get_pull", &view(number as u32)).await? {
563+ Outcome::Ok(detail) => Some(detail.pull),
564+ Outcome::Fail(_) => None,
565+ },
566+ None => None,
567+ };
568+ let base_sha = match &pull {
569+ Some(pull) if pull.fork.is_none() => self.branch_head(repo, pull.base_branch(&repo.default_branch)).await?,
570+ _ => None,
571+ };
524572 let followed = payload::FollowedRun {
573+ pull: pull.as_ref(),
574+ base_sha: base_sha.as_deref(),
525575 data,
526576 workflow_id: row.as_ref().map(|r| r.workflow_id.as_str()),
527577 attempt: row.as_ref().map(|r| r.attempt),
574624 "sender": payload::user(sender),
575625 });
576626 payload::changed(repo, &mut payload, data);
627+ self.label_colors(repo, ws, &mut payload).await?;
577628 if event_name == "issue_comment" {
578629 let comment_id = data["commentId"].as_str();
579630 if action == Some("deleted") {
764815 if copied_in && matches!(event_name, "push" | "create") {
765816 if subject.paths.is_none() {
766817 let (base, head) = subject.compare.clone().unwrap_or((None, subject.sha.clone()));
767− subject.paths = Some(self.changed_paths(&repo, &ws, base, head).await?);
818+ subject.changed(self.changed_paths(&repo, &ws, base, head).await?);
768819 }
769820 if crate::mirrored::touches_workflows(subject.paths.as_deref().unwrap_or_default())
770821 && let Some(mirror) = &repo.mirror
818869 actor_id: Option<&str>,
819870 sender: &str,
820871 ) -> Result<()> {
821− for file in read.files {
822− let parsed = workflow::parse(&file.source);
872+ let parsed: Vec<_> = read.files.into_iter().map(|file| (workflow::parse(&file.source), file)).collect();
873+ // A push some workflow filters by path is compared before any run
874+ // starts, so every run's payload lists what its commit changed.
875+ let filters_paths = |(parsed, _): &(std::result::Result<Workflow, _>, _)| {
876+ parsed.as_ref().ok().and_then(|w: &Workflow| w.trigger(event_name)).is_some_and(|t| t.paths.is_set())
877+ };
878+ if event_name == "push" && subject.paths.is_none() && parsed.iter().any(filters_paths) {
879+ let (base, head) = subject.compare.clone().unwrap_or((None, subject.sha.clone()));
880+ subject.changed(self.changed_paths(repo, ws, base, head).await?);
881+ }
882+ for (parsed, file) in parsed {
823883 let workflow = match parsed {
824884 Ok(workflow) => workflow,
825885 Err(problem) => {
904964 if trigger.paths.is_set() {
905965 if subject.paths.is_none() {
906966 let (base, head) = subject.compare.clone().unwrap_or((None, subject.sha.clone()));
907− subject.paths = Some(self.changed_paths(repo, ws, base, head).await?);
967+ subject.changed(self.changed_paths(repo, ws, base, head).await?);
908968 }
909969 if !trigger.paths.allows_paths(subject.paths.as_deref().unwrap_or_default()) {
910970 return Ok(false);
14531513 use g1t_contracts::{Membership, PrincipalKind};
14541514
14551515 #[test]
1516+ fn a_pushers_email_is_their_noreply_address() {
1517+ assert_eq!(pusher_email(Some("usr_01j9zq4m8x7k2v5n3b6c1d0efg"), "Ada").as_deref(), Some("6c1d0efg+ada@users.noreply.g1t.sh"));
1518+ assert_eq!(pusher_email(Some(AGENT_ID), AGENT_NAME).as_deref(), Some(g1t_contracts::system::EMAIL));
1519+ assert_eq!(pusher_email(Some("wsp_1"), "acme"), None);
1520+ assert_eq!(pusher_email(None, "acme"), None);
1521+ }
1522+
1523+ #[test]
14561524 fn a_pull_request_builds_its_merge_commit_and_nothing_while_it_conflicts() {
14571525 let merged = PullMergeRef::Merged { head: "h".into(), base: "b".into(), merge: "m".into() };
14581526 let conflict = PullMergeRef::Conflict { head: "h".into(), base: "b".into(), paths: vec!["a".into()] };
+2−2
99 "state": "open",
1010 "state_reason": null,
1111 "locked": false,
12− "labels": [{ "id": 1362934389, "name": "bug", "color": "d73a4a" }],
12+ "labels": [{ "id": 1362934389, "node_id": "MDU6TGFiZWwxMzYyOTM0Mzg5", "name": "bug", "color": "d73a4a", "description": "Something isn't working", "default": true }],
1313 "milestone": { "id": 4317517, "number": 1, "title": "v1.0", "html_url": "https://github.com/acme/web/milestone/1" },
1414 "user": { "login": "ada", "id": 21031068, "node_id": "MDQ6VXNlcjIxMDMxMDY4", "type": "User", "html_url": "https://github.com/ada", "url": "https://api.github.com/users/ada" },
1515 "assignee": { "login": "bo", "id": 21031069, "type": "User", "html_url": "https://github.com/bo", "url": "https://api.github.com/users/bo" },
2222 "url": "https://api.github.com/repos/acme/web/issues/1",
2323 "repository_url": "https://api.github.com/repos/acme/web"
2424 },
25− "label": { "id": 1362934389, "name": "bug", "color": "d73a4a" },
25+ "label": { "id": 1362934389, "node_id": "MDU6TGFiZWwxMzYyOTM0Mzg5", "name": "bug", "color": "d73a4a", "description": "Something isn't working", "default": true },
2626 "repository": { "id": 186853002, "node_id": "MDEwOlJlcG9zaXRvcnkxODY4NTMwMDI=", "full_name": "acme/web", "private": false, "owner": { "login": "acme", "id": 21031067, "type": "Organization" } },
2727 "sender": { "login": "ada", "id": 21031068, "type": "User" }
2828 }
+1−1
1515 "merged_by": null,
1616 "merge_commit_sha": null,
1717 "labels": [
18− { "id": 1362934389, "name": "bug", "color": "d73a4a" }
18+ { "id": 1362934389, "node_id": "MDU6TGFiZWwxMzYyOTM0Mzg5", "name": "bug", "color": "d73a4a", "description": "Something isn't working", "default": true }
1919 ],
2020 "milestone": {
2121 "id": 4317517,
+15−1
3434 "artifacts_url": "https://api.github.com/repos/acme/web/actions/runs/30433642/artifacts",
3535 "workflow_url": "https://api.github.com/repos/acme/web/actions/workflows/159038",
3636 "pull_requests": [
37− { "number": 2, "url": "https://api.github.com/repos/acme/web/pulls/2" }
37+ {
38+ "url": "https://api.github.com/repos/acme/web/pulls/2",
39+ "id": 279147437,
40+ "number": 2,
41+ "head": {
42+ "ref": "changes",
43+ "sha": "ec26c3e57ca3a959ca5aad62de7213c562f8c821",
44+ "repo": { "id": 186853002, "url": "https://api.github.com/repos/acme/web", "name": "web" }
45+ },
46+ "base": {
47+ "ref": "main",
48+ "sha": "f95f852bd8fca8fcc58a9a2d6c842781e32a215e",
49+ "repo": { "id": 186853002, "url": "https://api.github.com/repos/acme/web", "name": "web" }
50+ }
51+ }
3852 ],
3953 "repository": { "id": 186853002, "full_name": "acme/web", "owner": { "login": "acme", "id": 21031067, "type": "Organization" } },
4054 "head_repository": { "id": 186853002, "full_name": "acme/web", "owner": { "login": "acme", "id": 21031067, "type": "Organization" } }

Comments

Sign in to comment.