///|
priv struct GitHubActionEnsureResult {
action_root : String?
did_fetch : Bool
errors : Array[String]
}
///|
priv struct GitHubRepoEnsureResult {
repo_root : String?
did_fetch : Bool
errors : Array[String]
}
///|
fn prefetch_parent_dir(path : String) -> String {
let parts : Array[String] = []
for part in path.split("/") {
parts.push(part.to_owned())
}
if parts.length() <= 1 {
return "."
}
let dirs : Array[String] = []
for idx in 0..<(parts.length() - 1) {
dirs.push(parts[idx])
}
let joined = dirs.join("/")
if joined.length() > 0 {
joined
} else if path.has_prefix("/") {
"/"
} else {
"."
}
}
///|
fn prefetch_trim_output(text : String) -> String {
text.trim(chars=" \t\n\r").to_owned()
}
///|
fn github_action_git_bin(git_bin : String?) -> String {
match git_bin {
Some(value) => value
None => @xsys.get_env_var("ACTRUN_GIT_BIN").unwrap_or("git")
}
}
///|
fn github_action_base_url(github_base_url : String?) -> String {
let base = match github_base_url {
Some(value) => value
None =>
@xsys.get_env_var("ACTRUN_GITHUB_BASE_URL").unwrap_or(
"https://github.com",
)
}
let trimmed = base.trim(chars=" \t\n\r").to_owned()
if trimmed.has_suffix("/") {
String::unsafe_substring(trimmed, start=0, end=trimmed.length() - 1)
} else {
trimmed
}
}
///|
fn github_action_repo_url(
owner : String,
repo : String,
github_base_url : String?,
) -> String {
github_action_base_url(github_base_url) + "/" + owner + "/" + repo + ".git"
}
///|
fn prefetch_command_message(stdout : String, stderr : String) -> String {
let stderr_text = prefetch_trim_output(stderr)
if stderr_text.length() > 0 {
return stderr_text
}
let stdout_text = prefetch_trim_output(stdout)
if stdout_text.length() > 0 {
stdout_text
} else {
"unknown error"
}
}
///|
async fn prefetch_mkdir_p(path : String) -> Bool {
if path.length() == 0 || path == "." {
return true
}
let (code, _, _) = run_command("mkdir", ["-p", path], cwd=".")
code == 0
}
///|
fn is_commit_sha(version : String) -> Bool {
if version.length() < 7 {
return false
}
for c in version {
if !((c >= '0' && c <= '9') ||
(c >= 'a' && c <= 'f') ||
(c >= 'A' && c <= 'F')) {
return false
}
}
true
}
///|
async fn prefetch_ensure_cached_github_repo_root(
action_ref : ActionRef,
git_bin : String?,
github_base_url : String?,
) -> GitHubRepoEnsureResult {
match github_action_cache_layout(action_ref) {
None => { repo_root: None, did_fetch: false, errors: [] }
Some(layout) =>
if @xfs.path_exists(layout.repo_root) {
{ repo_root: Some(layout.repo_root), did_fetch: false, errors: [] }
} else {
let parent_dir = prefetch_parent_dir(layout.repo_root)
guard prefetch_mkdir_p(parent_dir) else {
return {
repo_root: None,
did_fetch: false,
errors: [
"action '\{layout.uses}' failed to create cache dir '\{parent_dir}'",
],
}
}
let clone_url = github_action_repo_url(
layout.owner,
layout.repo,
github_base_url,
)
if is_commit_sha(layout.version) {
let (init_code, init_stdout, init_stderr) = run_command(
github_action_git_bin(git_bin),
["init", layout.repo_root],
cwd=".",
)
if init_code != 0 {
return {
repo_root: None,
did_fetch: false,
errors: [
"action '\{layout.uses}' git init failed: " +
prefetch_command_message(init_stdout, init_stderr),
],
}
}
let (fetch_code, fetch_stdout, fetch_stderr) = run_command(
github_action_git_bin(git_bin),
["fetch", "--depth", "1", clone_url, layout.version],
cwd=layout.repo_root,
)
if fetch_code != 0 {
return {
repo_root: None,
did_fetch: false,
errors: [
"action '\{layout.uses}' git fetch failed: " +
prefetch_command_message(fetch_stdout, fetch_stderr),
],
}
}
let (checkout_code, checkout_stdout, checkout_stderr) = run_command(
github_action_git_bin(git_bin),
["checkout", "FETCH_HEAD"],
cwd=layout.repo_root,
)
if checkout_code != 0 {
return {
repo_root: None,
did_fetch: false,
errors: [
"action '\{layout.uses}' git checkout failed: " +
prefetch_command_message(checkout_stdout, checkout_stderr),
],
}
}
} else {
let (code, stdout, stderr) = run_command(
github_action_git_bin(git_bin),
[
"clone",
"--depth",
"1",
"--branch",
layout.version,
clone_url,
layout.repo_root,
],
cwd=".",
)
if code != 0 {
return {
repo_root: None,
did_fetch: false,
errors: [
"action '\{layout.uses}' git clone failed: " +
prefetch_command_message(stdout, stderr),
],
}
}
}
{ repo_root: Some(layout.repo_root), did_fetch: true, errors: [] }
}
}
}
///|
async fn prefetch_ensure_cached_github_action_root(
action_ref : ActionRef,
git_bin : String?,
github_base_url : String?,
) -> GitHubActionEnsureResult {
let repo_result = prefetch_ensure_cached_github_repo_root(
action_ref, git_bin, github_base_url,
)
match github_action_cache_layout(action_ref) {
None => { action_root: None, did_fetch: false, errors: [] }
Some(layout) =>
if repo_result.errors.length() > 0 {
{
action_root: None,
did_fetch: repo_result.did_fetch,
errors: repo_result.errors,
}
} else if has_action_manifest(layout.action_root) {
{
action_root: Some(layout.action_root),
did_fetch: repo_result.did_fetch,
errors: [],
}
} else if @xfs.path_exists(layout.repo_root) {
{
action_root: None,
did_fetch: repo_result.did_fetch,
errors: [
"action '\{layout.uses}' cache exists at '\{layout.repo_root}' but '\{layout.action_root}' is missing action.yml or action.yaml",
],
}
} else {
{
action_root: None,
did_fetch: repo_result.did_fetch,
errors: [
"action '\{layout.uses}' fetched repo cache but '\{layout.action_root}' is missing action.yml or action.yaml",
],
}
}
}
}
///|
fn prefetch_remote_reusable_workflow_ref(path : String) -> ActionRef? {
let parsed = parse_action_ref(path)
guard parsed.action is Some(action_ref) else { return None }
match action_ref {
GitHubRepo(_, _, version, Some(subpath)) =>
if version.length() > 0 && subpath.length() > 0 {
Some(action_ref)
} else {
None
}
_ => None
}
}
///|
async fn prefetch_action_uses(
uses : String,
workspace_root : String,
current_action_root : String?,
git_bin : String?,
github_base_url : String?,
visited : Map[String, Bool],
fetched : Array[String],
errors : Array[String],
) -> Unit {
let parsed = parse_action_ref(uses)
guard parsed.action is Some(action_ref) else { return }
match action_ref {
LocalPath(path) => {
let resolved_root = match current_action_root {
Some(root) => normalize_local_action_path(join_path(root, path))
None => path
}
let visit_key = "local:" + resolved_root
if visited.get(visit_key) is Some(_) {
return
}
visited[visit_key] = true
guard find_local_action_manifest(workspace_root, resolved_root)
is Some((_, manifest_text)) else {
return
}
let parsed_action = parse_local_action_yaml(manifest_text)
guard parsed_action.action is Some(action) else { return }
for step in action.steps {
if step.uses is Some(nested_uses) {
prefetch_action_uses(
nested_uses,
workspace_root,
Some(resolved_root),
git_bin,
github_base_url,
visited,
fetched,
errors,
)
}
}
}
GitHubRepo(_, _, _, _) => {
if resolve_action_ref(action_ref).action is Some(_) {
return
}
match github_action_cache_layout(action_ref) {
Some(layout) => {
let visit_key = "github:" + layout.action_root
if visited.get(visit_key) is Some(_) {
return
}
visited[visit_key] = true
let ensured = prefetch_ensure_cached_github_action_root(
action_ref, git_bin, github_base_url,
)
for err in ensured.errors {
errors.push(err)
}
if ensured.did_fetch {
fetched.push(layout.uses)
}
guard ensured.action_root is Some(action_root) else { return }
guard find_local_action_manifest(workspace_root, action_root)
is Some((_, manifest_text)) else {
return
}
let parsed_action = parse_local_action_yaml(manifest_text)
guard parsed_action.action is Some(action) else { return }
for step in action.steps {
if step.uses is Some(nested_uses) {
prefetch_action_uses(
nested_uses,
workspace_root,
Some(action_root),
git_bin,
github_base_url,
visited,
fetched,
errors,
)
}
}
}
None => ()
}
}
_ => ()
}
}
///|
async fn prefetch_workflow_dependencies(
workflow : WorkflowSpec,
workspace_root : String,
git_bin : String?,
github_base_url : String?,
visited : Map[String, Bool],
fetched : Array[String],
errors : Array[String],
) -> Unit {
for job in workflow.jobs {
match job.reusable_workflow {
Some(path) =>
if path.has_prefix("./") {
let workflow_path = join_path(
workspace_root,
normalize_local_action_path(path),
)
let visit_key = "workflow-local:" + workflow_path
if visited.get(visit_key) is None {
visited[visit_key] = true
match read_text(workflow_path) {
Some(text) => {
let parsed = parse_workflow_yaml(text)
for err in parsed.errors {
errors.push("reusable workflow '\{path}' parse error: " + err)
}
if parsed.workflow is Some(callee) {
prefetch_workflow_dependencies(
callee, workspace_root, git_bin, github_base_url, visited, fetched,
errors,
)
}
}
None => ()
}
}
} else {
match prefetch_remote_reusable_workflow_ref(path) {
Some(action_ref) =>
match github_action_cache_layout(action_ref) {
Some(layout) => {
let visit_key = "workflow-github:" + layout.action_root
if visited.get(visit_key) is None {
visited[visit_key] = true
let ensured = prefetch_ensure_cached_github_repo_root(
action_ref, git_bin, github_base_url,
)
for err in ensured.errors {
errors.push(err)
}
if ensured.did_fetch {
fetched.push(layout.uses)
}
guard ensured.repo_root is Some(repo_root) else { () }
match read_text(layout.action_root) {
Some(text) => {
let parsed = parse_workflow_yaml(text)
for err in parsed.errors {
errors.push(
"reusable workflow '\{path}' parse error: " + err,
)
}
if parsed.workflow is Some(callee) {
prefetch_workflow_dependencies(
callee, repo_root, git_bin, github_base_url, visited,
fetched, errors,
)
}
}
None =>
errors.push(
"reusable workflow '\{path}' fetched repo cache but '\{layout.action_root}' is missing",
)
}
}
}
None => ()
}
None => ()
}
}
None => ()
}
for step in job.steps {
if step.uses is Some(uses) {
prefetch_action_uses(
uses,
workspace_root,
None,
git_bin,
github_base_url,
visited,
fetched,
errors,
)
}
}
}
}
///|
pub async fn prefetch_workflow_github_actions_native(
workflow : WorkflowSpec,
workspace_root : String,
git_bin? : String? = None,
github_base_url? : String? = None,
) -> GitHubActionPrefetchResult {
let fetched : Array[String] = []
let errors : Array[String] = []
let visited : Map[String, Bool] = {}
prefetch_workflow_dependencies(
workflow, workspace_root, git_bin, github_base_url, visited, fetched, errors,
)
{ fetched, errors }
}