g1t/services/actions/src/settings.rs

509 lines22,754 bytesCodeBlame

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.

Secrets and variables: one list, rows per environment, for workflows and deployments1//! Secrets and variables, a repository's or its workspace's: one list for
2//! every reader, shaped like Vercel's environment variables. Each row is a
3//! key, its type (a secret, or a variable shown as Config), the
4//! environments it applies to and who reads it: workflows, deployments, or
5//! both. A key may have one row per environment, so production and
6//! previews can hold different values; a key's rows never overlap.
7//!
8//! A reader asking for an environment gets the row naming it, else the
9//! key's row for every environment. A repository's row overrides its
10//! workspace's of the same key. Names are upper-cased, as GitHub treats
11//! them without regard to case. Agents never read any.
GitHub Actions on g1t, part two: running workflows12
Secrets and variables: one list, rows per environment, for workflows and deployments13use g1t_contracts::actions::{
14 CONSUMERS, DeleteSettingArgs, ResolveSettingsArgs, ResolvedSettings, SetSettingArgs, Setting, SettingsArgs,
15 SettingsOwner,
16};
GitHub Actions on g1t, part two: running workflows17use g1t_contracts::time::rfc3339;
18use g1t_contracts::{FailureCode, Outcome, PrincipalKind, Role, User, new_id};
19use g1t_kit::now_ms;
20use serde::Deserialize;
21use serde_json::{Map, Value};
22use worker::Result;
Secrets and variables: one list, rows per environment, for workflows and deployments23use worker::wasm_bindgen::JsValue;
GitHub Actions on g1t, part two: running workflows24
25use crate::{Actions, check, fail};
26
27/// The largest value, as on GitHub.
28const MAX_VALUE_BYTES: usize = 48 * 1024;
Secrets and variables: one list, rows per environment, for workflows and deployments29const MAX_PER_OWNER: u32 = 200;
30const MAX_NOTE: usize = 500;
GitHub Actions on g1t, part two: running workflows31
32#[derive(Deserialize)]
33struct SettingRow {
34 id: String,
35 scope: String,
Secrets and variables: one list, rows per environment, for workflows and deployments36 kind: String,
GitHub Actions on g1t, part two: running workflows37 name: String,
38 value: String,
39 updated_at: String,
Secrets and variables: one list, rows per environment, for workflows and deployments40 available_to: String,
41 environments: String,
42 repositories: Option<String>,
43 note: Option<String>,
44 updated_by: Option<String>,
GitHub Actions on g1t, part two: running workflows45}
46
47/// A name GitHub would accept: letters, digits and `_`, not starting with
Secrets and variables: one list, rows per environment, for workflows and deployments48/// a digit, `GITHUB_` or `G1T_`, which are g1t's own (`G1T_TOKEN` and its
49/// alias `GITHUB_TOKEN`).
GitHub Actions on g1t, part two: running workflows50fn valid_name(name: &str) -> Result<String, String> {
51 let upper = name.trim().to_ascii_uppercase();
52 if upper.is_empty() || upper.len() > 100 {
53 return Err("A name is 1 to 100 characters.".to_owned());
54 }
55 if !upper.chars().all(|c| c.is_ascii_alphanumeric() || c == '_') {
56 return Err("A name has only letters, digits and underscores.".to_owned());
57 }
58 if upper.starts_with(|c: char| c.is_ascii_digit()) {
59 return Err("A name cannot start with a digit.".to_owned());
60 }
Secrets and variables: one list, rows per environment, for workflows and deployments61 if upper.starts_with("GITHUB_") || upper.starts_with("G1T_") {
62 return Err("Names starting with G1T_ or GITHUB_ are kept for g1t's own, such as G1T_TOKEN.".to_owned());
GitHub Actions on g1t, part two: running workflows63 }
64 Ok(upper)
65}
66
Secrets and variables: one list, rows per environment, for workflows and deployments67/// Environments' names: lowercase letters, digits, `-` and `_`, each once.
68fn valid_environments(list: &[String]) -> Result<Vec<String>, String> {
69 let mut out: Vec<String> = Vec::new();
70 for name in list {
71 let lower = name.trim().to_ascii_lowercase();
72 if lower.is_empty() {
73 continue;
74 }
75 if lower.len() > 40 || !lower.chars().all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_') {
76 return Err(format!("`{name}` is not an environment's name: up to 40 letters, digits, - and _."));
77 }
78 if !out.contains(&lower) {
79 out.push(lower);
80 }
81 }
82 out.sort();
83 Ok(out)
84}
85
86fn consumers(list: &[String]) -> Result<Vec<String>, String> {
87 let mut out: Vec<String> = Vec::new();
88 for item in list {
89 let item = item.trim().to_ascii_lowercase();
90 if !CONSUMERS.contains(&item.as_str()) {
91 return Err(format!("`{item}` is not a reader: use workflows or deployments."));
92 }
93 if !out.contains(&item) {
94 out.push(item);
95 }
96 }
97 if out.is_empty() {
98 return Err("Choose who reads it: workflows, deployments, or both.".to_owned());
99 }
100 Ok(out)
101}
102
103fn split(list: &str) -> Vec<String> {
104 list.split(',').filter(|s| !s.is_empty()).map(str::to_owned).collect()
105}
106
107impl SettingRow {
108 fn environments(&self) -> Vec<String> {
109 split(&self.environments)
110 }
111
112 fn repositories(&self) -> Vec<String> {
113 self.repositories.as_deref().and_then(|json| serde_json::from_str(json).ok()).unwrap_or_default()
114 }
115
116 fn reaches(&self, repo: &str) -> bool {
117 let list = self.repositories();
118 list.is_empty() || list.iter().any(|r| r.eq_ignore_ascii_case(repo))
119 }
120
121 /// Whether it and rows for `environments` would both apply somewhere.
122 fn overlaps(&self, environments: &[String]) -> bool {
123 let mine = self.environments();
124 mine.is_empty() == environments.is_empty() && (mine.is_empty() || mine.iter().any(|e| environments.contains(e)))
125 }
126
127 fn describe(self) -> Setting {
128 Setting {
129 available_to: split(&self.available_to),
130 environments: self.environments(),
131 repositories: self.repositories(),
132 value: (self.kind == "variable").then_some(self.value),
133 id: self.id,
134 name: self.name,
135 kind: self.kind,
136 scope: self.scope,
137 updated_at: self.updated_at,
138 note: self.note,
139 updated_by: self.updated_by,
140 }
141 }
142}
143
GitHub Actions on g1t, part two: running workflows144/// Where settings live: `(scope, owner)` with the owner a repository id or
Secrets and variables: one list, rows per environment, for workflows and deployments145/// a workspace slug.
GitHub Actions on g1t, part two: running workflows146struct Place {
147 scope: &'static str,
148 owner: String,
149 namespace: String,
150}
151
152impl Actions {
153 async fn place(&self, actor: &User, owner: &SettingsOwner, changing: bool) -> Result<Outcome<Place>> {
154 if actor.kind == PrincipalKind::Agent {
155 return Ok(fail(FailureCode::Forbidden, "Agents cannot read or change secrets and variables."));
156 }
Secrets and variables: one list, rows per environment, for workflows and deployments157 // A workspace's tokens, G1T_TOKEN among them, read the names but
158 // never change them: a workflow must not rewrite what it runs with.
159 if changing && actor.kind == PrincipalKind::Workspace {
160 return Ok(fail(
161 FailureCode::Forbidden,
162 "A workspace's tokens, G1T_TOKEN included, cannot change secrets and variables. Use a person's token or the site.",
163 ));
164 }
GitHub Actions on g1t, part two: running workflows165 match (&owner.repo, &owner.workspace) {
166 (Some(path), _) => {
167 if !actor.is_member(&path.namespace.to_lowercase()) {
168 return Ok(fail(FailureCode::Forbidden, format!("Only members of {} can see its secrets and variables.", path.namespace)));
169 }
170 let Some(repo) = self.visible_repo(path, &Some(actor.clone())).await? else {
171 return Ok(fail(FailureCode::NotFound, "There is no such repository."));
172 };
173 Ok(Outcome::Ok(Place { scope: "repository", owner: repo.id, namespace: repo.namespace }))
174 }
175 (None, Some(slug)) => {
176 let slug = slug.to_lowercase();
177 let role = actor.workspaces.iter().find(|m| m.slug.eq_ignore_ascii_case(&slug)).map(|m| m.role);
178 match role {
179 None => Ok(fail(FailureCode::Forbidden, format!("Only members of {slug} can see its secrets and variables."))),
180 Some(Role::Member) if changing => Ok(fail(FailureCode::Forbidden, format!("Only owners of {slug} can change its secrets and variables."))),
181 Some(_) => Ok(Outcome::Ok(Place { scope: "workspace", owner: slug.clone(), namespace: slug })),
182 }
183 }
184 (None, None) => Ok(fail(FailureCode::Invalid, "Give `repo` or `workspace`.")),
185 }
186 }
187
Secrets and variables: one list, rows per environment, for workflows and deployments188 /// `secret`, `variable`, or `None` for both.
189 fn kind(kind: &str) -> Outcome<Option<&'static str>> {
GitHub Actions on g1t, part two: running workflows190 match kind {
Secrets and variables: one list, rows per environment, for workflows and deployments191 "secret" | "secrets" => Outcome::Ok(Some("secret")),
192 "variable" | "variables" | "config" => Outcome::Ok(Some("variable")),
193 "" | "all" => Outcome::Ok(None),
GitHub Actions on g1t, part two: running workflows194 _ => fail(FailureCode::Invalid, "`kind` is `secret` or `variable`."),
195 }
196 }
197
Secrets and variables: one list, rows per environment, for workflows and deployments198 async fn rows(&self, owner: &str) -> Result<Vec<SettingRow>> {
199 self.db
200 .prepare("SELECT * FROM settings WHERE owner = ? ORDER BY name, environments")
201 .bind(&[owner.into()])?
202 .all()
203 .await?
204 .results::<SettingRow>()
205 }
206
GitHub Actions on g1t, part two: running workflows207 pub async fn settings(&self, a: SettingsArgs) -> Result<Outcome<Vec<Setting>>> {
208 let kind = check!(Self::kind(&a.kind));
209 let place = check!(self.place(&a.actor, &a.owner, false).await?);
Secrets and variables: one list, rows per environment, for workflows and deployments210 let repo_name = a.owner.repo.as_ref().map(|r| r.name.clone());
GitHub Actions on g1t, part two: running workflows211 let mut out: Vec<Setting> = Vec::new();
Secrets and variables: one list, rows per environment, for workflows and deployments212 // A repository's list shows the workspace's rows that reach it, but
213 // for keys it sets itself.
214 if place.scope == "repository" {
215 let own: Vec<String> = self.rows(&place.owner).await?.into_iter().map(|row| row.name).collect();
216 for row in self.rows(&place.namespace.to_lowercase()).await? {
217 if repo_name.as_deref().is_some_and(|name| row.reaches(name)) && !own.contains(&row.name) {
218 out.push(row.describe());
219 }
GitHub Actions on g1t, part two: running workflows220 }
221 }
Secrets and variables: one list, rows per environment, for workflows and deployments222 out.extend(self.rows(&place.owner).await?.into_iter().map(SettingRow::describe));
223 out.retain(|setting| kind.is_none_or(|kind| setting.kind == kind));
224 out.sort_by(|a, b| a.name.cmp(&b.name).then(a.environments.cmp(&b.environments)));
GitHub Actions on g1t, part two: running workflows225 Ok(Outcome::Ok(out))
226 }
227
Secrets and variables: one list, rows per environment, for workflows and deployments228 fn seal(&self, value: &str, id: &str) -> Outcome<String> {
229 match &self.sealer {
230 Some(sealer) => Outcome::Ok(sealer.seal(value, id)),
231 None => fail(FailureCode::Conflict, "Secrets cannot be saved yet: g1t's key for them is not set."),
232 }
233 }
234
GitHub Actions on g1t, part two: running workflows235 pub async fn set_setting(&self, a: SetSettingArgs) -> Result<Outcome<Setting>> {
Secrets and variables: one list, rows per environment, for workflows and deployments236 let Some(kind) = check!(Self::kind(&a.kind)) else {
237 return Ok(fail(FailureCode::Invalid, "`kind` is `secret` or `variable`."));
238 };
GitHub Actions on g1t, part two: running workflows239 let name = match valid_name(&a.name) {
240 Ok(name) => name,
241 Err(problem) => return Ok(fail(FailureCode::Invalid, problem)),
242 };
Secrets and variables: one list, rows per environment, for workflows and deployments243 if a.value.as_ref().is_some_and(|v| v.len() > MAX_VALUE_BYTES) {
GitHub Actions on g1t, part two: running workflows244 return Ok(fail(FailureCode::Invalid, "A value is at most 48 KB."));
245 }
Secrets and variables: one list, rows per environment, for workflows and deployments246 if a.note.as_ref().is_some_and(|n| n.len() > MAX_NOTE) {
247 return Ok(fail(FailureCode::Invalid, "A note is at most 500 characters."));
248 }
249 let readers = match a.available_to.as_deref().map(consumers).transpose() {
250 Ok(readers) => readers,
251 Err(problem) => return Ok(fail(FailureCode::Invalid, problem)),
252 };
253 let environments = match a.environments.as_deref().map(valid_environments).transpose() {
254 Ok(environments) => environments,
255 Err(problem) => return Ok(fail(FailureCode::Invalid, problem)),
256 };
GitHub Actions on g1t, part two: running workflows257 let place = check!(self.place(&a.actor, &a.owner, true).await?);
Secrets and variables: one list, rows per environment, for workflows and deployments258 if a.repositories.as_ref().is_some_and(|r| !r.is_empty()) && place.scope != "workspace" {
259 return Ok(fail(FailureCode::Invalid, "Only a workspace's rows choose repositories."));
GitHub Actions on g1t, part two: running workflows260 }
Secrets and variables: one list, rows per environment, for workflows and deployments261 let rows = self.rows(&place.owner).await?;
262 // A secret and a variable may share a key, as on GitHub, where
263 // workflows read them apart (`secrets.X`, `vars.X`).
264 let same_key: Vec<&SettingRow> = rows.iter().filter(|row| row.name == name).collect();
265 // The row being changed: by id, else the key's row for every
266 // environment (GitHub's API names a secret by its key alone).
267 let existing = match &a.id {
268 Some(id) => match rows.iter().find(|row| &row.id == id) {
269 Some(row) => Some(row),
270 None => return Ok(fail(FailureCode::NotFound, "There is no such row.")),
271 },
272 None if a.environments.is_none() => same_key.iter().copied().find(|row| row.environments.is_empty() && row.kind == kind),
273 None => None,
274 };
275 if existing.is_some_and(|row| row.kind == "secret" && kind == "variable") {
276 return Ok(fail(FailureCode::Invalid, "A secret cannot become config: its value is sealed. Add a config row and remove the secret."));
277 }
278 let environments = environments.unwrap_or_else(|| existing.map(SettingRow::environments).unwrap_or_default());
279 // A key's rows never apply to the same environment twice.
280 if let Some(clash) = same_key
281 .iter()
282 .find(|row| row.kind == kind && existing.is_none_or(|e| e.id != row.id) && row.overlaps(&environments))
283 {
Deploy scripts live in the repository284 let at = if clash.environments.is_empty() { "all environments".to_owned() } else { clash.environments.replace(',', ", ") };
285 let what = if kind == "secret" { "secret" } else { "config" };
Secrets and variables: one list, rows per environment, for workflows and deployments286 return Ok(fail(
287 FailureCode::Conflict,
Deploy scripts live in the repository288 format!("{name} already has a {what} row for {at}. Edit that row, or choose other environments."),
Secrets and variables: one list, rows per environment, for workflows and deployments289 ));
290 }
291 if existing.is_none() && rows.len() as u32 >= MAX_PER_OWNER {
292 return Ok(fail(FailureCode::Invalid, format!("There can be at most {MAX_PER_OWNER} secrets and variables here.")));
293 }
294 let id = existing.map(|row| row.id.clone()).unwrap_or_else(|| new_id("set", now_ms()));
295 let value = match (&a.value, existing) {
296 (Some(value), _) if kind == "secret" => check!(self.seal(value, &id)),
297 (Some(value), _) => value.clone(),
298 // Config becoming a secret: its value is sealed now.
299 (None, Some(row)) if row.kind == "variable" && kind == "secret" => check!(self.seal(&row.value, &id)),
300 (None, Some(row)) => row.value.clone(),
301 (None, None) => return Ok(fail(FailureCode::Invalid, "A new row needs a `value`.")),
GitHub Actions on g1t, part two: running workflows302 };
Secrets and variables: one list, rows per environment, for workflows and deployments303 let available_to = readers
304 .map(|r| r.join(","))
305 .or_else(|| existing.map(|row| row.available_to.clone()))
306 .unwrap_or_else(|| CONSUMERS.join(","));
307 let repositories: Option<String> = match &a.repositories {
308 Some(list) if list.is_empty() => None,
309 Some(list) => Some(serde_json::to_string(list).unwrap_or_default()),
310 None => existing.and_then(|row| row.repositories.clone()),
311 };
312 let note = match &a.note {
313 Some(note) if note.trim().is_empty() => None,
314 Some(note) => Some(note.trim().to_owned()),
315 None => existing.and_then(|row| row.note.clone()),
316 };
GitHub Actions on g1t, part two: running workflows317 let at = rfc3339(now_ms());
Secrets and variables: one list, rows per environment, for workflows and deployments318 let optional = |v: Option<&str>| v.map_or(JsValue::NULL, JsValue::from);
GitHub Actions on g1t, part two: running workflows319 self.db
320 .prepare(
Secrets and variables: one list, rows per environment, for workflows and deployments321 "INSERT INTO settings (id, scope, owner, kind, name, value, updated_at, available_to, environments, repositories, note, updated_by)
322 VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
323 ON CONFLICT (id) DO UPDATE SET kind = excluded.kind, value = excluded.value, updated_at = excluded.updated_at,
324 available_to = excluded.available_to, environments = excluded.environments,
325 repositories = excluded.repositories, note = excluded.note, updated_by = excluded.updated_by",
GitHub Actions on g1t, part two: running workflows326 )
Secrets and variables: one list, rows per environment, for workflows and deployments327 .bind(&[
328 id.as_str().into(),
329 place.scope.into(),
330 place.owner.as_str().into(),
331 kind.into(),
332 name.as_str().into(),
333 value.into(),
334 at.as_str().into(),
335 available_to.as_str().into(),
336 environments.join(",").into(),
337 optional(repositories.as_deref()),
338 optional(note.as_deref()),
339 a.actor.username.as_str().into(),
340 ])?
GitHub Actions on g1t, part two: running workflows341 .run()
342 .await?;
Secrets and variables: one list, rows per environment, for workflows and deployments343 let row = self
344 .db
345 .prepare("SELECT * FROM settings WHERE id = ?")
346 .bind(&[id.as_str().into()])?
347 .first::<SettingRow>(None)
348 .await?
349 .expect("just written");
350 Ok(Outcome::Ok(row.describe()))
GitHub Actions on g1t, part two: running workflows351 }
352
353 pub async fn delete_setting(&self, a: DeleteSettingArgs) -> Result<Outcome<bool>> {
354 let kind = check!(Self::kind(&a.kind));
355 let place = check!(self.place(&a.actor, &a.owner, true).await?);
Secrets and variables: one list, rows per environment, for workflows and deployments356 let name = a.name.trim().to_ascii_uppercase();
357 let removed = match &a.id {
358 Some(id) => self
359 .db
360 .prepare("DELETE FROM settings WHERE owner = ? AND id = ? RETURNING id")
361 .bind(&[place.owner.as_str().into(), id.as_str().into()])?
362 .all()
363 .await?,
364 None => self
365 .db
366 .prepare("DELETE FROM settings WHERE owner = ? AND name = ? AND (?3 IS NULL OR kind = ?3) RETURNING id")
367 .bind(&[place.owner.as_str().into(), name.as_str().into(), kind.map_or(JsValue::NULL, JsValue::from)])?
368 .all()
369 .await?,
370 };
371 Ok(if removed.results::<Value>()?.is_empty() {
372 fail(FailureCode::NotFound, format!("There is nothing called {} here.", a.name))
373 } else {
374 Outcome::Ok(true)
GitHub Actions on g1t, part two: running workflows375 })
376 }
377
Secrets and variables: one list, rows per environment, for workflows and deployments378 /// What one reader of a repository gets: per key, the row for
379 /// `environment`, else the row for every environment; the repository's
380 /// over its workspace's. No secrets unless `trusted`.
381 #[allow(clippy::too_many_arguments)]
382 async fn resolved(
383 &self,
384 repo_id: &str,
385 repo_name: &str,
386 namespace: &str,
387 kind: &str,
388 consumer: &str,
389 environment: Option<&str>,
390 trusted: bool,
391 ) -> Result<Map<String, Value>> {
392 if kind == "secret" && !trusted {
393 return Ok(Map::new());
394 }
395 let environment = environment.map(str::to_ascii_lowercase);
GitHub Actions on g1t, part two: running workflows396 let mut out = Map::new();
397 for owner in [namespace.to_lowercase(), repo_id.to_owned()] {
Secrets and variables: one list, rows per environment, for workflows and deployments398 let rows: Vec<SettingRow> = self
399 .rows(&owner)
GitHub Actions on g1t, part two: running workflows400 .await?
Secrets and variables: one list, rows per environment, for workflows and deployments401 .into_iter()
402 .filter(|row| row.kind == kind)
403 .filter(|row| split(&row.available_to).iter().any(|r| r == consumer))
404 .filter(|row| row.scope != "workspace" || row.reaches(repo_name))
405 .collect();
406 let mut names: Vec<&str> = rows.iter().map(|row| row.name.as_str()).collect();
407 names.dedup();
408 for name in names {
409 let of_key: Vec<&SettingRow> = rows.iter().filter(|row| row.name == name).collect();
410 let chosen = environment
411 .as_deref()
412 .and_then(|env| of_key.iter().find(|row| row.environments().iter().any(|e| e == env)))
413 .or_else(|| of_key.iter().find(|row| row.environments.is_empty()));
414 let Some(row) = chosen else {
415 // Rows only for other environments: this reader gets
416 // none, nor the workspace's.
417 out.remove(name);
418 continue;
419 };
GitHub Actions on g1t, part two: running workflows420 let value = if kind == "secret" {
421 match self.sealer.as_ref().and_then(|sealer| sealer.open(&row.value, &row.id)) {
422 Some(value) => value,
423 None => continue,
424 }
425 } else {
Secrets and variables: one list, rows per environment, for workflows and deployments426 row.value.clone()
GitHub Actions on g1t, part two: running workflows427 };
Secrets and variables: one list, rows per environment, for workflows and deployments428 out.insert(name.to_owned(), Value::String(value));
GitHub Actions on g1t, part two: running workflows429 }
430 }
431 Ok(out)
432 }
433
Secrets and variables: one list, rows per environment, for workflows and deployments434 /// The `vars` context of a repository's runs. `environment` is the job's
435 /// `environment:`, when it has one.
436 pub async fn variables_for(&self, repo_id: &str, repo: &str, environment: Option<&str>, trusted: bool) -> Result<Map<String, Value>> {
437 let (namespace, name) = repo.split_once('/').unwrap_or((repo, ""));
438 self.resolved(repo_id, name, namespace, "variable", "workflows", environment, trusted).await
GitHub Actions on g1t, part two: running workflows439 }
440
441 /// The `secrets` context of a repository's runs, opened.
Secrets and variables: one list, rows per environment, for workflows and deployments442 pub async fn secrets_for(&self, repo_id: &str, repo: &str, environment: Option<&str>, trusted: bool) -> Result<Map<String, Value>> {
443 let (namespace, name) = repo.split_once('/').unwrap_or((repo, ""));
444 self.resolved(repo_id, name, namespace, "secret", "workflows", environment, trusted).await
GitHub Actions on g1t, part two: running workflows445 }
Secrets and variables: one list, rows per environment, for workflows and deployments446
447 /// `resolve_settings`, for the deployments service: what a deploy build
448 /// and its running app get.
449 pub async fn resolve_settings(&self, a: ResolveSettingsArgs) -> Result<ResolvedSettings> {
450 let environment = a.environment.as_deref();
451 Ok(ResolvedSettings {
452 secrets: self
453 .resolved(&a.repo_id, &a.repo.name, &a.repo.namespace, "secret", &a.consumer, environment, a.trusted)
454 .await?,
455 variables: self
456 .resolved(&a.repo_id, &a.repo.name, &a.repo.namespace, "variable", &a.consumer, environment, a.trusted)
457 .await?,
458 })
459 }
GitHub Actions on g1t, part two: running workflows460}
461
462#[cfg(test)]
463mod tests {
Secrets and variables: one list, rows per environment, for workflows and deployments464 use super::{SettingRow, consumers, valid_environments, valid_name};
GitHub Actions on g1t, part two: running workflows465
Secrets and variables: one list, rows per environment, for workflows and deployments466 fn row(environments: &str) -> SettingRow {
467 SettingRow {
468 id: "set_1".into(),
469 scope: "repository".into(),
470 kind: "secret".into(),
471 name: "STRIPE_KEY".into(),
472 value: String::new(),
473 updated_at: String::new(),
474 available_to: "workflows,deployments".into(),
475 environments: environments.into(),
476 repositories: None,
477 note: None,
478 updated_by: None,
479 }
480 }
481
GitHub Actions on g1t, part two: running workflows482 #[test]
Secrets and variables: one list, rows per environment, for workflows and deployments483 fn names_follow_githubs_rules_and_keep_g1ts_own() {
GitHub Actions on g1t, part two: running workflows484 assert_eq!(valid_name("npm_token").unwrap(), "NPM_TOKEN");
485 assert!(valid_name("GITHUB_TOKEN").is_err());
Secrets and variables: one list, rows per environment, for workflows and deployments486 assert!(valid_name("G1T_TOKEN").is_err());
GitHub Actions on g1t, part two: running workflows487 assert!(valid_name("1PASSWORD").is_err());
488 assert!(valid_name("MY-TOKEN").is_err());
489 assert!(valid_name("").is_err());
490 }
Secrets and variables: one list, rows per environment, for workflows and deployments491
492 #[test]
493 fn environments_and_readers_are_checked() {
494 assert_eq!(valid_environments(&["Production".into(), "preview".into(), "production".into()]).unwrap(), vec!["preview", "production"]);
495 assert!(valid_environments(&["staging env".into()]).is_err());
496 assert_eq!(consumers(&["Deployments".into(), "deployments".into()]).unwrap(), vec!["deployments"]);
497 assert!(consumers(&["agents".into()]).is_err());
498 assert!(consumers(&[]).is_err());
499 }
500
501 #[test]
502 fn a_keys_rows_cannot_share_an_environment() {
503 assert!(row("production").overlaps(&["production".into(), "preview".into()]));
504 assert!(!row("production").overlaps(&["preview".into()]));
505 // One row for every environment, and others for some, live together.
506 assert!(!row("").overlaps(&["preview".into()]));
507 assert!(row("").overlaps(&[]));
508 }
GitHub Actions on g1t, part two: running workflows509}