g1t/crates/actions/src/events.rs
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.
| GitHub Actions on g1t, part one: reading workflows | 1 | //! g1t's events as GitHub's: which event and activity type each one is, |
| 2 | //! and the `github` context and `GITHUB_*` variables a run sees. | |
| 3 | ||
| 4 | use serde::{Deserialize, Serialize}; | |
| 5 | use serde_json::{Map, Value, json}; | |
| 6 | ||
| 7 | /// The GitHub event and activity type for a g1t event, if it has one. | |
| 8 | /// A g1t event can be more than one GitHub event: a pull request opening | |
| 9 | /// is `pull_request` and `pull_request_target`. | |
| 10 | pub fn github_events(kind: &str) -> Vec<(&'static str, Option<&'static str>)> { | |
| 11 | let pull = |action| vec![("pull_request", Some(action)), ("pull_request_target", Some(action))]; | |
| 12 | match kind { | |
| 13 | "git.push" => vec![("push", None)], | |
| 14 | "pull.opened" => pull("opened"), | |
| 15 | "pull.updated" => pull("synchronize"), | |
| 16 | "pull.ready" => pull("ready_for_review"), | |
| 17 | "pull.closed" | "pull.merged" => pull("closed"), | |
| 18 | "issue.opened" => vec![("issues", Some("opened"))], | |
| 19 | "issue.updated" => vec![("issues", Some("edited"))], | |
| 20 | "issue.closed" => vec![("issues", Some("closed"))], | |
| 21 | "issue.reopened" => vec![("issues", Some("reopened"))], | |
| 22 | "issue.assigned" => vec![("issues", Some("assigned"))], | |
| 23 | "comment.created" => vec![("issue_comment", Some("created"))], | |
| 24 | "review.completed" => vec![("pull_request_review", Some("submitted"))], | |
| Actions: workflow_run, workflow.completed, artifacts on the run page, Node 24 | 25 | "workflow.completed" => vec![("workflow_run", Some("completed"))], |
| GitHub Actions on g1t, part one: reading workflows | 26 | _ => Vec::new(), |
| 27 | } | |
| 28 | } | |
| 29 | ||
| 30 | /// What a run is about: enough to fill the `github` context. | |
| 31 | #[derive(Clone, Debug, Default, Serialize, Deserialize)] | |
| 32 | #[serde(rename_all = "camelCase")] | |
| 33 | pub struct RunInfo { | |
| 34 | /// `acme/web`. | |
| 35 | pub repository: String, | |
| 36 | pub repository_id: String, | |
| 37 | pub default_branch: String, | |
| 38 | pub event_name: String, | |
| 39 | /// The webhook-shaped payload, `github.event`. | |
| 40 | pub event: Value, | |
| 41 | /// `refs/heads/main`, `refs/tags/v1`, `refs/pull/3/merge`. | |
| 42 | pub git_ref: String, | |
| 43 | pub sha: String, | |
| 44 | /// For pull requests: the head and base branches. | |
| 45 | pub head_ref: Option<String>, | |
| 46 | pub base_ref: Option<String>, | |
| 47 | pub actor: String, | |
| 48 | pub actor_id: String, | |
| 49 | pub triggering_actor: String, | |
| 50 | pub run_id: String, | |
| 51 | pub run_number: u64, | |
| 52 | pub run_attempt: u64, | |
| 53 | /// The workflow's name. | |
| 54 | pub workflow: String, | |
| GitHub Actions on g1t, part three: .g1t/workflows, the pages, the docs | 55 | /// `.g1t/workflows/ci.yml`. |
| GitHub Actions on g1t, part one: reading workflows | 56 | pub workflow_path: String, |
| 57 | pub server_url: String, | |
| 58 | pub api_url: String, | |
| 59 | } | |
| 60 | ||
| 61 | impl RunInfo { | |
| 62 | pub fn ref_name(&self) -> String { | |
| 63 | self.git_ref | |
| 64 | .strip_prefix("refs/heads/") | |
| 65 | .or_else(|| self.git_ref.strip_prefix("refs/tags/")) | |
| 66 | .or_else(|| self.git_ref.strip_prefix("refs/")) | |
| 67 | .unwrap_or(&self.git_ref) | |
| 68 | .to_owned() | |
| 69 | } | |
| 70 | ||
| 71 | pub fn ref_type(&self) -> &'static str { | |
| 72 | if self.git_ref.starts_with("refs/tags/") { "tag" } else { "branch" } | |
| 73 | } | |
| 74 | ||
| 75 | fn owner(&self) -> &str { | |
| 76 | self.repository.split('/').next().unwrap_or_default() | |
| 77 | } | |
| 78 | ||
| 79 | /// The `github` context for a job. `token` is `github.token` (and | |
| 80 | /// `secrets.GITHUB_TOKEN`); `job` is the job's id. | |
| 81 | pub fn context(&self, job: &str, token: &str, action: Option<&str>) -> Value { | |
| 82 | json!({ | |
| 83 | "action": action.unwrap_or_default(), | |
| 84 | "action_path": "", | |
| 85 | "action_ref": "", | |
| 86 | "action_repository": "", | |
| 87 | "actor": self.actor, | |
| 88 | "actor_id": self.actor_id, | |
| 89 | "api_url": self.api_url, | |
| 90 | "base_ref": self.base_ref.clone().unwrap_or_default(), | |
| 91 | "env": "", | |
| 92 | "event": self.event, | |
| 93 | "event_name": self.event_name, | |
| 94 | "event_path": "/home/runner/_temp/event.json", | |
| 95 | "graphql_url": "", | |
| 96 | "head_ref": self.head_ref.clone().unwrap_or_default(), | |
| 97 | "job": job, | |
| 98 | "path": "", | |
| 99 | "ref": self.git_ref, | |
| 100 | "ref_name": self.ref_name(), | |
| 101 | "ref_protected": self.ref_name() == self.default_branch, | |
| 102 | "ref_type": self.ref_type(), | |
| 103 | "repository": self.repository, | |
| 104 | "repository_id": self.repository_id, | |
| 105 | "repository_owner": self.owner(), | |
| 106 | "repository_owner_id": "", | |
| 107 | "repositoryUrl": format!("{}/{}.git", self.server_url, self.repository), | |
| 108 | "retention_days": 14, | |
| 109 | "run_attempt": self.run_attempt.to_string(), | |
| 110 | "run_id": self.run_id, | |
| 111 | "run_number": self.run_number.to_string(), | |
| 112 | "secret_source": "Actions", | |
| 113 | "server_url": self.server_url, | |
| 114 | "sha": self.sha, | |
| 115 | "token": token, | |
| 116 | "triggering_actor": self.triggering_actor, | |
| 117 | "workflow": self.workflow, | |
| 118 | "workflow_ref": format!("{}/{}@{}", self.repository, self.workflow_path, self.git_ref), | |
| 119 | "workflow_sha": self.sha, | |
| 120 | "workspace": WORKSPACE, | |
| 121 | }) | |
| 122 | } | |
| 123 | ||
| 124 | /// The `GITHUB_*` and `RUNNER_*` variables every step gets. | |
| 125 | pub fn variables(&self, job: &str) -> Map<String, Value> { | |
| 126 | let mut vars = Map::new(); | |
| 127 | let mut set = |key: &str, value: String| { | |
| 128 | vars.insert(key.to_owned(), Value::String(value)); | |
| 129 | }; | |
| 130 | set("CI", "true".into()); | |
| 131 | set("GITHUB_ACTIONS", "true".into()); | |
| 132 | set("G1T", "true".into()); | |
| 133 | set("GITHUB_ACTOR", self.actor.clone()); | |
| 134 | set("GITHUB_ACTOR_ID", self.actor_id.clone()); | |
| 135 | set("GITHUB_API_URL", self.api_url.clone()); | |
| 136 | set("GITHUB_BASE_REF", self.base_ref.clone().unwrap_or_default()); | |
| 137 | set("GITHUB_EVENT_NAME", self.event_name.clone()); | |
| 138 | set("GITHUB_EVENT_PATH", "/home/runner/_temp/event.json".into()); | |
| 139 | set("GITHUB_GRAPHQL_URL", String::new()); | |
| 140 | set("GITHUB_HEAD_REF", self.head_ref.clone().unwrap_or_default()); | |
| 141 | set("GITHUB_JOB", job.to_owned()); | |
| 142 | set("GITHUB_REF", self.git_ref.clone()); | |
| 143 | set("GITHUB_REF_NAME", self.ref_name()); | |
| 144 | set("GITHUB_REF_PROTECTED", (self.ref_name() == self.default_branch).to_string()); | |
| 145 | set("GITHUB_REF_TYPE", self.ref_type().into()); | |
| 146 | set("GITHUB_REPOSITORY", self.repository.clone()); | |
| 147 | set("GITHUB_REPOSITORY_ID", self.repository_id.clone()); | |
| 148 | set("GITHUB_REPOSITORY_OWNER", self.owner().to_owned()); | |
| 149 | set("GITHUB_RETENTION_DAYS", "14".into()); | |
| 150 | set("GITHUB_RUN_ATTEMPT", self.run_attempt.to_string()); | |
| 151 | set("GITHUB_RUN_ID", self.run_id.clone()); | |
| 152 | set("GITHUB_RUN_NUMBER", self.run_number.to_string()); | |
| 153 | set("GITHUB_SERVER_URL", self.server_url.clone()); | |
| 154 | set("GITHUB_SHA", self.sha.clone()); | |
| 155 | set("GITHUB_TRIGGERING_ACTOR", self.triggering_actor.clone()); | |
| 156 | set("GITHUB_WORKFLOW", self.workflow.clone()); | |
| 157 | set("GITHUB_WORKFLOW_REF", format!("{}/{}@{}", self.repository, self.workflow_path, self.git_ref)); | |
| 158 | set("GITHUB_WORKFLOW_SHA", self.sha.clone()); | |
| 159 | set("GITHUB_WORKSPACE", WORKSPACE.into()); | |
| 160 | set("RUNNER_ARCH", "X64".into()); | |
| 161 | set("RUNNER_NAME", "g1t".into()); | |
| 162 | set("RUNNER_OS", "Linux".into()); | |
| 163 | set("RUNNER_TEMP", "/home/runner/_temp".into()); | |
| 164 | set("RUNNER_TOOL_CACHE", "/home/runner/_tool".into()); | |
| 165 | set("RUNNER_ENVIRONMENT", "github-hosted".into()); | |
| 166 | vars | |
| 167 | } | |
| 168 | } | |
| 169 | ||
| 170 | /// Where a job's repository is checked out, as on GitHub's runners. | |
| 171 | pub const WORKSPACE: &str = "/home/runner/work/repo"; | |
| 172 | ||
| 173 | /// The `runner` context. | |
| 174 | pub fn runner_context() -> Value { | |
| 175 | json!({ | |
| 176 | "name": "g1t", | |
| 177 | "os": "Linux", | |
| 178 | "arch": "X64", | |
| 179 | "temp": "/home/runner/_temp", | |
| 180 | "tool_cache": "/home/runner/_tool", | |
| 181 | "environment": "github-hosted", | |
| 182 | "debug": "", | |
| 183 | }) | |
| 184 | } | |
| 185 | ||
| 186 | #[cfg(test)] | |
| 187 | mod tests { | |
| 188 | use super::*; | |
| 189 | ||
| 190 | #[test] | |
| 191 | fn g1t_events_are_github_events() { | |
| 192 | assert_eq!(github_events("git.push"), [("push", None)]); | |
| 193 | assert_eq!(github_events("pull.updated")[0], ("pull_request", Some("synchronize"))); | |
| 194 | assert_eq!(github_events("pull.merged")[1], ("pull_request_target", Some("closed"))); | |
| 195 | assert_eq!(github_events("comment.created"), [("issue_comment", Some("created"))]); | |
| 196 | assert!(github_events("session.appended").is_empty()); | |
| 197 | } | |
| 198 | ||
| 199 | #[test] | |
| 200 | fn contexts_and_variables_agree() { | |
| 201 | let info = RunInfo { | |
| 202 | repository: "acme/web".into(), | |
| 203 | default_branch: "main".into(), | |
| 204 | event_name: "push".into(), | |
| 205 | git_ref: "refs/tags/v1.2.0".into(), | |
| 206 | sha: "abc".into(), | |
| 207 | run_number: 7, | |
| 208 | run_attempt: 1, | |
| 209 | server_url: "https://g1t.sh".into(), | |
| 210 | ..RunInfo::default() | |
| 211 | }; | |
| 212 | let github = info.context("build", "tok", None); | |
| 213 | assert_eq!(github["ref_name"], "v1.2.0"); | |
| 214 | assert_eq!(github["ref_type"], "tag"); | |
| 215 | assert_eq!(github["repository_owner"], "acme"); | |
| 216 | assert_eq!(github["run_number"], "7"); | |
| 217 | let vars = info.variables("build"); | |
| 218 | assert_eq!(vars["GITHUB_REF_NAME"], "v1.2.0"); | |
| 219 | assert_eq!(vars["GITHUB_JOB"], "build"); | |
| 220 | assert_eq!(vars["RUNNER_OS"], "Linux"); | |
| 221 | } | |
| 222 | } |