gh_workflow/
ctx.rs

1//! A type-safe implementation of workflow context: <https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/accessing-contextual-information-about-workflow-runs>
2
3use std::fmt;
4use std::marker::PhantomData;
5use std::rc::Rc;
6
7use gh_workflow_macros::Context;
8
9use crate::Expression;
10
11#[derive(Clone)]
12pub struct Context<A> {
13    marker: PhantomData<A>,
14    step: Step,
15}
16
17#[derive(Default, Clone)]
18enum Step {
19    #[default]
20    Root,
21    Select {
22        name: Rc<String>,
23        object: Box<Step>,
24    },
25    Eq {
26        left: Box<Step>,
27        right: Box<Step>,
28    },
29    And {
30        left: Box<Step>,
31        right: Box<Step>,
32    },
33    Or {
34        left: Box<Step>,
35        right: Box<Step>,
36    },
37    Literal(String),
38    Concat {
39        left: Box<Step>,
40        right: Box<Step>,
41    },
42}
43
44impl<A> Context<A> {
45    fn new() -> Self {
46        Context { marker: PhantomData, step: Step::Root }
47    }
48
49    fn select<B>(&self, path: impl Into<String>) -> Context<B> {
50        Context {
51            marker: PhantomData,
52            step: Step::Select {
53                name: Rc::new(path.into()),
54                object: Box::new(self.step.clone()),
55            },
56        }
57    }
58
59    pub fn eq(&self, other: Context<A>) -> Context<bool> {
60        Context {
61            marker: Default::default(),
62            step: Step::Eq {
63                left: Box::new(self.step.clone()),
64                right: Box::new(other.step.clone()),
65            },
66        }
67    }
68
69    pub fn and(&self, other: Context<A>) -> Context<bool> {
70        Context {
71            marker: Default::default(),
72            step: Step::And {
73                left: Box::new(self.step.clone()),
74                right: Box::new(other.step.clone()),
75            },
76        }
77    }
78
79    pub fn or(&self, other: Context<A>) -> Context<bool> {
80        Context {
81            marker: Default::default(),
82            step: Step::Or {
83                left: Box::new(self.step.clone()),
84                right: Box::new(other.step.clone()),
85            },
86        }
87    }
88}
89
90impl Context<String> {
91    pub fn concat(&self, other: Context<String>) -> Context<String> {
92        Context {
93            marker: Default::default(),
94            step: Step::Concat {
95                left: Box::new(self.step.clone()),
96                right: Box::new(other.step),
97            },
98        }
99    }
100}
101
102#[allow(unused)]
103#[derive(Context)]
104pub struct Github {
105    /// The name of the action currently running, or the id of a step.
106    action: String,
107    /// The path where an action is located. This property is only supported in
108    /// composite actions.
109    action_path: String,
110    /// For a step executing an action, this is the ref of the action being
111    /// executed.
112    action_ref: String,
113    /// For a step executing an action, this is the owner and repository name of
114    /// the action.
115    action_repository: String,
116    /// For a composite action, the current result of the composite action.
117    action_status: String,
118    /// The username of the user that triggered the initial workflow run.
119    actor: String,
120    /// The account ID of the person or app that triggered the initial workflow
121    /// run.
122    actor_id: String,
123    /// The URL of the GitHub REST API.
124    api_url: String,
125    /// The base_ref or target branch of the pull request in a workflow run.
126    base_ref: String,
127    /// Path on the runner to the file that sets environment variables from
128    /// workflow commands.
129    env: String,
130    /// The full event webhook payload.
131    event: serde_json::Value,
132    /// The name of the event that triggered the workflow run.
133    event_name: String,
134    /// The path to the file on the runner that contains the full event webhook
135    /// payload.
136    event_path: String,
137    /// The URL of the GitHub GraphQL API.
138    graphql_url: String,
139    /// The head_ref or source branch of the pull request in a workflow run.
140    head_ref: String,
141    /// The job id of the current job.
142    job: String,
143    /// The path of the repository.
144    path: String,
145    /// The short ref name of the branch or tag that triggered the workflow run.
146    ref_name: String,
147    /// true if branch protections are configured for the ref that triggered the
148    /// workflow run.
149    ref_protected: bool,
150    /// The type of ref that triggered the workflow run. Valid values are branch
151    /// or tag.
152    ref_type: String,
153    /// The owner and repository name.
154    repository: String,
155    /// The ID of the repository.
156    repository_id: String,
157    /// The repository owner's username.
158    repository_owner: String,
159    /// The repository owner's account ID.
160    repository_owner_id: String,
161    /// The Git URL to the repository.
162    repository_url: String,
163    /// The number of days that workflow run logs and artifacts are kept.
164    retention_days: String,
165    /// A unique number for each workflow run within a repository.
166    run_id: String,
167    /// A unique number for each run of a particular workflow in a repository.
168    run_number: String,
169    /// A unique number for each attempt of a particular workflow run in a
170    /// repository.
171    run_attempt: String,
172    /// The source of a secret used in a workflow.
173    secret_source: String,
174    /// The URL of the GitHub server.
175    server_url: String,
176    /// The commit SHA that triggered the workflow.
177    sha: String,
178    /// A token to authenticate on behalf of the GitHub App installed on your
179    /// repository.
180    token: String,
181    /// The username of the user that initiated the workflow run.
182    triggering_actor: String,
183    /// The name of the workflow.
184    workflow: String,
185    /// The ref path to the workflow.
186    workflow_ref: String,
187    /// The commit SHA for the workflow file.
188    workflow_sha: String,
189    /// The default working directory on the runner for steps.
190    workspace: String,
191}
192
193impl Context<Github> {
194    pub fn ref_(&self) -> Context<String> {
195        self.select("ref")
196    }
197}
198
199impl fmt::Display for Step {
200    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
201        match self {
202            Step::Root => write!(f, ""),
203            Step::Select { name, object } => {
204                if matches!(**object, Step::Root) {
205                    write!(f, "{}", name)
206                } else {
207                    write!(f, "{}.{}", object, name)
208                }
209            }
210            Step::Eq { left, right } => {
211                write!(f, "{} == {}", left, right)
212            }
213            Step::And { left, right } => {
214                write!(f, "{} && {}", left, right)
215            }
216            Step::Or { left, right } => {
217                write!(f, "{} || {}", left, right)
218            }
219            Step::Literal(value) => {
220                write!(f, "'{}'", value)
221            }
222            Step::Concat { left, right } => {
223                write!(f, "{}{}", left, right)
224            }
225        }
226    }
227}
228
229impl<A> fmt::Display for Context<A> {
230    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
231        write!(f, "${{{{ {} }}}}", self.step.to_string().replace('"', ""))
232    }
233}
234
235impl<A> From<Context<A>> for Expression {
236    fn from(value: Context<A>) -> Self {
237        Expression::new(value.to_string())
238    }
239}
240
241impl<T: Into<String>> From<T> for Context<String> {
242    fn from(value: T) -> Self {
243        Context {
244            marker: Default::default(),
245            step: Step::Literal(value.into()),
246        }
247    }
248}
249
250#[allow(unused)]
251#[derive(Context)]
252/// The job context contains information about the currently running job.
253pub struct Job {
254    /// A unique number for each container in a job. This property is only
255    /// available if the job uses a container.
256    container: Container,
257
258    /// The services configured for a job. This property is only available if
259    /// the job uses service containers.
260    services: Services,
261
262    /// The status of the current job.
263    status: JobStatus,
264}
265
266/// The status of a job execution
267#[derive(Clone)]
268pub enum JobStatus {
269    /// The job completed successfully
270    Success,
271    /// The job failed
272    Failure,
273    /// The job was cancelled
274    Cancelled,
275}
276
277#[derive(Context)]
278#[allow(unused)]
279/// Container information for a job. This is only available if the job runs in a
280/// container.
281pub struct Container {
282    /// The ID of the container
283    id: String,
284    /// The container network
285    network: String,
286}
287
288#[derive(Context)]
289
290/// Services configured for a job. This is only available if the job uses
291/// service containers.
292pub struct Services {}
293
294#[cfg(test)]
295mod test {
296    use pretty_assertions::assert_eq;
297
298    use super::*;
299
300    #[test]
301    fn test_expr() {
302        let github = Context::github(); // Expr<Github>
303
304        assert_eq!(github.to_string(), "${{ github }}");
305
306        let action = github.action(); // Expr<String>
307        assert_eq!(action.to_string(), "${{ github.action }}");
308
309        let action_path = github.action_path(); // Expr<String>
310        assert_eq!(action_path.to_string(), "${{ github.action_path }}");
311    }
312
313    #[test]
314    fn test_expr_eq() {
315        let github = Context::github();
316        let action = github.action();
317        let action_path = github.action_path();
318
319        let expr = action.eq(action_path);
320
321        assert_eq!(
322            expr.to_string(),
323            "${{ github.action == github.action_path }}"
324        );
325    }
326
327    #[test]
328    fn test_expr_and() {
329        let push = Context::github().event_name().eq("push".into());
330        let main = Context::github().ref_().eq("ref/heads/main".into());
331        let expr = push.and(main);
332
333        assert_eq!(
334            expr.to_string(),
335            "${{ github.event_name == 'push' && github.ref == 'ref/heads/main' }}"
336        )
337    }
338
339    #[test]
340    fn test_expr_or() {
341        let github = Context::github();
342        let action = github.action();
343        let action_path = github.action_path();
344        let action_ref = github.action_ref();
345
346        let expr = action.eq(action_path).or(action.eq(action_ref));
347
348        assert_eq!(
349            expr.to_string(),
350            "${{ github.action == github.action_path || github.action == github.action_ref }}"
351        );
352    }
353}