github_actions_models/
action.rs

1//! Data models for GitHub Actions action definitions.
2//!
3//! Resources:
4//! * [Metadata syntax for GitHub Actions]
5//! * [JSON Schema definition for GitHub Actions]
6//!
7//! [Metadata syntax for GitHub Actions]: https://docs.github.com/en/actions/creating-actions/metadata-syntax-for-github-actions
8//! [JSON Schema definition for GitHub Actions]: https://json.schemastore.org/github-action.json
9
10use indexmap::IndexMap;
11use serde::Deserialize;
12
13use crate::common::{
14    expr::{BoE, LoE},
15    Env, If, Uses,
16};
17
18/// A GitHub Actions action definition.
19#[derive(Deserialize)]
20#[serde(rename_all = "kebab-case")]
21pub struct Action {
22    /// The action's name.
23    ///
24    /// NOTE: GitHub documents the action's name as required, but experimentally it is not.
25    pub name: Option<String>,
26    pub author: Option<String>,
27    pub description: Option<String>,
28    #[serde(default)]
29    pub inputs: IndexMap<String, Input>,
30    #[serde(default)]
31    pub outputs: IndexMap<String, Output>,
32    pub runs: Runs,
33}
34
35/// An action input.
36#[derive(Deserialize)]
37#[serde(rename_all = "kebab-case")]
38pub struct Input {
39    // NOTE: documented as required, but experimentally it is not.
40    pub description: Option<String>,
41    pub required: Option<bool>,
42    pub default: Option<String>,
43}
44
45/// An action output.
46#[derive(Deserialize)]
47#[serde(rename_all = "kebab-case")]
48pub struct Output {
49    // NOTE: documented as required, but experimentally it is not.
50    pub description: Option<String>,
51    // NOTE: not optional for composite actions, but this is not worth modeling.
52    pub value: Option<String>,
53}
54
55/// An action `runs` definition.
56///
57/// A `runs` definition can be either a JavaScript action, a "composite" action
58/// (made up of several constituent actions), or a Docker action.
59#[derive(Deserialize)]
60#[serde(rename_all = "kebab-case", untagged)]
61pub enum Runs {
62    JavaScript(JavaScript),
63    Composite(Composite),
64    Docker(Docker),
65}
66
67/// A `runs` definition for a JavaScript action.
68#[derive(Deserialize)]
69#[serde(rename_all = "kebab-case")]
70pub struct JavaScript {
71    /// The Node runtime to use for this action. This is one of:
72    ///
73    /// `"node12" | "node16" | "node20"`
74    pub using: String,
75
76    /// The action's entrypoint, as a JavaScript file.
77    pub main: String,
78
79    /// An optional script to run, before [`JavaScript::main`].
80    pub pre: Option<String>,
81
82    /// An optional expression that triggers [`JavaScript::pre`] if it evaluates to `true`.
83    ///
84    /// If not present, defaults to `always()`
85    pub pre_if: Option<If>,
86
87    /// An optional script to run, after [`JavaScript::main`].
88    pub post: Option<String>,
89
90    /// An optional expression that triggers [`JavaScript::post`] if it evaluates to `true`.
91    ///
92    /// If not present, defaults to `always()`
93    pub post_if: Option<If>,
94}
95
96/// A `runs` definition for a composite action.
97#[derive(Deserialize)]
98#[serde(rename_all = "kebab-case")]
99pub struct Composite {
100    /// Invariant: `"composite"`
101    pub using: String,
102    /// The individual steps that make up this composite action.
103    pub steps: Vec<Step>,
104}
105
106/// An individual composite action step.
107///
108/// This is similar, but not identical to [`crate::workflow::job::Step`].
109#[derive(Deserialize)]
110#[serde(rename_all = "kebab-case")]
111pub struct Step {
112    /// An optional ID for this composite step.
113    pub id: Option<String>,
114
115    /// An optional expression that prevents this composite step from running unless it evaluates to `true`.
116    pub r#if: Option<If>,
117
118    /// An optional name for this composite step.
119    pub name: Option<String>,
120
121    /// An optional boolean or expression that, if `true`, prevents the job from failing when
122    /// this composite step fails.
123    #[serde(default)]
124    pub continue_on_error: BoE,
125
126    /// The `run:` or `uses:` body for this composite step.
127    #[serde(flatten)]
128    pub body: StepBody,
129}
130
131/// The body of a composite action step.
132#[derive(Deserialize)]
133#[serde(rename_all = "kebab-case", untagged)]
134pub enum StepBody {
135    /// A step that uses another GitHub Action.
136    Uses {
137        /// The GitHub Action being used.
138        #[serde(deserialize_with = "crate::common::step_uses")]
139        uses: Uses,
140
141        /// Any inputs to the action being used.
142        #[serde(default)]
143        with: Env,
144    },
145    /// A step that runs a command in a shell.
146    Run {
147        /// The command to run.
148        run: String,
149
150        /// The shell to run in.
151        shell: String,
152
153        /// An optional environment mapping for this step.
154        #[serde(default)]
155        env: LoE<Env>,
156
157        /// An optional working directory to run [`RunShell::run`] from.
158        working_directory: Option<String>,
159    },
160}
161
162/// A `runs` definition for a Docker action.
163#[derive(Deserialize)]
164#[serde(rename_all = "kebab-case")]
165pub struct Docker {
166    /// Invariant: `"docker"`
167    pub using: String,
168
169    /// The Docker image to use.
170    pub image: String,
171
172    /// An optional environment mapping for this step.
173    #[serde(default)]
174    pub env: Env,
175
176    /// An optional Docker entrypoint, potentially overriding the image's
177    /// default entrypoint.
178    pub entrypoint: Option<String>,
179
180    /// An optional "pre" entrypoint to run, before [`Docker::entrypoint`].
181    pub pre_entrypoint: Option<String>,
182
183    /// An optional expression that triggers [`Docker::pre_entrypoint`] if it evaluates to `true`.
184    ///
185    /// If not present, defaults to `always()`
186    pub pre_if: Option<If>,
187
188    /// An optional "post" entrypoint to run, after [`Docker::entrypoint`] or the default
189    /// entrypoint.
190    pub post_entrypoint: Option<String>,
191
192    /// An optional expression that triggers [`Docker::post_entrypoint`] if it evaluates to `true`.
193    ///
194    /// If not present, defaults to `always()`
195    pub post_if: Option<If>,
196}