1#![deny(missing_docs)]
2
3use std::error::Error;
42use std::fmt;
43use std::fs::{self, File, OpenOptions};
44use std::future::Future;
45use std::io::{self, BufRead, BufReader, BufWriter, Read, Write};
46use std::os::unix::fs::{OpenOptionsExt, PermissionsExt};
47use std::path::{Path, PathBuf};
48use std::pin::Pin;
49use std::process::{Command, Stdio};
50use std::str;
51
52use backtrace::Backtrace;
53use bytes::Buf;
54use futures::pin_mut;
55use futures::prelude::*;
56use http::Response;
57use http_body::Body;
58use http_body_util::BodyStream;
59use linked_hash_map::LinkedHashMap;
60use log::{debug, info, warn};
61use octocrab::models::JobId;
62use octocrab::models::RunId;
63use octocrab::{params, Octocrab};
64use serde_yaml::Value;
65
66mod contexts;
67mod expressions;
68mod local_docker_backend;
69mod models;
70
71use contexts::*;
72use expressions::ContextResolver;
73pub use local_docker_backend::*;
74use models::*;
75
76#[derive(Clone)]
79pub struct RunnerContext {
80 pub github: Octocrab,
84 pub owner: String,
86 pub repo: String,
88 pub commit_sha: Vec<u8>,
90 pub commit_ref: Option<String>,
92 pub global_dir_host: PathBuf,
95 pub workflow_repo_path: String,
97 pub run_id: RunId,
101 pub run_number: i64,
104 pub job_id: JobId,
106 pub actor: String,
108 pub token: String,
112 pub override_env: Vec<(String, String)>,
115}
116
117const GITHUB_WORKSPACE: &str = "/github/workspace";
118const GITHUB_COM: &str = "https://github.com";
119const API_GITHUB_COM: &str = "https://api.github.com";
120const API_GITHUB_COM_GRAPHQL: &str = "https://api.github.com/graphql";
121
122impl RunnerContext {
123 fn create_env(&self, workflow_name: Option<String>) -> LinkedHashMap<String, String> {
125 let mut ret = LinkedHashMap::new();
126 let mut insert = |key: &str, value: String| {
127 ret.insert(key.to_string(), value);
128 };
129 insert("CI", "true".to_string());
130 insert(
131 "GITHUB_WORKFLOW",
132 workflow_name.unwrap_or_else(|| self.workflow_repo_path.clone()),
133 );
134 insert("GITHUB_RUN_ID", self.run_id.to_string());
135 insert("GITHUB_RUN_NUMBER", self.run_number.to_string());
136 insert("GITHUB_JOB", self.job_id.to_string());
137 insert("GITHUB_ACTIONS", "true".to_string());
140 insert("GITHUB_ACTOR", self.actor.clone());
141 insert("GITHUB_REPOSITORY", format!("{}/{}", self.owner, self.repo));
142 insert("GITHUB_EVENT_NAME", "push".to_string());
145 insert("GITHUB_EVENT_PATH", "/github/.gha-runner/event".to_string());
146 insert("GITHUB_PATH", "/github/.gha-runner/path".to_string());
147 insert("GITHUB_ENV", "/github/.gha-runner/env".to_string());
148 insert("GITHUB_WORKSPACE", GITHUB_WORKSPACE.to_string());
149 insert("GITHUB_SHA", hex::encode(&self.commit_sha));
150 if let Some(r) = self.commit_ref.as_ref() {
151 insert("GITHUB_REF", r.to_string());
152 }
153 insert("GITHUB_SERVER_URL", GITHUB_COM.to_string());
154 insert("GITHUB_API_URL", API_GITHUB_COM.to_string());
155 insert("GITHUB_GRAPHQL_URL", API_GITHUB_COM_GRAPHQL.to_string());
156 insert("GITHUB_TOKEN", self.token.clone());
157 insert("RUNNER_OS", "Linux".to_string());
158 insert("RUNNER_TEMP", "/tmp".to_string());
159 insert("RUNNER_TOOL_CACHE", "/opt/hostedtoolcache".to_string());
160 ret
161 }
162 fn apply_env(
163 &self,
164 mut target: LinkedHashMap<String, String>,
165 ) -> LinkedHashMap<String, String> {
166 for v in self.override_env.iter() {
167 target.insert(v.0.to_string(), v.1.to_string());
168 }
169 target
170 }
171}
172
173#[derive(Debug)]
175pub struct JobDescription {
176 name: String,
177 matrix_values: LinkedHashMap<String, Value>,
178 job_env: LinkedHashMap<String, String>,
179 runs_on: Vec<String>,
180 steps: Vec<models::Step>,
181}
182
183pub struct DockerImageMapping {
185 pub ubuntu_18_04: ContainerImage,
187 pub ubuntu_20_04: ContainerImage,
189}
190
191impl JobDescription {
192 pub fn name(&self) -> &str {
194 &self.name
195 }
196 pub fn runs_on(&self) -> &[String] {
198 &self.runs_on
199 }
200 pub fn main_container(&self, image_mapping: &DockerImageMapping) -> Option<ContainerImage> {
202 if self.runs_on.is_empty() {
203 return None;
204 }
205 let img = match self.runs_on[0].as_str() {
206 "ubuntu-latest" | "ubuntu-20.04" => &image_mapping.ubuntu_20_04,
208 "ubuntu-18.04" => &image_mapping.ubuntu_18_04,
209 _ => return None,
210 };
211 Some(img.clone())
212 }
213}
214
215#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Debug)]
219pub struct StepIndex(pub u32);
220
221pub struct JobRunner {
225 ctx: RunnerContext,
226 job_name: String,
227 matrix_values: LinkedHashMap<String, Value>,
228 env: LinkedHashMap<String, String>,
230 paths: Vec<String>,
234 container_images: Vec<ContainerImage>,
236 steps: Vec<models::Step>,
238 outputs: Vec<LinkedHashMap<String, String>>,
239 step_index: StepIndex,
240}
241
242#[derive(Clone, Eq, PartialEq)]
244pub struct ContainerImage(pub String);
245
246impl From<&str> for ContainerImage {
247 fn from(s: &str) -> ContainerImage {
248 ContainerImage(s.to_string())
249 }
250}
251
252impl From<String> for ContainerImage {
253 fn from(s: String) -> ContainerImage {
254 ContainerImage(s)
255 }
256}
257
258#[derive(Clone, Copy, Eq, PartialEq)]
260pub struct ContainerId(pub usize);
261
262pub trait RunnerBackend {
264 fn run<'a, F: FnMut(&[u8])>(
274 &'a mut self,
275 container: ContainerId,
276 command: &'a str,
277 stdout_filter: &'a mut F,
278 ) -> Pin<Box<dyn Future<Output = i32> + 'a>>;
279}
280
281async fn untar_response<B: Body>(
282 action: &str,
283 response: Response<B>,
284 dir: &Path,
285) -> Result<(), RunnerErrorKind>
286where
287 B::Error: std::error::Error + Send + Sync + 'static,
288{
289 let response = BodyStream::new(response.into_body());
290 pin_mut!(response);
291 let mut command = Command::new("tar");
292 command
295 .args(&["zxf", "-", "--strip-components=1"])
296 .current_dir(dir)
297 .stdin(Stdio::piped());
298 let mut child = command.spawn().expect("Can't find 'tar'");
299 while let Some(b) =
300 response
301 .next()
302 .await
303 .transpose()
304 .map_err(|e| RunnerErrorKind::ActionDownloadError {
305 action: action.to_string(),
306 inner: Box::new(e),
307 })?
308 {
309 let b = match b.into_data() {
310 Ok(b) => b,
311 Err(_) => continue,
312 };
313 child
314 .stdin
315 .as_mut()
316 .unwrap()
317 .write_all(b.chunk())
318 .map_err(|e| RunnerErrorKind::ActionDownloadError {
319 action: action.to_string(),
320 inner: Box::new(e),
321 })?;
322 }
323 child.stdin = None;
325 let status = child.wait().unwrap();
326 if !status.success() {
327 return Err(RunnerErrorKind::ActionDownloadError {
328 action: action.to_string(),
329 inner: format!("tar failed: {}", status).into(),
330 });
331 }
332 Ok(())
333}
334
335fn envify(s: &str) -> String {
336 let mut ret = String::new();
337 for ch in s.chars() {
338 let replace = match ch {
339 'A'..='Z' | '0'..='9' => ch,
340 'a'..='z' => ch.to_ascii_uppercase(),
341 _ => '_',
342 };
343 ret.push(replace);
344 }
345 ret
346}
347
348fn shell_quote(s: &str) -> String {
349 let mut ret = String::new();
350 let mut quote = false;
351 ret.push('\'');
352 for ch in s.chars() {
353 if ch == '\'' {
354 quote = true;
355 ret.push_str("'\"'\"'");
356 } else {
357 if !matches!(ch, 'A'..='Z' | 'a'..='z' | '0'..='9' | '_' | '-' | '=' | '.' | '/' | ':')
358 {
359 quote = true;
360 }
361 ret.push(ch);
362 }
363 }
364 ret.push('\'');
365 if quote {
366 ret
367 } else {
368 s.to_string()
369 }
370}
371
372struct RunSpec {
373 env: LinkedHashMap<String, String>,
374 paths: Vec<String>,
376 working_directory: Option<String>,
377 command: Vec<String>,
378}
379
380struct LineBuffer {
381 buf: Vec<u8>,
382}
383
384impl LineBuffer {
385 fn new() -> LineBuffer {
386 LineBuffer { buf: Vec::new() }
387 }
388 fn append(&mut self, data: &[u8]) {
389 self.buf.extend_from_slice(data);
390 }
391 fn take_lines<F: FnMut(&[u8])>(&mut self, mut f: F) {
393 let mut offset = 0;
394 while let Some(next) = self.buf[offset..].iter().position(|c| *c == b'\n') {
395 let next = offset + next;
396 f(&self.buf[offset..next]);
397 offset = next + 1;
398 }
399 self.buf.drain(..offset);
400 }
401}
402
403fn parse_simple_shell_command(s: &str) -> Option<Vec<String>> {
404 for ch in s.chars() {
405 if !matches!(ch, '0'..='9' | 'a'..='z' | 'A'..='Z' | '-' | '/' | '.' | ' ' | '\n') {
406 return None;
407 }
408 }
409 Some(s.split_ascii_whitespace().map(|v| v.to_string()).collect())
410}
411
412fn create_public_writable_dir(path: &Path) {
414 let _ = fs::create_dir(&path);
415 fs::set_permissions(&path, fs::Permissions::from_mode(0o777)).unwrap();
416}
417
418fn create_public_writable_file(path: &Path) {
420 OpenOptions::new()
421 .write(true)
422 .create(true)
423 .truncate(true)
424 .mode(0o666)
425 .open(path)
426 .unwrap();
427 fs::set_permissions(&path, fs::Permissions::from_mode(0o666)).unwrap();
430}
431
432impl JobRunner {
433 pub fn container_images(&self) -> &[ContainerImage] {
437 &self.container_images
438 }
439 pub fn step_count(&self) -> StepIndex {
441 StepIndex(self.steps.len() as u32)
442 }
443 pub fn next_step_index(&self) -> StepIndex {
445 self.step_index
446 }
447 pub fn next_step_name(&self) -> Result<Option<String>, RunnerError> {
449 let step_context = StepContext::new(
450 &self.ctx,
451 &self.matrix_values,
452 &self.job_name,
453 self.step_index,
454 );
455 self.steps[self.step_index.0 as usize]
456 .clone()
457 .take_name(&step_context)
458 }
459 pub fn job_name(&self) -> &str {
461 &self.job_name
462 }
463 pub fn find_step_by_name(&self, step_name: &str) -> Result<Option<StepIndex>, RunnerError> {
466 for (index, step) in self.steps[(self.step_index.0 as usize)..]
467 .iter()
468 .enumerate()
469 {
470 let step_index = StepIndex(index as u32);
471 let step_context =
472 PreStepContext::new(&self.ctx, &self.matrix_values, &self.job_name, step_index);
473 let mut step = step.clone();
474 let name = step.take_name_pre(&step_context)?;
475 if name.as_deref() == Some(step_name) {
476 return Ok(Some(step_index));
477 }
478 }
479 Ok(None)
480 }
481 pub fn peek_step_env(
484 &self,
485 step_index: StepIndex,
486 ) -> Result<LinkedHashMap<String, String>, RunnerError> {
487 let mut step = self.steps[step_index.0 as usize].clone();
488 let step_context =
489 PreStepContext::new(&self.ctx, &self.matrix_values, &self.job_name, step_index);
490 step.take_env_pre(&step_context)
491 .map(|env| self.ctx.apply_env(env))
492 }
493 pub async fn run_next_step<B: RunnerBackend, I>(
495 &mut self,
496 interpose: I,
497 backend: &mut B,
498 ) -> Result<i32, RunnerError>
499 where
500 I: FnOnce(&mut Vec<String>),
501 {
502 if self.step_index.0 == 0 {
503 let _ = create_public_writable_dir(&self.ctx.global_dir_host.join("workspace"));
504 let _ = create_public_writable_dir(&self.ctx.global_dir_host.join(".gha-runner"));
505 fs::write(self.ctx.global_dir_host.join(".gha-runner/event"), "{}").unwrap();
506 let _ = create_public_writable_dir(
507 &self.ctx.global_dir_host.join(".gha-runner/hostedtoolcache"),
508 );
509 }
510 let _ = fs::create_dir(
511 self.ctx
512 .global_dir_host
513 .join(format!(".gha-runner/step{}", self.step_index.0)),
514 );
515
516 let mut step = self.steps[self.step_index.0 as usize].clone();
517 let spec = {
518 let step_context = StepContext::new(
519 &self.ctx,
520 &self.matrix_values,
521 &self.job_name,
522 self.step_index,
523 );
524 let mut env = self.env.clone();
525 for (k, v) in self.ctx.apply_env(step.take_env(&step_context)?) {
526 env.insert(k, v);
527 }
528 let mut spec = RunSpec {
529 env: env,
530 paths: self.paths.clone(),
531 working_directory: step.take_working_directory(&step_context)?,
532 command: Vec::new(),
533 };
534 if let Some(uses) = step.take_uses(&step_context)? {
535 self.configure_action(&mut step, uses, &mut spec).await?;
536 } else if let Some(run) = step.take_run(&step_context)? {
537 self.configure_command(&mut step, run, &mut spec).await?;
538 } else {
539 return Err(RunnerErrorKind::RequiredFieldMissing {
540 field_name: "uses/run",
541 got: step.0,
542 }
543 .error(&step_context));
544 }
545 spec
546 };
547 let ret = self.run_command("run", spec, interpose, backend).await;
548 self.step_index.0 += 1;
549 ret
550 }
551
552 async fn run_command<B: RunnerBackend, I>(
553 &mut self,
554 script_name: &str,
555 mut spec: RunSpec,
556 interpose: I,
557 backend: &mut B,
558 ) -> Result<i32, RunnerError>
559 where
560 I: FnOnce(&mut Vec<String>),
561 {
562 let script_path = format!(".gha-runner/step{}/{}", self.step_index.0, script_name);
563 {
564 let mut script_file = BufWriter::new(
566 OpenOptions::new()
567 .write(true)
568 .create_new(true)
569 .mode(0o777)
570 .open(self.ctx.global_dir_host.join(&script_path))
571 .unwrap(),
572 );
573 writeln!(&mut script_file, "#!/bin/bash -l").unwrap();
575 for (k, v) in spec.env {
576 if k.contains('=') {
577 return Err(RunnerErrorKind::InvalidEnvironmentVariableName { name: k }.into());
578 }
579 writeln!(
580 &mut script_file,
581 "export {}={}",
582 shell_quote(&k),
583 shell_quote(&v)
584 )
585 .unwrap();
586 }
587 if !spec.paths.is_empty() {
588 write!(&mut script_file, "export PATH=").unwrap();
589 for p in spec.paths.iter().rev() {
590 write!(&mut script_file, "{}:", p).unwrap();
591 }
592 writeln!(&mut script_file, "$PATH").unwrap();
593 }
594 if self.step_index.0 == 0 {
595 writeln!(
596 &mut script_file,
597 "ln -s /github/.gha-runner/hostedtoolcache $RUNNER_TOOL_CACHE"
598 )
599 .unwrap();
600 }
601 writeln!(&mut script_file, "cd /github/workspace").unwrap();
602 if let Some(d) = spec.working_directory {
603 writeln!(&mut script_file, "cd {}", shell_quote(&d)).unwrap();
604 }
605 write!(&mut script_file, "exec").unwrap();
606 interpose(&mut spec.command);
607 for arg in spec.command {
608 write!(&mut script_file, " {}", shell_quote(&arg)).unwrap();
609 }
610 }
611
612 let mut line_buffer = LineBuffer::new();
613 let mut stop_token: Option<Vec<u8>> = None;
614 let mut outputs = LinkedHashMap::new();
615 let mut stdout_filter = |data: &[u8]| {
616 line_buffer.append(data);
617 line_buffer.take_lines(|line| {
618 if line.len() < 2 || &line[0..2] != b"::" {
619 return;
620 }
621 if let Some(token) = stop_token.as_ref() {
622 if line[2..].ends_with(b"::") && &line[2..(line.len() - 2)] == token {
623 stop_token = None;
624 }
625 return;
626 }
627 if let Some(token) = line.strip_prefix(b"::stop-commands::") {
628 stop_token = Some(token.to_vec());
629 return;
630 }
631 if let Some(rest) = line.strip_prefix(b"::set-output name=") {
632 if let Ok(rest) = str::from_utf8(rest) {
633 if let Some(p) = rest.find("::") {
634 outputs.insert(rest[..p].to_string(), rest[(p + 2)..].to_string());
635 } else {
636 warn!(
637 "No '::' in set-output command: {}",
638 String::from_utf8_lossy(line)
639 );
640 }
641 } else {
642 warn!(
643 "Non-UTF8 set-output command: {}",
644 String::from_utf8_lossy(line)
645 );
646 }
647 return;
648 }
649 if line == b"::save-state" {
650 warn!("Ignoring ::save-state for now");
651 }
652 })
653 };
654 create_public_writable_file(&self.github_path_path());
655 create_public_writable_file(&self.github_env_path());
656 let ret = backend
657 .run(
658 ContainerId(0),
659 &format!("/github/{}", script_path),
660 &mut stdout_filter,
661 )
662 .await;
663 self.outputs.push(outputs);
664 self.update_env_from_file();
665 self.update_path_from_file();
666 Ok(ret)
667 }
668
669 fn github_path_path(&self) -> PathBuf {
670 self.ctx.global_dir_host.join(".gha-runner/path")
671 }
672 fn github_env_path(&self) -> PathBuf {
673 self.ctx.global_dir_host.join(".gha-runner/env")
674 }
675
676 fn update_env_from_file(&mut self) {
677 let mut env_file = BufReader::new(File::open(self.github_env_path()).unwrap());
678 let mut buf = Vec::new();
679 loop {
680 buf.clear();
681 let len = env_file.read_until(b'\n', &mut buf).unwrap();
682 if len == 0 {
683 break;
684 }
685 debug!(
686 "env line for step {}: {}",
687 self.step_index.0,
688 String::from_utf8_lossy(&buf)
689 );
690 if buf.last() == Some(&b'\n') {
691 buf.truncate(buf.len() - 1);
692 }
693 if let Ok(line) = str::from_utf8(&buf) {
694 if let Some(p) = line.find('=') {
695 self.env
696 .insert(line[..p].to_string(), line[(p + 1)..].to_string());
697 } else if let Some(p) = line.find("<<") {
698 let name = line[..p].to_string();
699 let delimiter = line[(p + 2)..].to_string();
700 let mut value = String::new();
701 let mut err = false;
702 loop {
703 let len = env_file.read_until(b'\n', &mut buf).unwrap();
704 if len == 0 {
705 warn!(
706 "Multiline string value not delimited for step {} value named {}",
707 self.step_index.0, name
708 );
709 err = true;
710 break;
711 }
712 debug!(
713 "env line for step {}: {}",
714 self.step_index.0,
715 String::from_utf8_lossy(&buf)
716 );
717 if buf.last() == Some(&b'\n') {
718 buf.truncate(buf.len() - 1);
719 }
720 if buf == delimiter.as_bytes() {
721 break;
722 }
723 if let Ok(s) = str::from_utf8(&buf) {
724 value.push_str(s);
725 value.push('\n');
726 } else {
727 warn!(
728 "Multiline string part not UTF8 for step {} value named {}",
729 self.step_index.0, name
730 );
731 err = true;
732 }
733 }
734 if !err {
735 self.env.insert(name, value);
736 }
737 } else {
738 warn!(
739 "No '=' in environment line for step {}: {}",
740 self.step_index.0,
741 String::from_utf8_lossy(&buf)
742 );
743 }
744 } else {
745 warn!(
746 "Non-UTF8 environment line for step {}: {}",
747 self.step_index.0,
748 String::from_utf8_lossy(&buf)
749 );
750 }
751 }
752 }
753
754 fn update_path_from_file(&mut self) {
755 let mut path_file = BufReader::new(File::open(self.github_path_path()).unwrap());
756 let mut buf = Vec::new();
757 loop {
758 buf.clear();
759 let len = path_file.read_until(b'\n', &mut buf).unwrap();
760 if len == 0 {
761 break;
762 }
763 if buf.last() == Some(&b'\n') {
764 buf.truncate(buf.len() - 1);
765 }
766 if let Ok(path) = str::from_utf8(&buf) {
767 self.paths.push(path.to_string());
768 } else {
769 warn!(
770 "Non-UTF8 path line for step {}: {}",
771 self.step_index.0,
772 String::from_utf8_lossy(&buf)
773 );
774 }
775 }
776 }
777
778 async fn configure_action(
779 &mut self,
780 step: &mut Step,
781 action_name: String,
782 spec: &mut RunSpec,
783 ) -> Result<(), RunnerError> {
784 let (action_host_path, action_path) = self.download_action(&action_name).await?;
785 let mut action = self.read_action_yaml(&action_name, &action_host_path)?;
786 let step_context = StepContext::new(
787 &self.ctx,
788 &self.matrix_values,
789 &self.job_name,
790 self.step_index,
791 );
792 let mut with = step.take_with(&step_context)?;
793 let action_context =
794 ActionContext::new(&self.ctx, &action_name, &self.job_name, self.step_index);
795 for (k, mut v) in action.take_inputs(&action_context)? {
796 let value = if let Some(w) = with.remove(&k) {
797 w
798 } else if let Some(d) = v.take_default(&action_context)? {
799 d
800 } else {
801 continue;
802 };
803 spec.env.insert(format!("INPUT_{}", envify(&k)), value);
804 }
805 let mut runs = action.take_runs(&action_context)?;
806 if runs.take_pre(&action_context)?.is_some() {
807 return Err(RunnerErrorKind::UnsupportedPre {
808 action: action_name,
809 }
810 .into());
811 }
812 let using = runs.take_using(&action_context)?;
813 if using != "node12" {
814 return Err(RunnerErrorKind::UnsupportedActionType {
815 action: action_name,
816 using,
817 }
818 .into());
819 }
820 let main = runs.take_main(&action_context)?;
821
822 spec.command = vec![
823 "node".to_string(),
824 format!("/github/{}/{}", action_path, main),
825 ];
826 Ok(())
827 }
828 async fn configure_command(
829 &mut self,
830 step: &mut Step,
831 command: String,
832 spec: &mut RunSpec,
833 ) -> Result<(), RunnerError> {
834 let step_context = StepContext::new(
835 &self.ctx,
836 &self.matrix_values,
837 &self.job_name,
838 self.step_index,
839 );
840 match step.take_shell(&step_context)?.as_deref() {
841 None | Some("bash") => {
842 spec.command = if let Some(cmd) = parse_simple_shell_command(&command) {
843 cmd
844 } else {
845 vec![
846 "bash".to_string(),
847 "--noprofile".to_string(),
848 "--norc".to_string(),
849 "-eo".to_string(),
850 "pipefail".to_string(),
851 "-c".to_string(),
852 command,
853 ]
854 };
855 }
856 Some("sh") => {
857 spec.command = if let Some(cmd) = parse_simple_shell_command(&command) {
858 cmd
859 } else {
860 vec![
861 "sh".to_string(),
862 "-e".to_string(),
863 "-c".to_string(),
864 command,
865 ]
866 };
867 }
868 Some(shell) => {
869 return Err(RunnerErrorKind::UnsupportedShell {
870 shell: shell.to_string(),
871 }
872 .into())
873 }
874 }
875 Ok(())
876 }
877
878 async fn download_action(&self, action: &str) -> Result<(PathBuf, String), RunnerError> {
879 let mut action_parts = action.splitn(2, '@').collect::<Vec<_>>();
880 if action_parts.len() != 2 {
881 return Err(RunnerErrorKind::BadActionName {
882 action: action.to_string(),
883 }
884 .into());
885 }
886 let (action_ref, action_repo) = (action_parts.pop().unwrap(), action_parts.pop().unwrap());
887 let action_repo_parts = action_repo.splitn(2, '/').collect::<Vec<_>>();
888 if action_repo_parts.len() != 2 {
889 return Err(RunnerErrorKind::BadActionName {
890 action: action.to_string(),
891 }
892 .into());
893 }
894 let action_path = format!(".gha-runner/step{}/action", self.step_index.0);
895 let action_host_path = self.ctx.global_dir_host.join(&action_path);
896 let _ = fs::create_dir(&action_host_path);
897 let commit: params::repos::Commitish = action_ref.to_string().into();
898 let response = self
899 .ctx
900 .github
901 .repos(action_repo_parts[0], action_repo_parts[1])
902 .download_tarball(commit)
903 .await
904 .map_err(|e| RunnerErrorKind::ActionDownloadError {
905 action: action.to_string(),
906 inner: Box::new(e),
907 })?;
908 untar_response(action, response, &action_host_path).await?;
909 Ok((action_host_path, action_path))
910 }
911
912 fn read_action_yaml(&self, action: &str, path: &Path) -> Result<models::Action, RunnerError> {
913 let mut file = match File::open(path.join("action.yml")) {
914 Ok(f) => Ok(f),
915 Err(e) => {
916 if e.kind() == io::ErrorKind::NotFound {
917 File::open(path.join("action.yaml"))
918 } else {
919 Err(e)
920 }
921 }
922 }
923 .map_err(|_| RunnerErrorKind::ActionDownloadError {
924 action: action.to_string(),
925 inner: "action.yml not found".into(),
926 })?;
927 let mut buf = Vec::new();
928 file.read_to_end(&mut buf).unwrap();
929 Ok(Action(serde_yaml::from_slice(&buf).map_err(|e| {
930 RunnerErrorKind::InvalidActionYaml {
931 action: action.to_string(),
932 inner: e,
933 }
934 })?))
935 }
936}
937
938pub struct Runner {
940 ctx: RunnerContext,
941 workflow_env: LinkedHashMap<String, String>,
942 workflow_name: Option<String>,
943 job_descriptions: Vec<JobDescription>,
944}
945
946#[derive(Clone, Debug)]
948pub enum ErrorContextRoot {
949 Workflow,
951 Action(String),
953}
954
955impl fmt::Display for ErrorContextRoot {
956 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
957 match self {
958 ErrorContextRoot::Workflow => {
959 write!(f, "workflow")
960 }
961 ErrorContextRoot::Action(ref action) => {
962 write!(f, "action {}", action)
963 }
964 }
965 }
966}
967
968#[derive(Clone, Debug)]
970pub struct ErrorContext {
971 pub root: ErrorContextRoot,
973 pub job_name: Option<String>,
975 pub step: Option<StepIndex>,
977}
978
979impl ErrorContext {
980 pub(crate) fn new(root: ErrorContextRoot) -> ErrorContext {
981 ErrorContext {
982 root: root,
983 job_name: None,
984 step: None,
985 }
986 }
987 pub(crate) fn job_name(mut self, job_name: Option<String>) -> ErrorContext {
988 self.job_name = job_name;
989 self
990 }
991 pub(crate) fn step(mut self, step: StepIndex) -> ErrorContext {
992 self.step = Some(step);
993 self
994 }
995}
996
997impl fmt::Display for ErrorContext {
998 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
999 if let Some(job) = self.job_name.as_ref() {
1000 if let Some(step) = self.step {
1001 write!(f, "{} job '{}' step {}", self.root, job, step.0)
1002 } else {
1003 write!(f, "{} job '{}'", self.root, job)
1004 }
1005 } else {
1006 write!(f, "{}", self.root)
1007 }
1008 }
1009}
1010
1011#[derive(Debug)]
1013pub enum RunnerErrorKind {
1014 InvalidWorkflowYaml {
1016 inner: serde_yaml::Error,
1018 },
1019 TypeMismatch {
1021 expected: String,
1023 got: Value,
1025 },
1026 RequiredFieldMissing {
1028 field_name: &'static str,
1030 got: Value,
1032 },
1033 ExpressionParseError {
1035 expression: String,
1037 },
1038 ExpressionNonString {
1040 expression: String,
1042 value: Value,
1044 },
1045 UnsupportedPlatform {
1047 runs_on: String,
1049 },
1050 BadActionName {
1052 action: String,
1054 },
1055 ActionDownloadError {
1057 action: String,
1059 inner: Box<dyn Error + Sync + Send + 'static>,
1061 },
1062 InvalidActionYaml {
1064 action: String,
1066 inner: serde_yaml::Error,
1068 },
1069 InvalidEnvironmentVariableName {
1071 name: String,
1073 },
1074 UnsupportedActionType {
1076 action: String,
1078 using: String,
1080 },
1081 UnsupportedPre {
1083 action: String,
1085 },
1086 UnsupportedShell {
1088 shell: String,
1090 },
1091}
1092
1093impl RunnerErrorKind {
1094 fn source(&self) -> Option<&(dyn Error + 'static)> {
1095 Some(match self {
1096 RunnerErrorKind::InvalidWorkflowYaml { ref inner, .. } => &*inner,
1097 RunnerErrorKind::InvalidActionYaml { ref inner, .. } => &*inner,
1098 RunnerErrorKind::ActionDownloadError { ref inner, .. } => &**inner,
1099 _ => return None,
1100 })
1101 }
1102
1103 pub(crate) fn error<'a>(self, context: &impl ContextResolver<'a>) -> RunnerError {
1104 RunnerError {
1105 kind: self,
1106 backtrace: Backtrace::new(),
1107 context: Some(context.error_context()),
1108 }
1109 }
1110}
1111
1112impl fmt::Display for RunnerErrorKind {
1113 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
1114 match self {
1115 RunnerErrorKind::InvalidWorkflowYaml { ref inner } => {
1116 write!(f, "Invalid workflow YAML: {}", inner)
1117 }
1118 RunnerErrorKind::TypeMismatch {
1119 ref expected,
1120 ref got,
1121 } => {
1122 write!(f, "Expected {}, got {:?}", expected, got)
1123 }
1124 RunnerErrorKind::RequiredFieldMissing {
1125 field_name,
1126 ref got,
1127 } => {
1128 write!(f, "Expected field {}, missing in {:?}", field_name, got)
1129 }
1130 RunnerErrorKind::ExpressionParseError { ref expression } => {
1131 write!(f, "Error parsing expression '{}'", expression)
1132 }
1133 RunnerErrorKind::ExpressionNonString {
1134 ref expression,
1135 ref value,
1136 } => {
1137 write!(
1138 f,
1139 "Expression in string interpolation '{}' is not a string, got {:?}",
1140 expression, value
1141 )
1142 }
1143 RunnerErrorKind::UnsupportedPlatform { ref runs_on } => {
1144 write!(f, "Platform '{}' not supported", runs_on)
1145 }
1146 RunnerErrorKind::BadActionName { ref action } => {
1147 write!(f, "Cannot parse action name '{}'", action)
1148 }
1149 RunnerErrorKind::ActionDownloadError {
1150 ref action,
1151 ref inner,
1152 } => {
1153 write!(f, "Failed to download action '{}': {}", action, inner)
1154 }
1155 RunnerErrorKind::InvalidActionYaml {
1156 ref action,
1157 ref inner,
1158 } => {
1159 write!(f, "Invalid YAML for action '{}': {}", action, inner)
1160 }
1161 RunnerErrorKind::InvalidEnvironmentVariableName { ref name } => {
1162 write!(f, "Invalid evironment variable name '{}'", name)
1163 }
1164 RunnerErrorKind::UnsupportedActionType {
1165 ref action,
1166 ref using,
1167 } => {
1168 write!(f, "Unsupported type '{}' for action '{}'", using, action)
1169 }
1170 RunnerErrorKind::UnsupportedPre { ref action } => {
1171 write!(f, "'pre' rules not supported for action '{}'", action)
1172 }
1173 RunnerErrorKind::UnsupportedShell { ref shell } => {
1174 write!(f, "Shell '{}' not supported", shell)
1175 }
1176 }
1177 }
1178}
1179
1180#[derive(Debug)]
1182pub struct RunnerError {
1183 kind: RunnerErrorKind,
1184 backtrace: Backtrace,
1185 context: Option<ErrorContext>,
1186}
1187
1188impl RunnerError {
1189 pub fn kind(&self) -> &RunnerErrorKind {
1191 &self.kind
1192 }
1193 pub fn context(&self) -> Option<&ErrorContext> {
1195 self.context.as_ref()
1196 }
1197}
1198
1199impl From<RunnerErrorKind> for RunnerError {
1200 fn from(k: RunnerErrorKind) -> RunnerError {
1201 RunnerError {
1202 kind: k,
1203 backtrace: Backtrace::new(),
1204 context: None,
1205 }
1206 }
1207}
1208
1209impl fmt::Display for RunnerError {
1210 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
1211 if let Some(ctx) = self.context.as_ref() {
1212 write!(f, "{} at {} at {:?}", &self.kind, ctx, &self.backtrace)
1213 } else {
1214 write!(f, "{} at {:?}", &self.kind, &self.backtrace)
1215 }
1216 }
1217}
1218
1219impl Error for RunnerError {
1220 fn source(&self) -> Option<&(dyn Error + 'static)> {
1221 self.kind.source()
1222 }
1223}
1224
1225fn cartesian_product(
1226 keys: &LinkedHashMap<String, Vec<Value>>,
1227) -> Vec<LinkedHashMap<String, Value>> {
1228 fn inner(
1229 v: LinkedHashMap<String, Value>,
1230 mut keys: linked_hash_map::Iter<String, Vec<Value>>,
1231 ret: &mut Vec<LinkedHashMap<String, Value>>,
1232 ) {
1233 if let Some((k, vals)) = keys.next() {
1234 for val in vals {
1235 let mut vv = v.clone();
1236 vv.insert(k.clone(), val.clone());
1237 inner(vv, keys.clone(), ret);
1238 }
1239 } else {
1240 ret.push(v);
1241 }
1242 }
1243 let mut ret = Vec::new();
1244 inner(LinkedHashMap::new(), keys.iter(), &mut ret);
1245 ret
1246}
1247
1248impl Runner {
1249 pub async fn new(ctx: RunnerContext, workflow: &[u8]) -> Result<Runner, RunnerError> {
1253 let mut workflow = Workflow(
1254 serde_yaml::from_slice(workflow)
1255 .map_err(|e| RunnerErrorKind::InvalidWorkflowYaml { inner: e })?,
1256 );
1257 let mut job_descriptions = Vec::new();
1258 let root_context = RootContext::new(&ctx, ErrorContextRoot::Workflow);
1259 let workflow_env = workflow.take_env(&root_context)?;
1260 for (name, mut job) in workflow.take_jobs(&root_context)? {
1261 let job_context = JobContext::new(&ctx);
1262 if let Some(mut strategy) = job.take_strategy(&job_context)? {
1263 if let Some(mut matrix) = strategy.take_matrix(&job_context)? {
1264 let mut values = cartesian_product(&matrix.take_keys(&job_context)?);
1265 values.append(&mut matrix.take_include(&job_context)?);
1266 if !values.is_empty() {
1267 for v in values {
1268 let mut context = JobPostStrategyContext::new(&ctx, &v);
1269 let runs_on = job.clone_runs_on(&context)?;
1270 let name = job.clone_name(&context)?.unwrap_or_else(|| name.clone());
1271 context.set_job_name(name.clone());
1272 let job_env = job.clone_env(&context)?;
1273 let mut derived_name = format!("{} (", name);
1274 let mut first = true;
1275 for val in v.values() {
1276 if let Value::String(ref s) = val {
1277 if first {
1278 first = false;
1279 } else {
1280 derived_name.push_str(", ");
1281 }
1282 derived_name.push_str(&s[..]);
1283 } else {
1284 warn!(
1285 "Non-string matrix value, not sure how this gets rendered"
1286 );
1287 }
1288 }
1289 derived_name.push(')');
1290 let steps = job.clone_steps(&context)?;
1291 job_descriptions.push(JobDescription {
1292 name: derived_name,
1293 runs_on: runs_on,
1294 matrix_values: v,
1295 job_env: job_env,
1296 steps: steps,
1297 });
1298 }
1299 continue;
1300 }
1301 }
1302 }
1303 let empty = LinkedHashMap::new();
1304 let mut context = JobPostStrategyContext::new(&ctx, &empty);
1305 let name = job.clone_name(&context)?.unwrap_or(name);
1306 context.set_job_name(name.clone());
1307 let steps = job.clone_steps(&context)?;
1308 job_descriptions.push(JobDescription {
1309 name: name,
1310 runs_on: job.clone_runs_on(&context)?,
1311 matrix_values: LinkedHashMap::new(),
1312 job_env: job.clone_env(&context)?,
1313 steps: steps,
1314 });
1315 }
1316 let workflow_name = workflow.take_name(&root_context)?;
1317 info!(
1318 "Created Runner for workflow {}",
1319 workflow_name.as_deref().unwrap_or("<unknown>")
1320 );
1321 Ok(Runner {
1322 ctx: ctx,
1323 workflow_env: workflow_env,
1324 workflow_name: workflow_name,
1325 job_descriptions: job_descriptions,
1326 })
1327 }
1328 pub fn job_descriptions(&self) -> &[JobDescription] {
1330 &self.job_descriptions
1331 }
1332 pub async fn job_runner(
1334 &self,
1335 description: &JobDescription,
1336 image_mapping: &DockerImageMapping,
1337 ) -> Result<JobRunner, RunnerError> {
1338 let mut images = Vec::new();
1339 if let Some(image) = description.main_container(image_mapping) {
1340 images.push(image);
1341 } else {
1342 return Err(RunnerErrorKind::UnsupportedPlatform {
1343 runs_on: description
1344 .runs_on
1345 .first()
1346 .cloned()
1347 .unwrap_or_else(|| "<none>".to_string()),
1348 }
1349 .into());
1350 }
1351 let mut env = self.ctx.create_env(self.workflow_name.clone());
1352
1353 env.extend(
1354 self.workflow_env
1355 .iter()
1356 .map(|(k, v)| (k.clone(), v.clone())),
1357 );
1358 env.extend(
1359 description
1360 .job_env
1361 .iter()
1362 .map(|(k, v)| (k.clone(), v.clone())),
1363 );
1364 let steps = description.steps.clone();
1365 Ok(JobRunner {
1366 ctx: self.ctx.clone(),
1367 job_name: description.name().to_string(),
1368 matrix_values: description.matrix_values.clone(),
1369 container_images: images,
1370 steps: steps,
1371 outputs: Vec::new(),
1372 env: env,
1373 paths: Vec::new(),
1374 step_index: StepIndex(0),
1375 })
1376 }
1377}