Merge Actions: cross-repo workflows and actions, release and deployment triggers, step timeouts
| 693 | 693 | // reporting how it goes. The job's own token is the credential. | |
| 694 | 694 | ("POST", path) if path.starts_with("/actions/jobs/") => { | |
| 695 | 695 | let rest = path.trim_start_matches("/actions/jobs/"); | |
| 696 | − | let (job, method) = match rest.strip_suffix("/spec") { | |
| 697 | − | Some(job) => (job.to_owned(), "job_spec"), | |
| 698 | − | None => (rest.to_owned(), "job_report"), | |
| 696 | + | // `/action`: where to fetch another repository's action from (on | |
| 697 | + | // g1t, with a read token for a private one, or GitHub). | |
| 698 | + | let (job, method) = match (rest.strip_suffix("/spec"), rest.strip_suffix("/action")) { | |
| 699 | + | (Some(job), _) => (job.to_owned(), "job_spec"), | |
| 700 | + | (_, Some(job)) => (job.to_owned(), "job_action"), | |
| 701 | + | _ => (rest.to_owned(), "job_report"), | |
| 699 | 702 | }; | |
| 700 | 703 | let body = json_body(&mut request).await; | |
| 701 | 704 | let answered: Outcome<Value> = g1t_kit::call( | |
| ⋯ | |||
| 806 | 809 | None => reply(&value), | |
| 807 | 810 | } | |
| 808 | 811 | } | |
| 812 | + | // A deleted comment has nothing to say, as GitHub's says nothing. | |
| 813 | + | Outcome::Ok(_) if route.no_content() => Ok(Response::empty()?.with_status(204)), | |
| 809 | 814 | Outcome::Ok(value) => reply(&value), | |
| 810 | 815 | // A token without the scope a call needs is told which one. | |
| 811 | 816 | Outcome::Fail(refused) => match (refused.code, audit::missing_scope(route.op, &viewer, &input)) { | |
| 312 | 312 | Op::AssignIssue, | |
| 313 | 313 | Op::Delegate, | |
| 314 | 314 | Op::AddComment, | |
| 315 | + | Op::EditComment, | |
| 316 | + | Op::DeleteComment, | |
| 315 | 317 | Op::ListIssueLabels, | |
| 316 | 318 | Op::AddIssueLabels, | |
| 317 | 319 | Op::SetIssueLabels, | |
| ⋯ | |||
| 349 | 351 | Op::UpdatePullRequest, | |
| 350 | 352 | Op::GetPullRequestChanges, | |
| 351 | 353 | Op::MarkPullRequestReady, | |
| 354 | + | Op::ConvertPullRequestToDraft, | |
| 352 | 355 | Op::RequestReviewers, | |
| 353 | 356 | Op::RemoveRequestedReviewers, | |
| 354 | 357 | Op::ReviewPullRequest, | |
| 355 | 358 | Op::MergePullRequest, | |
| 356 | 359 | Op::ClosePullRequest, | |
| 360 | + | Op::ReopenPullRequest, | |
| 357 | 361 | Op::GetMergeQueue, | |
| 358 | 362 | Op::MessageAgent, | |
| 359 | 363 | Op::AnswerMessage, | |
| ⋯ | |||
| 450 | 454 | Op::Protection(ProtectionOp::SetWorkflowPermissions), | |
| 451 | 455 | Op::Protection(ProtectionOp::GetForkPrApproval), | |
| 452 | 456 | Op::Protection(ProtectionOp::SetForkPrApproval), | |
| 457 | + | Op::Protection(ProtectionOp::GetActionsAccess), | |
| 458 | + | Op::Protection(ProtectionOp::SetActionsAccess), | |
| 453 | 459 | Op::Protection(ProtectionOp::CreateRepositoryDispatch), | |
| 454 | 460 | Op::Protection(ProtectionOp::GetWorkspaceWorkflowPermissions), | |
| 455 | 461 | Op::Protection(ProtectionOp::SetWorkspaceWorkflowPermissions), | |
| ⋯ | |||
| 596 | 602 | Op::DeleteMilestone => "Delete a milestone", | |
| 597 | 603 | Op::UpdatePullRequest => "Update a pull request", | |
| 598 | 604 | Op::AddComment => "Add a comment", | |
| 605 | + | Op::EditComment => "Edit a comment", | |
| 606 | + | Op::DeleteComment => "Delete a comment", | |
| 599 | 607 | Op::ReviewPullRequest => "Review a pull request", | |
| 600 | 608 | Op::ListPullRequests => "List pull requests", | |
| 601 | 609 | Op::GetPullRequest => "Get a pull request", | |
| ⋯ | |||
| 604 | 612 | Op::ReadSession => "Read a session", | |
| 605 | 613 | Op::MarkPullRequestReady => "Mark a pull request ready", | |
| 606 | 614 | Op::ClosePullRequest => "Close a pull request", | |
| 615 | + | Op::ReopenPullRequest => "Reopen a pull request", | |
| 616 | + | Op::ConvertPullRequestToDraft => "Convert a pull request to a draft", | |
| 607 | 617 | Op::GetPullRequestChanges => "Get a pull request's changes", | |
| 608 | 618 | Op::MergePullRequest => "Merge a pull request", | |
| 609 | 619 | Op::ListEvents => "List repository events", | |
| ⋯ | |||
| 893 | 903 | ||
| 894 | 904 | let id = operation_id(route); | |
| 895 | 905 | let mut responses = Map::new(); | |
| 896 | − | responses.insert( | |
| 897 | − | "200".into(), | |
| 898 | − | json!({ | |
| 899 | − | "description": "Success.", | |
| 900 | − | "content": { "application/json": { "schema": {} } }, | |
| 901 | − | }), | |
| 902 | − | ); | |
| 906 | + | if route.no_content() { | |
| 907 | + | responses.insert("204".into(), json!({ "description": "Success. There is no body." })); | |
| 908 | + | } else { | |
| 909 | + | responses.insert( | |
| 910 | + | "200".into(), | |
| 911 | + | json!({ | |
| 912 | + | "description": "Success.", | |
| 913 | + | "content": { "application/json": { "schema": {} } }, | |
| 914 | + | }), | |
| 915 | + | ); | |
| 916 | + | } | |
| 903 | 917 | responses.insert( | |
| 904 | 918 | "401".into(), | |
| 905 | 919 | error_response("A token is required, or the one sent is not valid."), | |
| ⋯ | |||
| 1207 | 1221 | for (path, methods) in document["paths"].as_object().unwrap() { | |
| 1208 | 1222 | for (method, operation) in methods.as_object().unwrap() { | |
| 1209 | 1223 | known.push(operation["operationId"].as_str().unwrap().to_owned()); | |
| 1224 | + | // Nothing to show for a success without a body. | |
| 1225 | + | if operation["responses"]["204"].is_object() { | |
| 1226 | + | continue; | |
| 1227 | + | } | |
| 1210 | 1228 | let example = &operation["responses"]["200"]["content"]["application/json"]["example"]; | |
| 1211 | 1229 | assert!(!example.is_null(), "{method} {path} has no example response"); | |
| 1212 | 1230 | } | |
| 174 | 174 | UpdateMilestone, | |
| 175 | 175 | DeleteMilestone, | |
| 176 | 176 | AddComment, | |
| 177 | + | EditComment, | |
| 178 | + | DeleteComment, | |
| 177 | 179 | ReviewPullRequest, | |
| 178 | 180 | ListPullRequests, | |
| 179 | 181 | GetPullRequest, | |
| ⋯ | |||
| 182 | 184 | RecordSession, | |
| 183 | 185 | ReadSession, | |
| 184 | 186 | MarkPullRequestReady, | |
| 187 | + | ConvertPullRequestToDraft, | |
| 185 | 188 | ClosePullRequest, | |
| 189 | + | ReopenPullRequest, | |
| 186 | 190 | GetPullRequestChanges, | |
| 187 | 191 | MergePullRequest, | |
| 188 | 192 | ListEvents, | |
| ⋯ | |||
| 433 | 437 | properties | |
| 434 | 438 | } | |
| 435 | 439 | ||
| 440 | + | fn comment_id_schema() -> Value { | |
| 441 | + | json!({ | |
| 442 | + | "type": "string", | |
| 443 | + | "description": "The comment's id, such as \"cmt_01J9Z8\": each comment's id in get_issue or get_pull_request.", | |
| 444 | + | }) | |
| 445 | + | } | |
| 446 | + | ||
| 436 | 447 | fn workspace_schema() -> Value { | |
| 437 | 448 | json!({ "type": "string", "description": "The workspace's slug, e.g. \"flagon-io\"." }) | |
| 438 | 449 | } | |
| ⋯ | |||
| 671 | 682 | } | |
| 672 | 683 | ||
| 673 | 684 | impl Op { | |
| 674 | − | pub const ALL: [Op; 308] = [ | |
| 685 | + | pub const ALL: [Op; 314] = [ | |
| 675 | 686 | Op::Whoami, | |
| 676 | 687 | Op::GetWorkspace, | |
| 677 | 688 | Op::CreateWorkspace, | |
| ⋯ | |||
| 744 | 755 | Op::UpdateMilestone, | |
| 745 | 756 | Op::DeleteMilestone, | |
| 746 | 757 | Op::AddComment, | |
| 758 | + | Op::EditComment, | |
| 759 | + | Op::DeleteComment, | |
| 747 | 760 | Op::ReviewPullRequest, | |
| 748 | 761 | Op::ListPullRequests, | |
| 749 | 762 | Op::GetPullRequest, | |
| ⋯ | |||
| 752 | 765 | Op::RecordSession, | |
| 753 | 766 | Op::ReadSession, | |
| 754 | 767 | Op::MarkPullRequestReady, | |
| 768 | + | Op::ConvertPullRequestToDraft, | |
| 755 | 769 | Op::ClosePullRequest, | |
| 770 | + | Op::ReopenPullRequest, | |
| 756 | 771 | Op::GetPullRequestChanges, | |
| 757 | 772 | Op::MergePullRequest, | |
| 758 | 773 | Op::ListEvents, | |
| ⋯ | |||
| 954 | 969 | Op::Protection(ProtectionOp::SetWorkflowPermissions), | |
| 955 | 970 | Op::Protection(ProtectionOp::GetForkPrApproval), | |
| 956 | 971 | Op::Protection(ProtectionOp::SetForkPrApproval), | |
| 972 | + | Op::Protection(ProtectionOp::GetActionsAccess), | |
| 973 | + | Op::Protection(ProtectionOp::SetActionsAccess), | |
| 957 | 974 | Op::Protection(ProtectionOp::CreateRepositoryDispatch), | |
| 958 | 975 | Op::Protection(ProtectionOp::GetWorkspaceWorkflowPermissions), | |
| 959 | 976 | Op::Protection(ProtectionOp::SetWorkspaceWorkflowPermissions), | |
| ⋯ | |||
| 1061 | 1078 | Op::UpdateMilestone => "update_milestone", | |
| 1062 | 1079 | Op::DeleteMilestone => "delete_milestone", | |
| 1063 | 1080 | Op::AddComment => "add_comment", | |
| 1081 | + | Op::EditComment => "edit_comment", | |
| 1082 | + | Op::DeleteComment => "delete_comment", | |
| 1064 | 1083 | Op::ReviewPullRequest => "review_pull_request", | |
| 1065 | 1084 | Op::ListPullRequests => "list_pull_requests", | |
| 1066 | 1085 | Op::GetPullRequest => "get_pull_request", | |
| ⋯ | |||
| 1070 | 1089 | Op::ReadSession => "read_session", | |
| 1071 | 1090 | Op::MarkPullRequestReady => "mark_pull_request_ready", | |
| 1072 | 1091 | Op::ClosePullRequest => "close_pull_request", | |
| 1092 | + | Op::ReopenPullRequest => "reopen_pull_request", | |
| 1093 | + | Op::ConvertPullRequestToDraft => "convert_pull_request_to_draft", | |
| 1073 | 1094 | Op::GetPullRequestChanges => "get_pull_request_changes", | |
| 1074 | 1095 | Op::MergePullRequest => "merge_pull_request", | |
| 1075 | 1096 | Op::ListEvents => "list_events", | |
| ⋯ | |||
| 1394 | 1415 | Op::AddComment => { | |
| 1395 | 1416 | "Comment on an issue or a pull request. On a pull request, give path and line to comment on one line of the change." | |
| 1396 | 1417 | } | |
| 1418 | + | Op::EditComment => { | |
| 1419 | + | "Change the text of a comment on an issue or a pull request, named by comment_id (the id get_issue and get_pull_request give each comment). Its author may edit it, and so may anyone with the Maintain role or higher. Notes of what happened, such as \"closed this\", cannot be edited. Publishes comment.edited with what it said before." | |
| 1420 | + | } | |
| 1421 | + | Op::DeleteComment => { | |
| 1422 | + | "Delete a comment on an issue or a pull request, named by comment_id. Its author may delete it, and so may anyone with the Maintain role or higher. A review that approved or requested changes cannot be deleted, only edited, and notes of what happened cannot be deleted. This cannot be undone. Publishes comment.deleted with the comment as it was." | |
| 1423 | + | } | |
| 1397 | 1424 | Op::ReviewPullRequest => { | |
| 1398 | 1425 | "Give a verdict on a pull request: approve it, or request changes and say what. Read get_pull_request_changes first. You cannot review a pull request you opened, or one g1t made for you (you are its requested_by)." | |
| 1399 | 1426 | } | |
| ⋯ | |||
| 1407 | 1434 | "Start a change. Opens a draft pull request with its own fork of the repository and returns the fork's git remote. Clone it, commit your work there, push, record your session as you go, then call mark_pull_request_ready. Give the issue it is for whenever there is one. If the change is already on a branch pushed to the repository, give that branch instead: no fork is made and the pull request is ready for review at once. It merges into the default branch unless base names another existing branch; leave base out unless you were asked for another." | |
| 1408 | 1435 | } | |
| 1409 | 1436 | Op::UpdatePullRequest => { | |
| 1410 | − | "Change an open pull request: base, the branch it merges into (an existing branch; needs the Write role or higher); its labels (replacing the set, as set_issue_labels does); its milestone (a number, or null or 0 for none; needs the Triage role); and assignees and reviewers (each replacing the set). Only the fields given change. Its author, or whoever asked g1t for it, may change it; anyone else needs the Triage role or higher. A new base is a pull.base_changed event: it leaves the merge queue, and whether it is behind, merges cleanly and has the checks it needs is worked out against the new base." | |
| 1437 | + | "Change an open pull request: base, the branch it merges into (an existing branch; needs the Write role or higher); its labels (replacing the set, as set_issue_labels does); its milestone (a number, or null or 0 for none; needs the Triage role); and assignees and reviewers (each replacing the set). Only the fields given change. Its author, or whoever asked g1t for it, may change it; anyone else needs the Triage role or higher. A new base is a pull.base_changed event: it leaves the merge queue, and whether it is behind, merges cleanly and has the checks it needs is worked out against the new base. state open reopens a closed pull request, as reopen_pull_request does, before anything else changes; state closed closes it, as close_pull_request does, after." | |
| 1411 | 1438 | } | |
| 1412 | 1439 | Op::RecordSession => { | |
| 1413 | 1440 | "Append entries to a pull request's session: the prompt you were given, your reasoning, the tools you ran. This is how people later see why a change was made, so record as you work, not only at the end." | |
| ⋯ | |||
| 1417 | 1444 | "Mark a draft pull request ready for review. Push your commits first. The summary becomes its description and should say what changed and why." | |
| 1418 | 1445 | } | |
| 1419 | 1446 | Op::ClosePullRequest => "Close a pull request without merging it. Its author may close their own, and whoever asked g1t for one may close that one; anyone else needs the Triage role or higher.", | |
| 1447 | + | Op::ReopenPullRequest => "Reopen a closed pull request. It comes back as the draft it was if it was closed as one, and ready for review otherwise; a merged pull request cannot be reopened, nor one whose branch was deleted. Its author may reopen their own, and whoever asked g1t for one may reopen that one; anyone else needs the Triage role or higher. Publishes pull.reopened with its head commit.", | |
| 1448 | + | Op::ConvertPullRequestToDraft => "Turn a pull request that is ready for review back into a draft. A draft cannot be merged until it is marked ready again; it leaves the merge queue, and a merge waiting for it to catch up is called off. Its author may, and whoever asked g1t for it; anyone else needs the Triage role or higher. Publishes pull.converted_to_draft.", | |
| 1420 | 1449 | Op::GetPullRequestChanges => { | |
| 1421 | 1450 | "What a pull request changes: the files it touches and their line-by-line diff against the commit it started from. Use it to review a pull request or to compare several made for the same issue." | |
| 1422 | 1451 | } | |
| ⋯ | |||
| 2219 | 2248 | | Op::ReopenIssue | |
| 2220 | 2249 | | Op::GetPullRequest | |
| 2221 | 2250 | | Op::ClosePullRequest | |
| 2251 | + | | Op::ReopenPullRequest | |
| 2252 | + | | Op::ConvertPullRequestToDraft | |
| 2222 | 2253 | | Op::GetPullRequestChanges => just_numbered(), | |
| 2223 | 2254 | Op::CreateIssue => object( | |
| 2224 | 2255 | json!({ | |
| ⋯ | |||
| 2352 | 2383 | })), | |
| 2353 | 2384 | &["repo", "number", "body"], | |
| 2354 | 2385 | ), | |
| 2386 | + | Op::EditComment => object( | |
| 2387 | + | json!({ | |
| 2388 | + | "repo": repo_schema(), | |
| 2389 | + | "comment_id": comment_id_schema(), | |
| 2390 | + | "body": { "type": "string", "description": "The new text, in Markdown." }, | |
| 2391 | + | }), | |
| 2392 | + | &["repo", "comment_id", "body"], | |
| 2393 | + | ), | |
| 2394 | + | Op::DeleteComment => object( | |
| 2395 | + | json!({ "repo": repo_schema(), "comment_id": comment_id_schema() }), | |
| 2396 | + | &["repo", "comment_id"], | |
| 2397 | + | ), | |
| 2355 | 2398 | Op::ReviewPullRequest => object( | |
| 2356 | 2399 | numbered(json!({ | |
| 2357 | 2400 | "verdict": { "type": "string", "enum": ["approve", "request_changes"] }, | |
| ⋯ | |||
| 2374 | 2417 | ), | |
| 2375 | 2418 | Op::UpdatePullRequest => object( | |
| 2376 | 2419 | numbered(json!({ | |
| 2420 | + | "state": { | |
| 2421 | + | "type": "string", | |
| 2422 | + | "enum": ["open", "closed"], | |
| 2423 | + | "description": "open reopens it if it is closed (never once merged); closed closes it without merging. Either is left as it is when it already is.", | |
| 2424 | + | }, | |
| 2377 | 2425 | "base": { | |
| 2378 | 2426 | "type": "string", | |
| 2379 | 2427 | "description": "The branch it merges into: an existing branch other than its own. Needs the Write role.", | |
| ⋯ | |||
| 3192 | 3240 | | DeploymentsOp::GetEnvironment | |
| 3193 | 3241 | ) | |
| 3194 | 3242 | | Op::Protection( | |
| 3195 | − | ProtectionOp::GetPendingDeployments | ProtectionOp::GetWorkflowPermissions | ProtectionOp::GetForkPrApproval | |
| 3243 | + | ProtectionOp::GetPendingDeployments | |
| 3244 | + | | ProtectionOp::GetWorkflowPermissions | |
| 3245 | + | | ProtectionOp::GetForkPrApproval | |
| 3246 | + | | ProtectionOp::GetActionsAccess | |
| 3196 | 3247 | ) | |
| 3197 | 3248 | ) | |
| 3198 | 3249 | } | |
| ⋯ | |||
| 4420 | 4471 | .await | |
| 4421 | 4472 | } | |
| 4422 | 4473 | Op::UpdatePullRequest => { | |
| 4423 | − | pass( | |
| 4424 | − | work, | |
| 4425 | − | "update_pull", | |
| 4426 | − | &UpdatePullArgs { | |
| 4427 | − | actor: actor(), | |
| 4428 | − | repo, | |
| 4429 | − | number, | |
| 4430 | − | assignees: strings(input, "assignees"), | |
| 4431 | − | reviewers: strings(input, "reviewers"), | |
| 4432 | − | labels: strings(input, "labels"), | |
| 4433 | − | milestone: milestone_input(input), | |
| 4434 | − | base: optional_text(input, "base"), | |
| 4435 | − | }, | |
| 4436 | − | ) | |
| 4437 | − | .await | |
| 4474 | + | // `state` reopens a closed pull request (first, so that the | |
| 4475 | + | // rest can change it) or closes an open one (last). | |
| 4476 | + | let wanted = optional_text(input, "state"); | |
| 4477 | + | if wanted.as_deref().is_some_and(|state| state != "open" && state != "closed") { | |
| 4478 | + | return failed(FailureCode::Invalid, "state must be open or closed."); | |
| 4479 | + | } | |
| 4480 | + | let mut current = None; | |
| 4481 | + | if wanted.is_some() { | |
| 4482 | + | let found: Outcome<PullDetail> = call(work, "get_pull", &view()).await?; | |
| 4483 | + | match found { | |
| 4484 | + | Outcome::Ok(detail) => current = Some(detail.pull), | |
| 4485 | + | Outcome::Fail(failure) => return Ok(Outcome::Fail(failure)), | |
| 4486 | + | } | |
| 4487 | + | } | |
| 4488 | + | let status = current.as_ref().map(|pull| pull.status); | |
| 4489 | + | let mut answer = current.map(|pull| serde_json::to_value(pull)).transpose()?; | |
| 4490 | + | if wanted.as_deref() == Some("open") && status == Some(PullStatus::Closed) { | |
| 4491 | + | match pass(work, "reopen_pull", &pull_action()).await? { | |
| 4492 | + | Outcome::Ok(pull) => answer = Some(pull), | |
| 4493 | + | failure => return Ok(failure), | |
| 4494 | + | } | |
| 4495 | + | } | |
| 4496 | + | let changes = ["assignees", "reviewers", "labels", "milestone", "base"] | |
| 4497 | + | .iter() | |
| 4498 | + | .any(|key| input.get(*key).is_some()); | |
| 4499 | + | if changes || wanted.is_none() { | |
| 4500 | + | let updated = pass( | |
| 4501 | + | work, | |
| 4502 | + | "update_pull", | |
| 4503 | + | &UpdatePullArgs { | |
| 4504 | + | actor: actor(), | |
| 4505 | + | repo: repo.clone(), | |
| 4506 | + | number, | |
| 4507 | + | assignees: strings(input, "assignees"), | |
| 4508 | + | reviewers: strings(input, "reviewers"), | |
| 4509 | + | labels: strings(input, "labels"), | |
| 4510 | + | milestone: milestone_input(input), | |
| 4511 | + | base: optional_text(input, "base"), | |
| 4512 | + | }, | |
| 4513 | + | ) | |
| 4514 | + | .await?; | |
| 4515 | + | match updated { | |
| 4516 | + | Outcome::Ok(pull) => answer = Some(pull), | |
| 4517 | + | failure => return Ok(failure), | |
| 4518 | + | } | |
| 4519 | + | } | |
| 4520 | + | if wanted.as_deref() == Some("closed") && status.is_some_and(PullStatus::is_active) { | |
| 4521 | + | return pass(work, "close_pull", &pull_action()).await; | |
| 4522 | + | } | |
| 4523 | + | Ok(Outcome::Ok(answer.unwrap_or(Value::Null))) | |
| 4438 | 4524 | } | |
| 4439 | 4525 | Op::AddComment | Op::ReviewPullRequest => { | |
| 4440 | 4526 | let verdict = match (self, input["verdict"].as_str()) { | |
| ⋯ | |||
| 4538 | 4624 | Op::ReadSession => pass(work, "read_session", &view()).await, | |
| 4539 | 4625 | Op::MarkPullRequestReady => pass(work, "ready_pull", &pull_action()).await, | |
| 4540 | 4626 | Op::ClosePullRequest => pass(work, "close_pull", &pull_action()).await, | |
| 4627 | + | Op::ReopenPullRequest => pass(work, "reopen_pull", &pull_action()).await, | |
| 4628 | + | Op::ConvertPullRequestToDraft => pass(work, "convert_pull_to_draft", &pull_action()).await, | |
| 4629 | + | Op::EditComment | Op::DeleteComment => { | |
| 4630 | + | let asked = CommentActionArgs { | |
| 4631 | + | actor: actor(), | |
| 4632 | + | repo, | |
| 4633 | + | comment_id: text(input, "comment_id"), | |
| 4634 | + | body: text(input, "body"), | |
| 4635 | + | }; | |
| 4636 | + | let method = if self == Op::EditComment { "edit_comment" } else { "delete_comment" }; | |
| 4637 | + | pass(work, method, &asked).await | |
| 4638 | + | } | |
| 4541 | 4639 | Op::MergePullRequest => pass(work, "merge_pull", &pull_action()).await, | |
| 4542 | 4640 | Op::GetPullRequestChanges => { | |
| 4543 | 4641 | let found: Outcome<PullDetail> = call(work, "get_pull", &view()).await?; | |
| 24 | 24 | SetWorkflowPermissions, | |
| 25 | 25 | GetForkPrApproval, | |
| 26 | 26 | SetForkPrApproval, | |
| 27 | + | GetActionsAccess, | |
| 28 | + | SetActionsAccess, | |
| 27 | 29 | CreateRepositoryDispatch, | |
| 28 | 30 | GetWorkspaceWorkflowPermissions, | |
| 29 | 31 | SetWorkspaceWorkflowPermissions, | |
| ⋯ | |||
| 33 | 35 | /// Every one: `Op::ALL` lists each as `Op::Protection(…)`, which a test | |
| 34 | 36 | /// checks against this. | |
| 35 | 37 | #[cfg(test)] | |
| 36 | − | pub const ALL: [ProtectionOp; 12] = [ | |
| 38 | + | pub const ALL: [ProtectionOp; 14] = [ | |
| 37 | 39 | ProtectionOp::UpdateEnvironment, | |
| 38 | 40 | ProtectionOp::DeleteEnvironment, | |
| 39 | 41 | ProtectionOp::GetPendingDeployments, | |
| ⋯ | |||
| 43 | 45 | ProtectionOp::SetWorkflowPermissions, | |
| 44 | 46 | ProtectionOp::GetForkPrApproval, | |
| 45 | 47 | ProtectionOp::SetForkPrApproval, | |
| 48 | + | ProtectionOp::GetActionsAccess, | |
| 49 | + | ProtectionOp::SetActionsAccess, | |
| 46 | 50 | ProtectionOp::CreateRepositoryDispatch, | |
| 47 | 51 | ProtectionOp::GetWorkspaceWorkflowPermissions, | |
| 48 | 52 | ProtectionOp::SetWorkspaceWorkflowPermissions, | |
| ⋯ | |||
| 59 | 63 | ProtectionOp::SetWorkflowPermissions => "set_workflow_permissions", | |
| 60 | 64 | ProtectionOp::GetForkPrApproval => "get_fork_pr_approval", | |
| 61 | 65 | ProtectionOp::SetForkPrApproval => "set_fork_pr_approval", | |
| 66 | + | ProtectionOp::GetActionsAccess => "get_actions_access", | |
| 67 | + | ProtectionOp::SetActionsAccess => "set_actions_access", | |
| 62 | 68 | ProtectionOp::CreateRepositoryDispatch => "create_repository_dispatch", | |
| 63 | 69 | ProtectionOp::GetWorkspaceWorkflowPermissions => "get_workspace_workflow_permissions", | |
| 64 | 70 | ProtectionOp::SetWorkspaceWorkflowPermissions => "set_workspace_workflow_permissions", | |
| ⋯ | |||
| 82 | 88 | ProtectionOp::SetWorkflowPermissions => "Set the default workflow permissions", | |
| 83 | 89 | ProtectionOp::GetForkPrApproval => "Get the approval policy for outside pull requests", | |
| 84 | 90 | ProtectionOp::SetForkPrApproval => "Set the approval policy for outside pull requests", | |
| 91 | + | ProtectionOp::GetActionsAccess => "Get who may use a repository's actions and workflows", | |
| 92 | + | ProtectionOp::SetActionsAccess => "Set who may use a repository's actions and workflows", | |
| 85 | 93 | ProtectionOp::CreateRepositoryDispatch => "Create a repository dispatch event", | |
| 86 | 94 | ProtectionOp::GetWorkspaceWorkflowPermissions => "Get a workspace's default workflow permissions", | |
| 87 | 95 | ProtectionOp::SetWorkspaceWorkflowPermissions => "Set a workspace's default workflow permissions", | |
| ⋯ | |||
| 99 | 107 | ProtectionOp::SetWorkflowPermissions => "Set default_workflow_permissions to read, write (refused where the workspace's maximum is read) or inherit (back to the workspace's default, or write for a repository made before restricted tokens), and can_approve_pull_request_reviews, \"Allow g1t Actions to create and approve pull requests\" (refused where the workspace does not allow it). Workflows that write `permissions:` get what they write either way, and a pull request's run from outside gets read-only. Needs the Admin role.", | |
| 100 | 108 | ProtectionOp::GetForkPrApproval => "Which pull requests' runs wait for someone with the Write role to approve them before anything runs (approve_workflow_run): approval_policy is first_time_contributors (a pull request from someone outside the workspace who has not had one merged here), outside_contributors (the default: also everyone outside who cannot push here) or all_external_contributors (everyone outside the workspace, outside collaborators included). Members never wait, nor does g1t's own work. Needs the Read role.", | |
| 101 | 109 | ProtectionOp::SetForkPrApproval => "Set approval_policy: first_time_contributors, outside_contributors or all_external_contributors. Needs the Admin role.", | |
| 110 | + | ProtectionOp::GetActionsAccess => "Which other repositories' workflows may use this private repository's actions (uses: owner/repo@ref) and reusable workflows (jobs.<id>.uses: owner/repo/.g1t/workflows/build.yml@ref): access_level is none (the default: only this repository) or organization (private repositories in the same workspace). A public repository's actions and workflows are anyone's, whatever this says, and a public repository's workflows never use a private one's. Needs the Read role.", | |
| 111 | + | ProtectionOp::SetActionsAccess => "Set access_level: none, or organization to let the workspace's other private repositories use this repository's actions and reusable workflows (user is read as organization). Needs the Admin role.", | |
| 102 | 112 | ProtectionOp::GetWorkspaceWorkflowPermissions => "A workspace's policy for its repositories' job tokens: default_workflow_permissions (read, the default, or write) is what a repository made from now on gets until it chooses; max_workflow_permissions (write, the default, or read) is the most any repository's default may be, so read holds every repository to read-only; can_approve_pull_request_reviews (off by default) lets its repositories allow jobs to open and approve pull requests. Members only.", | |
| 103 | 113 | ProtectionOp::SetWorkspaceWorkflowPermissions => "Change a workspace's default_workflow_permissions, max_workflow_permissions and can_approve_pull_request_reviews; fields left out stay as they are. A maximum of read makes the default read too. Owners only.", | |
| 104 | 114 | ProtectionOp::CreateRepositoryDispatch => "Start the default branch's workflows that run `on: repository_dispatch` for event_type (those listing it under types, or with none). client_payload, a JSON object of at most 10 properties and 64 KB, is github.event.client_payload; github.event.action is event_type. A workflow job's own token may send one: with workflow_dispatch, it is how one workflow starts another. Needs the Write role (code:write). Returns how many runs started.", | |
| ⋯ | |||
| 112 | 122 | ProtectionOp::GetPendingDeployments | |
| 113 | 123 | | ProtectionOp::GetWorkflowPermissions | |
| 114 | 124 | | ProtectionOp::GetForkPrApproval | |
| 125 | + | | ProtectionOp::GetActionsAccess | |
| 115 | 126 | | ProtectionOp::GetWorkspaceWorkflowPermissions | |
| 116 | 127 | ) | |
| 117 | 128 | } | |
| ⋯ | |||
| 174 | 185 | }), | |
| 175 | 186 | &["repo", "id", "state"], | |
| 176 | 187 | ), | |
| 177 | − | ProtectionOp::GetWorkflowPermissions | ProtectionOp::GetForkPrApproval => (json!({ "repo": repo }), &["repo"]), | |
| 188 | + | ProtectionOp::GetWorkflowPermissions | ProtectionOp::GetForkPrApproval | ProtectionOp::GetActionsAccess => (json!({ "repo": repo }), &["repo"]), | |
| 189 | + | ProtectionOp::SetActionsAccess => ( | |
| 190 | + | json!({ | |
| 191 | + | "repo": repo, | |
| 192 | + | "access_level": { "type": "string", "enum": ["none", "organization", "user"] }, | |
| 193 | + | }), | |
| 194 | + | &["repo", "access_level"], | |
| 195 | + | ), | |
| 178 | 196 | ProtectionOp::SetWorkflowPermissions => ( | |
| 179 | 197 | json!({ | |
| 180 | 198 | "repo": repo, | |
| ⋯ | |||
| 541 | 559 | }; | |
| 542 | 560 | map(settings, |s| json!({ "approval_policy": s["approvalPolicy"] })) | |
| 543 | 561 | } | |
| 562 | + | ProtectionOp::GetActionsAccess | ProtectionOp::SetActionsAccess => { | |
| 563 | + | let settings: Outcome<Value> = if op == ProtectionOp::GetActionsAccess { | |
| 564 | + | g1t_kit::call(actions, "actions_settings", &json!({ "viewer": viewer, "repo": repo })).await? | |
| 565 | + | } else { | |
| 566 | + | g1t_kit::call( | |
| 567 | + | actions, | |
| 568 | + | "set_actions_settings", | |
| 569 | + | &json!({ "actor": actor(), "repo": repo, "accessLevel": text(input, "access_level").unwrap_or_default() }), | |
| 570 | + | ) | |
| 571 | + | .await? | |
| 572 | + | }; | |
| 573 | + | map(settings, |s| json!({ "access_level": s["accessLevel"] })) | |
| 574 | + | } | |
| 544 | 575 | ProtectionOp::CreateRepositoryDispatch => { | |
| 545 | 576 | let started: Outcome<u32> = g1t_kit::call( | |
| 546 | 577 | actions, | |
| 1472 | 1472 | "created_at": "2026-10-01T18:12:30.551Z" | |
| 1473 | 1473 | } | |
| 1474 | 1474 | }, | |
| 1475 | + | "edit_comment": { | |
| 1476 | + | "params": { | |
| 1477 | + | "comment_id": "cmt_01m43sv4c8e2g6j0m4q8t2x6a1" | |
| 1478 | + | }, | |
| 1479 | + | "request": { | |
| 1480 | + | "body": "Should an empty name fall back to \"world\", or print the usage?" | |
| 1481 | + | }, | |
| 1482 | + | "response": { | |
| 1483 | + | "id": "cmt_01m43sv4c8e2g6j0m4q8t2x6a1", | |
| 1484 | + | "kind": "comment", | |
| 1485 | + | "author": { | |
| 1486 | + | "id": "usr_01kkntcg1eeb98j62xjm7eh09p", | |
| 1487 | + | "username": "syntaqx", | |
| 1488 | + | "kind": "user", | |
| 1489 | + | "verified": true, | |
| 1490 | + | "workspaces": [ | |
| 1491 | + | { | |
| 1492 | + | "slug": "flagon-io", | |
| 1493 | + | "role": "owner" | |
| 1494 | + | } | |
| 1495 | + | ] | |
| 1496 | + | }, | |
| 1497 | + | "body": "Should an empty name fall back to \"world\", or print the usage?", | |
| 1498 | + | "path": null, | |
| 1499 | + | "line": null, | |
| 1500 | + | "verdict": null, | |
| 1501 | + | "created_at": "2026-10-01T18:12:30.551Z", | |
| 1502 | + | "edited_at": "2026-10-01T18:30:04.210Z" | |
| 1503 | + | }, | |
| 1504 | + | "notes": "Its author, or someone with the Maintain role or higher. A note of something that happened answers `409`." | |
| 1505 | + | }, | |
| 1506 | + | "delete_comment": { | |
| 1507 | + | "params": { | |
| 1508 | + | "comment_id": "cmt_01m43sv4c8e2g6j0m4q8t2x6a1" | |
| 1509 | + | }, | |
| 1510 | + | "notes": "Answers `204` with no body. Its author, or someone with the Maintain role or higher. A review that approved or requested changes answers `409`: edit it instead." | |
| 1511 | + | }, | |
| 1475 | 1512 | "list_labels": { | |
| 1476 | 1513 | "response": [ | |
| 1477 | 1514 | { | |
| ⋯ | |||
| 2252 | 2289 | "updated_at": "2026-10-01T19:02:48.760Z" | |
| 2253 | 2290 | } | |
| 2254 | 2291 | }, | |
| 2292 | + | "reopen_pull_request": { | |
| 2293 | + | "response": { | |
| 2294 | + | "id": "pr_01m43smh3vexsr5pmp60qwv0vs", | |
| 2295 | + | "repo_id": "rep_01m3m5q6p0e2qaw6mmjahk0qrr", | |
| 2296 | + | "number": 14, | |
| 2297 | + | "issue": 12, | |
| 2298 | + | "title": "Greeting should name the caller", | |
| 2299 | + | "body": "Reads a name from the first argument. `hello ana` prints \"Hello, ana!\".", | |
| 2300 | + | "agent": "claude-code", | |
| 2301 | + | "runtime": "external", | |
| 2302 | + | "status": "open", | |
| 2303 | + | "fork": { | |
| 2304 | + | "namespace": "pulls", | |
| 2305 | + | "name": "pr_01m43smh3vexsr5pmp60qwv0vs" | |
| 2306 | + | }, | |
| 2307 | + | "fork_repo_id": "rep_01m43smh5xd7aw2kq9tv0b4c8e", | |
| 2308 | + | "branch": null, | |
| 2309 | + | "head_commit": "9f2c4e1a7b3d5f60812a4c6e8b0d2f4a6c8e0b13", | |
| 2310 | + | "merge_base": null, | |
| 2311 | + | "merged_by": null, | |
| 2312 | + | "merged_at": null, | |
| 2313 | + | "superseded_by": null, | |
| 2314 | + | "check_status": null, | |
| 2315 | + | "files": [ | |
| 2316 | + | { | |
| 2317 | + | "path": "src/main.rs", | |
| 2318 | + | "additions": 6, | |
| 2319 | + | "deletions": 2 | |
| 2320 | + | } | |
| 2321 | + | ], | |
| 2322 | + | "assignees": [], | |
| 2323 | + | "reviewers": [], | |
| 2324 | + | "labels": [], | |
| 2325 | + | "milestone": null, | |
| 2326 | + | "base": "main", | |
| 2327 | + | "author": { | |
| 2328 | + | "id": "usr_01kkntcg1eeb98j62xjm7eh09p", | |
| 2329 | + | "username": "syntaqx", | |
| 2330 | + | "kind": "user", | |
| 2331 | + | "verified": false, | |
| 2332 | + | "workspaces": [] | |
| 2333 | + | }, | |
| 2334 | + | "requested_by": null, | |
| 2335 | + | "created_at": "2026-10-01T18:20:02.117Z", | |
| 2336 | + | "updated_at": "2026-10-01T19:14:05.322Z" | |
| 2337 | + | }, | |
| 2338 | + | "notes": "A merged pull request, or one that is not closed, answers `409`. A pull request closed as a draft comes back as a draft; any other comes back ready for review. A pull request from a branch of the repository whose branch was deleted answers `409` until the branch is pushed again. The same as `PATCH /repos/{owner}/{repo}/pulls/{number}` with `state` `open`." | |
| 2339 | + | }, | |
| 2340 | + | "convert_pull_request_to_draft": { | |
| 2341 | + | "response": { | |
| 2342 | + | "id": "pr_01m43smh3vexsr5pmp60qwv0vs", | |
| 2343 | + | "repo_id": "rep_01m3m5q6p0e2qaw6mmjahk0qrr", | |
| 2344 | + | "number": 14, | |
| 2345 | + | "issue": 12, | |
| 2346 | + | "title": "Greeting should name the caller", | |
| 2347 | + | "body": "Reads a name from the first argument. `hello ana` prints \"Hello, ana!\".", | |
| 2348 | + | "agent": "claude-code", | |
| 2349 | + | "runtime": "external", | |
| 2350 | + | "status": "draft", | |
| 2351 | + | "fork": { | |
| 2352 | + | "namespace": "pulls", | |
| 2353 | + | "name": "pr_01m43smh3vexsr5pmp60qwv0vs" | |
| 2354 | + | }, | |
| 2355 | + | "fork_repo_id": "rep_01m43smh5xd7aw2kq9tv0b4c8e", | |
| 2356 | + | "branch": null, | |
| 2357 | + | "head_commit": "9f2c4e1a7b3d5f60812a4c6e8b0d2f4a6c8e0b13", | |
| 2358 | + | "merge_base": null, | |
| 2359 | + | "merged_by": null, | |
| 2360 | + | "merged_at": null, | |
| 2361 | + | "superseded_by": null, | |
| 2362 | + | "check_status": null, | |
| 2363 | + | "files": [ | |
| 2364 | + | { | |
| 2365 | + | "path": "src/main.rs", | |
| 2366 | + | "additions": 6, | |
| 2367 | + | "deletions": 2 | |
| 2368 | + | } | |
| 2369 | + | ], | |
| 2370 | + | "assignees": [], | |
| 2371 | + | "reviewers": [], | |
| 2372 | + | "labels": [], | |
| 2373 | + | "milestone": null, | |
| 2374 | + | "base": "main", | |
| 2375 | + | "author": { | |
| 2376 | + | "id": "usr_01kkntcg1eeb98j62xjm7eh09p", | |
| 2377 | + | "username": "syntaqx", | |
| 2378 | + | "kind": "user", | |
| 2379 | + | "verified": false, | |
| 2380 | + | "workspaces": [] | |
| 2381 | + | }, | |
| 2382 | + | "requested_by": null, | |
| 2383 | + | "created_at": "2026-10-01T18:20:02.117Z", | |
| 2384 | + | "updated_at": "2026-10-01T19:20:41.908Z" | |
| 2385 | + | }, | |
| 2386 | + | "notes": "Only a pull request that is open and ready for review can be made a draft; any other answers `409`. It leaves the [merge queue](/guides/merge-queue/)." | |
| 2387 | + | }, | |
| 2255 | 2388 | "get_merge_queue": { | |
| 2256 | 2389 | "response": { | |
| 2257 | 2390 | "enabled": true, | |
| ⋯ | |||
| 10331 | 10464 | "approval_policy": "all_external_contributors" | |
| 10332 | 10465 | } | |
| 10333 | 10466 | }, | |
| 10467 | + | "get_actions_access": { | |
| 10468 | + | "params": { | |
| 10469 | + | "owner": "flagon-io", | |
| 10470 | + | "name": "shared-workflows" | |
| 10471 | + | }, | |
| 10472 | + | "response": { | |
| 10473 | + | "access_level": "none" | |
| 10474 | + | } | |
| 10475 | + | }, | |
| 10476 | + | "set_actions_access": { | |
| 10477 | + | "params": { | |
| 10478 | + | "owner": "flagon-io", | |
| 10479 | + | "name": "shared-workflows" | |
| 10480 | + | }, | |
| 10481 | + | "request": { | |
| 10482 | + | "access_level": "organization" | |
| 10483 | + | }, | |
| 10484 | + | "response": { | |
| 10485 | + | "access_level": "organization" | |
| 10486 | + | } | |
| 10487 | + | }, | |
| 10334 | 10488 | "create_repository_dispatch": { | |
| 10335 | 10489 | "params": { | |
| 10336 | 10490 | "owner": "flagon-io", | |
| 176 | 176 | Op::GetPullRequest => through::<work::PullDetail>(op, sent), | |
| 177 | 177 | Op::MarkPullRequestReady | |
| 178 | 178 | | Op::ClosePullRequest | |
| 179 | + | | Op::ReopenPullRequest | |
| 180 | + | | Op::ConvertPullRequestToDraft | |
| 179 | 181 | | Op::MergePullRequest | |
| 180 | 182 | | Op::AssignIssue | |
| 181 | 183 | | Op::RequestReviewers | |
| ⋯ | |||
| 289 | 291 | fn every_route_has_a_sample() { | |
| 290 | 292 | let document = document(); | |
| 291 | 293 | for route in crate::rest::ROUTES { | |
| 294 | + | if route.no_content() { | |
| 295 | + | continue; | |
| 296 | + | } | |
| 292 | 297 | let path = route | |
| 293 | 298 | .path | |
| 294 | 299 | .split('/') | |
| 376 | 376 | Op::Protection(ProtectionOp::SetForkPrApproval), | |
| 377 | 377 | &[], | |
| 378 | 378 | ), | |
| 379 | + | route("GET", "/repos/:owner/:name/actions/permissions/access", Op::Protection(ProtectionOp::GetActionsAccess), &[]), | |
| 380 | + | route("PUT", "/repos/:owner/:name/actions/permissions/access", Op::Protection(ProtectionOp::SetActionsAccess), &[]), | |
| 379 | 381 | route("POST", "/repos/:owner/:name/dispatches", Op::Protection(ProtectionOp::CreateRepositoryDispatch), &[]), | |
| 380 | 382 | route( | |
| 381 | 383 | "GET", | |
| ⋯ | |||
| 842 | 844 | Op::AddComment, | |
| 843 | 845 | &[], | |
| 844 | 846 | ), | |
| 847 | + | route("PATCH", "/repos/:owner/:name/issues/comments/:comment_id", Op::EditComment, &[]), | |
| 848 | + | route("DELETE", "/repos/:owner/:name/issues/comments/:comment_id", Op::DeleteComment, &[]), | |
| 845 | 849 | route( | |
| 846 | 850 | "GET", | |
| 847 | 851 | "/repos/:owner/:name/pulls", | |
| ⋯ | |||
| 908 | 912 | Op::MarkPullRequestReady, | |
| 909 | 913 | &[], | |
| 910 | 914 | ), | |
| 915 | + | route("POST", "/repos/:owner/:name/pulls/:number/draft", Op::ConvertPullRequestToDraft, &[]), | |
| 911 | 916 | route( | |
| 912 | 917 | "POST", | |
| 913 | 918 | "/repos/:owner/:name/pulls/:number/close", | |
| 914 | 919 | Op::ClosePullRequest, | |
| 915 | 920 | &[], | |
| 916 | 921 | ), | |
| 922 | + | route("POST", "/repos/:owner/:name/pulls/:number/reopen", Op::ReopenPullRequest, &[]), | |
| 917 | 923 | route( | |
| 918 | 924 | "POST", | |
| 919 | 925 | "/repos/:owner/:name/pulls/:number/merge", | |
| ⋯ | |||
| 923 | 929 | ]; | |
| 924 | 930 | ||
| 925 | 931 | impl Route { | |
| 932 | + | /// Whether the route answers success with `204` and no body, as | |
| 933 | + | /// GitHub's address for the same does. MCP still answers `true`. | |
| 934 | + | pub fn no_content(&self) -> bool { | |
| 935 | + | self.op == Op::DeleteComment | |
| 936 | + | } | |
| 937 | + | ||
| 926 | 938 | /// The names of the route's path parameters, in order. | |
| 927 | 939 | pub fn params(&self) -> impl Iterator<Item = &'static str> { | |
| 928 | 940 | self.path | |
| ⋯ | |||
| 1257 | 1269 | } | |
| 1258 | 1270 | ||
| 1259 | 1271 | #[test] | |
| 1272 | + | fn pull_requests_reopen_and_turn_draft_and_comments_are_named_by_id() { | |
| 1273 | + | let op = |method: &str, path: &str| resolve(method, path, &[], Value::Null).unwrap().0.op; | |
| 1274 | + | assert_eq!(op("POST", "/repos/acme/web/pulls/9/reopen"), Op::ReopenPullRequest); | |
| 1275 | + | assert_eq!(op("POST", "/repos/acme/web/pulls/9/draft"), Op::ConvertPullRequestToDraft); | |
| 1276 | + | assert_eq!(op("POST", "/repos/acme/web/pulls/9/close"), Op::ClosePullRequest); | |
| 1277 | + | // Reopening and closing with a PATCH, as `state`. | |
| 1278 | + | let (route, input) = resolve("PATCH", "/repos/acme/web/pulls/9", &[], json!({ "state": "open" })).unwrap(); | |
| 1279 | + | assert_eq!(route.op, Op::UpdatePullRequest); | |
| 1280 | + | assert_eq!(input, json!({ "state": "open", "number": 9, "repo": "acme/web" })); | |
| 1281 | + | let (route, input) = | |
| 1282 | + | resolve("PATCH", "/repos/acme/web/issues/comments/cmt_1", &[], json!({ "body": "Better" })).unwrap(); | |
| 1283 | + | assert_eq!(route.op, Op::EditComment); | |
| 1284 | + | assert_eq!(input, json!({ "body": "Better", "comment_id": "cmt_1", "repo": "acme/web" })); | |
| 1285 | + | let (route, input) = resolve("DELETE", "/repos/acme/web/issues/comments/cmt_1", &[], Value::Null).unwrap(); | |
| 1286 | + | assert_eq!(route.op, Op::DeleteComment); | |
| 1287 | + | assert_eq!(input, json!({ "comment_id": "cmt_1", "repo": "acme/web" })); | |
| 1288 | + | // Still an issue's comments, labels and subscription. | |
| 1289 | + | assert_eq!(op("POST", "/repos/acme/web/issues/7/comments"), Op::AddComment); | |
| 1290 | + | assert_eq!(op("DELETE", "/repos/acme/web/issues/7/labels"), Op::RemoveIssueLabels); | |
| 1291 | + | assert_eq!(op("DELETE", "/repos/acme/web/issues/7/subscription"), Op::DeleteThreadSubscription); | |
| 1292 | + | } | |
| 1293 | + | ||
| 1294 | + | #[test] | |
| 1260 | 1295 | fn billing_is_addressed_by_workspace() { | |
| 1261 | 1296 | let query = [("from".to_owned(), "2026-10-01".to_owned()), ("products".to_owned(), "agent,sandboxes".to_owned())]; | |
| 1262 | 1297 | let (route, input) = resolve("GET", "/workspaces/acme/usage", &query, Value::Null).unwrap(); | |
| 143 | 143 | a("close", Op::CloseIssue, "Close it without a pull request"), | |
| 144 | 144 | a("reopen", Op::ReopenIssue, "Reopen it"), | |
| 145 | 145 | a("comment", Op::AddComment, "Comment on an issue or pull request; path and line for one line of a change"), | |
| 146 | + | a("edit_comment", Op::EditComment, "Change a comment's text: your own, or any with the Maintain role"), | |
| 147 | + | a("delete_comment", Op::DeleteComment, "Delete a comment: your own, or any with the Maintain role"), | |
| 146 | 148 | a("import", Op::ImportIssue, "Open an issue from a Jira, Linear or Sentry item"), | |
| 147 | 149 | ], | |
| 148 | 150 | }, | |
| ⋯ | |||
| 160 | 162 | a("record_session", Op::RecordSession, "Append prompt, reasoning and tool entries to its session"), | |
| 161 | 163 | a("read_session", Op::ReadSession, "Its recorded session"), | |
| 162 | 164 | a("ready", Op::MarkPullRequestReady, "Mark a draft ready, with a summary"), | |
| 165 | + | a("draft", Op::ConvertPullRequestToDraft, "Turn it back into a draft"), | |
| 163 | 166 | a("request_reviewers", Op::RequestReviewers, "Ask people or teams to review it"), | |
| 164 | 167 | a("remove_requested_reviewers", Op::RemoveRequestedReviewers, "Stop asking people or teams to review it"), | |
| 165 | 168 | a("review", Op::ReviewPullRequest, "Approve or request changes"), | |
| 166 | 169 | a("close", Op::ClosePullRequest, "Close without merging"), | |
| 170 | + | a("reopen", Op::ReopenPullRequest, "Reopen a closed one"), | |
| 167 | 171 | a("merge", Op::MergePullRequest, "Land it, or join the merge queue"), | |
| 168 | 172 | a("merge_queue", Op::GetMergeQueue, "The repository's merge queue"), | |
| 169 | 173 | ], | |
| ⋯ | |||
| 251 | 255 | a("set_permissions", Op::Protection(ProtectionOp::SetWorkflowPermissions), "Set it: read or write"), | |
| 252 | 256 | a("get_approval_policy", Op::Protection(ProtectionOp::GetForkPrApproval), "Which pull requests' runs wait for approval"), | |
| 253 | 257 | a("set_approval_policy", Op::Protection(ProtectionOp::SetForkPrApproval), "Set which pull requests' runs wait for approval"), | |
| 258 | + | a("get_access", Op::Protection(ProtectionOp::GetActionsAccess), "Which repositories may use this one's actions and workflows"), | |
| 259 | + | a("set_access", Op::Protection(ProtectionOp::SetActionsAccess), "Let the workspace's private repositories use them, or not"), | |
| 254 | 260 | a("repository_dispatch", Op::Protection(ProtectionOp::CreateRepositoryDispatch), "Start repository_dispatch workflows with an event"), | |
| 255 | 261 | a("get_workspace_permissions", Op::Protection(ProtectionOp::GetWorkspaceWorkflowPermissions), "A workspace's default and maximum token permissions"), | |
| 256 | 262 | a("set_workspace_permissions", Op::Protection(ProtectionOp::SetWorkspaceWorkflowPermissions), "Set them, and whether jobs may open pull requests"), | |
| 246 | 246 | ||
| 247 | 247 | ### Workflow features not supported yet | |
| 248 | 248 | ||
| 249 | − | - Reusable workflows from another repository. Ones in the same repository | |
| 250 | − | work. | |
| 251 | 249 | - Actions that upload or download artifacts with the toolkit's artifact | |
| 252 | 250 | library themselves. The library refuses to run against any server but | |
| 253 | 251 | github.com. `actions/upload-artifact`, `actions/download-artifact` and |
| 43 | 43 | | Change visibility | | | | | Yes, if the [member privileges](/guides/workspaces/#member-privileges) allow | | |
| 44 | 44 | | Transfer or delete the repository | | | | | Owners, or Admins if the member privileges allow | | |
| 45 | 45 | ||
| 46 | + | Everyone can edit and delete their own comments. Maintain and Admin can | |
| 47 | + | edit and delete anyone's; see | |
| 48 | + | [editing and deleting comments](/guides/pull-requests/#editing-and-deleting-comments). | |
| 49 | + | ||
| 46 | 50 | Changing a repository's visibility, transferring it and deleting it also | |
| 47 | 51 | depend on its workspace's [member privileges](/guides/workspaces/#member-privileges). | |
| 48 | 52 | By default, a member with Admin can change visibility, and only an owner |
| 29 | 29 | ||
| 30 | 30 | | On GitHub | On g1t | | |
| 31 | 31 | | --- | --- | | |
| 32 | − | | `on:` `push` (branches, tags, paths), `pull_request`, `pull_request_target`, `issues`, `issue_comment`, `pull_request_review`, `schedule`, `workflow_dispatch`, `workflow_run`, `merge_group`, `create`, `repository_dispatch` | The same, from g1t's own pushes, pull requests, issues, comments and [merge queue](/guides/merge-queue/). `create` starts on each new branch or tag; `repository_dispatch` on [a dispatch event](#repository-dispatch). | | |
| 32 | + | | `on:` `push` (branches, tags, paths), `pull_request`, `pull_request_target`, `issues`, `issue_comment`, `pull_request_review`, `schedule`, `workflow_dispatch`, `workflow_run`, `merge_group`, `create`, `repository_dispatch`, `release`, `deployment`, `deployment_status` | The same, from g1t's own pushes, pull requests, issues, comments, [releases](#releases), [deployments](#deployments) and [merge queue](/guides/merge-queue/). `create` starts on each new branch or tag; `repository_dispatch` on [a dispatch event](#repository-dispatch). | | |
| 33 | 33 | | `jobs`, `needs`, `if`, `outputs`, `env`, `defaults`, `timeout-minutes`, `continue-on-error` | The same. | | |
| 34 | + | | `timeout-minutes` and `continue-on-error` on a step | The same, for `run:` and `uses:` steps alike. A `uses:` step's action is stopped at its limit, with every process it started; a step inside a composite action stops at its own limit or the `uses:` step's, whichever comes first. A step stopped this way fails, unless `continue-on-error` lets the job go on. | | |
| 34 | 35 | | `strategy.matrix` with `include` and `exclude`, `fail-fast`, `max-parallel`, a matrix from `fromJSON(needs.…)` | The same. | | |
| 35 | 36 | | `concurrency` with `cancel-in-progress`, for the workflow or for one job | The same: one run, or one job, of a group at a time. | | |
| 36 | 37 | | `permissions:` for the workflow or for one job, `read-all`, `write-all` | The same: they decide what [the job's token](#the-jobs-token) may do. | | |
| 37 | 38 | | `${{ }}` expressions: every operator, function and context | The same, including `hashFiles`, `success()`, `failure()`, `always()` and `cancelled()`. | | |
| 38 | 39 | | `run:` with `bash`, `sh`, `python` or a custom shell | The same. | | |
| 39 | − | | JavaScript actions (`uses: owner/repo@v7`) | Fetched from GitHub and run as they are, on Node 24, the runtime current actions declare. | | |
| 40 | + | | JavaScript actions (`uses: owner/repo@v7`, `owner/repo/path@v7`) | Fetched from that repository on g1t when g1t has it and your repository may use it, otherwise from GitHub, and run as they are, on Node 24, the runtime current actions declare. See [actions and workflows from other repositories](#actions-and-workflows-from-other-repositories). | | |
| 40 | 41 | | Composite actions | The same. | | |
| 41 | − | | Reusable workflows in the repository (`jobs.<id>.uses: ./.g1t/workflows/build.yml`) | The same: `with:` inputs, `on.workflow_call` outputs, and nesting up to four deep. `./.github/workflows/…` finds the workflow under `.g1t/` after the move. Their jobs read the repository's secrets and variables. | | |
| 42 | + | | Reusable workflows (`jobs.<id>.uses: ./.g1t/workflows/build.yml`, or `owner/repo/.g1t/workflows/build.yml@v1` in another repository) | The same: `with:` inputs, `secrets:` by name or `secrets: inherit`, `on.workflow_call` outputs, and nesting up to four deep. `.github/workflows/…` finds the workflow under `.g1t/` after the move. See [actions and workflows from other repositories](#actions-and-workflows-from-other-repositories). | | |
| 42 | 43 | | `actions/checkout` | Checks out from g1t, with `ref`, `fetch-depth`, `path`, `repository`, `token` and `submodules`. | | |
| 43 | 44 | | `GITHUB_OUTPUT`, `GITHUB_ENV`, `GITHUB_PATH`, `GITHUB_STATE`, `GITHUB_STEP_SUMMARY` | The same. | | |
| 44 | 45 | | `::error::`, `::warning::`, `::notice::`, `::group::`, `::add-mask::` | The same: errors and warnings become annotations on the run, and [masked](#masking-secrets) values stay hidden. | | |
| ⋯ | |||
| 66 | 67 | - **Docker's `type=gha` build cache.** Buildx skips it on g1t, and the | |
| 67 | 68 | build runs without a cache. Use a registry cache instead; see | |
| 68 | 69 | [caching image builds](#caching-image-builds). | |
| 69 | − | - **Reusable workflows from other repositories** (`uses: owner/repo/.github/workflows/x.yml@v1`); ones in the same repository work. | |
| 70 | 70 | - **Actions that upload artifacts with the toolkit's artifact library | |
| 71 | 71 | themselves.** The library refuses to run against any server but | |
| 72 | 72 | github.com. `actions/upload-artifact`, `actions/download-artifact` and | |
| ⋯ | |||
| 78 | 78 | Why each of these is missing, and what to use instead, is on | |
| 79 | 79 | [What g1t can't do yet](/about/limitations/#actions-and-runners). | |
| 80 | 80 | ||
| 81 | + | ## Actions and workflows from other repositories | |
| 82 | + | ||
| 83 | + | A step's `uses: owner/repo@ref` (or `owner/repo/path@ref`) and a job's | |
| 84 | + | `uses: owner/repo/.g1t/workflows/build.yml@ref` name another repository. | |
| 85 | + | g1t looks for it on g1t first: | |
| 86 | + | ||
| 87 | + | | The repository | What happens | | |
| 88 | + | | --- | --- | | |
| 89 | + | | On g1t and public | Your workflows use it, from any workspace. | | |
| 90 | + | | On g1t, private, in your workspace, with **Access** set to *Accessible from repositories in* the workspace | Your private repositories' workflows use it. A job reads it with a read-only token for that repository alone, which ends with the job. | | |
| 91 | + | | On g1t, private, and not shared that way | The step or job fails, and says why. A private repository's actions are never used by a public repository's workflows, whose logs anyone can read, nor from another workspace. | | |
| 92 | + | | Not on g1t, or private in a workspace you cannot see | An action is fetched from GitHub, as before; a reusable workflow is read from a public repository on GitHub. | | |
| 93 | + | ||
| 94 | + | `ref` is a branch, a tag or a commit. A reusable workflow may be under | |
| 95 | + | `.g1t/workflows/` or `.github/workflows/`; a `.github/workflows/` path | |
| 96 | + | also finds the file under `.g1t/workflows/` in a repository moved to | |
| 97 | + | g1t. A `./.g1t/workflows/…` call inside a workflow from another | |
| 98 | + | repository reads from that repository, at the same ref. | |
| 99 | + | ||
| 100 | + | To share a private repository's actions and workflows with the rest of its | |
| 101 | + | workspace, an admin chooses **Settings → Actions → Access → Accessible from | |
| 102 | + | repositories in** the workspace, or calls | |
| 103 | + | `PUT /repos/{owner}/{repo}/actions/permissions/access` with | |
| 104 | + | `{"access_level": "organization"}` (`none` to stop). | |
| 105 | + | ||
| 106 | + | ### Secrets for a called workflow | |
| 107 | + | ||
| 108 | + | A called workflow gets only the secrets its caller passes, plus | |
| 109 | + | `G1T_TOKEN` (`GITHUB_TOKEN`): | |
| 110 | + | ||
| 111 | + | ```yaml | |
| 112 | + | jobs: | |
| 113 | + | build: | |
| 114 | + | uses: acme/shared/.g1t/workflows/build.yml@v2 | |
| 115 | + | with: | |
| 116 | + | node-version: 24 | |
| 117 | + | secrets: | |
| 118 | + | npm-token: ${{ secrets.NPM_TOKEN }} | |
| 119 | + | ||
| 120 | + | deploy: | |
| 121 | + | uses: ./.g1t/workflows/deploy.yml | |
| 122 | + | secrets: inherit | |
| 123 | + | ``` | |
| 124 | + | ||
| 125 | + | - `secrets:` with names passes each as the called workflow names it, | |
| 126 | + | read from the caller's `secrets`, `needs`, `inputs`, `matrix`, | |
| 127 | + | `github` and `vars`. | |
| 128 | + | - `secrets: inherit` passes every secret the caller has. | |
| 129 | + | - A job in the called workflow with its own `environment:` also reads that | |
| 130 | + | environment's secrets, over what was passed. | |
| 131 | + | - A secret the called workflow marks `required: true` under | |
| 132 | + | `on.workflow_call.secrets` that the caller does not pass fails the | |
| 133 | + | calling job before anything runs. | |
| 134 | + | ||
| 135 | + | `vars` are the calling repository's, and a called workflow's jobs run | |
| 136 | + | with the calling run's `github` context: `actions/checkout` checks out the | |
| 137 | + | calling repository. | |
| 138 | + | ||
| 139 | + | ## Releases | |
| 140 | + | ||
| 141 | + | Workflows with `on: release` start when a release changes, at the commit | |
| 142 | + | its tag names (`GITHUB_REF` is `refs/tags/<tag>`). Each change is one or | |
| 143 | + | more activity types, which `types:` chooses among: | |
| 144 | + | ||
| 145 | + | | Change | Activity types | | |
| 146 | + | | --- | --- | | |
| 147 | + | | A draft made | `created` | | |
| 148 | + | | A release made and published | `created`, `published`, and `released` (or `prereleased` for a prerelease) | | |
| 149 | + | | A draft published | `published`, and `released` or `prereleased` | | |
| 150 | + | | A prerelease made a full release | `edited` and `released` | | |
| 151 | + | | Made a draft again | `unpublished` | | |
| 152 | + | | Title, notes or prerelease changed | `edited`, with `github.event.changes` holding the old title and notes | | |
| 153 | + | | Deleted (the tag stays) | `deleted` | | |
| 154 | + | ||
| 155 | + | ```yaml | |
| 156 | + | on: | |
| 157 | + | release: | |
| 158 | + | types: [published] | |
| 159 | + | ``` | |
| 160 | + | ||
| 161 | + | `github.event.release` has `tag_name`, `name`, `body`, `draft`, | |
| 162 | + | `prerelease`, `target_commitish`, `author` and `html_url`. A release a | |
| 163 | + | job's own token makes or changes starts no workflows. | |
| 164 | + | ||
| 165 | + | ## Deployments | |
| 166 | + | ||
| 167 | + | `on: deployment` starts when a deployment is made, and | |
| 168 | + | `on: deployment_status` when one has a new status: one reported through | |
| 169 | + | the [deployments API](/guides/deployments-api/) or a | |
| 170 | + | [g1t.page](/guides/deployments/) build. The run is at the commit deployed; | |
| 171 | + | `GITHUB_REF` is the branch or tag deployed, and empty for a bare commit. | |
| 172 | + | ||
| 173 | + | ```yaml | |
| 174 | + | on: deployment_status | |
| 175 | + | ||
| 176 | + | jobs: | |
| 177 | + | smoke: | |
| 178 | + | if: github.event.deployment_status.state == 'success' | |
| 179 | + | runs-on: ubuntu-latest | |
| 180 | + | steps: | |
| 181 | + | - run: curl -fsS "${{ github.event.deployment_status.environment_url }}" | |
| 182 | + | ``` | |
| 183 | + | ||
| 184 | + | `github.event.deployment` has `environment`, `ref`, `sha`, `task` and | |
| 185 | + | `payload`; `github.event.deployment_status` has `state`, `environment_url` | |
| 186 | + | and `log_url`. Deployments a workflow makes, with `environment:` or with | |
| 187 | + | its job's token, start no workflows, so a workflow cannot set itself off. | |
| 188 | + | ||
| 81 | 189 | ## The runner | |
| 82 | 190 | ||
| 83 | 191 | Jobs run in a fresh sandbox each: Debian with Node 24, Python 3, Go, Rust, | |
| ⋯ | |||
| 652 | 760 | marks it ready, which on g1t is when it first has code. Each head runs | |
| 653 | 761 | each workflow once. | |
| 654 | 762 | ||
| 655 | − | They also start on the activity types `labeled`, `unlabeled`, | |
| 656 | − | `milestoned`, `demilestoned`, `assigned`, `review_requested` and | |
| 657 | − | `closed`, and `edited` when the branch a pull request merges into | |
| 658 | − | changes; `issues` workflows on `labeled`, `unlabeled`, `milestoned` and | |
| 763 | + | They also start on the activity types `reopened` (also run by | |
| 764 | + | default, as `opened` and `synchronize` are), `converted_to_draft`, | |
| 765 | + | `ready_for_review`, `labeled`, `unlabeled`, `milestoned`, `demilestoned`, | |
| 766 | + | `assigned`, `review_requested` and `closed`, and `edited` when the branch | |
| 767 | + | a pull request merges into changes; `issue_comment` workflows on | |
| 768 | + | `created`, `edited` (with `github.event.changes.body.from`) and `deleted`; `issues` workflows on `labeled`, `unlabeled`, `milestoned` and | |
| 659 | 769 | `demilestoned` too. List them under `types:` to run on them. For | |
| 660 | 770 | `labeled` and `unlabeled`, `github.event.label` names the label. A pull | |
| 661 | 771 | request's `branches` filter, `github.base_ref` and | |
| ⋯ | |||
| 1014 | 1124 | | `get_permissions`, `set_permissions` | `GET` and `PUT /repos/{owner}/{repo}/actions/permissions/workflow`, with `default_workflow_permissions` (`read`, `write` or `inherit`) and `can_approve_pull_request_reviews` | | |
| 1015 | 1125 | | `get_workspace_permissions`, `set_workspace_permissions` | `GET` and `PUT /workspaces/{workspace}/actions/permissions/workflow`, with `default_workflow_permissions`, `max_workflow_permissions` and `can_approve_pull_request_reviews` | | |
| 1016 | 1126 | | `get_approval_policy`, `set_approval_policy` | `GET` and `PUT /repos/{owner}/{repo}/actions/permissions/fork-pr-contributor-approval` | | |
| 1127 | + | | `get_access`, `set_access` | `GET` and `PUT /repos/{owner}/{repo}/actions/permissions/access`, with `access_level` (`none` or `organization`) | | |
| 1017 | 1128 | | `repository_dispatch` | `POST /repos/{owner}/{repo}/dispatches` with `event_type` and `client_payload` | | |
| 1018 | 1129 | | `list_artifacts` | `GET /repos/{owner}/{repo}/actions/artifacts`, with `name`, `page`, `per_page` | | |
| 1019 | 1130 | | `run_artifacts` | `GET …/actions/runs/{id}/artifacts`, with `name` | | |
| 1 | 1 | --- | |
| 2 | 2 | title: Pull requests and checks | |
| 3 | − | description: What the merge box shows before a pull request can merge, the checks your workflows report and which ones a merge needs, and conflicts found before anyone tries to merge. | |
| 3 | + | description: What the merge box shows before a pull request can merge, the checks your workflows report and which ones a merge needs, drafts, closing and reopening, editing and deleting comments, and conflicts found before anyone tries to merge. | |
| 4 | 4 | --- | |
| 5 | 5 | ||
| 6 | 6 | At the foot of an open pull request's conversation, the **merge box** says | |
| ⋯ | |||
| 160 | 160 | the MCP server they are the `pull_request` tool's `request_reviewers` and | |
| 161 | 161 | `remove_requested_reviewers` actions. | |
| 162 | 162 | ||
| 163 | + | ## Drafts, closing and reopening | |
| 164 | + | ||
| 165 | + | A draft is still being worked on: it can be reviewed, but it cannot merge | |
| 166 | + | until it is marked ready for review. Its author, whoever asked g1t for it, | |
| 167 | + | and anyone with the Triage [role](/guides/access-and-roles/) or higher can | |
| 168 | + | move a pull request between these states. An | |
| 169 | + | [archived](/guides/managing-repositories/) repository refuses all of them. | |
| 170 | + | ||
| 171 | + | ### Convert to a draft | |
| 172 | + | ||
| 173 | + | To take a pull request that is ready for review back to a draft, select | |
| 174 | + | **Convert to draft** under the comment box. It leaves the | |
| 175 | + | [merge queue](/guides/merge-queue/) if it is in it, and a merge that was | |
| 176 | + | waiting for it to catch up is called off. Mark it ready again with | |
| 177 | + | **Mark ready for review**. | |
| 178 | + | ||
| 179 | + | Only an open pull request can be converted; a draft, a closed or a merged | |
| 180 | + | one is refused with `409`. | |
| 181 | + | ||
| 182 | + | ### Reopen a pull request | |
| 183 | + | ||
| 184 | + | To open a closed pull request again, select **Reopen pull request** under | |
| 185 | + | the comment box. It comes back as it was when it was closed: a draft if it | |
| 186 | + | was closed as a draft, otherwise ready for review. Its checks and whether | |
| 187 | + | it merges cleanly are worked out again. | |
| 188 | + | ||
| 189 | + | A merged pull request cannot be reopened. Neither can one from a branch of | |
| 190 | + | the repository whose branch was deleted: push the branch again first. | |
| 191 | + | ||
| 192 | + | ### From the API | |
| 193 | + | ||
| 194 | + | | To | Call | MCP | | |
| 195 | + | | --- | --- | --- | | |
| 196 | + | | Convert to a draft | `POST /repos/{owner}/{name}/pulls/{number}/draft` | `pull_request` with `"action": "draft"` | | |
| 197 | + | | Close | `POST /repos/{owner}/{name}/pulls/{number}/close`, or `PATCH /repos/{owner}/{name}/pulls/{number}` with `"state": "closed"` | `pull_request` with `"action": "close"` | | |
| 198 | + | | Reopen | `POST /repos/{owner}/{name}/pulls/{number}/reopen`, or `PATCH /repos/{owner}/{name}/pulls/{number}` with `"state": "open"` | `pull_request` with `"action": "reopen"` | | |
| 199 | + | ||
| 200 | + | Each answers with the pull request as it is now. Converting publishes | |
| 201 | + | `pull.converted_to_draft` and reopening publishes `pull.reopened`, with the | |
| 202 | + | head commit in `commit`; both reach [webhooks](/guides/webhooks/) and can | |
| 203 | + | start [workflows](/guides/actions/). | |
| 204 | + | ||
| 205 | + | ## Editing and deleting comments | |
| 206 | + | ||
| 207 | + | You can edit and delete your own comments on issues and pull requests. | |
| 208 | + | Anyone with the Maintain [role](/guides/access-and-roles/) or higher can | |
| 209 | + | edit and delete anyone's. | |
| 210 | + | ||
| 211 | + | - To edit a comment, select **Edit** under it, change the text and select | |
| 212 | + | **Save**. The comment shows **edited** beside its time. | |
| 213 | + | - To delete a comment, select **Delete** under it and confirm. It is | |
| 214 | + | removed for everyone and cannot be brought back. | |
| 215 | + | ||
| 216 | + | A review that approved or requested changes can be edited but not deleted, | |
| 217 | + | so its verdict stays on record. The notes in the timeline of what happened, | |
| 218 | + | such as "closed this", cannot be edited or deleted. | |
| 219 | + | ||
| 220 | + | Through the API, `PATCH /repos/{owner}/{name}/issues/comments/{comment_id}` | |
| 221 | + | with `body` edits a comment and answers with it, and | |
| 222 | + | `DELETE /repos/{owner}/{name}/issues/comments/{comment_id}` deletes it and | |
| 223 | + | answers `204`. Both work for comments on issues and on pull requests; each | |
| 224 | + | comment's `id` is in `GET /repos/{owner}/{name}/issues/{number}` and | |
| 225 | + | `GET /repos/{owner}/{name}/pulls/{number}`. On the MCP server they are the | |
| 226 | + | `issue` tool's `edit_comment` and `delete_comment` actions. Editing | |
| 227 | + | publishes `comment.edited`, with what the comment said before in | |
| 228 | + | `changes.body.from`; deleting publishes `comment.deleted`, with the comment | |
| 229 | + | as it was in `comment`. | |
| 230 | + | ||
| 163 | 231 | ## Conflicts | |
| 164 | 232 | ||
| 165 | 233 | g1t works out whether a pull request merges cleanly into its target before | |
| 54 | 54 | git push origin :refs/tags/v1.2.0 | |
| 55 | 55 | ``` | |
| 56 | 56 | ||
| 57 | + | ## Workflows and webhooks | |
| 58 | + | ||
| 59 | + | Each change to a release starts the repository's workflows that run | |
| 60 | + | `on: release`, at the commit its tag names, and is sent to webhooks as | |
| 61 | + | `release.created`, `release.published` and the rest. See | |
| 62 | + | [releases in Actions](/guides/actions/#releases) for which change is which | |
| 63 | + | activity type, and [webhooks](/guides/webhooks/) for what each event | |
| 64 | + | carries. A release a workflow job's own token makes starts no workflows. | |
| 65 | + | ||
| 57 | 66 | ## From the API and MCP | |
| 58 | 67 | ||
| 59 | 68 | | Route | MCP | What it does | |
| 83 | 83 | | `pull.base_changed` | The branch a pull request merges into changed. `data.base` names it. See [pull requests into other branches](/guides/base-branches/). | | |
| 84 | 84 | | `issue.opened`, `issue.updated`, `issue.assigned`, `issue.closed`, `issue.reopened` | An issue changed. `data.number` and `data.author` (`id` and `username`); on close, `data.reason` and `data.resolved_by`; on assignment, `data.assignees` and the newly assigned `data.added`. For an issue g1t's agent filed while at work, `data.author` is g1t and `data.requested_by` is the person it was working for. | | |
| 85 | 85 | | `comment.created` | A comment or review on an issue or pull request. | | |
| 86 | + | | `comment.edited` | A comment's text was [edited](/guides/pull-requests/#editing-and-deleting-comments). `data.comment_id`, `data.number`, `data.pull_id` on a pull request, and `data.changes.body.from`, what it said before. | | |
| 87 | + | | `comment.deleted` | A comment was [deleted](/guides/pull-requests/#editing-and-deleting-comments). `data.comment_id`, `data.number`, `data.pull_id` on a pull request, and `data.comment` as it was: `id`, `body`, `author` (`id` and `username`), `created_at`, `path` and `line`. | | |
| 86 | 88 | | `pull.opened`, `pull.ready`, `pull.updated`, `pull.merge_requested`, `pull.merged`, `pull.closed` | A pull request changed. `data.number`, `data.issue` and `data.author` (`id` and `username`); on merge, `data.commit`. For a change g1t made, `data.author` is g1t and `data.requested_by` is the person who asked for it; `actor` is still whoever caused the event. On a change by g1t, once g1t has worked it out, `data.confidence`: `level` (`high`, `medium` or `low`), `reasons`, `self_reported`, `uncertain_about`, `run_id` and `assessed_at`. See [how sure the agent is](/guides/working-with-g1t/#how-sure-the-agent-is). | | |
| 89 | + | | `pull.reopened`, `pull.converted_to_draft` | A closed pull request was [reopened](/guides/pull-requests/#reopen-a-pull-request), or one ready for review was [turned back into a draft](/guides/pull-requests/#convert-to-a-draft). The same data as `pull.closed`; on reopen, `data.commit` is its head. | | |
| 87 | 90 | | `pull.assigned` | People were assigned to a pull request. `data.assignees` is everyone assigned now, `data.added` those newly assigned. | | |
| 88 | 91 | | `pull.review_requested`, `pull.review_request_removed` | Reviewers were asked for a pull request, or no longer are. `data.reviewers` names the people. `data.teams` lists the [teams](/guides/teams/#review-requests) asked, or no longer asked, each with `team` (`workspace/team`), `notified` (who in it was told) and `assigned` (who review assignment picked). `data.code_owners` is `true` when its [CODEOWNERS file](/guides/codeowners/) asked them. | | |
| 89 | 92 | | `pull.stalled`, `pull.resumed` | g1t stopped seeing a pull request through until a person steps in, with why in `data.detail`; or it picked back up. | | |
| ⋯ | |||
| 101 | 104 | | `deployment.succeeded`, `deployment.failed` | A g1t.page build of a [project](/guides/deployments/) finished, for production or a pull request's preview. `data.deployment_id`, `data.project`, `data.kind` (`production` or `preview`), `data.number` for a preview, `data.commit`, `data.path`, `data.error` on failure, and `data.recovered` when a success follows a failure. | | |
| 102 | 105 | | `deployment.created` | A deployment was made, from any source: reported through the [API](/guides/deployments-api/), made by a g1t Actions job with an `environment:`, or a g1t.page build. `data.repo_id` and `data.deployment`, without its `payload`. | | |
| 103 | 106 | | `deployment_status.created` | A deployment got a status, from any source. `data.repo_id`, `data.deployment` without its `payload`, and `data.deployment_status`. | | |
| 107 | + | | `release.created`, `.published`, `.released`, `.prereleased`, `.edited`, `.unpublished`, `.deleted` | A [release](/guides/releases/) changed; one change can send several, as the [release activity types](/guides/actions/#releases) say. `data.release_id`, `data.tag_name`, and `data.release` (`tag_name`, `target`, `name`, `body`, `draft`, `prerelease`, `author`, `created_at`, `published_at`), as it was for `release.deleted`. On `release.edited`, `data.changes` holds the old `name` and `body`. | | |
| 104 | 108 | | `queue.changed` | The merge queue gained, lost or settled an entry. | | |
| 105 | 109 | | `session.appended` | An agent's session grew. Busy: choose it only if you need it. | | |
| 106 | 110 | | `agent.asked` | An agent asked the agent on another pull request a question, or handed it work, while that one was not at work; g1t wakes it to answer. | | |
| 323 | 323 | | [`close`](/reference/api/issues/close-issue/) | Close it as `completed` or `not_planned`. | `repo`, `number` | `issues:write` | | |
| 324 | 324 | | [`reopen`](/reference/api/issues/reopen-issue/) | Reopen a closed issue. | `repo`, `number` | `issues:write` | | |
| 325 | 325 | | [`comment`](/reference/api/issues/add-comment/) | Comment on an issue or a pull request; with `path` and `line`, on one line of a pull request's change. | `repo`, `number`, `body` | `issues:write` | | |
| 326 | + | | [`edit_comment`](/reference/api/issues/edit-comment/) | Change a comment's text, by its id from `get`. Your own, or anyone's with the Maintain role or higher. Notes of what happened cannot be edited. | `repo`, `comment_id`, `body` | `issues:write` | | |
| 327 | + | | [`delete_comment`](/reference/api/issues/delete-comment/) | Delete a comment, by its id. Your own, or anyone's with the Maintain role or higher. A review that approved or requested changes cannot be deleted. | `repo`, `comment_id` | `issues:write` | | |
| 326 | 328 | | [`import`](/reference/api/integrations/import-issue/) | Open an issue from a ticket, linked to it. `assign` assigns it to g1t. | `repo`, `reference` | `issues:write` | | |
| 327 | 329 | ||
| 328 | 330 | `import` with `assign` also needs `agents:run`, since it puts an agent to | |
| ⋯ | |||
| 340 | 342 | | [`get`](/reference/api/pull-requests/get-pull-request/) | Status, head commit, comments and reviews, who is asked to review (`pull.reviewers`, and `pull.team_reviewers` as `workspace/team`), its issue, its checks (`statuses`, and `required_checks`: each check the default branch requires, as `success`, `failure`, `pending` or `expected`), `code_owners` (whose approval the changed files need, and what is still `missing`), `behind`, and `overlaps`. | `repo`, `number` | `pull_requests:read` | | |
| 341 | 343 | | [`changes`](/reference/api/pull-requests/get-pull-request-changes/) | The files it changes, with line-by-line diffs. | `repo`, `number` | `pull_requests:read` | | |
| 342 | 344 | | [`create`](/reference/api/pull-requests/create-pull-request/) | Open a draft pull request with its own fork and get its git remote; or, with `branch`, one from a branch already pushed. Give `issue` whenever there is one. It merges into the default branch unless `base` names another. | `repo` | `pull_requests:write` | | |
| 343 | − | | [`update`](/reference/api/pull-requests/update-pull-request/) | Change its `base` (the branch it merges into; Write role), `labels`, `milestone`, `assignees` or `reviewers`. | `repo`, `number` | `pull_requests:write` | | |
| 345 | + | | [`update`](/reference/api/pull-requests/update-pull-request/) | Change its `base` (the branch it merges into; Write role), `labels`, `milestone`, `assignees` or `reviewers`. `state` `open` reopens it and `closed` closes it. | `repo`, `number` | `pull_requests:write` | | |
| 344 | 346 | | [`record_session`](/reference/api/sessions/record-session/) | Append entries to a pull request's session. Each has `kind` and `text`, and `tool` for tool entries. | `repo`, `number`, `entries` | `pull_requests:write` | | |
| 345 | 347 | | [`read_session`](/reference/api/sessions/read-session/) | The recorded session, oldest first. `after` skips to entries after a sequence number. | `repo`, `number` | `pull_requests:read` | | |
| 346 | 348 | | [`ready`](/reference/api/pull-requests/mark-pull-request-ready/) | Mark a draft ready for review. The summary becomes its description. | `repo`, `number`, `summary` | `pull_requests:write` | | |
| 349 | + | | [`draft`](/reference/api/pull-requests/convert-pull-request-to-draft/) | Turn a pull request that is ready for review back into a draft. It leaves the merge queue. Its author, or the Triage role. | `repo`, `number` | `pull_requests:write` | | |
| 347 | 350 | | [`request_reviewers`](/reference/api/pull-requests/request-reviewers/) | Ask more people (`reviewers`, by username; `g1t` for a g1t agent) or teams (`team_reviewers`, as `workspace/team` or a slug of the repository's workspace) to review it, added to whoever is asked already. Its author, or the Triage role. | `repo`, `number` | `pull_requests:write` | | |
| 348 | 351 | | [`remove_requested_reviewers`](/reference/api/pull-requests/remove-requested-reviewers/) | Stop asking them. Reviews they gave stay. | `repo`, `number` | `pull_requests:write` | | |
| 349 | 352 | | [`review`](/reference/api/pull-requests/review-pull-request/) | `approve`, or `request_changes` with a `body`. Not on your own pull request, nor one g1t made for you. | `repo`, `number`, `verdict` | `pull_requests:write` | | |
| 350 | 353 | | [`close`](/reference/api/pull-requests/close-pull-request/) | Close it without merging. | `repo`, `number` | `pull_requests:write` | | |
| 354 | + | | [`reopen`](/reference/api/pull-requests/reopen-pull-request/) | Reopen a closed pull request, as the draft it was if it was closed as one. Never a merged one. Its author, or the Triage role. | `repo`, `number` | `pull_requests:write` | | |
| 351 | 355 | | [`merge`](/reference/api/pull-requests/merge-pull-request/) | Land it on its [base](/guides/base-branches/), or add it to the [merge queue](/guides/merge-queue/), once it meets every [rule](/guides/rules/) of its base, required checks included; `bypass_rules` merges past rules a ruleset lets you bypass. Into the default branch, it resolves its issue. `ignore_checks` bypasses required checks where the repository allows it. Write role. | `repo`, `number` | `pull_requests:write` | | |
| 352 | 356 | | [`merge_queue`](/reference/api/pull-requests/get-merge-queue/) | The pull requests waiting to land, in order, each with the state it is tested in and how that went; then those that recently landed or left. | `repo` | `pull_requests:read` | | |
| 353 | 357 | ||
| ⋯ | |||
| 455 | 459 | | [`set_workspace_permissions`](/reference/api/run-protection/set-workspace-workflow-permissions/) | Change them: `default_workflow_permissions`, `max_workflow_permissions`, `can_approve_pull_request_reviews`. Owners. | `workspace` | `workspace:admin` | | |
| 456 | 460 | | [`get_approval_policy`](/reference/api/run-protection/get-fork-pr-approval/) | Which pull requests' runs wait for approval. | `repo` | `repo:read` | | |
| 457 | 461 | | [`set_approval_policy`](/reference/api/run-protection/set-fork-pr-approval/) | Set `approval_policy`: `first_time_contributors`, `outside_contributors` or `all_external_contributors`. Admin role. | `repo`, `approval_policy` | `repo:admin` | | |
| 462 | + | | [`get_access`](/reference/api/run-protection/get-actions-access/) | Who may use this private repository's actions and reusable workflows: `access_level` `none` (only itself) or `organization` (the workspace's private repositories). | `repo` | `repo:read` | | |
| 463 | + | | [`set_access`](/reference/api/run-protection/set-actions-access/) | Set `access_level`: `none` or `organization`. Admin role. | `repo`, `access_level` | `repo:admin` | | |
| 458 | 464 | | [`repository_dispatch`](/reference/api/run-protection/create-repository-dispatch/) | Start the default branch's `repository_dispatch` workflows for `event_type`, with `client_payload`. Write role. | `repo`, `event_type` | `code:write` | | |
| 459 | 465 | | [`list_runners`](/reference/api/runners/list-runners-for-workspace/) | [Self-hosted runners](/guides/self-hosted-runners/): a workspace's (`workspace`), or a repository's own and the workspace's it may use (`repo`), with status, labels and what each is running. | `workspace` or `repo` | `runners:read` | | |
| 460 | 466 | | [`create_runner_token`](/reference/api/runners/create-runner-registration-token-for-workspace/) | A registration token for `g1t-runner register`, an hour long; `group` for a workspace's. Owners, or a repository's admins; not workspace tokens. | `workspace` or `repo` | `runners:admin` | | |
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
This change is too large to show in full.