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}