A workflow can be read on its own: get_workflow (GET /repos/{owner}/{name}/actions/workflows/{workflow}, and get on the MCP workflow tool) names a workflow by its id, file name, path or name and answers with what list_workflows says of it, plus source, its file as written on ref (the default branch unless a branch or tag is given), and the ref read. It needs workflows:read, reads only what the viewer may see, and says which when the branch, tag or file is not there. The OpenAPI document, the CLI's operation table and the MCP reference list it.
15 files+168−20/15 viewed
| 391 | 391 | "GitHub Actions workflows in .g1t/workflows, their runs, and their jobs' logs.", | |
| 392 | 392 | &[ | |
| 393 | 393 | Op::ListWorkflows, | |
| 394 | + | Op::GetWorkflow, | |
| 394 | 395 | Op::ListWorkflowRuns, | |
| 395 | 396 | Op::GetWorkflowRun, | |
| 396 | 397 | Op::GetJobLogs, | |
| ⋯ | |||
| 679 | 680 | Op::ListWebhookDeliveries => "List webhook deliveries", | |
| 680 | 681 | Op::RedeliverWebhook => "Redeliver a webhook delivery", | |
| 681 | 682 | Op::ListWorkflows => "List workflows", | |
| 683 | + | Op::GetWorkflow => "Get a workflow", | |
| 682 | 684 | Op::ListWorkflowRuns => "List workflow runs", | |
| 683 | 685 | Op::GetWorkflowRun => "Get a workflow run", | |
| 684 | 686 | Op::GetJobLogs => "Get a job's log", | |
| 206 | 206 | ListWebhookDeliveries, | |
| 207 | 207 | RedeliverWebhook, | |
| 208 | 208 | ListWorkflows, | |
| 209 | + | GetWorkflow, | |
| 209 | 210 | ListWorkflowRuns, | |
| 210 | 211 | GetWorkflowRun, | |
| 211 | 212 | GetJobLogs, | |
| ⋯ | |||
| 686 | 687 | } | |
| 687 | 688 | ||
| 688 | 689 | impl Op { | |
| 689 | − | pub const ALL: [Op; 353] = [ | |
| 690 | + | pub const ALL: [Op; 354] = [ | |
| 690 | 691 | Op::Whoami, | |
| 691 | 692 | Op::GetWorkspace, | |
| 692 | 693 | Op::CreateWorkspace, | |
| ⋯ | |||
| 783 | 784 | Op::ListWebhookDeliveries, | |
| 784 | 785 | Op::RedeliverWebhook, | |
| 785 | 786 | Op::ListWorkflows, | |
| 787 | + | Op::GetWorkflow, | |
| 786 | 788 | Op::ListWorkflowRuns, | |
| 787 | 789 | Op::GetWorkflowRun, | |
| 788 | 790 | Op::GetJobLogs, | |
| ⋯ | |||
| 1145 | 1147 | Op::ListWebhookDeliveries => "list_webhook_deliveries", | |
| 1146 | 1148 | Op::RedeliverWebhook => "redeliver_webhook", | |
| 1147 | 1149 | Op::ListWorkflows => "list_workflows", | |
| 1150 | + | Op::GetWorkflow => "get_workflow", | |
| 1148 | 1151 | Op::ListWorkflowRuns => "list_workflow_runs", | |
| 1149 | 1152 | Op::GetWorkflowRun => "get_workflow_run", | |
| 1150 | 1153 | Op::GetJobLogs => "get_job_logs", | |
| ⋯ | |||
| 1504 | 1507 | Op::ListWorkflows => { | |
| 1505 | 1508 | "A repository's GitHub Actions workflows, read from .g1t/workflows (GitHub's format, so a repository moves by renaming .github to .g1t) on its default branch: the events that start each, whether it is on, any problem with its file, notes on anything that runs differently on g1t, its manual-run inputs, and its last run." | |
| 1506 | 1509 | } | |
| 1510 | + | Op::GetWorkflow => { | |
| 1511 | + | "One workflow, named by its id, its file name (ci.yml), its path or its name (CI): what list_workflows says of it, with `source`, its file as written on `ref` (the default branch unless you give a branch or tag), and `ref`, the branch or tag read." | |
| 1512 | + | } | |
| 1507 | 1513 | Op::ListWorkflowRuns => { | |
| 1508 | 1514 | "A repository's workflow runs, newest first: of one workflow (its id or file name), a branch, an event, a pull request's number, or a commit." | |
| 1509 | 1515 | } | |
| ⋯ | |||
| 2470 | 2476 | ), | |
| 2471 | 2477 | Op::ListWebhooks => object(hook_owner(json!({})), &[]), | |
| 2472 | 2478 | Op::ListWorkflows => repo_only(), | |
| 2479 | + | Op::GetWorkflow => object( | |
| 2480 | + | json!({ | |
| 2481 | + | "repo": repo_schema(), | |
| 2482 | + | "workflow": { "type": "string", "description": "The workflow's id, file name (ci.yml), path or name." }, | |
| 2483 | + | "ref": { "type": "string", "description": "A branch or tag to read its file on. The default branch if not given." }, | |
| 2484 | + | }), | |
| 2485 | + | &["repo", "workflow"], | |
| 2486 | + | ), | |
| 2473 | 2487 | Op::ListWorkflowRuns => object( | |
| 2474 | 2488 | json!({ | |
| 2475 | 2489 | "repo": repo_schema(), | |
| ⋯ | |||
| 4503 | 4517 | .await | |
| 4504 | 4518 | } | |
| 4505 | 4519 | Op::ListWorkflows => pass(actions, "workflows", &json!({ "repo": repo, "viewer": viewer })).await, | |
| 4520 | + | Op::GetWorkflow => { | |
| 4521 | + | pass( | |
| 4522 | + | actions, | |
| 4523 | + | "workflow", | |
| 4524 | + | &json!({ "repo": repo, "viewer": viewer, "workflow": text(input, "workflow"), "ref": optional_text(input, "ref") }), | |
| 4525 | + | ) | |
| 4526 | + | .await | |
| 4527 | + | } | |
| 4506 | 4528 | Op::ListWorkflowRuns => { | |
| 4507 | 4529 | pass( | |
| 4508 | 4530 | actions, | |
| 2291 | 2291 | ], | |
| 2292 | 2292 | "notes": "`state` is `active` or `disabled`. `dispatch` holds the `workflow_dispatch` inputs, or is `null` when the workflow cannot be run by hand. See [GitHub Actions](/guides/actions/)." | |
| 2293 | 2293 | }, | |
| 2294 | + | "get_workflow": { | |
| 2295 | + | "params": { | |
| 2296 | + | "workflow": "release.yml" | |
| 2297 | + | }, | |
| 2298 | + | "query": { | |
| 2299 | + | "ref": "main" | |
| 2300 | + | }, | |
| 2301 | + | "response": { | |
| 2302 | + | "id": "wfl_01kpw2c5d8e1f4g7h0j3k6m9nr", | |
| 2303 | + | "path": ".g1t/workflows/release.yml", | |
| 2304 | + | "name": "Release", | |
| 2305 | + | "events": [ | |
| 2306 | + | "workflow_dispatch" | |
| 2307 | + | ], | |
| 2308 | + | "state": "active", | |
| 2309 | + | "error": null, | |
| 2310 | + | "notes": [], | |
| 2311 | + | "dispatch": { | |
| 2312 | + | "channel": { | |
| 2313 | + | "type": "choice", | |
| 2314 | + | "options": [ | |
| 2315 | + | "beta", | |
| 2316 | + | "stable" | |
| 2317 | + | ], | |
| 2318 | + | "default": "beta" | |
| 2319 | + | } | |
| 2320 | + | }, | |
| 2321 | + | "last_run": null, | |
| 2322 | + | "ref": "main", | |
| 2323 | + | "source": "name: Release\non:\n workflow_dispatch:\n inputs:\n channel:\n type: choice\n options: [beta, stable]\n default: beta\njobs:\n release:\n runs-on: ubuntu-latest\n steps:\n - uses: actions/checkout@v4\n - run: ./scripts/release.sh ${{ inputs.channel }}\n" | |
| 2324 | + | }, | |
| 2325 | + | "notes": "`workflow` is the workflow's id, file name, path or name. `source` is its file as written on `ref`; `404` when the file is not on that branch or tag." | |
| 2326 | + | }, | |
| 2294 | 2327 | "list_workflow_runs": { | |
| 2295 | 2328 | "query": { | |
| 2296 | 2329 | "workflow": "ci.yml", |
| 190 | 190 | through::<work::Pull>(op, sent) | |
| 191 | 191 | } | |
| 192 | 192 | Op::ListWorkflows => through::<Vec<actions::Workflow>>(op, sent), | |
| 193 | + | Op::GetWorkflow => through::<actions::WorkflowDetail>(op, sent), | |
| 193 | 194 | Op::ListWorkflowRuns => through::<Vec<actions::WorkflowRun>>(op, sent), | |
| 194 | 195 | Op::GetWorkflowRun => through::<actions::RunDetail>(op, sent), | |
| 195 | 196 | Op::GetJobLogs => through::<actions::JobLog>(op, sent), |
| 712 | 712 | ), | |
| 713 | 713 | route( | |
| 714 | 714 | "GET", | |
| 715 | + | "/repos/:owner/:name/actions/workflows/:workflow", | |
| 716 | + | Op::GetWorkflow, | |
| 717 | + | &[("ref", "ref")], | |
| 718 | + | ), | |
| 719 | + | route( | |
| 720 | + | "GET", | |
| 715 | 721 | "/repos/:owner/:name/actions/workflows/:workflow/runs", | |
| 716 | 722 | Op::ListWorkflowRuns, | |
| 717 | 723 | &[("branch", "branch"), ("event", "event"), ("per_page", "limit")], |
| 194 | 194 | default_action: None, | |
| 195 | 195 | actions: &[ | |
| 196 | 196 | a("list", Op::ListWorkflows, "Workflows on the default branch"), | |
| 197 | + | a("get", Op::GetWorkflow, "One workflow by id, file or name, with its file on a branch or tag"), | |
| 197 | 198 | a("list_runs", Op::ListWorkflowRuns, "Runs, newest first"), | |
| 198 | 199 | a("get_run", Op::GetWorkflowRun, "One run with its jobs and steps; an earlier attempt with attempt"), | |
| 199 | 200 | a("job_logs", Op::GetJobLogs, "A job's log after a sequence number"), |
| 392 | 392 | | Action | What it does | Required | Scope | | |
| 393 | 393 | | --- | --- | --- | --- | | |
| 394 | 394 | | [`list`](/reference/api/actions/list-workflows/) | The workflows, with their events, state, problems, notes on what runs differently, manual-run inputs and last run. | `repo` | `workflows:read` | | |
| 395 | + | | [`get`](/reference/api/actions/get-workflow/) | One workflow by its id, file name, path or name, as `list` gives it, with `source`, its file on `ref` (the default branch unless given). | `repo`, `workflow` | `workflows:read` | | |
| 395 | 396 | | [`list_runs`](/reference/api/actions/list-runs-of-workflow/) | Runs, newest first; filter by `workflow`, `branch`, `event`, `pull` or `sha`. | `repo` | `workflows:read` | | |
| 396 | 397 | | [`get_run`](/reference/api/actions/get-workflow-run/) | A run with its jobs, their steps and annotations, and its `attempts`; `attempt` reads an earlier attempt, with the jobs it had then. | `repo`, `id` | `workflows:read` | | |
| 397 | 398 | | [`job_logs`](/reference/api/actions/get-job-logs/) | A job's log after `after`; `done` says if more will come. | `repo`, `job` | `workflows:read` | |
Binary or large file; its contents are not shown.
| 629 | 629 | pub viewer: Viewer, | |
| 630 | 630 | } | |
| 631 | 631 | ||
| 632 | + | /// `workflow`: one workflow, named by its id (`wfl_…`), its file name | |
| 633 | + | /// (`ci.yml`), its path or its name, with its file as of `ref` (the | |
| 634 | + | /// default branch when absent). Returns `Outcome<WorkflowDetail>`. | |
| 635 | + | #[derive(Debug, Serialize, Deserialize)] | |
| 636 | + | pub struct WorkflowArgs { | |
| 637 | + | pub repo: RepoPath, | |
| 638 | + | pub viewer: Viewer, | |
| 639 | + | pub workflow: String, | |
| 640 | + | #[serde(default, rename = "ref")] | |
| 641 | + | pub git_ref: Option<String>, | |
| 642 | + | } | |
| 643 | + | ||
| 644 | + | /// A workflow with its file. | |
| 645 | + | #[derive(Clone, Debug, Serialize, Deserialize)] | |
| 646 | + | #[serde(rename_all = "camelCase")] | |
| 647 | + | pub struct WorkflowDetail { | |
| 648 | + | #[serde(flatten)] | |
| 649 | + | pub workflow: Workflow, | |
| 650 | + | /// The branch or tag the file was read from. | |
| 651 | + | #[serde(rename = "ref")] | |
| 652 | + | pub git_ref: String, | |
| 653 | + | /// The file, as written on `ref`. | |
| 654 | + | pub source: String, | |
| 655 | + | } | |
| 656 | + | ||
| 632 | 657 | /// `runs`: newest first. Returns `Outcome<Vec<WorkflowRun>>`. | |
| 633 | 658 | #[derive(Debug, Serialize, Deserialize)] | |
| 634 | 659 | pub struct RunsArgs { |
| 282 | 282 | "get_context", | |
| 283 | 283 | "search", | |
| 284 | 284 | "list_workflows", | |
| 285 | + | "get_workflow", | |
| 285 | 286 | "list_workflow_runs", | |
| 286 | 287 | "get_workflow_run", | |
| 287 | 288 | "get_job_logs", |
| 1122 | 1122 | ("remove_requested_reviewers", Scope::PullRequestsWrite), | |
| 1123 | 1123 | // Workflows. | |
| 1124 | 1124 | ("list_workflows", Scope::WorkflowsRead), | |
| 1125 | + | ("get_workflow", Scope::WorkflowsRead), | |
| 1125 | 1126 | ("list_workflow_runs", Scope::WorkflowsRead), | |
| 1126 | 1127 | ("get_workflow_run", Scope::WorkflowsRead), | |
| 1127 | 1128 | ("get_job_logs", Scope::WorkflowsRead), |
| 474 | 474 | body: &[], | |
| 475 | 475 | }, | |
| 476 | 476 | Operation { | |
| 477 | + | id: "get_workflow", | |
| 478 | + | method: "GET", | |
| 479 | + | path: "/repos/{owner}/{name}/actions/workflows/{workflow}", | |
| 480 | + | tag: "Actions", | |
| 481 | + | summary: "Get a workflow", | |
| 482 | + | scope: Some("workflows:read"), | |
| 483 | + | params: &[ | |
| 484 | + | Param { name: "owner", location: In::Path, kind: "string", required: true, values: &[] }, | |
| 485 | + | Param { name: "name", location: In::Path, kind: "string", required: true, values: &[] }, | |
| 486 | + | Param { name: "workflow", location: In::Path, kind: "string", required: true, values: &[] }, | |
| 487 | + | Param { name: "ref", location: In::Query, kind: "string", required: false, values: &[] }, | |
| 488 | + | ], | |
| 489 | + | body: &[], | |
| 490 | + | }, | |
| 491 | + | Operation { | |
| 477 | 492 | id: "get_workflow_run", | |
| 478 | 493 | method: "GET", | |
| 479 | 494 | path: "/repos/{owner}/{name}/actions/runs/{id}", |
| 489 | 489 | ["request_reviewers", "pull_requests:write"], | |
| 490 | 490 | ["remove_requested_reviewers", "pull_requests:write"], | |
| 491 | 491 | ["list_workflows", "workflows:read"], | |
| 492 | + | ["get_workflow", "workflows:read"], | |
| 492 | 493 | ["list_workflow_runs", "workflows:read"], | |
| 493 | 494 | ["get_workflow_run", "workflows:read"], | |
| 494 | 495 | ["get_job_logs", "workflows:read"], |
| 234 | 234 | let service = Actions::new(&env)?; | |
| 235 | 235 | match method.as_str() { | |
| 236 | 236 | "workflows" => reply(&service.workflows(args(body)?).await?), | |
| 237 | + | "workflow" => reply(&service.workflow(args(body)?).await?), | |
| 237 | 238 | "runs" => reply(&service.runs(args(body)?).await?), | |
| 238 | 239 | "run" => reply(&service.run(args(body)?).await?), | |
| 239 | 240 | "logs" => reply(&service.logs(args(body)?).await?), |
| 6 | 6 | use g1t_contracts::checks::{ActionsChecksArgs, MAX_COMMITS}; | |
| 7 | 7 | use g1t_contracts::actions::{ | |
| 8 | 8 | Annotation, Job, JobLog, JobLogText, JobLogTextArgs, JobSummary, LogChunk, LogsArgs, MAX_RUN_LOG_BYTES, RunArgs, RunAttempt, RunDetail, | |
| 9 | − | RunLogsArgs, RunsArgs, SetWorkflowEnabledArgs, StepState, StepSummary, SummariesArgs, Workflow, WorkflowNote, WorkflowRun, WorkflowsArgs, | |
| 9 | + | RunLogsArgs, RunsArgs, SetWorkflowEnabledArgs, StepState, StepSummary, SummariesArgs, Workflow, WorkflowArgs, WorkflowDetail, WorkflowNote, WorkflowRun, | |
| 10 | + | WorkflowsArgs, | |
| 10 | 11 | }; | |
| 11 | 12 | use g1t_contracts::{FailureCode, Outcome}; | |
| 12 | 13 | use serde::Deserialize; | |
| ⋯ | |||
| 125 | 126 | } | |
| 126 | 127 | } | |
| 127 | 128 | ||
| 129 | + | /// The workflow `wanted` names: its id, its path, its file name, or its | |
| 130 | + | /// name (any case), in that order of preference. | |
| 131 | + | fn pick_workflow<'a>(rows: &'a [WorkflowRow], wanted: &str) -> Option<&'a WorkflowRow> { | |
| 132 | + | let wanted = wanted.trim(); | |
| 133 | + | let file = wanted.trim_start_matches(".g1t/workflows/"); | |
| 134 | + | rows.iter() | |
| 135 | + | .find(|row| row.id == wanted || row.path == wanted) | |
| 136 | + | .or_else(|| rows.iter().find(|row| row.path == format!("{}/{file}", workflow::FOLDER))) | |
| 137 | + | .or_else(|| rows.iter().find(|row| row.path.rsplit('/').next() == Some(file))) | |
| 138 | + | .or_else(|| rows.iter().find(|row| row.name.eq_ignore_ascii_case(wanted))) | |
| 139 | + | } | |
| 140 | + | ||
| 128 | 141 | impl Actions { | |
| 129 | 142 | async fn summary(&self, row: &WorkflowRow) -> Result<Workflow> { | |
| 130 | 143 | let last_run = self | |
| ⋯ | |||
| 194 | 207 | Ok(Outcome::Ok(out)) | |
| 195 | 208 | } | |
| 196 | 209 | ||
| 210 | + | /// `workflow`: one workflow, by its id, file name, path or name, with its | |
| 211 | + | /// file as of a branch or tag. | |
| 212 | + | pub async fn workflow(&self, a: WorkflowArgs) -> Result<Outcome<WorkflowDetail>> { | |
| 213 | + | let Some(repo) = self.visible_repo(&a.repo, &a.viewer).await? else { | |
| 214 | + | return Ok(fail(FailureCode::NotFound, "There is no such repository.")); | |
| 215 | + | }; | |
| 216 | + | let ws = self.workspace_actor(&repo.namespace).await?; | |
| 217 | + | if !self.synced(&repo.id).await? | |
| 218 | + | && let Some(ws) = &ws | |
| 219 | + | { | |
| 220 | + | self.sync(&repo, ws).await?; | |
| 221 | + | } | |
| 222 | + | let rows = self | |
| 223 | + | .db | |
| 224 | + | .prepare("SELECT * FROM workflows WHERE repo_id = ? ORDER BY name") | |
| 225 | + | .bind(&[repo.id.as_str().into()])? | |
| 226 | + | .all() | |
| 227 | + | .await? | |
| 228 | + | .results::<WorkflowRow>()?; | |
| 229 | + | let Some(row) = pick_workflow(&rows, &a.workflow) else { | |
| 230 | + | return Ok(fail(FailureCode::NotFound, format!("There is no workflow {} in {}/{}.", a.workflow.trim(), repo.namespace, repo.name))); | |
| 231 | + | }; | |
| 232 | + | let workflow = self.summary(row).await?; | |
| 233 | + | let (git_ref, source) = match a.git_ref.as_deref().map(str::trim).filter(|r| !r.is_empty()) { | |
| 234 | + | None => (repo.default_branch.clone(), row.source.clone()), | |
| 235 | + | Some(wanted) => { | |
| 236 | + | let Some(ws) = &ws else { | |
| 237 | + | return Ok(fail(FailureCode::NotFound, "There is no such workspace.")); | |
| 238 | + | }; | |
| 239 | + | let short = wanted.trim_start_matches("refs/heads/").trim_start_matches("refs/tags/"); | |
| 240 | + | let read = self.read_for(&repo, ws, Some(short)).await?; | |
| 241 | + | if read.head.is_none() { | |
| 242 | + | return Ok(fail(FailureCode::NotFound, format!("There is no branch or tag called {short}."))); | |
| 243 | + | } | |
| 244 | + | let Some(file) = read.files.into_iter().find(|file| file.path == row.path) else { | |
| 245 | + | return Ok(fail(FailureCode::NotFound, format!("{} is not on {short}.", row.path))); | |
| 246 | + | }; | |
| 247 | + | (short.to_owned(), file.source) | |
| 248 | + | } | |
| 249 | + | }; | |
| 250 | + | Ok(Outcome::Ok(WorkflowDetail { workflow, git_ref, source })) | |
| 251 | + | } | |
| 252 | + | ||
| 197 | 253 | pub async fn runs(&self, a: RunsArgs) -> Result<Outcome<Vec<WorkflowRun>>> { | |
| 198 | 254 | let Some(repo) = self.visible_repo(&a.repo, &a.viewer).await? else { | |
| 199 | 255 | return Ok(fail(FailureCode::NotFound, "There is no such repository.")); | |
Comments
Sign in to comment.