///|
/// Static features extracted from one GitHub Actions workflow.
pub(all) struct WorkflowFacts {
path : String
name : String?
triggers : Array[String]
permissions : Array[String]
commands : Array[String]
actions : Array[String]
jobs : Array[String]
has_moon_setup : Bool
has_moon_check : Bool
has_moon_build : Bool
has_moon_test : Bool
has_moon_package : Bool
has_artifact_upload : Bool
} derive(Debug, Eq)
///|
pub fn WorkflowFacts::new(path : String) -> WorkflowFacts {
{
path,
name: None,
triggers: [],
permissions: [],
commands: [],
actions: [],
jobs: [],
has_moon_setup: false,
has_moon_check: false,
has_moon_build: false,
has_moon_test: false,
has_moon_package: false,
has_artifact_upload: false,
}
}
///|
pub fn WorkflowFacts::summary(self : WorkflowFacts) -> String {
"\{self.jobs.length()} jobs, \{self.commands.length()} commands, moon check=\{self.has_moon_check}, moon test=\{self.has_moon_test}"
}
///|
fn yaml_value(line : String) -> String? {
let parts = line.split(":").to_array()
if parts.length() < 2 {
None
} else {
let values : Array[StringView] = []
for index in 1.. String? {
let trimmed = line.trim().to_owned()
let parts = trimmed.split(":").to_array()
if parts.length() == 0 {
None
} else {
Some(parts[0].trim().to_owned())
}
}
///|
fn indentation(line : String) -> Int {
let mut count = 0
for character in line {
if character == ' ' {
count += 1
} else {
break
}
}
count
}
///|
fn record_command(facts : WorkflowFacts, command : String) -> WorkflowFacts {
let commands = facts.commands
commands.push(command)
{
path: facts.path,
name: facts.name,
triggers: facts.triggers,
permissions: facts.permissions,
commands,
actions: facts.actions,
jobs: facts.jobs,
has_moon_setup: facts.has_moon_setup ||
command.contains("cli.moonbitlang.com"),
has_moon_check: facts.has_moon_check || command.contains("moon check"),
has_moon_build: facts.has_moon_build || command.contains("moon build"),
has_moon_test: facts.has_moon_test || command.contains("moon test"),
has_moon_package: facts.has_moon_package || command.contains("moon package"),
has_artifact_upload: facts.has_artifact_upload ||
command.contains("upload-artifact"),
}
}
///|
fn record_action(facts : WorkflowFacts, action : String) -> WorkflowFacts {
let actions = facts.actions
actions.push(action)
{
path: facts.path,
name: facts.name,
triggers: facts.triggers,
permissions: facts.permissions,
commands: facts.commands,
actions,
jobs: facts.jobs,
has_moon_setup: facts.has_moon_setup,
has_moon_check: facts.has_moon_check,
has_moon_build: facts.has_moon_build,
has_moon_test: facts.has_moon_test,
has_moon_package: facts.has_moon_package,
has_artifact_upload: facts.has_artifact_upload ||
action.contains("actions/upload-artifact"),
}
}
///|
fn add_trigger(facts : WorkflowFacts, trigger : String) -> WorkflowFacts {
let triggers = facts.triggers
if !triggers.contains(trigger) {
triggers.push(trigger)
}
{
path: facts.path,
name: facts.name,
triggers,
permissions: facts.permissions,
commands: facts.commands,
actions: facts.actions,
jobs: facts.jobs,
has_moon_setup: facts.has_moon_setup,
has_moon_check: facts.has_moon_check,
has_moon_build: facts.has_moon_build,
has_moon_test: facts.has_moon_test,
has_moon_package: facts.has_moon_package,
has_artifact_upload: facts.has_artifact_upload,
}
}
///|
/// Extract release-relevant YAML facts without implementing a general YAML parser.
pub fn analyze_workflow(file : ProjectFile) -> WorkflowFacts {
let mut facts = WorkflowFacts::new(file.path)
let mut in_run = false
let mut run_indent = 0
let mut in_jobs = false
for raw in file.content.split("\n") {
let line = raw.to_owned()
let trimmed = line.trim().to_owned()
if trimmed.length() == 0 || trimmed.has_prefix("#") {
continue
}
let indent = indentation(line)
if in_run && indent > run_indent {
facts = record_command(facts, trimmed)
continue
}
in_run = false
if trimmed == "on:" {
facts = add_trigger(facts, "configured")
}
if trimmed.has_prefix("- uses:") || trimmed.has_prefix("uses:") {
match yaml_value(trimmed) {
Some(action) => facts = record_action(facts, action)
None => ()
}
}
if trimmed.has_prefix("run:") {
match yaml_value(trimmed) {
Some(command) => {
facts = record_command(facts, command)
in_run = command == "|"
run_indent = indent
}
None => ()
}
}
if trimmed == "jobs:" {
in_jobs = true
continue
}
if in_jobs && indent == 2 && trimmed.has_suffix(":") {
match yaml_key(trimmed) {
Some(job) => facts.jobs.push(job)
None => ()
}
}
if trimmed.has_prefix("permissions:") {
facts.permissions.push("configured")
}
}
facts
}
///|
pub fn analyze_workflows(inventory : ProjectInventory) -> Array[WorkflowFacts] {
let workflows : Array[WorkflowFacts] = []
for file in inventory.files_under(".github/workflows") {
if file.path.has_suffix(".yml") || file.path.has_suffix(".yaml") {
workflows.push(analyze_workflow(file))
}
}
workflows
}
///|
pub fn workflow_release_report(inventory : ProjectInventory) -> String {
let builder = StringBuilder()
for workflow in analyze_workflows(inventory) {
builder.write_string(workflow.path + ": " + workflow.summary() + "\n")
}
builder.to_string()
}