~ knotless Search/
↑↓ move↵ open⇧↵ all results

chadtmiller.com/knotless

git that lives in your Atmosphere account

knotless.dev
1 collaborator

knotless 0.13.0 Latest 2 packages, 1 image

1
10

knotless-run: A run says which secrets it was not granted

Merged chadtmiller.com asked to merge into main from chadtmiller.com/knotless:run/secret-not-granted
.changeset/secret-not-granted.mdnew+7−0
@@ -0,0 +1,7 @@
---
default: patch
---
# A run says which secrets it was not granted
A run whose workflow lists a secret the runner does not grant it now says so at the top of its log, `==> NAME is not granted to <repo> at <ref>`, rather than leaving the secret silently absent. A pull's or merge-queue batch's run says the same of each, as it gets none.
crates/knotless-run/src/lib.rs+31−6
@@ -2606,7 +2606,7 @@ impl Runner {
cancelled,
said,
} = started;
let (vars, secrets) = self.vars(watch, subject, wf)?;
let (vars, secrets, ungranted) = self.vars(watch, subject, wf)?;
let dir = match &self.opts.work {
Some(work) => {
std::fs::create_dir_all(work)?;
@@ -2649,6 +2649,7 @@ impl Runner {
let given = given(subject, wf).map(|(k, v)| format!(" {k}={v}"));
writeln!(log, "==> asked for by name{}", given.collect::<String>())?;
}
log.write_all(not_granted(&ungranted, watch, subject).as_bytes())?;
// What it writes goes to the appview as it is written, ending
// once all of it is, before it is filed.
let rkey = run_rkey(watch, subject, &wf.name);
@@ -2966,13 +2967,19 @@ impl Runner {
/// What a step sees beyond the runner's tools: the workflow's own
/// variables, those a run asked for by name was given, the run's, and
/// the secrets both the workflow asks for and
/// the runner grants this repository, never to a pull's run; and those
/// secrets' values, to keep out of the log.
fn vars(&self, watch: &Watch, subject: &Subject, wf: &Workflow) -> Result<(Vars, Vec<String>)> {
/// the runner grants this repository, never to a pull's run; those
/// secrets' values, to keep out of the log; and the names of those it
/// asks for and is not given.
fn vars(
&self,
watch: &Watch,
subject: &Subject,
wf: &Workflow,
) -> Result<(Vars, Vec<String>, Vec<String>)> {
let mut env = wf.env.clone();
env.extend(given(subject, wf).cloned());
let mut vars: Vars = env.into_iter().collect();
let mut secrets = Vec::new();
let (mut secrets, mut given) = (Vec::new(), Vec::new());
let pull = subject.pull.as_ref().map(|p| p.uri.as_str());
for (k, v) in [
("KNOTLESS_RUN", Some("1")),
@@ -2998,13 +3005,31 @@ impl Runner {
if let Some(value) = granted.remove(name).and_then(|s| s.to(&subject.ref_name)) {
secrets.push(value.clone());
vars.push((name.clone(), value));
given.push(name);
}
}
}
Ok((vars, secrets))
let ungranted = wf.secrets.iter().filter(|n| !given.contains(n));
Ok((vars, secrets, ungranted.cloned().collect()))
}
}
/// What a run's log says of each secret its workflow asks for and it is
/// not given, which would otherwise be simply absent: a pull's or a
/// batch's run gets none, and a push gets only those the runner grants
/// its repository at its ref.
fn not_granted(names: &[String], watch: &Watch, subject: &Subject) -> String {
let to = if subject.proposed() {
"a pull's or a merge-queue batch's run, which gets no secrets".to_string()
} else {
format!("{} at {}", watch.label, subject.ref_name)
};
let lines = names
.iter()
.map(|n| format!("==> {n} is not granted to {to}\n"));
lines.collect()
}
/// Takes the grants to `label` out of the secrets file at `path`, if it
/// has any, keeping the rest as they are.
fn ungrant(path: &std::path::Path, label: &str) -> Result<()> {
crates/knotless-run/tests/run.rs+11−0
@@ -290,6 +290,13 @@ fn a_push_runs_its_workflows() {
assert!(!log.contains("s3cret"), "{log}");
// A secret granted to some refs alone reaches only their pushes.
assert!(log.contains("main=*** tags=none"), "{log}");
// The log says which it asked for and was not given.
let label = format!("{}/proj", owner.account.handle);
for name in ["UNGRANTED", "TAGS_ONLY"] {
let said = format!("==> {name} is not granted to {label} at refs/heads/main\n");
assert!(log.contains(&said), "{log}");
}
assert!(!log.contains("TOKEN is not granted"), "{log}");
let fail = check_of(&runner, &owner, &key, "fail");
assert_eq!(fail["status"], "failure");
@@ -2172,6 +2179,10 @@ fn a_merged_pull_passes_its_push() {
assert_eq!(ran("ci"), format!("{head}\n"));
// A trusted pull's run that passed keeps what it wrote to its cache.
assert_eq!(kept().len(), 1);
// A pull's run of what asks for secrets gets none, and says so.
let secret = log_of(&check_on(&runner, &owner, &key, &topic, "secret"));
let none = "==> TOKEN is not granted to a pull's or a merge-queue batch's run, which gets no secrets\n";
assert!(secret.contains(none), "{secret}");
// Squashed into main: its tree is the one the pull ran at.
let squashed = merged(&["merge", &url, &topic]);
docs/design/runner.md+4−1
@@ -746,7 +746,10 @@ only if the workflow lists it, the file grants it to the repository and,
when the secret names `refs`, to the ref, and the run is not a pull's or a
batch's. A secret limited to `refs/heads/main` cannot be read from another
branch, which matters because a branch can change the workflow itself. A
secret not granted is simply absent.
secret not granted is absent from the step's environment, and the log
names it before the steps (`==> NAME is not granted to <repo> at <ref>`),
so a workflow that fails for want of one says why. The line names the
secret, never a value.
The runner re-reads the file on every run that needs secrets, so a change
takes effect without a restart. A workflow that asks for none never reads
docs/reference/workflows.md+4−2
@@ -258,8 +258,10 @@ those refs:
}
```
`refs` match as a trigger's do. A secret not granted is simply absent. A
pull's or merge-queue batch's run gets no secrets. In the log, which is
`refs` match as a trigger's do. A secret not granted is absent, and the
log says so before the steps: `==> NAME is not granted to <repo> at
<ref>`. A pull's or merge-queue batch's run gets no secrets, and its log
says that of each the workflow lists. In the log, which is
public, each granted secret's value is replaced by `***`, and so is each
trimmed line of a value with several lines, such as a PEM key, the longest
first. Values shorter than four bytes are not masked.