Skip to content

Commit

Merge Actions: cross-repo workflows and actions, release and deployment triggers, step timeouts

syntaqxcommitted Parents70b5eb949b18c0Browse files
58 files+594−420/58 viewed
+8−3
693693 // reporting how it goes. The job's own token is the credential.
694694 ("POST", path) if path.starts_with("/actions/jobs/") => {
695695 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"),
699702 };
700703 let body = json_body(&mut request).await;
701704 let answered: Outcome<Value> = g1t_kit::call(
806809 None => reply(&value),
807810 }
808811 }
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)),
809814 Outcome::Ok(value) => reply(&value),
810815 // A token without the scope a call needs is told which one.
811816 Outcome::Fail(refused) => match (refused.code, audit::missing_scope(route.op, &viewer, &input)) {
+25−7
312312 Op::AssignIssue,
313313 Op::Delegate,
314314 Op::AddComment,
315+ Op::EditComment,
316+ Op::DeleteComment,
315317 Op::ListIssueLabels,
316318 Op::AddIssueLabels,
317319 Op::SetIssueLabels,
349351 Op::UpdatePullRequest,
350352 Op::GetPullRequestChanges,
351353 Op::MarkPullRequestReady,
354+ Op::ConvertPullRequestToDraft,
352355 Op::RequestReviewers,
353356 Op::RemoveRequestedReviewers,
354357 Op::ReviewPullRequest,
355358 Op::MergePullRequest,
356359 Op::ClosePullRequest,
360+ Op::ReopenPullRequest,
357361 Op::GetMergeQueue,
358362 Op::MessageAgent,
359363 Op::AnswerMessage,
450454 Op::Protection(ProtectionOp::SetWorkflowPermissions),
451455 Op::Protection(ProtectionOp::GetForkPrApproval),
452456 Op::Protection(ProtectionOp::SetForkPrApproval),
457+ Op::Protection(ProtectionOp::GetActionsAccess),
458+ Op::Protection(ProtectionOp::SetActionsAccess),
453459 Op::Protection(ProtectionOp::CreateRepositoryDispatch),
454460 Op::Protection(ProtectionOp::GetWorkspaceWorkflowPermissions),
455461 Op::Protection(ProtectionOp::SetWorkspaceWorkflowPermissions),
596602 Op::DeleteMilestone => "Delete a milestone",
597603 Op::UpdatePullRequest => "Update a pull request",
598604 Op::AddComment => "Add a comment",
605+ Op::EditComment => "Edit a comment",
606+ Op::DeleteComment => "Delete a comment",
599607 Op::ReviewPullRequest => "Review a pull request",
600608 Op::ListPullRequests => "List pull requests",
601609 Op::GetPullRequest => "Get a pull request",
604612 Op::ReadSession => "Read a session",
605613 Op::MarkPullRequestReady => "Mark a pull request ready",
606614 Op::ClosePullRequest => "Close a pull request",
615+ Op::ReopenPullRequest => "Reopen a pull request",
616+ Op::ConvertPullRequestToDraft => "Convert a pull request to a draft",
607617 Op::GetPullRequestChanges => "Get a pull request's changes",
608618 Op::MergePullRequest => "Merge a pull request",
609619 Op::ListEvents => "List repository events",
893903
894904 let id = operation_id(route);
895905 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+ }
903917 responses.insert(
904918 "401".into(),
905919 error_response("A token is required, or the one sent is not valid."),
12071221 for (path, methods) in document["paths"].as_object().unwrap() {
12081222 for (method, operation) in methods.as_object().unwrap() {
12091223 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+ }
12101228 let example = &operation["responses"]["200"]["content"]["application/json"]["example"];
12111229 assert!(!example.is_null(), "{method} {path} has no example response");
12121230 }
+116−18
174174 UpdateMilestone,
175175 DeleteMilestone,
176176 AddComment,
177+ EditComment,
178+ DeleteComment,
177179 ReviewPullRequest,
178180 ListPullRequests,
179181 GetPullRequest,
182184 RecordSession,
183185 ReadSession,
184186 MarkPullRequestReady,
187+ ConvertPullRequestToDraft,
185188 ClosePullRequest,
189+ ReopenPullRequest,
186190 GetPullRequestChanges,
187191 MergePullRequest,
188192 ListEvents,
433437 properties
434438 }
435439
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+
436447 fn workspace_schema() -> Value {
437448 json!({ "type": "string", "description": "The workspace's slug, e.g. \"flagon-io\"." })
438449 }
671682 }
672683
673684 impl Op {
674− pub const ALL: [Op; 308] = [
685+ pub const ALL: [Op; 314] = [
675686 Op::Whoami,
676687 Op::GetWorkspace,
677688 Op::CreateWorkspace,
744755 Op::UpdateMilestone,
745756 Op::DeleteMilestone,
746757 Op::AddComment,
758+ Op::EditComment,
759+ Op::DeleteComment,
747760 Op::ReviewPullRequest,
748761 Op::ListPullRequests,
749762 Op::GetPullRequest,
752765 Op::RecordSession,
753766 Op::ReadSession,
754767 Op::MarkPullRequestReady,
768+ Op::ConvertPullRequestToDraft,
755769 Op::ClosePullRequest,
770+ Op::ReopenPullRequest,
756771 Op::GetPullRequestChanges,
757772 Op::MergePullRequest,
758773 Op::ListEvents,
954969 Op::Protection(ProtectionOp::SetWorkflowPermissions),
955970 Op::Protection(ProtectionOp::GetForkPrApproval),
956971 Op::Protection(ProtectionOp::SetForkPrApproval),
972+ Op::Protection(ProtectionOp::GetActionsAccess),
973+ Op::Protection(ProtectionOp::SetActionsAccess),
957974 Op::Protection(ProtectionOp::CreateRepositoryDispatch),
958975 Op::Protection(ProtectionOp::GetWorkspaceWorkflowPermissions),
959976 Op::Protection(ProtectionOp::SetWorkspaceWorkflowPermissions),
10611078 Op::UpdateMilestone => "update_milestone",
10621079 Op::DeleteMilestone => "delete_milestone",
10631080 Op::AddComment => "add_comment",
1081+ Op::EditComment => "edit_comment",
1082+ Op::DeleteComment => "delete_comment",
10641083 Op::ReviewPullRequest => "review_pull_request",
10651084 Op::ListPullRequests => "list_pull_requests",
10661085 Op::GetPullRequest => "get_pull_request",
10701089 Op::ReadSession => "read_session",
10711090 Op::MarkPullRequestReady => "mark_pull_request_ready",
10721091 Op::ClosePullRequest => "close_pull_request",
1092+ Op::ReopenPullRequest => "reopen_pull_request",
1093+ Op::ConvertPullRequestToDraft => "convert_pull_request_to_draft",
10731094 Op::GetPullRequestChanges => "get_pull_request_changes",
10741095 Op::MergePullRequest => "merge_pull_request",
10751096 Op::ListEvents => "list_events",
13941415 Op::AddComment => {
13951416 "Comment on an issue or a pull request. On a pull request, give path and line to comment on one line of the change."
13961417 }
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+ }
13971424 Op::ReviewPullRequest => {
13981425 "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)."
13991426 }
14071434 "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."
14081435 }
14091436 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."
14111438 }
14121439 Op::RecordSession => {
14131440 "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."
14171444 "Mark a draft pull request ready for review. Push your commits first. The summary becomes its description and should say what changed and why."
14181445 }
14191446 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.",
14201449 Op::GetPullRequestChanges => {
14211450 "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."
14221451 }
22192248 | Op::ReopenIssue
22202249 | Op::GetPullRequest
22212250 | Op::ClosePullRequest
2251+ | Op::ReopenPullRequest
2252+ | Op::ConvertPullRequestToDraft
22222253 | Op::GetPullRequestChanges => just_numbered(),
22232254 Op::CreateIssue => object(
22242255 json!({
23522383 })),
23532384 &["repo", "number", "body"],
23542385 ),
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+ ),
23552398 Op::ReviewPullRequest => object(
23562399 numbered(json!({
23572400 "verdict": { "type": "string", "enum": ["approve", "request_changes"] },
23742417 ),
23752418 Op::UpdatePullRequest => object(
23762419 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+ },
23772425 "base": {
23782426 "type": "string",
23792427 "description": "The branch it merges into: an existing branch other than its own. Needs the Write role.",
31923240 | DeploymentsOp::GetEnvironment
31933241 )
31943242 | Op::Protection(
3195− ProtectionOp::GetPendingDeployments | ProtectionOp::GetWorkflowPermissions | ProtectionOp::GetForkPrApproval
3243+ ProtectionOp::GetPendingDeployments
3244+ | ProtectionOp::GetWorkflowPermissions
3245+ | ProtectionOp::GetForkPrApproval
3246+ | ProtectionOp::GetActionsAccess
31963247 )
31973248 )
31983249 }
44204471 .await
44214472 }
44224473 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)))
44384524 }
44394525 Op::AddComment | Op::ReviewPullRequest => {
44404526 let verdict = match (self, input["verdict"].as_str()) {
45384624 Op::ReadSession => pass(work, "read_session", &view()).await,
45394625 Op::MarkPullRequestReady => pass(work, "ready_pull", &pull_action()).await,
45404626 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+ }
45414639 Op::MergePullRequest => pass(work, "merge_pull", &pull_action()).await,
45424640 Op::GetPullRequestChanges => {
45434641 let found: Outcome<PullDetail> = call(work, "get_pull", &view()).await?;
+33−2
2424 SetWorkflowPermissions,
2525 GetForkPrApproval,
2626 SetForkPrApproval,
27+ GetActionsAccess,
28+ SetActionsAccess,
2729 CreateRepositoryDispatch,
2830 GetWorkspaceWorkflowPermissions,
2931 SetWorkspaceWorkflowPermissions,
3335 /// Every one: `Op::ALL` lists each as `Op::Protection(…)`, which a test
3436 /// checks against this.
3537 #[cfg(test)]
36− pub const ALL: [ProtectionOp; 12] = [
38+ pub const ALL: [ProtectionOp; 14] = [
3739 ProtectionOp::UpdateEnvironment,
3840 ProtectionOp::DeleteEnvironment,
3941 ProtectionOp::GetPendingDeployments,
4345 ProtectionOp::SetWorkflowPermissions,
4446 ProtectionOp::GetForkPrApproval,
4547 ProtectionOp::SetForkPrApproval,
48+ ProtectionOp::GetActionsAccess,
49+ ProtectionOp::SetActionsAccess,
4650 ProtectionOp::CreateRepositoryDispatch,
4751 ProtectionOp::GetWorkspaceWorkflowPermissions,
4852 ProtectionOp::SetWorkspaceWorkflowPermissions,
5963 ProtectionOp::SetWorkflowPermissions => "set_workflow_permissions",
6064 ProtectionOp::GetForkPrApproval => "get_fork_pr_approval",
6165 ProtectionOp::SetForkPrApproval => "set_fork_pr_approval",
66+ ProtectionOp::GetActionsAccess => "get_actions_access",
67+ ProtectionOp::SetActionsAccess => "set_actions_access",
6268 ProtectionOp::CreateRepositoryDispatch => "create_repository_dispatch",
6369 ProtectionOp::GetWorkspaceWorkflowPermissions => "get_workspace_workflow_permissions",
6470 ProtectionOp::SetWorkspaceWorkflowPermissions => "set_workspace_workflow_permissions",
8288 ProtectionOp::SetWorkflowPermissions => "Set the default workflow permissions",
8389 ProtectionOp::GetForkPrApproval => "Get the approval policy for outside pull requests",
8490 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",
8593 ProtectionOp::CreateRepositoryDispatch => "Create a repository dispatch event",
8694 ProtectionOp::GetWorkspaceWorkflowPermissions => "Get a workspace's default workflow permissions",
8795 ProtectionOp::SetWorkspaceWorkflowPermissions => "Set a workspace's default workflow permissions",
99107 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.",
100108 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.",
101109 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.",
102112 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.",
103113 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.",
104114 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.",
112122 ProtectionOp::GetPendingDeployments
113123 | ProtectionOp::GetWorkflowPermissions
114124 | ProtectionOp::GetForkPrApproval
125+ | ProtectionOp::GetActionsAccess
115126 | ProtectionOp::GetWorkspaceWorkflowPermissions
116127 )
117128 }
174185 }),
175186 &["repo", "id", "state"],
176187 ),
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+ ),
178196 ProtectionOp::SetWorkflowPermissions => (
179197 json!({
180198 "repo": repo,
541559 };
542560 map(settings, |s| json!({ "approval_policy": s["approvalPolicy"] }))
543561 }
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+ }
544575 ProtectionOp::CreateRepositoryDispatch => {
545576 let started: Outcome<u32> = g1t_kit::call(
546577 actions,
+154−0
14721472 "created_at": "2026-10-01T18:12:30.551Z"
14731473 }
14741474 },
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+ },
14751512 "list_labels": {
14761513 "response": [
14771514 {
22522289 "updated_at": "2026-10-01T19:02:48.760Z"
22532290 }
22542291 },
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+ },
22552388 "get_merge_queue": {
22562389 "response": {
22572390 "enabled": true,
1033110464 "approval_policy": "all_external_contributors"
1033210465 }
1033310466 },
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+ },
1033410488 "create_repository_dispatch": {
1033510489 "params": {
1033610490 "owner": "flagon-io",
+5−0
176176 Op::GetPullRequest => through::<work::PullDetail>(op, sent),
177177 Op::MarkPullRequestReady
178178 | Op::ClosePullRequest
179+ | Op::ReopenPullRequest
180+ | Op::ConvertPullRequestToDraft
179181 | Op::MergePullRequest
180182 | Op::AssignIssue
181183 | Op::RequestReviewers
289291 fn every_route_has_a_sample() {
290292 let document = document();
291293 for route in crate::rest::ROUTES {
294+ if route.no_content() {
295+ continue;
296+ }
292297 let path = route
293298 .path
294299 .split('/')
+35−0
376376 Op::Protection(ProtectionOp::SetForkPrApproval),
377377 &[],
378378 ),
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), &[]),
379381 route("POST", "/repos/:owner/:name/dispatches", Op::Protection(ProtectionOp::CreateRepositoryDispatch), &[]),
380382 route(
381383 "GET",
842844 Op::AddComment,
843845 &[],
844846 ),
847+ route("PATCH", "/repos/:owner/:name/issues/comments/:comment_id", Op::EditComment, &[]),
848+ route("DELETE", "/repos/:owner/:name/issues/comments/:comment_id", Op::DeleteComment, &[]),
845849 route(
846850 "GET",
847851 "/repos/:owner/:name/pulls",
908912 Op::MarkPullRequestReady,
909913 &[],
910914 ),
915+ route("POST", "/repos/:owner/:name/pulls/:number/draft", Op::ConvertPullRequestToDraft, &[]),
911916 route(
912917 "POST",
913918 "/repos/:owner/:name/pulls/:number/close",
914919 Op::ClosePullRequest,
915920 &[],
916921 ),
922+ route("POST", "/repos/:owner/:name/pulls/:number/reopen", Op::ReopenPullRequest, &[]),
917923 route(
918924 "POST",
919925 "/repos/:owner/:name/pulls/:number/merge",
923929 ];
924930
925931 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+
926938 /// The names of the route's path parameters, in order.
927939 pub fn params(&self) -> impl Iterator<Item = &'static str> {
928940 self.path
12571269 }
12581270
12591271 #[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]
12601295 fn billing_is_addressed_by_workspace() {
12611296 let query = [("from".to_owned(), "2026-10-01".to_owned()), ("products".to_owned(), "agent,sandboxes".to_owned())];
12621297 let (route, input) = resolve("GET", "/workspaces/acme/usage", &query, Value::Null).unwrap();
+6−0
143143 a("close", Op::CloseIssue, "Close it without a pull request"),
144144 a("reopen", Op::ReopenIssue, "Reopen it"),
145145 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"),
146148 a("import", Op::ImportIssue, "Open an issue from a Jira, Linear or Sentry item"),
147149 ],
148150 },
160162 a("record_session", Op::RecordSession, "Append prompt, reasoning and tool entries to its session"),
161163 a("read_session", Op::ReadSession, "Its recorded session"),
162164 a("ready", Op::MarkPullRequestReady, "Mark a draft ready, with a summary"),
165+ a("draft", Op::ConvertPullRequestToDraft, "Turn it back into a draft"),
163166 a("request_reviewers", Op::RequestReviewers, "Ask people or teams to review it"),
164167 a("remove_requested_reviewers", Op::RemoveRequestedReviewers, "Stop asking people or teams to review it"),
165168 a("review", Op::ReviewPullRequest, "Approve or request changes"),
166169 a("close", Op::ClosePullRequest, "Close without merging"),
170+ a("reopen", Op::ReopenPullRequest, "Reopen a closed one"),
167171 a("merge", Op::MergePullRequest, "Land it, or join the merge queue"),
168172 a("merge_queue", Op::GetMergeQueue, "The repository's merge queue"),
169173 ],
251255 a("set_permissions", Op::Protection(ProtectionOp::SetWorkflowPermissions), "Set it: read or write"),
252256 a("get_approval_policy", Op::Protection(ProtectionOp::GetForkPrApproval), "Which pull requests' runs wait for approval"),
253257 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"),
254260 a("repository_dispatch", Op::Protection(ProtectionOp::CreateRepositoryDispatch), "Start repository_dispatch workflows with an event"),
255261 a("get_workspace_permissions", Op::Protection(ProtectionOp::GetWorkspaceWorkflowPermissions), "A workspace's default and maximum token permissions"),
256262 a("set_workspace_permissions", Op::Protection(ProtectionOp::SetWorkspaceWorkflowPermissions), "Set them, and whether jobs may open pull requests"),
+0−2
246246
247247 ### Workflow features not supported yet
248248
249−- Reusable workflows from another repository. Ones in the same repository
250− work.
251249 - Actions that upload or download artifacts with the toolkit's artifact
252250 library themselves. The library refuses to run against any server but
253251 github.com. `actions/upload-artifact`, `actions/download-artifact` and
+4−0
4343 | Change visibility | | | | | Yes, if the [member privileges](/guides/workspaces/#member-privileges) allow |
4444 | Transfer or delete the repository | | | | | Owners, or Admins if the member privileges allow |
4545
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+
4650 Changing a repository's visibility, transferring it and deleting it also
4751 depend on its workspace's [member privileges](/guides/workspaces/#member-privileges).
4852 By default, a member with Admin can change visibility, and only an owner
+119−8
2929
3030 | On GitHub | On g1t |
3131 | --- | --- |
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). |
3333 | `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. |
3435 | `strategy.matrix` with `include` and `exclude`, `fail-fast`, `max-parallel`, a matrix from `fromJSON(needs.…)` | The same. |
3536 | `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. |
3637 | `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. |
3738 | `${{ }}` expressions: every operator, function and context | The same, including `hashFiles`, `success()`, `failure()`, `always()` and `cancelled()`. |
3839 | `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). |
4041 | 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). |
4243 | `actions/checkout` | Checks out from g1t, with `ref`, `fetch-depth`, `path`, `repository`, `token` and `submodules`. |
4344 | `GITHUB_OUTPUT`, `GITHUB_ENV`, `GITHUB_PATH`, `GITHUB_STATE`, `GITHUB_STEP_SUMMARY` | The same. |
4445 | `::error::`, `::warning::`, `::notice::`, `::group::`, `::add-mask::` | The same: errors and warnings become annotations on the run, and [masked](#masking-secrets) values stay hidden. |
6667 - **Docker's `type=gha` build cache.** Buildx skips it on g1t, and the
6768 build runs without a cache. Use a registry cache instead; see
6869 [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.
7070 - **Actions that upload artifacts with the toolkit's artifact library
7171 themselves.** The library refuses to run against any server but
7272 github.com. `actions/upload-artifact`, `actions/download-artifact` and
7878 Why each of these is missing, and what to use instead, is on
7979 [What g1t can't do yet](/about/limitations/#actions-and-runners).
8080
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+
81189 ## The runner
82190
83191 Jobs run in a fresh sandbox each: Debian with Node 24, Python 3, Go, Rust,
652760 marks it ready, which on g1t is when it first has code. Each head runs
653761 each workflow once.
654762
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
659769 `demilestoned` too. List them under `types:` to run on them. For
660770 `labeled` and `unlabeled`, `github.event.label` names the label. A pull
661771 request's `branches` filter, `github.base_ref` and
10141124 | `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` |
10151125 | `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` |
10161126 | `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`) |
10171128 | `repository_dispatch` | `POST /repos/{owner}/{repo}/dispatches` with `event_type` and `client_payload` |
10181129 | `list_artifacts` | `GET /repos/{owner}/{repo}/actions/artifacts`, with `name`, `page`, `per_page` |
10191130 | `run_artifacts` | `GET …/actions/runs/{id}/artifacts`, with `name` |
+69−1
11 ---
22 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.
44 ---
55
66 At the foot of an open pull request's conversation, the **merge box** says
160160 the MCP server they are the `pull_request` tool's `request_reviewers` and
161161 `remove_requested_reviewers` actions.
162162
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+
163231 ## Conflicts
164232
165233 g1t works out whether a pull request merges cleanly into its target before
+9−0
5454 git push origin :refs/tags/v1.2.0
5555 ```
5656
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+
5766 ## From the API and MCP
5867
5968 | Route | MCP | What it does |
+4−0
8383 | `pull.base_changed` | The branch a pull request merges into changed. `data.base` names it. See [pull requests into other branches](/guides/base-branches/). |
8484 | `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. |
8585 | `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`. |
8688 | `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. |
8790 | `pull.assigned` | People were assigned to a pull request. `data.assignees` is everyone assigned now, `data.added` those newly assigned. |
8891 | `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. |
8992 | `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. |
101104 | `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. |
102105 | `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`. |
103106 | `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`. |
104108 | `queue.changed` | The merge queue gained, lost or settled an entry. |
105109 | `session.appended` | An agent's session grew. Busy: choose it only if you need it. |
106110 | `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. |
+7−1
323323 | [`close`](/reference/api/issues/close-issue/) | Close it as `completed` or `not_planned`. | `repo`, `number` | `issues:write` |
324324 | [`reopen`](/reference/api/issues/reopen-issue/) | Reopen a closed issue. | `repo`, `number` | `issues:write` |
325325 | [`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` |
326328 | [`import`](/reference/api/integrations/import-issue/) | Open an issue from a ticket, linked to it. `assign` assigns it to g1t. | `repo`, `reference` | `issues:write` |
327329
328330 `import` with `assign` also needs `agents:run`, since it puts an agent to
340342 | [`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` |
341343 | [`changes`](/reference/api/pull-requests/get-pull-request-changes/) | The files it changes, with line-by-line diffs. | `repo`, `number` | `pull_requests:read` |
342344 | [`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` |
344346 | [`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` |
345347 | [`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` |
346348 | [`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` |
347350 | [`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` |
348351 | [`remove_requested_reviewers`](/reference/api/pull-requests/remove-requested-reviewers/) | Stop asking them. Reviews they gave stay. | `repo`, `number` | `pull_requests:write` |
349352 | [`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` |
350353 | [`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` |
351355 | [`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` |
352356 | [`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` |
353357
455459 | [`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` |
456460 | [`get_approval_policy`](/reference/api/run-protection/get-fork-pr-approval/) | Which pull requests' runs wait for approval. | `repo` | `repo:read` |
457461 | [`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` |
458464 | [`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` |
459465 | [`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` |
460466 | [`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` |
+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

This change is too large to show in full.