flagon-io/g1t

public

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

g1t/services/actions/src/settings.rs

552 lines24,748 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
Projects: what a workspace builds and runs, first on every page9//! key's row for every environment. A project's row overrides its
10//! workspace's of the same key.
11//!
12//! A repository's rows belong to its project (its primary one, when it
13//! carries several): asked for by repository, as GitHub's API does, they
14//! are the project's. Rows from before projects move over the first time
15//! they are touched. Names are upper-cased, as GitHub treats
Secrets and variables: one list, rows per environment, for workflows and deployments16//! them without regard to case. Agents never read any.
GitHub Actions on g1t, part two: running workflows17
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look18use g1t_contracts::access::Capability;
Secrets and variables: one list, rows per environment, for workflows and deployments19use g1t_contracts::actions::{
20 CONSUMERS, DeleteSettingArgs, ResolveSettingsArgs, ResolvedSettings, SetSettingArgs, Setting, SettingsArgs,
21 SettingsOwner,
22};
Projects: what a workspace builds and runs, first on every page23use g1t_contracts::projects::{ByRepoArgs, ProjectRef};
GitHub Actions on g1t, part two: running workflows24use g1t_contracts::time::rfc3339;
25use g1t_contracts::{FailureCode, Outcome, PrincipalKind, Role, User, new_id};
26use g1t_kit::now_ms;
27use serde::Deserialize;
28use serde_json::{Map, Value};
29use worker::Result;
Secrets and variables: one list, rows per environment, for workflows and deployments30use worker::wasm_bindgen::JsValue;
GitHub Actions on g1t, part two: running workflows31
32use crate::{Actions, check, fail};
33
34/// The largest value, as on GitHub.
35const MAX_VALUE_BYTES: usize = 48 * 1024;
Secrets and variables: one list, rows per environment, for workflows and deployments36const MAX_PER_OWNER: u32 = 200;
37const MAX_NOTE: usize = 500;
GitHub Actions on g1t, part two: running workflows38
39#[derive(Deserialize)]
40struct SettingRow {
41 id: String,
42 scope: String,
Secrets and variables: one list, rows per environment, for workflows and deployments43 kind: String,
GitHub Actions on g1t, part two: running workflows44 name: String,
45 value: String,
46 updated_at: String,
Secrets and variables: one list, rows per environment, for workflows and deployments47 available_to: String,
48 environments: String,
49 repositories: Option<String>,
50 note: Option<String>,
51 updated_by: Option<String>,
GitHub Actions on g1t, part two: running workflows52}
53
54/// A name GitHub would accept: letters, digits and `_`, not starting with
Secrets and variables: one list, rows per environment, for workflows and deployments55/// a digit, `GITHUB_` or `G1T_`, which are g1t's own (`G1T_TOKEN` and its
56/// alias `GITHUB_TOKEN`).
GitHub Actions on g1t, part two: running workflows57fn valid_name(name: &str) -> Result<String, String> {
58 let upper = name.trim().to_ascii_uppercase();
59 if upper.is_empty() || upper.len() > 100 {
60 return Err("A name is 1 to 100 characters.".to_owned());
61 }
62 if !upper.chars().all(|c| c.is_ascii_alphanumeric() || c == '_') {
63 return Err("A name has only letters, digits and underscores.".to_owned());
64 }
65 if upper.starts_with(|c: char| c.is_ascii_digit()) {
66 return Err("A name cannot start with a digit.".to_owned());
67 }
Secrets and variables: one list, rows per environment, for workflows and deployments68 if upper.starts_with("GITHUB_") || upper.starts_with("G1T_") {
69 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 workflows70 }
71 Ok(upper)
72}
73
Secrets and variables: one list, rows per environment, for workflows and deployments74/// Environments' names: lowercase letters, digits, `-` and `_`, each once.
75fn valid_environments(list: &[String]) -> Result<Vec<String>, String> {
76 let mut out: Vec<String> = Vec::new();
77 for name in list {
78 let lower = name.trim().to_ascii_lowercase();
79 if lower.is_empty() {
80 continue;
81 }
82 if lower.len() > 40 || !lower.chars().all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_') {
83 return Err(format!("`{name}` is not an environment's name: up to 40 letters, digits, - and _."));
84 }
85 if !out.contains(&lower) {
86 out.push(lower);
87 }
88 }
89 out.sort();
90 Ok(out)
91}
92
93fn consumers(list: &[String]) -> Result<Vec<String>, String> {
94 let mut out: Vec<String> = Vec::new();
95 for item in list {
96 let item = item.trim().to_ascii_lowercase();
97 if !CONSUMERS.contains(&item.as_str()) {
98 return Err(format!("`{item}` is not a reader: use workflows or deployments."));
99 }
100 if !out.contains(&item) {
101 out.push(item);
102 }
103 }
104 if out.is_empty() {
105 return Err("Choose who reads it: workflows, deployments, or both.".to_owned());
106 }
107 Ok(out)
108}
109
110fn split(list: &str) -> Vec<String> {
111 list.split(',').filter(|s| !s.is_empty()).map(str::to_owned).collect()
112}
113
114impl SettingRow {
115 fn environments(&self) -> Vec<String> {
116 split(&self.environments)
117 }
118
Projects: what a workspace builds and runs, first on every page119 /// A workspace's row: the projects it reaches. The column predates
120 /// projects; it holds their slugs.
121 fn projects(&self) -> Vec<String> {
Secrets and variables: one list, rows per environment, for workflows and deployments122 self.repositories.as_deref().and_then(|json| serde_json::from_str(json).ok()).unwrap_or_default()
123 }
124
Projects: what a workspace builds and runs, first on every page125 fn reaches(&self, project: &str) -> bool {
126 let list = self.projects();
127 list.is_empty() || list.iter().any(|p| p.eq_ignore_ascii_case(project))
Secrets and variables: one list, rows per environment, for workflows and deployments128 }
129
130 /// Whether it and rows for `environments` would both apply somewhere.
131 fn overlaps(&self, environments: &[String]) -> bool {
132 let mine = self.environments();
133 mine.is_empty() == environments.is_empty() && (mine.is_empty() || mine.iter().any(|e| environments.contains(e)))
134 }
135
136 fn describe(self) -> Setting {
137 Setting {
138 available_to: split(&self.available_to),
139 environments: self.environments(),
Projects: what a workspace builds and runs, first on every page140 projects: self.projects(),
Secrets and variables: one list, rows per environment, for workflows and deployments141 value: (self.kind == "variable").then_some(self.value),
142 id: self.id,
143 name: self.name,
144 kind: self.kind,
145 scope: self.scope,
146 updated_at: self.updated_at,
147 note: self.note,
148 updated_by: self.updated_by,
149 }
150 }
151}
152
Projects: what a workspace builds and runs, first on every page153/// Where settings live: `(scope, owner)` with the owner a project id or a
154/// workspace slug.
GitHub Actions on g1t, part two: running workflows155struct Place {
156 scope: &'static str,
157 owner: String,
158 namespace: String,
Projects: what a workspace builds and runs, first on every page159 /// The project's slug, for a project's place.
160 slug: String,
GitHub Actions on g1t, part two: running workflows161}
162
163impl Actions {
164 async fn place(&self, actor: &User, owner: &SettingsOwner, changing: bool) -> Result<Outcome<Place>> {
165 if actor.kind == PrincipalKind::Agent {
166 return Ok(fail(FailureCode::Forbidden, "Agents cannot read or change secrets and variables."));
167 }
Secrets and variables: one list, rows per environment, for workflows and deployments168 // A workspace's tokens, G1T_TOKEN among them, read the names but
169 // never change them: a workflow must not rewrite what it runs with.
170 if changing && actor.kind == PrincipalKind::Workspace {
171 return Ok(fail(
172 FailureCode::Forbidden,
173 "A workspace's tokens, G1T_TOKEN included, cannot change secrets and variables. Use a person's token or the site.",
174 ));
175 }
GitHub Actions on g1t, part two: running workflows176 match (&owner.repo, &owner.workspace) {
177 (Some(path), _) => {
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look178 // A repository's secrets and variables, seen or changed,
179 // go with its webhooks and deployments: the Admin role.
180 let repo = match self.may(actor, path, Capability::ManageIntegrations).await? {
181 Outcome::Ok(repo) => repo,
182 Outcome::Fail(refused) => return Ok(Outcome::Fail(refused)),
GitHub Actions on g1t, part two: running workflows183 };
Projects: what a workspace builds and runs, first on every page184 let Some(project) = self.project_of(&repo.id).await? else {
185 return Ok(fail(FailureCode::NotFound, "The repository has no project."));
186 };
187 Ok(Outcome::Ok(Place { scope: "project", owner: project.id, namespace: repo.namespace, slug: project.slug }))
GitHub Actions on g1t, part two: running workflows188 }
189 (None, Some(slug)) => {
190 let slug = slug.to_lowercase();
191 let role = actor.workspaces.iter().find(|m| m.slug.eq_ignore_ascii_case(&slug)).map(|m| m.role);
192 match role {
193 None => Ok(fail(FailureCode::Forbidden, format!("Only members of {slug} can see its secrets and variables."))),
194 Some(Role::Member) if changing => Ok(fail(FailureCode::Forbidden, format!("Only owners of {slug} can change its secrets and variables."))),
Projects: what a workspace builds and runs, first on every page195 Some(_) => Ok(Outcome::Ok(Place { scope: "workspace", owner: slug.clone(), namespace: slug, slug: String::new() })),
GitHub Actions on g1t, part two: running workflows196 }
197 }
198 (None, None) => Ok(fail(FailureCode::Invalid, "Give `repo` or `workspace`.")),
199 }
200 }
201
Projects: what a workspace builds and runs, first on every page202 /// A repository's primary project, with the rows kept under the
203 /// repository before projects moved to it.
204 pub(crate) async fn project_of(&self, repo_id: &str) -> Result<Option<ProjectRef>> {
205 let projects: Vec<ProjectRef> =
206 g1t_kit::call(&self.projects, "by_repo", &ByRepoArgs { repo_id: repo_id.to_owned() }).await?;
207 let Some(project) = projects.into_iter().find(|p| p.primary) else {
208 return Ok(None);
209 };
210 self.db
211 .prepare("UPDATE settings SET owner = ?, scope = 'project' WHERE owner = ?")
212 .bind(&[project.id.as_str().into(), repo_id.into()])?
213 .run()
214 .await?;
215 Ok(Some(project))
216 }
217
Secrets and variables: one list, rows per environment, for workflows and deployments218 /// `secret`, `variable`, or `None` for both.
219 fn kind(kind: &str) -> Outcome<Option<&'static str>> {
GitHub Actions on g1t, part two: running workflows220 match kind {
Secrets and variables: one list, rows per environment, for workflows and deployments221 "secret" | "secrets" => Outcome::Ok(Some("secret")),
222 "variable" | "variables" | "config" => Outcome::Ok(Some("variable")),
223 "" | "all" => Outcome::Ok(None),
GitHub Actions on g1t, part two: running workflows224 _ => fail(FailureCode::Invalid, "`kind` is `secret` or `variable`."),
225 }
226 }
227
Secrets and variables: one list, rows per environment, for workflows and deployments228 async fn rows(&self, owner: &str) -> Result<Vec<SettingRow>> {
229 self.db
230 .prepare("SELECT * FROM settings WHERE owner = ? ORDER BY name, environments")
231 .bind(&[owner.into()])?
232 .all()
233 .await?
234 .results::<SettingRow>()
235 }
236
GitHub Actions on g1t, part two: running workflows237 pub async fn settings(&self, a: SettingsArgs) -> Result<Outcome<Vec<Setting>>> {
238 let kind = check!(Self::kind(&a.kind));
239 let place = check!(self.place(&a.actor, &a.owner, false).await?);
240 let mut out: Vec<Setting> = Vec::new();
Projects: what a workspace builds and runs, first on every page241 // A project's list shows the workspace's rows that reach it, but for
242 // keys it sets itself.
243 if place.scope == "project" {
Secrets and variables: one list, rows per environment, for workflows and deployments244 let own: Vec<String> = self.rows(&place.owner).await?.into_iter().map(|row| row.name).collect();
245 for row in self.rows(&place.namespace.to_lowercase()).await? {
Projects: what a workspace builds and runs, first on every page246 if row.reaches(&place.slug) && !own.contains(&row.name) {
Secrets and variables: one list, rows per environment, for workflows and deployments247 out.push(row.describe());
248 }
GitHub Actions on g1t, part two: running workflows249 }
250 }
Secrets and variables: one list, rows per environment, for workflows and deployments251 out.extend(self.rows(&place.owner).await?.into_iter().map(SettingRow::describe));
252 out.retain(|setting| kind.is_none_or(|kind| setting.kind == kind));
253 out.sort_by(|a, b| a.name.cmp(&b.name).then(a.environments.cmp(&b.environments)));
GitHub Actions on g1t, part two: running workflows254 Ok(Outcome::Ok(out))
255 }
256
Secrets and variables: one list, rows per environment, for workflows and deployments257 fn seal(&self, value: &str, id: &str) -> Outcome<String> {
258 match &self.sealer {
259 Some(sealer) => Outcome::Ok(sealer.seal(value, id)),
260 None => fail(FailureCode::Conflict, "Secrets cannot be saved yet: g1t's key for them is not set."),
261 }
262 }
263
GitHub Actions on g1t, part two: running workflows264 pub async fn set_setting(&self, a: SetSettingArgs) -> Result<Outcome<Setting>> {
Secrets and variables: one list, rows per environment, for workflows and deployments265 let Some(kind) = check!(Self::kind(&a.kind)) else {
266 return Ok(fail(FailureCode::Invalid, "`kind` is `secret` or `variable`."));
267 };
GitHub Actions on g1t, part two: running workflows268 let name = match valid_name(&a.name) {
269 Ok(name) => name,
270 Err(problem) => return Ok(fail(FailureCode::Invalid, problem)),
271 };
Secrets and variables: one list, rows per environment, for workflows and deployments272 if a.value.as_ref().is_some_and(|v| v.len() > MAX_VALUE_BYTES) {
GitHub Actions on g1t, part two: running workflows273 return Ok(fail(FailureCode::Invalid, "A value is at most 48 KB."));
274 }
Secrets and variables: one list, rows per environment, for workflows and deployments275 if a.note.as_ref().is_some_and(|n| n.len() > MAX_NOTE) {
276 return Ok(fail(FailureCode::Invalid, "A note is at most 500 characters."));
277 }
278 let readers = match a.available_to.as_deref().map(consumers).transpose() {
279 Ok(readers) => readers,
280 Err(problem) => return Ok(fail(FailureCode::Invalid, problem)),
281 };
282 let environments = match a.environments.as_deref().map(valid_environments).transpose() {
283 Ok(environments) => environments,
284 Err(problem) => return Ok(fail(FailureCode::Invalid, problem)),
285 };
GitHub Actions on g1t, part two: running workflows286 let place = check!(self.place(&a.actor, &a.owner, true).await?);
Projects: what a workspace builds and runs, first on every page287 if a.projects.as_ref().is_some_and(|r| !r.is_empty()) && place.scope != "workspace" {
288 return Ok(fail(FailureCode::Invalid, "Only a workspace's rows choose projects."));
GitHub Actions on g1t, part two: running workflows289 }
Secrets and variables: one list, rows per environment, for workflows and deployments290 let rows = self.rows(&place.owner).await?;
291 // A secret and a variable may share a key, as on GitHub, where
292 // workflows read them apart (`secrets.X`, `vars.X`).
293 let same_key: Vec<&SettingRow> = rows.iter().filter(|row| row.name == name).collect();
294 // The row being changed: by id, else the key's row for every
295 // environment (GitHub's API names a secret by its key alone).
296 let existing = match &a.id {
297 Some(id) => match rows.iter().find(|row| &row.id == id) {
298 Some(row) => Some(row),
299 None => return Ok(fail(FailureCode::NotFound, "There is no such row.")),
300 },
301 None if a.environments.is_none() => same_key.iter().copied().find(|row| row.environments.is_empty() && row.kind == kind),
302 None => None,
303 };
304 if existing.is_some_and(|row| row.kind == "secret" && kind == "variable") {
305 return Ok(fail(FailureCode::Invalid, "A secret cannot become config: its value is sealed. Add a config row and remove the secret."));
306 }
307 let environments = environments.unwrap_or_else(|| existing.map(SettingRow::environments).unwrap_or_default());
308 // A key's rows never apply to the same environment twice.
309 if let Some(clash) = same_key
310 .iter()
311 .find(|row| row.kind == kind && existing.is_none_or(|e| e.id != row.id) && row.overlaps(&environments))
312 {
Deploy scripts live in the repository313 let at = if clash.environments.is_empty() { "all environments".to_owned() } else { clash.environments.replace(',', ", ") };
314 let what = if kind == "secret" { "secret" } else { "config" };
Secrets and variables: one list, rows per environment, for workflows and deployments315 return Ok(fail(
316 FailureCode::Conflict,
Deploy scripts live in the repository317 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 deployments318 ));
319 }
320 if existing.is_none() && rows.len() as u32 >= MAX_PER_OWNER {
321 return Ok(fail(FailureCode::Invalid, format!("There can be at most {MAX_PER_OWNER} secrets and variables here.")));
322 }
323 let id = existing.map(|row| row.id.clone()).unwrap_or_else(|| new_id("set", now_ms()));
324 let value = match (&a.value, existing) {
325 (Some(value), _) if kind == "secret" => check!(self.seal(value, &id)),
326 (Some(value), _) => value.clone(),
327 // Config becoming a secret: its value is sealed now.
328 (None, Some(row)) if row.kind == "variable" && kind == "secret" => check!(self.seal(&row.value, &id)),
329 (None, Some(row)) => row.value.clone(),
330 (None, None) => return Ok(fail(FailureCode::Invalid, "A new row needs a `value`.")),
GitHub Actions on g1t, part two: running workflows331 };
Secrets and variables: one list, rows per environment, for workflows and deployments332 let available_to = readers
333 .map(|r| r.join(","))
334 .or_else(|| existing.map(|row| row.available_to.clone()))
335 .unwrap_or_else(|| CONSUMERS.join(","));
Projects: what a workspace builds and runs, first on every page336 let repositories: Option<String> = match &a.projects {
Secrets and variables: one list, rows per environment, for workflows and deployments337 Some(list) if list.is_empty() => None,
338 Some(list) => Some(serde_json::to_string(list).unwrap_or_default()),
339 None => existing.and_then(|row| row.repositories.clone()),
340 };
341 let note = match &a.note {
342 Some(note) if note.trim().is_empty() => None,
343 Some(note) => Some(note.trim().to_owned()),
344 None => existing.and_then(|row| row.note.clone()),
345 };
GitHub Actions on g1t, part two: running workflows346 let at = rfc3339(now_ms());
Secrets and variables: one list, rows per environment, for workflows and deployments347 let optional = |v: Option<&str>| v.map_or(JsValue::NULL, JsValue::from);
GitHub Actions on g1t, part two: running workflows348 self.db
349 .prepare(
Secrets and variables: one list, rows per environment, for workflows and deployments350 "INSERT INTO settings (id, scope, owner, kind, name, value, updated_at, available_to, environments, repositories, note, updated_by)
351 VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
352 ON CONFLICT (id) DO UPDATE SET kind = excluded.kind, value = excluded.value, updated_at = excluded.updated_at,
353 available_to = excluded.available_to, environments = excluded.environments,
354 repositories = excluded.repositories, note = excluded.note, updated_by = excluded.updated_by",
GitHub Actions on g1t, part two: running workflows355 )
Secrets and variables: one list, rows per environment, for workflows and deployments356 .bind(&[
357 id.as_str().into(),
358 place.scope.into(),
359 place.owner.as_str().into(),
360 kind.into(),
361 name.as_str().into(),
362 value.into(),
363 at.as_str().into(),
364 available_to.as_str().into(),
365 environments.join(",").into(),
366 optional(repositories.as_deref()),
367 optional(note.as_deref()),
368 a.actor.username.as_str().into(),
369 ])?
GitHub Actions on g1t, part two: running workflows370 .run()
371 .await?;
Secrets and variables: one list, rows per environment, for workflows and deployments372 let row = self
373 .db
374 .prepare("SELECT * FROM settings WHERE id = ?")
375 .bind(&[id.as_str().into()])?
376 .first::<SettingRow>(None)
377 .await?
378 .expect("just written");
379 Ok(Outcome::Ok(row.describe()))
GitHub Actions on g1t, part two: running workflows380 }
381
382 pub async fn delete_setting(&self, a: DeleteSettingArgs) -> Result<Outcome<bool>> {
383 let kind = check!(Self::kind(&a.kind));
384 let place = check!(self.place(&a.actor, &a.owner, true).await?);
Secrets and variables: one list, rows per environment, for workflows and deployments385 let name = a.name.trim().to_ascii_uppercase();
386 let removed = match &a.id {
387 Some(id) => self
388 .db
389 .prepare("DELETE FROM settings WHERE owner = ? AND id = ? RETURNING id")
390 .bind(&[place.owner.as_str().into(), id.as_str().into()])?
391 .all()
392 .await?,
393 None => self
394 .db
395 .prepare("DELETE FROM settings WHERE owner = ? AND name = ? AND (?3 IS NULL OR kind = ?3) RETURNING id")
396 .bind(&[place.owner.as_str().into(), name.as_str().into(), kind.map_or(JsValue::NULL, JsValue::from)])?
397 .all()
398 .await?,
399 };
400 Ok(if removed.results::<Value>()?.is_empty() {
401 fail(FailureCode::NotFound, format!("There is nothing called {} here.", a.name))
402 } else {
403 Outcome::Ok(true)
GitHub Actions on g1t, part two: running workflows404 })
405 }
406
Secrets and variables: one list, rows per environment, for workflows and deployments407 /// What one reader of a repository gets: per key, the row for
408 /// `environment`, else the row for every environment; the repository's
409 /// over its workspace's. No secrets unless `trusted`.
410 #[allow(clippy::too_many_arguments)]
411 async fn resolved(
412 &self,
Projects: what a workspace builds and runs, first on every page413 project_id: &str,
414 project_slug: &str,
Secrets and variables: one list, rows per environment, for workflows and deployments415 namespace: &str,
416 kind: &str,
417 consumer: &str,
418 environment: Option<&str>,
419 trusted: bool,
420 ) -> Result<Map<String, Value>> {
421 if kind == "secret" && !trusted {
422 return Ok(Map::new());
423 }
424 let environment = environment.map(str::to_ascii_lowercase);
GitHub Actions on g1t, part two: running workflows425 let mut out = Map::new();
Projects: what a workspace builds and runs, first on every page426 for owner in [namespace.to_lowercase(), project_id.to_owned()] {
Secrets and variables: one list, rows per environment, for workflows and deployments427 let rows: Vec<SettingRow> = self
428 .rows(&owner)
GitHub Actions on g1t, part two: running workflows429 .await?
Secrets and variables: one list, rows per environment, for workflows and deployments430 .into_iter()
431 .filter(|row| row.kind == kind)
432 .filter(|row| split(&row.available_to).iter().any(|r| r == consumer))
Projects: what a workspace builds and runs, first on every page433 .filter(|row| row.scope != "workspace" || row.reaches(project_slug))
Secrets and variables: one list, rows per environment, for workflows and deployments434 .collect();
435 let mut names: Vec<&str> = rows.iter().map(|row| row.name.as_str()).collect();
436 names.dedup();
437 for name in names {
438 let of_key: Vec<&SettingRow> = rows.iter().filter(|row| row.name == name).collect();
439 let chosen = environment
440 .as_deref()
441 .and_then(|env| of_key.iter().find(|row| row.environments().iter().any(|e| e == env)))
442 .or_else(|| of_key.iter().find(|row| row.environments.is_empty()));
443 let Some(row) = chosen else {
444 // Rows only for other environments: this reader gets
445 // none, nor the workspace's.
446 out.remove(name);
447 continue;
448 };
GitHub Actions on g1t, part two: running workflows449 let value = if kind == "secret" {
450 match self.sealer.as_ref().and_then(|sealer| sealer.open(&row.value, &row.id)) {
451 Some(value) => value,
452 None => continue,
453 }
454 } else {
Secrets and variables: one list, rows per environment, for workflows and deployments455 row.value.clone()
GitHub Actions on g1t, part two: running workflows456 };
Secrets and variables: one list, rows per environment, for workflows and deployments457 out.insert(name.to_owned(), Value::String(value));
GitHub Actions on g1t, part two: running workflows458 }
459 }
460 Ok(out)
461 }
462
Secrets and variables: one list, rows per environment, for workflows and deployments463 /// The `vars` context of a repository's runs. `environment` is the job's
464 /// `environment:`, when it has one.
465 pub async fn variables_for(&self, repo_id: &str, repo: &str, environment: Option<&str>, trusted: bool) -> Result<Map<String, Value>> {
Projects: what a workspace builds and runs, first on every page466 let (namespace, _) = repo.split_once('/').unwrap_or((repo, ""));
467 let Some(project) = self.project_of(repo_id).await? else {
468 return Ok(Map::new());
469 };
470 self.resolved(&project.id, &project.slug, namespace, "variable", "workflows", environment, trusted).await
GitHub Actions on g1t, part two: running workflows471 }
472
473 /// The `secrets` context of a repository's runs, opened.
Secrets and variables: one list, rows per environment, for workflows and deployments474 pub async fn secrets_for(&self, repo_id: &str, repo: &str, environment: Option<&str>, trusted: bool) -> Result<Map<String, Value>> {
Projects: what a workspace builds and runs, first on every page475 let (namespace, _) = repo.split_once('/').unwrap_or((repo, ""));
476 let Some(project) = self.project_of(repo_id).await? else {
477 return Ok(Map::new());
478 };
479 self.resolved(&project.id, &project.slug, namespace, "secret", "workflows", environment, trusted).await
GitHub Actions on g1t, part two: running workflows480 }
Secrets and variables: one list, rows per environment, for workflows and deployments481
482 /// `resolve_settings`, for the deployments service: what a deploy build
483 /// and its running app get.
484 pub async fn resolve_settings(&self, a: ResolveSettingsArgs) -> Result<ResolvedSettings> {
485 let environment = a.environment.as_deref();
Projects live: fixes from walking them end to end486 // Rows kept under the repository from before projects move to its
487 // primary project first, however the project is named here.
488 let primary = self.project_of(&a.repo_id).await?;
489 let (project_id, slug) = match (a.project_id, a.project_slug, primary) {
490 (Some(id), Some(slug), _) => (id, slug),
491 (_, _, Some(project)) => (project.id, project.slug),
492 _ => return Ok(ResolvedSettings::default()),
Projects: what a workspace builds and runs, first on every page493 };
Secrets and variables: one list, rows per environment, for workflows and deployments494 Ok(ResolvedSettings {
495 secrets: self
Projects: what a workspace builds and runs, first on every page496 .resolved(&project_id, &slug, &a.repo.namespace, "secret", &a.consumer, environment, a.trusted)
Secrets and variables: one list, rows per environment, for workflows and deployments497 .await?,
498 variables: self
Projects: what a workspace builds and runs, first on every page499 .resolved(&project_id, &slug, &a.repo.namespace, "variable", &a.consumer, environment, a.trusted)
Secrets and variables: one list, rows per environment, for workflows and deployments500 .await?,
501 })
502 }
GitHub Actions on g1t, part two: running workflows503}
504
505#[cfg(test)]
506mod tests {
Secrets and variables: one list, rows per environment, for workflows and deployments507 use super::{SettingRow, consumers, valid_environments, valid_name};
GitHub Actions on g1t, part two: running workflows508
Secrets and variables: one list, rows per environment, for workflows and deployments509 fn row(environments: &str) -> SettingRow {
510 SettingRow {
511 id: "set_1".into(),
512 scope: "repository".into(),
513 kind: "secret".into(),
514 name: "STRIPE_KEY".into(),
515 value: String::new(),
516 updated_at: String::new(),
517 available_to: "workflows,deployments".into(),
518 environments: environments.into(),
519 repositories: None,
520 note: None,
521 updated_by: None,
522 }
523 }
524
GitHub Actions on g1t, part two: running workflows525 #[test]
Secrets and variables: one list, rows per environment, for workflows and deployments526 fn names_follow_githubs_rules_and_keep_g1ts_own() {
GitHub Actions on g1t, part two: running workflows527 assert_eq!(valid_name("npm_token").unwrap(), "NPM_TOKEN");
528 assert!(valid_name("GITHUB_TOKEN").is_err());
Secrets and variables: one list, rows per environment, for workflows and deployments529 assert!(valid_name("G1T_TOKEN").is_err());
GitHub Actions on g1t, part two: running workflows530 assert!(valid_name("1PASSWORD").is_err());
531 assert!(valid_name("MY-TOKEN").is_err());
532 assert!(valid_name("").is_err());
533 }
Secrets and variables: one list, rows per environment, for workflows and deployments534
535 #[test]
536 fn environments_and_readers_are_checked() {
537 assert_eq!(valid_environments(&["Production".into(), "preview".into(), "production".into()]).unwrap(), vec!["preview", "production"]);
538 assert!(valid_environments(&["staging env".into()]).is_err());
539 assert_eq!(consumers(&["Deployments".into(), "deployments".into()]).unwrap(), vec!["deployments"]);
540 assert!(consumers(&["agents".into()]).is_err());
541 assert!(consumers(&[]).is_err());
542 }
543
544 #[test]
545 fn a_keys_rows_cannot_share_an_environment() {
546 assert!(row("production").overlaps(&["production".into(), "preview".into()]));
547 assert!(!row("production").overlaps(&["preview".into()]));
548 // One row for every environment, and others for some, live together.
549 assert!(!row("").overlaps(&["preview".into()]));
550 assert!(row("").overlaps(&[]));
551 }
GitHub Actions on g1t, part two: running workflows552}