diff --git a/dsl/extdeps/github/workflow_runs.dag b/dsl/extdeps/github/workflow_runs.dag new file mode 100644 index 00000000000..cb51378d8ce --- /dev/null +++ b/dsl/extdeps/github/workflow_runs.dag @@ -0,0 +1,151 @@ +module extdeps.github.workflow_runs + +import extdeps.github.github { GitHubUser, default_api_base, default_per_page } +import extdeps.github.errors { GitHubErrorShape } +import std.types { CommitSha, Timestamp, Url } +import std.credentials { EnvVar } +import extdeps.external_authority { ExternalAuthority } +import extdeps.uri { Uri, Https } +import extdeps.github.workflow_runs_contracts { + workflow_run_status_wire_contract, + workflow_run_conclusion_wire_contract +} + +data extdeps_external_authority_anchor: ExternalAuthority = ExternalAuthority { + uri: Uri { + scheme: Https + locator: "docs.github.com/en/rest/actions/workflow-runs" + } +} + +type WorkflowRunStatus + = Queued + | InProgress + | Completed + | Waiting + | Requested + | Pending + +type WorkflowRunConclusion + = Success + | Failure + | Cancelled + | Neutral + | Skipped + | TimedOut + | ActionRequired + | StartupFailure + +data dissolve_on_conclusion_authority_consolidation: List = [ + "closure:unify_run_and_check_conclusions|WorkflowRunConclusion (workflow runs REST API) and CheckConclusion (checks REST API, extdeps.github.actions) share 7 of 8 variants: WorkflowRunConclusion adds StartupFailure, CheckConclusion adds Stale. These are genuinely different API shapes today; dissolve when a shared conclusion authority is grounded that subsumes both (GitHub API evolution or a typed union representation)|trigger:GitHub API unifies conclusion values across both APIs, or std gains a tagged-union type allowing a shared core with API-specific extensions" +] + +type WorkflowRun { + id: Int + name: String? + head_sha: CommitSha + status: WorkflowRunStatus + conclusion: WorkflowRunConclusion? + workflow_id: Int + run_number: Int + html_url: Url + created_at: Timestamp + updated_at: Timestamp +} + +type WorkflowRunList { + total_count: Int + workflow_runs: List +} + +fn parse_workflow_run_status(raw: String) -> WorkflowRunStatus? { + match raw { + "queued" => Present { value: Queued } + "in_progress" => Present { value: InProgress } + "completed" => Present { value: Completed } + "waiting" => Present { value: Waiting } + "requested" => Present { value: Requested } + "pending" => Present { value: Pending } + _ => none + } +} + +fn workflow_run_status_wire_label(status: WorkflowRunStatus) -> String { + match status { + Queued => "queued" + InProgress => "in_progress" + Completed => "completed" + Waiting => "waiting" + Requested => "requested" + Pending => "pending" + } +} + +fn parse_workflow_run_conclusion(raw: String) -> WorkflowRunConclusion? { + match raw { + "success" => Present { value: Success } + "failure" => Present { value: Failure } + "cancelled" => Present { value: Cancelled } + "neutral" => Present { value: Neutral } + "skipped" => Present { value: Skipped } + "timed_out" => Present { value: TimedOut } + "action_required" => Present { value: ActionRequired } + "startup_failure" => Present { value: StartupFailure } + _ => none + } +} + +fn workflow_run_conclusion_wire_label(conclusion: WorkflowRunConclusion) -> String { + match conclusion { + Success => "success" + Failure => "failure" + Cancelled => "cancelled" + Neutral => "neutral" + Skipped => "skipped" + TimedOut => "timed_out" + ActionRequired => "action_required" + StartupFailure => "startup_failure" + } +} + +data structural_coverage_gap_workflow_run_codec_hand_rolled: List = [ + "closure:consolidate_variant_string_mapping|parse_workflow_run_status/workflow_run_status_wire_label and parse_workflow_run_conclusion/workflow_run_conclusion_wire_label hand-roll the SnakeCase mapping already declared in the wire contracts. Dissolve when the codec-from-contract bridge derives decode and encode from CoproductWireContract rows directly." +] + +service github.WorkflowRuns { + config { + endpoint: default_api_base + auth: Bearer + auth_source: EnvVar { name: "GITHUB_TOKEN" } + + rate_limit: { requests: 5000, per: hour, scope: core } + + retry: { max_attempts: 3, backoff: exponential, retry_on: [429, 500, 502, 503, 504] } + } + + operation ListForRef { + input { + auth_token: Secret + owner: String + repo: String + head_sha: CommitSha + per_page: Int = default_per_page + } + output { + runs: WorkflowRunList + } + readonly + transport rest { + method: GET, + path: "/repos/\{owner\}/\{repo\}/actions/runs", + query: { head_sha: head_sha, per_page: per_page } + } + response { + 200 => WorkflowRunList + 401 => GitHubErrorShape + 403 => GitHubErrorShape + 404 => GitHubErrorShape + 500 => GitHubErrorShape + } + } +} diff --git a/dsl/extdeps/github/workflow_runs_contracts.dag b/dsl/extdeps/github/workflow_runs_contracts.dag new file mode 100644 index 00000000000..39115abab0f --- /dev/null +++ b/dsl/extdeps/github/workflow_runs_contracts.dag @@ -0,0 +1,22 @@ +module extdeps.github.workflow_runs_contracts + +import extdeps.external_authority { ExternalAuthority } +import extdeps.uri { Uri, Https } +import std.serialization { CoproductWireContract, StringVariant, SnakeCase } + +data extdeps_external_authority_anchor: ExternalAuthority = ExternalAuthority { + uri: Uri { + scheme: Https + locator: "docs.github.com/en/rest/actions/workflow-runs" + } +} + +data workflow_run_status_wire_contract: CoproductWireContract = { + coproduct: "WorkflowRunStatus" + encoding: StringVariant { naming: SnakeCase } +} + +data workflow_run_conclusion_wire_contract: CoproductWireContract = { + coproduct: "WorkflowRunConclusion" + encoding: StringVariant { naming: SnakeCase } +} diff --git a/dsl/test/claim/github_workflow_runs_witness_test.dag b/dsl/test/claim/github_workflow_runs_witness_test.dag new file mode 100644 index 00000000000..203eb19e1e1 --- /dev/null +++ b/dsl/test/claim/github_workflow_runs_witness_test.dag @@ -0,0 +1,176 @@ +module test.claim.github_workflow_runs_witness + +import extdeps.github.workflow_runs { + WorkflowRunStatus, WorkflowRunConclusion, WorkflowRun, WorkflowRunList, + Queued, InProgress, Completed, Waiting, Requested, Pending, + Success, Failure, Cancelled, Neutral, Skipped, TimedOut, ActionRequired, StartupFailure, + parse_workflow_run_status, workflow_run_status_wire_label, + parse_workflow_run_conclusion, workflow_run_conclusion_wire_label +} + +data sample_run: WorkflowRun = WorkflowRun { + id: 12345678 + name: "CI" + head_sha: "abc123def456abc123def456abc123def456abc123" + status: Completed + conclusion: Present { value: Success } + workflow_id: 987 + run_number: 42 + html_url: "https://github.com/owner/repo/actions/runs/12345678" + created_at: "2026-06-23T10:00:00Z" + updated_at: "2026-06-23T10:15:00Z" +} + +data sample_run_list: WorkflowRunList = WorkflowRunList { + total_count: 1 + workflow_runs: [sample_run] +} + +test fn workflow_run_status_wire_label_queued() -> Bool { + workflow_run_status_wire_label(Queued) == "queued" +} + +test fn workflow_run_status_wire_label_in_progress() -> Bool { + workflow_run_status_wire_label(InProgress) == "in_progress" +} + +test fn workflow_run_status_wire_label_completed() -> Bool { + workflow_run_status_wire_label(Completed) == "completed" +} + +test fn workflow_run_status_wire_label_waiting() -> Bool { + workflow_run_status_wire_label(Waiting) == "waiting" +} + +test fn workflow_run_status_wire_label_requested() -> Bool { + workflow_run_status_wire_label(Requested) == "requested" +} + +test fn workflow_run_status_wire_label_pending() -> Bool { + workflow_run_status_wire_label(Pending) == "pending" +} + +test fn parse_workflow_run_status_queued() -> Bool { + match parse_workflow_run_status("queued") { + Present { value: Queued } => true + _ => false + } +} + +test fn parse_workflow_run_status_in_progress() -> Bool { + match parse_workflow_run_status("in_progress") { + Present { value: InProgress } => true + _ => false + } +} + +test fn parse_workflow_run_status_completed() -> Bool { + match parse_workflow_run_status("completed") { + Present { value: Completed } => true + _ => false + } +} + +test fn parse_workflow_run_status_invalid_returns_none() -> Bool { + match parse_workflow_run_status("unknown_status") { + Absent => true + _ => false + } +} + +test fn parse_workflow_run_status_case_sensitive() -> Bool { + match parse_workflow_run_status("Queued") { + Absent => true + _ => false + } +} + +test fn workflow_run_conclusion_wire_label_success() -> Bool { + workflow_run_conclusion_wire_label(Success) == "success" +} + +test fn workflow_run_conclusion_wire_label_failure() -> Bool { + workflow_run_conclusion_wire_label(Failure) == "failure" +} + +test fn workflow_run_conclusion_wire_label_startup_failure() -> Bool { + workflow_run_conclusion_wire_label(StartupFailure) == "startup_failure" +} + +test fn workflow_run_conclusion_wire_label_action_required() -> Bool { + workflow_run_conclusion_wire_label(ActionRequired) == "action_required" +} + +test fn workflow_run_conclusion_wire_label_timed_out() -> Bool { + workflow_run_conclusion_wire_label(TimedOut) == "timed_out" +} + +test fn parse_workflow_run_conclusion_success() -> Bool { + match parse_workflow_run_conclusion("success") { + Present { value: Success } => true + _ => false + } +} + +test fn parse_workflow_run_conclusion_startup_failure() -> Bool { + match parse_workflow_run_conclusion("startup_failure") { + Present { value: StartupFailure } => true + _ => false + } +} + +test fn parse_workflow_run_conclusion_neutral() -> Bool { + match parse_workflow_run_conclusion("neutral") { + Present { value: Neutral } => true + _ => false + } +} + +test fn parse_workflow_run_conclusion_invalid_returns_none() -> Bool { + match parse_workflow_run_conclusion("stale") { + Absent => true + _ => false + } +} + +test fn parse_workflow_run_conclusion_case_sensitive() -> Bool { + match parse_workflow_run_conclusion("Success") { + Absent => true + _ => false + } +} + +test fn workflow_run_status_roundtrip_queued() -> Bool { + (parse_workflow_run_status(workflow_run_status_wire_label(Queued))) == Present { value: Queued } +} + +test fn workflow_run_status_roundtrip_in_progress() -> Bool { + (parse_workflow_run_status(workflow_run_status_wire_label(InProgress))) == Present { value: InProgress } +} + +test fn workflow_run_status_roundtrip_completed() -> Bool { + (parse_workflow_run_status(workflow_run_status_wire_label(Completed))) == Present { value: Completed } +} + +test fn workflow_run_conclusion_roundtrip_all() -> Bool { + let r_success = (parse_workflow_run_conclusion(workflow_run_conclusion_wire_label(Success))) == Present { value: Success } + let r_failure = (parse_workflow_run_conclusion(workflow_run_conclusion_wire_label(Failure))) == Present { value: Failure } + let r_cancelled = (parse_workflow_run_conclusion(workflow_run_conclusion_wire_label(Cancelled))) == Present { value: Cancelled } + let r_neutral = (parse_workflow_run_conclusion(workflow_run_conclusion_wire_label(Neutral))) == Present { value: Neutral } + let r_skipped = (parse_workflow_run_conclusion(workflow_run_conclusion_wire_label(Skipped))) == Present { value: Skipped } + let r_timed_out = (parse_workflow_run_conclusion(workflow_run_conclusion_wire_label(TimedOut))) == Present { value: TimedOut } + let r_action_required = (parse_workflow_run_conclusion(workflow_run_conclusion_wire_label(ActionRequired))) == Present { value: ActionRequired } + let r_startup_failure = (parse_workflow_run_conclusion(workflow_run_conclusion_wire_label(StartupFailure))) == Present { value: StartupFailure } + r_success && r_failure && r_cancelled && r_neutral && r_skipped && r_timed_out && r_action_required && r_startup_failure +} + +test fn sample_run_has_success_conclusion() -> Bool { + match sample_run.conclusion { + Present { value: Success } => true + _ => false + } +} + +test fn sample_run_list_has_one_run() -> Bool { + sample_run_list.total_count == 1 +}