Skip to content

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

g1t.sh

Commit

A workspace can manage workflows in its repositories, as GitHub's required workflows do, through g1t's managed mechanism. An owner adds a workflow file from one of the workspace's own repositories (`owner/repo/.g1t/workflows/file.yml@ref`) with a name, a mode and a target, kept in the actions service's new `workspace_managed_workflows` table (migration 0017): the file is copied at the commit its ref names when it is saved, so only an owner changes what runs with every repository's secrets, and copying it again takes up a change. The target is repository names and patterns (`*`, `!` to leave one out, the last match deciding; empty is every repository) and a visibility, and a source in a private repository runs in private repositories only, as reach.rs keeps a public repository from using a private one's workflows. Wherever a repository's workflows are read, a fork's pull request and the merge queue included, `g1t_actions::managed::arrange` puts its files and the managed workflows together: a required workflow runs as `managed/<name>.yml` and sets aside a file of its `name:`, so nothing else reports its check and a ruleset can require it, and can't be turned off in the repository; a default one gives way to a file of its name, as the workflows g1t's services install do, and a repository admin can turn it off. It runs as the repository's own workflow, with its token and secrets. A change syncs the first 20 repositories it reaches at once and leaves the rest to be synced when their workflows are listed or by the minute's sweep, a few a minute, their managed schedules waiting until then; it is recorded in the audit log. A workflow's view says what manages it (`managedBy`), and the Actions list now includes managed workflows, which it had left out. The API has `GET` and `POST /workspaces/{workspace}/actions/managed-workflows` and `PATCH` and `DELETE /workspaces/{workspace}/actions/managed-workflows/{workflow}`, owners only, and the `workflow` MCP tool `list_managed_workflows`, `add_managed_workflow`, `update_managed_workflow` and `remove_managed_workflow`; the reference and the OpenAPI copy say so.

Claude-Session:
https://claude.ai/code/session_01PnwTRSNaC7vEHTAuBzjhrn
Claudeagent and Claudeagentcommitted Parentc5c643bBrowse files
24 files+713−228 files not counted0/24 viewed
+4−0
496496 Op::Protection(ProtectionOp::CreateRepositoryDispatch),
497497 Op::Protection(ProtectionOp::GetWorkspaceWorkflowPermissions),
498498 Op::Protection(ProtectionOp::SetWorkspaceWorkflowPermissions),
499+ Op::Protection(ProtectionOp::ListWorkspaceManagedWorkflows),
500+ Op::Protection(ProtectionOp::CreateWorkspaceManagedWorkflow),
501+ Op::Protection(ProtectionOp::UpdateWorkspaceManagedWorkflow),
502+ Op::Protection(ProtectionOp::DeleteWorkspaceManagedWorkflow),
499503 ],
500504 ),
501505 (
+5−1
711711 }
712712
713713 impl Op {
714− pub const ALL: [Op; 380] = [
714+ pub const ALL: [Op; 384] = [
715715 Op::Whoami,
716716 Op::GetWorkspace,
717717 Op::CreateWorkspace,
10521052 Op::Protection(ProtectionOp::CreateRepositoryDispatch),
10531053 Op::Protection(ProtectionOp::GetWorkspaceWorkflowPermissions),
10541054 Op::Protection(ProtectionOp::SetWorkspaceWorkflowPermissions),
1055+ Op::Protection(ProtectionOp::ListWorkspaceManagedWorkflows),
1056+ Op::Protection(ProtectionOp::CreateWorkspaceManagedWorkflow),
1057+ Op::Protection(ProtectionOp::UpdateWorkspaceManagedWorkflow),
1058+ Op::Protection(ProtectionOp::DeleteWorkspaceManagedWorkflow),
10551059 Op::Tokens(TokenOp::GetTokenPolicy),
10561060 Op::Tokens(TokenOp::SetTokenPolicy),
10571061 Op::Tokens(TokenOp::ListMemberTokens),
+151−6
2929 CreateRepositoryDispatch,
3030 GetWorkspaceWorkflowPermissions,
3131 SetWorkspaceWorkflowPermissions,
32+ ListWorkspaceManagedWorkflows,
33+ CreateWorkspaceManagedWorkflow,
34+ UpdateWorkspaceManagedWorkflow,
35+ DeleteWorkspaceManagedWorkflow,
3236 }
3337
3438 impl ProtectionOp {
3539 /// Every one: `Op::ALL` lists each as `Op::Protection(…)`, which a test
3640 /// checks against this.
3741 #[cfg(test)]
38− pub const ALL: [ProtectionOp; 14] = [
42+ pub const ALL: [ProtectionOp; 18] = [
3943 ProtectionOp::UpdateEnvironment,
4044 ProtectionOp::DeleteEnvironment,
4145 ProtectionOp::GetPendingDeployments,
5054 ProtectionOp::CreateRepositoryDispatch,
5155 ProtectionOp::GetWorkspaceWorkflowPermissions,
5256 ProtectionOp::SetWorkspaceWorkflowPermissions,
57+ ProtectionOp::ListWorkspaceManagedWorkflows,
58+ ProtectionOp::CreateWorkspaceManagedWorkflow,
59+ ProtectionOp::UpdateWorkspaceManagedWorkflow,
60+ ProtectionOp::DeleteWorkspaceManagedWorkflow,
5361 ];
5462
5563 pub fn name(self) -> &'static str {
6876 ProtectionOp::CreateRepositoryDispatch => "create_repository_dispatch",
6977 ProtectionOp::GetWorkspaceWorkflowPermissions => "get_workspace_workflow_permissions",
7078 ProtectionOp::SetWorkspaceWorkflowPermissions => "set_workspace_workflow_permissions",
79+ ProtectionOp::ListWorkspaceManagedWorkflows => "list_workspace_managed_workflows",
80+ ProtectionOp::CreateWorkspaceManagedWorkflow => "create_workspace_managed_workflow",
81+ ProtectionOp::UpdateWorkspaceManagedWorkflow => "update_workspace_managed_workflow",
82+ ProtectionOp::DeleteWorkspaceManagedWorkflow => "delete_workspace_managed_workflow",
7183 }
7284 }
7385
7486 /// Whether it is about one repository, named by `repo`; the rest are a
7587 /// workspace's.
7688 pub fn needs_repo(self) -> bool {
77− !matches!(self, ProtectionOp::GetWorkspaceWorkflowPermissions | ProtectionOp::SetWorkspaceWorkflowPermissions)
89+ !self.managed() && !matches!(self, ProtectionOp::GetWorkspaceWorkflowPermissions | ProtectionOp::SetWorkspaceWorkflowPermissions)
90+ }
91+
92+ /// Whether it is about a workspace's managed workflows.
93+ fn managed(self) -> bool {
94+ matches!(
95+ self,
96+ ProtectionOp::ListWorkspaceManagedWorkflows
97+ | ProtectionOp::CreateWorkspaceManagedWorkflow
98+ | ProtectionOp::UpdateWorkspaceManagedWorkflow
99+ | ProtectionOp::DeleteWorkspaceManagedWorkflow
100+ )
78101 }
79102
80103 pub fn title(self) -> &'static str {
93116 ProtectionOp::CreateRepositoryDispatch => "Create a repository dispatch event",
94117 ProtectionOp::GetWorkspaceWorkflowPermissions => "Get a workspace's default workflow permissions",
95118 ProtectionOp::SetWorkspaceWorkflowPermissions => "Set a workspace's default workflow permissions",
119+ ProtectionOp::ListWorkspaceManagedWorkflows => "List the workflows a workspace manages",
120+ ProtectionOp::CreateWorkspaceManagedWorkflow => "Add a workflow a workspace manages",
121+ ProtectionOp::UpdateWorkspaceManagedWorkflow => "Update a workflow a workspace manages",
122+ ProtectionOp::DeleteWorkspaceManagedWorkflow => "Remove a workflow a workspace manages",
96123 }
97124 }
98125
111138 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.",
112139 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.",
113140 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.",
141+ ProtectionOp::ListWorkspaceManagedWorkflows => "The workflows a workspace's owners added to its repositories, g1t's required workflows: each runs in every repository it targets as that repository's own workflow (its token, its secrets, its checks), under the path managed/<name>.yml, from a copy of its source file taken when it was saved. Each has name, path, workflow_name (its `name:`), source (owner/repo/path@ref), source_sha (the commit it was copied at), source_private, mode, repositories, visibility, content (the copy that runs) and when and by whom it was last saved. Owners only.",
142+ ProtectionOp::CreateWorkspaceManagedWorkflow => "Add a workflow to a workspace's repositories. name (lowercase letters, digits and hyphens) makes its path managed/<name>.yml. source is a workflow file in one of the workspace's repositories, owner/repo/.g1t/workflows/file.yml@ref; g1t copies it at the commit ref names, and the copy runs until it is copied again (update_workspace_managed_workflow with refresh). mode is required (a repository's workflow of the same `name:` is set aside, and no one in the repository can turn it off, so its check can be required by a ruleset) or default (a repository's own workflow of the same name runs in its place, and the repository's admins can turn it off). repositories lists names and patterns (* matches any characters, ! excludes), read in order, the last match deciding; empty or left out is every repository. visibility is all (the default), public or private. A source in a private repository runs in private repositories only, so visibility must be private. Owners only. Returns the workflow.",
143+ ProtectionOp::UpdateWorkspaceManagedWorkflow => "Change a workflow a workspace manages: its source, mode, repositories or visibility; fields left out stay as they are. Giving source, or refresh: true, copies the source again at the commit its ref names now. Owners only. Returns the workflow.",
144+ ProtectionOp::DeleteWorkspaceManagedWorkflow => "Take a workflow out of every repository of the workspace. Its runs stay. Owners only.",
114145 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.",
115146 }
116147 }
124155 | ProtectionOp::GetForkPrApproval
125156 | ProtectionOp::GetActionsAccess
126157 | ProtectionOp::GetWorkspaceWorkflowPermissions
158+ | ProtectionOp::ListWorkspaceManagedWorkflows
127159 )
128160 }
129161
201233 }),
202234 &["repo"],
203235 ),
204− ProtectionOp::GetWorkspaceWorkflowPermissions => (json!({ "workspace": workspace }), &["workspace"]),
236+ ProtectionOp::GetWorkspaceWorkflowPermissions | ProtectionOp::ListWorkspaceManagedWorkflows => (json!({ "workspace": workspace }), &["workspace"]),
237+ ProtectionOp::CreateWorkspaceManagedWorkflow => (
238+ json!({
239+ "workspace": workspace,
240+ "name": { "type": "string", "description": "Lowercase letters, digits and hyphens: its path is managed/<name>.yml." },
241+ "source": managed_source(),
242+ "mode": managed_mode(),
243+ "repositories": managed_repositories(),
244+ "visibility": managed_visibility(),
245+ }),
246+ &["workspace", "name", "source", "mode"],
247+ ),
248+ ProtectionOp::UpdateWorkspaceManagedWorkflow => (
249+ json!({
250+ "workspace": workspace,
251+ "workflow": managed_name(),
252+ "source": managed_source(),
253+ "mode": managed_mode(),
254+ "repositories": managed_repositories(),
255+ "visibility": managed_visibility(),
256+ "refresh": { "type": "boolean", "description": "Copy the source again, at the commit its ref names now." },
257+ }),
258+ &["workspace", "workflow"],
259+ ),
260+ ProtectionOp::DeleteWorkspaceManagedWorkflow => (json!({ "workspace": workspace, "workflow": managed_name() }), &["workspace", "workflow"]),
205261 ProtectionOp::SetWorkspaceWorkflowPermissions => (
206262 json!({
207263 "workspace": workspace,
231287 }
232288 }
233289
290+fn managed_name() -> Value {
291+ json!({ "type": "string", "description": "The managed workflow's name, as in managed/<name>.yml." })
292+}
293+
294+fn managed_source() -> Value {
295+ json!({ "type": "string", "description": "A workflow file in one of the workspace's repositories: owner/repo/.g1t/workflows/file.yml@ref." })
296+}
297+
298+fn managed_mode() -> Value {
299+ json!({ "type": "string", "enum": ["required", "default"], "description": "required: a repository cannot take its place or turn it off. default: a repository's workflow of the same name takes its place." })
300+}
301+
302+fn managed_repositories() -> Value {
303+ json!({ "type": "array", "items": { "type": "string" }, "description": "Repository names and patterns (* matches any characters, ! excludes); empty is every repository." })
304+}
305+
306+fn managed_visibility() -> Value {
307+ json!({ "type": "string", "enum": ["all", "public", "private"], "description": "Which repositories, by visibility. A private source needs private." })
308+}
309+
310+/// A workspace's managed workflow, in the API's shape.
311+fn managed_view(workflow: &Value) -> Value {
312+ json!({
313+ "name": workflow["name"],
314+ "path": workflow["path"],
315+ "workflow_name": workflow["workflowName"],
316+ "source": workflow["source"],
317+ "source_sha": workflow["sourceSha"],
318+ "source_private": workflow["sourcePrivate"],
319+ "mode": workflow["mode"],
320+ "repositories": workflow["repositories"],
321+ "visibility": workflow["visibility"],
322+ "content": workflow["content"],
323+ "created_at": workflow["createdAt"],
324+ "updated_at": workflow["updatedAt"],
325+ "updated_by": workflow["updatedBy"],
326+ })
327+}
328+
329+/// A workspace's managed workflows: listed, added, changed and removed.
330+async fn run_managed(op: ProtectionOp, actions: &worker::Fetcher, viewer: &Viewer, actor: g1t_contracts::User, workspace: String, input: &Value) -> Result<Outcome<Value>> {
331+ let strings = |key: &str| -> Option<Vec<String>> {
332+ input.get(key).and_then(Value::as_array).map(|items| items.iter().filter_map(Value::as_str).map(str::to_owned).collect())
333+ };
334+ Ok(match op {
335+ ProtectionOp::ListWorkspaceManagedWorkflows => {
336+ let listed: Outcome<Vec<Value>> = g1t_kit::call(actions, "workspace_managed_workflows", &json!({ "viewer": viewer, "workspace": workspace })).await?;
337+ map(listed, |list| json!({ "total_count": list.len(), "managed_workflows": list.iter().map(managed_view).collect::<Vec<_>>() }))
338+ }
339+ ProtectionOp::CreateWorkspaceManagedWorkflow | ProtectionOp::UpdateWorkspaceManagedWorkflow => {
340+ let create = op == ProtectionOp::CreateWorkspaceManagedWorkflow;
341+ let name = if create { text(input, "name") } else { text(input, "workflow") };
342+ let saved: Outcome<Value> = g1t_kit::call(
343+ actions,
344+ "set_workspace_managed_workflow",
345+ &json!({
346+ "actor": actor,
347+ "workspace": workspace,
348+ "name": name.unwrap_or_default(),
349+ "create": create,
350+ "source": text(input, "source"),
351+ "mode": text(input, "mode"),
352+ "repositories": strings("repositories"),
353+ "visibility": text(input, "visibility"),
354+ "refresh": flag(input, "refresh").unwrap_or(false),
355+ }),
356+ )
357+ .await?;
358+ map(saved, |workflow| managed_view(&workflow))
359+ }
360+ ProtectionOp::DeleteWorkspaceManagedWorkflow => {
361+ let removed: Outcome<bool> = g1t_kit::call(
362+ actions,
363+ "remove_workspace_managed_workflow",
364+ &json!({ "actor": actor, "workspace": workspace, "name": text(input, "workflow").unwrap_or_default() }),
365+ )
366+ .await?;
367+ map(removed, |removed| json!({ "deleted": removed }))
368+ }
369+ _ => Outcome::fail(FailureCode::Invalid, "Not a managed workflow operation."),
370+ })
371+}
372+
234373 fn text(input: &Value, key: &str) -> Option<String> {
235374 match &input[key] {
236375 Value::String(text) if !text.trim().is_empty() => Some(text.trim().to_owned()),
455594 let Some(workspace) = text(input, "workspace") else {
456595 return Ok(Outcome::fail(FailureCode::Invalid, "Name the workspace."));
457596 };
597+ if op.managed() {
598+ return run_managed(op, actions, viewer, actor(), workspace, input).await;
599+ }
458600 let settings: Outcome<Value> = if op == ProtectionOp::GetWorkspaceWorkflowPermissions {
459601 g1t_kit::call(actions, "workspace_actions_settings", &json!({ "viewer": viewer, "workspace": workspace })).await?
460602 } else {
587729 map(started, |runs| json!({ "runs": runs }))
588730 }
589731 // Answered above, before a repository is read.
590− ProtectionOp::GetWorkspaceWorkflowPermissions | ProtectionOp::SetWorkspaceWorkflowPermissions => {
591− Outcome::fail(FailureCode::Invalid, "Name the workspace.")
592− }
732+ ProtectionOp::GetWorkspaceWorkflowPermissions
733+ | ProtectionOp::SetWorkspaceWorkflowPermissions
734+ | ProtectionOp::ListWorkspaceManagedWorkflows
735+ | ProtectionOp::CreateWorkspaceManagedWorkflow
736+ | ProtectionOp::UpdateWorkspaceManagedWorkflow
737+ | ProtectionOp::DeleteWorkspaceManagedWorkflow => Outcome::fail(FailureCode::Invalid, "Name the workspace."),
593738 })
594739 }
595740
+98−0
1222812228 },
1222912229 "notes": "A maximum of read holds every repository's default to read-only, and makes the workspace's default read too."
1223012230 },
12231+ "list_workspace_managed_workflows": {
12232+ "params": {
12233+ "workspace": "flagon-io"
12234+ },
12235+ "response": {
12236+ "total_count": 1,
12237+ "managed_workflows": [
12238+ {
12239+ "name": "security-scan",
12240+ "path": "managed/security-scan.yml",
12241+ "workflow_name": "Security scan",
12242+ "source": "flagon-io/policies/.g1t/workflows/security-scan.yml@main",
12243+ "source_sha": "4f1c2a9e0b7d3c5a8e6f1b2d4c7a9e0f3b5d8c1a",
12244+ "source_private": false,
12245+ "mode": "required",
12246+ "repositories": [
12247+ "*",
12248+ "!sandbox-*"
12249+ ],
12250+ "visibility": "all",
12251+ "content": "name: Security scan\non: [push, pull_request]\njobs:\n scan:\n runs-on: ubuntu-latest\n steps:\n - uses: actions/checkout@v4\n - run: ./scripts/scan.sh\n",
12252+ "created_at": "2026-10-12T09:30:00Z",
12253+ "updated_at": "2026-10-12T09:30:00Z",
12254+ "updated_by": "ada"
12255+ }
12256+ ]
12257+ }
12258+ },
12259+ "create_workspace_managed_workflow": {
12260+ "params": {
12261+ "workspace": "flagon-io"
12262+ },
12263+ "request": {
12264+ "name": "security-scan",
12265+ "source": "flagon-io/policies/.g1t/workflows/security-scan.yml@main",
12266+ "mode": "required",
12267+ "repositories": [
12268+ "*",
12269+ "!sandbox-*"
12270+ ]
12271+ },
12272+ "response": {
12273+ "name": "security-scan",
12274+ "path": "managed/security-scan.yml",
12275+ "workflow_name": "Security scan",
12276+ "source": "flagon-io/policies/.g1t/workflows/security-scan.yml@main",
12277+ "source_sha": "4f1c2a9e0b7d3c5a8e6f1b2d4c7a9e0f3b5d8c1a",
12278+ "source_private": false,
12279+ "mode": "required",
12280+ "repositories": [
12281+ "*",
12282+ "!sandbox-*"
12283+ ],
12284+ "visibility": "all",
12285+ "content": "name: Security scan\non: [push, pull_request]\njobs:\n scan:\n runs-on: ubuntu-latest\n steps:\n - uses: actions/checkout@v4\n - run: ./scripts/scan.sh\n",
12286+ "created_at": "2026-10-12T09:30:00Z",
12287+ "updated_at": "2026-10-12T09:30:00Z",
12288+ "updated_by": "ada"
12289+ },
12290+ "notes": "g1t copies the source when you save it, and that copy runs in each repository the workflow targets, as the repository's own workflow, with its token and secrets. A source in a private repository runs in private repositories only: give visibility private."
12291+ },
12292+ "update_workspace_managed_workflow": {
12293+ "params": {
12294+ "workspace": "flagon-io",
12295+ "workflow": "security-scan"
12296+ },
12297+ "request": {
12298+ "refresh": true
12299+ },
12300+ "response": {
12301+ "name": "security-scan",
12302+ "path": "managed/security-scan.yml",
12303+ "workflow_name": "Security scan",
12304+ "source": "flagon-io/policies/.g1t/workflows/security-scan.yml@main",
12305+ "source_sha": "4f1c2a9e0b7d3c5a8e6f1b2d4c7a9e0f3b5d8c1a",
12306+ "source_private": false,
12307+ "mode": "required",
12308+ "repositories": [
12309+ "*",
12310+ "!sandbox-*"
12311+ ],
12312+ "visibility": "all",
12313+ "content": "name: Security scan\non: [push, pull_request]\njobs:\n scan:\n runs-on: ubuntu-latest\n steps:\n - uses: actions/checkout@v4\n - run: ./scripts/scan.sh\n",
12314+ "created_at": "2026-10-12T09:30:00Z",
12315+ "updated_at": "2026-10-12T09:30:00Z",
12316+ "updated_by": "ada"
12317+ },
12318+ "notes": "refresh copies the source again at the commit its ref names now. Fields left out stay as they are."
12319+ },
12320+ "delete_workspace_managed_workflow": {
12321+ "params": {
12322+ "workspace": "flagon-io",
12323+ "workflow": "security-scan"
12324+ },
12325+ "response": {
12326+ "deleted": true
12327+ }
12328+ },
1223112329 "get_token_policy": {
1223212330 "params": {
1223312331 "workspace": "flagon-io"
+24−0
421421 Op::Protection(ProtectionOp::SetWorkspaceWorkflowPermissions),
422422 &[],
423423 ),
424+ route(
425+ "GET",
426+ "/workspaces/:workspace/actions/managed-workflows",
427+ Op::Protection(ProtectionOp::ListWorkspaceManagedWorkflows),
428+ &[],
429+ ),
430+ route(
431+ "POST",
432+ "/workspaces/:workspace/actions/managed-workflows",
433+ Op::Protection(ProtectionOp::CreateWorkspaceManagedWorkflow),
434+ &[],
435+ ),
436+ route(
437+ "PATCH",
438+ "/workspaces/:workspace/actions/managed-workflows/:workflow",
439+ Op::Protection(ProtectionOp::UpdateWorkspaceManagedWorkflow),
440+ &[],
441+ ),
442+ route(
443+ "DELETE",
444+ "/workspaces/:workspace/actions/managed-workflows/:workflow",
445+ Op::Protection(ProtectionOp::DeleteWorkspaceManagedWorkflow),
446+ &[],
447+ ),
424448 route("GET", "/repos/:owner/:name/queue", Op::GetMergeQueue, &[]),
425449 route(
426450 "POST",
+5−1
227227 Tool {
228228 name: "workflow",
229229 title: "Workflows",
230− description: "GitHub Actions workflows from .g1t/workflows: their runs, jobs and logs, and running, cancelling or rerunning them. Runs' artifacts: listing, a download link, deleting, and how long they are kept. Deployments wherever they run (reported from any CI, made by jobs with an `environment:`, or built on g1t.page), their statuses and environments, and reporting your own; environments' protection rules, approving or rejecting the jobs they hold, approving a pull request's run from outside, the token's default permissions and repository dispatch. Checks on commits: statuses, check runs (a g1t Actions job is one) and check suites, to read where a commit stands or report on it from CI or an integration. Also the self-hosted runners they run on: a workspace's (`workspace`) or a repository's own (`repo`), their groups, and where agent work runs.",
230+ description: "GitHub Actions workflows from .g1t/workflows: their runs, jobs and logs, and running, cancelling or rerunning them. Runs' artifacts: listing, a download link, deleting, and how long they are kept. Deployments wherever they run (reported from any CI, made by jobs with an `environment:`, or built on g1t.page), their statuses and environments, and reporting your own; environments' protection rules, approving or rejecting the jobs they hold, approving a pull request's run from outside, the token's default permissions, repository dispatch, and the workflows a workspace runs in its repositories without a file in them. Checks on commits: statuses, check runs (a g1t Actions job is one) and check suites, to read where a commit stands or report on it from CI or an integration. Also the self-hosted runners they run on: a workspace's (`workspace`) or a repository's own (`repo`), their groups, and where agent work runs.",
231231 default_action: None,
232232 actions: &[
233233 a("list", Op::ListWorkflows, "Workflows on the default branch"),
279279 a("repository_dispatch", Op::Protection(ProtectionOp::CreateRepositoryDispatch), "Start repository_dispatch workflows with an event"),
280280 a("get_workspace_permissions", Op::Protection(ProtectionOp::GetWorkspaceWorkflowPermissions), "A workspace's default and maximum token permissions"),
281281 a("set_workspace_permissions", Op::Protection(ProtectionOp::SetWorkspaceWorkflowPermissions), "Set them, and whether jobs may open pull requests"),
282+ a("list_managed_workflows", Op::Protection(ProtectionOp::ListWorkspaceManagedWorkflows), "The workflows a workspace runs in its repositories"),
283+ a("add_managed_workflow", Op::Protection(ProtectionOp::CreateWorkspaceManagedWorkflow), "Run a workflow in the workspace's repositories, required or default"),
284+ a("update_managed_workflow", Op::Protection(ProtectionOp::UpdateWorkspaceManagedWorkflow), "Change one's repositories or mode, or copy its source again"),
285+ a("remove_managed_workflow", Op::Protection(ProtectionOp::DeleteWorkspaceManagedWorkflow), "Take one out of the workspace's repositories"),
282286 a("list_runners", Op::ListRunners, "Self-hosted runners, with status, labels and what each is doing"),
283287 a("create_runner_token", Op::CreateRunnerRegistrationToken, "A one-hour token for g1t-runner register"),
284288 a("remove_runner", Op::RemoveRunner, "Remove a self-hosted runner"),
+4−0
474474 | [`set_permissions`](/reference/api/run-protection/set-workflow-permissions/) | Set `default_workflow_permissions` (`read`, `write` or `inherit`) and `can_approve_pull_request_reviews`. Admin role. | `repo` | `repo:admin` |
475475 | [`get_workspace_permissions`](/reference/api/run-protection/get-workspace-workflow-permissions/) | A workspace's default for new repositories, its maximum, and whether its repositories may let jobs open and approve pull requests. Members. | `workspace` | `workspace:read` |
476476 | [`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` |
477+| [`list_managed_workflows`](/reference/api/run-protection/list-workspace-managed-workflows/) | The [workflows a workspace manages](/guides/actions/#workflows-a-workspace-manages) in its repositories, each with its source, the commit it was copied at, its `mode`, `repositories` and `visibility`, and the copy that runs. Owners. | `workspace` | `workspace:read` |
478+| [`add_managed_workflow`](/reference/api/run-protection/create-workspace-managed-workflow/) | Run a workflow file of one of the workspace's repositories (`source`, `owner/repo/.g1t/workflows/file.yml@ref`, copied now) in its repositories as `managed/<name>.yml`: `mode` `required` or `default`, `repositories` (names and patterns, `!` excludes; empty for all) and `visibility` (`all`, `public` or `private`; a private source needs `private`). Owners. | `workspace`, `name`, `source`, `mode` | `workspace:admin` |
479+| [`update_managed_workflow`](/reference/api/run-protection/update-workspace-managed-workflow/) | Change one's `source`, `mode`, `repositories` or `visibility`; `refresh` copies its source again. Owners. | `workspace`, `workflow` | `workspace:admin` |
480+| [`remove_managed_workflow`](/reference/api/run-protection/delete-workspace-managed-workflow/) | Take one out of every repository; its runs stay. Owners. | `workspace`, `workflow` | `workspace:admin` |
477481 | [`get_approval_policy`](/reference/api/run-protection/get-fork-pr-approval/) | Which pull requests' runs wait for approval. | `repo` | `repo:read` |
478482 | [`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` |
479483 | [`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` |
+0−0

Binary or large file; its contents are not shown.

+220−14
55 //! [`FOLDER`] that no file can have.
66 //!
77 //! A repository takes one over by having a workflow of the same `name:` in
8−//! `.g1t/workflows/`: that file runs instead ([`prefer_repository`]). So
9−//! moving a managed workflow into the repository is committing its source.
8+//! `.g1t/workflows/`: that file runs instead ([`arrange`]). So moving a
9+//! managed workflow into the repository is committing its source. A
10+//! workspace's managed workflows ([`Target`], [`Mode`]) are the exception
11+//! when they are required: then the repository's file of that name is set
12+//! aside instead, and no repository can turn the workflow off.
1013 //!
1114 //! [`deploy_workflow`] writes the one deployments install: it calls g1t's
1215 //! own reusable deploy workflow with the project's settings as its inputs.
117120 workflow::parse(source).map(|parsed| parsed.display_name(path)).unwrap_or_else(|_| path.to_owned())
118121 }
119122
120−/// The managed workflows that run beside a repository's own `files`, each
121−/// as `(path, source)`: every one, except those a file of the same `name:`
122−/// takes the place of.
123−pub fn prefer_repository<'a>(files: impl IntoIterator<Item = (&'a str, &'a str)>, managed: Vec<(String, String)>) -> Vec<(String, String)> {
124− let theirs: Vec<String> = files.into_iter().map(|(path, source)| listed_name(path, source)).collect();
125− managed.into_iter().filter(|(path, source)| !theirs.contains(&listed_name(path, source))).collect()
123+/// Whether `name` can end a managed workflow's path: lowercase letters,
124+/// digits and hyphens, at most 64.
125+pub fn valid_name(name: &str) -> bool {
126+ !name.is_empty() && name.len() <= 64 && name.bytes().all(|byte| byte.is_ascii_lowercase() || byte.is_ascii_digit() || byte == b'-')
127+}
128+
129+/// How a workspace's managed workflow holds in its repositories.
130+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
131+pub enum Mode {
132+ /// It runs in every repository it targets: a workflow of the same
133+ /// `name:` in the repository is set aside, and no repository can turn
134+ /// it off, so its check can be required.
135+ Required,
136+ /// It runs where the repository has no workflow of the same `name:`,
137+ /// and the repository's admins can turn it off.
138+ Default,
139+}
140+
141+impl Mode {
142+ pub fn parse(text: &str) -> Option<Mode> {
143+ match text.trim().to_ascii_lowercase().as_str() {
144+ "required" => Some(Mode::Required),
145+ "default" => Some(Mode::Default),
146+ _ => None,
147+ }
148+ }
149+
150+ pub fn as_str(self) -> &'static str {
151+ match self {
152+ Mode::Required => "required",
153+ Mode::Default => "default",
154+ }
155+ }
156+}
157+
158+/// The visibilities a target can name.
159+pub const VISIBILITIES: [&str; 3] = ["all", "public", "private"];
160+
161+/// Which of a workspace's repositories one of its managed workflows runs in.
162+#[derive(Clone, Debug, Default, PartialEq, Eq)]
163+pub struct Target {
164+ /// Repository names and patterns (`*`, `[a-z]`; `!` excludes what an
165+ /// earlier one included), read in order, the last match deciding,
166+ /// without regard to case. Empty is every repository; only exclusions
167+ /// is every repository but those.
168+ pub repositories: Vec<String>,
169+ /// One of [`VISIBILITIES`].
170+ pub visibility: String,
171+}
172+
173+impl Target {
174+ /// Whether it takes in the repository `name`, private or not. A
175+ /// workflow read from a private repository (`source_private`) never
176+ /// runs in a public one, whose workflow page and logs anyone can read.
177+ pub fn includes(&self, name: &str, private: bool, source_private: bool) -> bool {
178+ if source_private && !private {
179+ return false;
180+ }
181+ match self.visibility.as_str() {
182+ "public" if private => return false,
183+ "private" if !private => return false,
184+ _ => {}
185+ }
186+ if self.repositories.is_empty() {
187+ return true;
188+ }
189+ let lowered: Vec<String> = self.repositories.iter().map(|pattern| pattern.trim().to_ascii_lowercase()).collect();
190+ let mut patterns = crate::filter::Patterns::new(&lowered);
191+ if patterns.0.iter().all(|pattern| pattern.negated) {
192+ patterns.0.insert(0, crate::filter::Pattern::parse("*"));
193+ }
194+ patterns.includes(&name.to_ascii_lowercase())
195+ }
196+}
197+
198+/// A workflow a repository runs, as [`arrange`] decides.
199+#[derive(Clone, Debug, PartialEq, Eq)]
200+pub struct Arranged {
201+ pub path: String,
202+ pub source: String,
203+ /// A workspace's required workflow: nothing turns it off.
204+ pub required: bool,
205+}
206+
207+/// The workflows a repository runs: its own `files`, and the managed
208+/// workflows offered to it, those `installed` in it by g1t's services (such
209+/// as a project's deploy workflow) and its workspace's (`workspace`, each
210+/// with its [`Mode`]), all as `(path, source)`.
211+///
212+/// - The workspace's required workflows run, and a file of the same
213+/// `name:` is set aside, so nothing else reports their checks.
214+/// - Then the repository's own files.
215+/// - Then each other managed workflow that no workflow before it of the
216+/// same `name:` or path takes the place of: those installed in the
217+/// repository first, then the workspace's defaults.
218+pub fn arrange(files: Vec<(String, String)>, installed: Vec<(String, String)>, workspace: Vec<(String, String, Mode)>) -> Vec<Arranged> {
219+ let mut names: Vec<String> = Vec::new();
220+ let mut paths: Vec<String> = Vec::new();
221+ let mut out: Vec<Arranged> = Vec::new();
222+ let (required, defaults): (Vec<_>, Vec<_>) = workspace.into_iter().partition(|(_, _, mode)| *mode == Mode::Required);
223+ let offer = |path: String, source: String, required: bool, out: &mut Vec<Arranged>, names: &mut Vec<String>, paths: &mut Vec<String>| {
224+ let name = listed_name(&path, &source);
225+ if names.contains(&name) || paths.contains(&path) {
226+ return;
227+ }
228+ names.push(name);
229+ paths.push(path.clone());
230+ out.push(Arranged { path, source, required });
231+ };
232+ for (path, source, _) in required {
233+ offer(path, source, true, &mut out, &mut names, &mut paths);
234+ }
235+ // A repository's files are set aside only for a required workflow's
236+ // name, never for each other's.
237+ let required_names = names.clone();
238+ for (path, source) in files {
239+ let name = listed_name(&path, &source);
240+ if required_names.contains(&name) {
241+ continue;
242+ }
243+ names.push(name);
244+ paths.push(path.clone());
245+ out.push(Arranged { path, source, required: false });
246+ }
247+ for (path, source) in installed.into_iter().chain(defaults.into_iter().map(|(path, source, _)| (path, source))) {
248+ offer(path, source, false, &mut out, &mut names, &mut paths);
249+ }
250+ out
126251 }
127252
128253 #[cfg(test)]
192317 assert_eq!(with["root-dir"], ".");
193318 }
194319
320+ fn own(path: &str, name: &str) -> (String, String) {
321+ (path.to_owned(), format!("name: {name}\non: push\njobs:\n a:\n runs-on: ubuntu-latest\n steps: [{{ run: 'true' }}]\n"))
322+ }
323+
324+ fn paths(arranged: &[Arranged]) -> Vec<&str> {
325+ arranged.iter().map(|kept| kept.path.as_str()).collect()
326+ }
327+
195328 #[test]
196329 fn a_repository_workflow_of_the_same_name_takes_its_place() {
197330 let deploy = (path("deploy"), deploy_workflow(&settings()).unwrap());
198− let other = (path("scan"), "name: Scan\non: push\njobs:\n a:\n runs-on: ubuntu-latest\n steps: [{ run: 'true' }]\n".to_owned());
199− let own = "name: g1t\non: push\njobs:\n a:\n runs-on: ubuntu-latest\n steps: [{ run: 'true' }]\n";
200− let kept = prefer_repository([(".g1t/workflows/g1t.yml", own)], vec![deploy.clone(), other.clone()]);
201− assert_eq!(kept, vec![other.clone()]);
202− let kept = prefer_repository([(".g1t/workflows/ci.yml", "name: CI\non: push\njobs: {}\n")], vec![deploy.clone(), other]);
203− assert_eq!(kept.len(), 2);
331+ let other = own(&path("scan"), "Scan");
332+ let kept = arrange(vec![own(".g1t/workflows/g1t.yml", "g1t")], vec![deploy.clone(), other.clone()], vec![]);
333+ assert_eq!(paths(&kept), [".g1t/workflows/g1t.yml", "managed/scan.yml"]);
334+ let kept = arrange(vec![own(".g1t/workflows/ci.yml", "CI")], vec![deploy.clone(), other], vec![]);
335+ assert_eq!(kept.len(), 3);
336+ assert!(kept.iter().all(|kept| !kept.required));
204337 assert!(is_managed(&deploy.0) && !is_managed(".g1t/workflows/g1t.yml") && !is_managed("managedx/a.yml"));
205338 }
339+
340+ #[test]
341+ fn a_required_workflow_sets_aside_a_file_of_its_name() {
342+ let files = vec![own(".g1t/workflows/scan.yml", "Scan"), own(".g1t/workflows/ci.yml", "CI")];
343+ let kept = arrange(files, vec![], vec![(path("scan"), own("", "Scan").1, Mode::Required)]);
344+ assert_eq!(paths(&kept), ["managed/scan.yml", ".g1t/workflows/ci.yml"]);
345+ assert!(kept[0].required && !kept[1].required);
346+ }
347+
348+ #[test]
349+ fn a_default_workflow_gives_way_to_a_file_of_its_name() {
350+ let files = vec![own(".g1t/workflows/lint.yml", "Lint")];
351+ let workspace = vec![(path("lint"), own("", "Lint").1, Mode::Default), (path("audit"), own("", "Audit").1, Mode::Default)];
352+ let kept = arrange(files, vec![], workspace);
353+ assert_eq!(paths(&kept), [".g1t/workflows/lint.yml", "managed/audit.yml"]);
354+ assert!(kept.iter().all(|kept| !kept.required));
355+ }
356+
357+ #[test]
358+ fn a_repository_keeps_two_files_of_one_name() {
359+ let files = vec![own(".g1t/workflows/a.yml", "Same"), own(".g1t/workflows/b.yml", "Same")];
360+ assert_eq!(arrange(files, vec![], vec![]).len(), 2);
361+ }
362+
363+ #[test]
364+ fn what_is_installed_in_the_repository_comes_between_required_and_default() {
365+ // The same path: required beats installed, installed beats default.
366+ let installed = vec![own(&path("deploy-web"), "Deploy")];
367+ let kept = arrange(vec![], installed.clone(), vec![(path("deploy-web"), own("", "Policy").1, Mode::Required)]);
368+ assert_eq!(kept.len(), 1);
369+ assert!(kept[0].source.contains("Policy") && kept[0].required);
370+ let kept = arrange(vec![], installed, vec![(path("deploy-web"), own("", "Policy").1, Mode::Default)]);
371+ assert_eq!(kept.len(), 1);
372+ assert!(kept[0].source.contains("Deploy") && !kept[0].required);
373+ // The same name at another path: the first offered stays.
374+ let kept = arrange(vec![], vec![own(&path("a"), "Same")], vec![(path("b"), own("", "Same").1, Mode::Default)]);
375+ assert_eq!(paths(&kept), ["managed/a.yml"]);
376+ }
377+
378+ #[test]
379+ fn targets_take_names_patterns_and_visibility() {
380+ let all = Target { repositories: vec![], visibility: "all".into() };
381+ assert!(all.includes("web", true, false) && all.includes("web", false, false));
382+ let some = Target { repositories: vec!["web".into(), "api-*".into()], visibility: "all".into() };
383+ assert!(some.includes("web", false, false) && some.includes("API-gateway", false, false));
384+ assert!(!some.includes("docs", false, false) && !some.includes("webx", false, false));
385+ let but = Target { repositories: vec!["!sandbox-*".into()], visibility: "all".into() };
386+ assert!(but.includes("web", false, false) && !but.includes("sandbox-1", false, false));
387+ let then_back = Target { repositories: vec!["*".into(), "!sandbox-*".into(), "sandbox-keep".into()], visibility: "all".into() };
388+ assert!(then_back.includes("sandbox-keep", false, false) && !then_back.includes("sandbox-2", false, false));
389+ let private = Target { repositories: vec![], visibility: "private".into() };
390+ assert!(private.includes("web", true, false) && !private.includes("web", false, false));
391+ let public = Target { repositories: vec![], visibility: "public".into() };
392+ assert!(!public.includes("web", true, false) && public.includes("web", false, false));
393+ }
394+
395+ #[test]
396+ fn a_private_source_never_runs_in_a_public_repository() {
397+ let all = Target { repositories: vec!["*".into()], visibility: "all".into() };
398+ assert!(all.includes("web", true, true));
399+ assert!(!all.includes("site", false, true));
400+ }
401+
402+ #[test]
403+ fn modes_and_names_read_as_written() {
404+ assert_eq!(Mode::parse("Required"), Some(Mode::Required));
405+ assert_eq!(Mode::parse("default").map(Mode::as_str), Some("default"));
406+ assert_eq!(Mode::parse("optional"), None);
407+ assert!(valid_name("deploy") && valid_name("code-scanning-2"));
408+ for bad in ["", "Deploy", "a/b", "a.yml", "../x", &"a".repeat(65)] {
409+ assert!(!valid_name(bad), "{bad}");
410+ }
411+ }
206412 }
+98−0
4747 /// `on.workflow_dispatch.inputs` as written, when it can be run by hand.
4848 pub dispatch: Option<Value>,
4949 pub last_run: Option<WorkflowRun>,
50+ /// For a managed workflow (`managed/<name>.yml`): what installed it.
51+ #[serde(default)]
52+ pub managed_by: Option<ManagedBy>,
5053 }
5154
55+/// What installed a managed workflow in a repository.
56+#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
57+#[serde(rename_all = "camelCase")]
58+pub struct ManagedBy {
59+ /// `g1t` for one g1t's services installed (such as a project's deploy
60+ /// workflow), or the workspace's name for one its owners added.
61+ pub origin: String,
62+ /// The workspace's: `required` or `default`. Empty for g1t's.
63+ #[serde(default)]
64+ pub mode: String,
65+ /// The workspace's: the file it was copied from, as
66+ /// `owner/repo/path@ref`.
67+ #[serde(default)]
68+ pub source: Option<String>,
69+}
70+
5271 #[derive(Clone, Debug, Serialize, Deserialize)]
5372 #[serde(rename_all = "camelCase")]
5473 pub struct WorkflowRun {
803822 pub production: bool,
804823 }
805824
825+/// A workflow a workspace's owners added to its repositories
826+/// (`g1t_actions::managed::Target`): it runs in each repository it targets
827+/// as that repository's own, with its token and secrets, under the path
828+/// `managed/<name>.yml`, from a copy of its source taken when it was saved.
829+#[derive(Clone, Debug, Serialize, Deserialize)]
830+#[serde(rename_all = "camelCase")]
831+pub struct WorkspaceManagedWorkflow {
832+ /// Lowercase letters, digits and hyphens: its path is
833+ /// `managed/<name>.yml`.
834+ pub name: String,
835+ pub path: String,
836+ /// Its `name:`, as repositories list it; a repository's workflow of
837+ /// this name takes a default one's place.
838+ pub workflow_name: String,
839+ /// Where it was copied from: `owner/repo/path@ref`, a repository of the
840+ /// same workspace.
841+ pub source: String,
842+ /// The commit it was copied at.
843+ pub source_sha: Option<String>,
844+ /// Whether the source repository is private, so the workflow runs in
845+ /// private repositories only.
846+ pub source_private: bool,
847+ /// `required` or `default`.
848+ pub mode: String,
849+ /// Names and patterns of the repositories it runs in; empty is every one.
850+ pub repositories: Vec<String>,
851+ /// `all`, `public` or `private`.
852+ pub visibility: String,
853+ /// The copy that runs.
854+ pub content: String,
855+ pub created_at: String,
856+ pub updated_at: String,
857+ pub updated_by: String,
858+}
859+
860+/// `workspace_managed_workflows`: owners only. Returns
861+/// `Outcome<Vec<WorkspaceManagedWorkflow>>`.
862+#[derive(Debug, Serialize, Deserialize)]
863+pub struct WorkspaceManagedWorkflowsArgs {
864+ pub viewer: Viewer,
865+ pub workspace: String,
866+}
867+
868+/// `set_workspace_managed_workflow`: owners only. Adds one (`create`, which
869+/// needs `source` and `mode`) or changes one; fields left out stay as they
870+/// are. The source is copied again when it is given or `refresh` is set.
871+/// Refused when the source is not a workflow file in a repository of the
872+/// workspace, or is private while the workflow could run in a public
873+/// repository. Returns `Outcome<WorkspaceManagedWorkflow>`.
874+#[derive(Debug, Serialize, Deserialize)]
875+#[serde(rename_all = "camelCase")]
876+pub struct SetWorkspaceManagedWorkflowArgs {
877+ pub actor: User,
878+ pub workspace: String,
879+ pub name: String,
880+ #[serde(default)]
881+ pub create: bool,
882+ /// `owner/repo/.g1t/workflows/file.yml@ref`.
883+ #[serde(default)]
884+ pub source: Option<String>,
885+ #[serde(default)]
886+ pub mode: Option<String>,
887+ #[serde(default)]
888+ pub repositories: Option<Vec<String>>,
889+ #[serde(default)]
890+ pub visibility: Option<String>,
891+ #[serde(default)]
892+ pub refresh: bool,
893+}
894+
895+/// `remove_workspace_managed_workflow`: owners only. Its runs stay.
896+/// Returns `Outcome<bool>`, whether there was one.
897+#[derive(Debug, Serialize, Deserialize)]
898+pub struct RemoveWorkspaceManagedWorkflowArgs {
899+ pub actor: User,
900+ pub workspace: String,
901+ pub name: String,
902+}
903+
806904 /// `remove_managed_workflow`: takes a managed workflow out of a repository.
807905 /// Its runs stay. Returns `bool`, whether there was one.
808906 #[derive(Debug, Serialize, Deserialize)]
+5−0
11941194 // A workspace's policy for its repositories' tokens.
11951195 ("get_workspace_workflow_permissions", Scope::WorkspaceRead),
11961196 ("set_workspace_workflow_permissions", Scope::WorkspaceAdmin),
1197+ // The workflows a workspace runs in its repositories: its owners'.
1198+ ("list_workspace_managed_workflows", Scope::WorkspaceRead),
1199+ ("create_workspace_managed_workflow", Scope::WorkspaceAdmin),
1200+ ("update_workspace_managed_workflow", Scope::WorkspaceAdmin),
1201+ ("delete_workspace_managed_workflow", Scope::WorkspaceAdmin),
11971202 // A workspace's rules for personal access tokens, and the members'
11981203 // tokens that reach it: who has access.
11991204 ("get_token_policy", Scope::WorkspaceRead),
+58−0
6161 notes: WorkflowNote[];
6262 dispatch: Record<string, DispatchInput> | null;
6363 lastRun: WorkflowRun | null;
64+ /** For a managed workflow (`managed/<name>.yml`): what installed it. */
65+ managedBy?: ManagedBy | null;
66+};
67+
68+/** What installed a managed workflow in a repository. */
69+export type ManagedBy = {
70+ /** `g1t` for one g1t's services installed, or the workspace's name. */
71+ origin: string;
72+ /** A workspace's: `required` or `default`; empty for g1t's. */
73+ mode: "required" | "default" | "";
74+ /** A workspace's: the file it was copied from, `owner/repo/path@ref`. */
75+ source: string | null;
76+};
77+
78+/**
79+ * A workflow a workspace's owners added to its repositories: it runs in
80+ * each repository it targets as that repository's own, under
81+ * `managed/<name>.yml`, from a copy of its source taken when it was saved.
82+ */
83+export type WorkspaceManagedWorkflow = {
84+ name: string;
85+ path: string;
86+ /** Its `name:`, as repositories list it. */
87+ workflowName: string;
88+ /** `owner/repo/.g1t/workflows/file.yml@ref`. */
89+ source: string;
90+ sourceSha: string | null;
91+ /** A private source runs in private repositories only. */
92+ sourcePrivate: boolean;
93+ mode: "required" | "default";
94+ /** Names and patterns; empty is every repository. */
95+ repositories: string[];
96+ visibility: "all" | "public" | "private";
97+ content: string;
98+ createdAt: string;
99+ updatedAt: string;
100+ updatedBy: string;
64101 };
65102
103+export type WorkspaceManagedWorkflowChange = {
104+ source?: string;
105+ mode?: "required" | "default";
106+ repositories?: string[];
107+ visibility?: "all" | "public" | "private";
108+ /** Copy the source again. */
109+ refresh?: boolean;
110+};
111+
66112 export type StepState = {
67113 number: number;
68114 name: string;
386432 workspace: string,
387433 change: Partial<WorkspaceActionsSettings>,
388434 ): Promise<Result<WorkspaceActionsSettings>>;
435+ /** Owners. */
436+ workspaceManagedWorkflows(workspace: string, viewer: Viewer): Promise<Result<WorkspaceManagedWorkflow[]>>;
437+ /** Owners. `create` adds one, which needs `source` and `mode`; otherwise left out is unchanged. */
438+ setWorkspaceManagedWorkflow(
439+ actor: User,
440+ workspace: string,
441+ name: string,
442+ change: WorkspaceManagedWorkflowChange,
443+ create?: boolean,
444+ ): Promise<Result<WorkspaceManagedWorkflow>>;
445+ /** Owners. */
446+ removeWorkspaceManagedWorkflow(actor: User, workspace: string, name: string): Promise<Result<boolean>>;
389447 /** Every environment the repository's rules, secrets, workflows or jobs name. */
390448 environments(repo: RepoPath, viewer: Viewer): Promise<Result<Environment[]>>;
391449 /** Admin role. */
+5−0
834834 workspaceActionsSettings: (workspace, viewer) => call("workspace_actions_settings", { workspace, viewer }),
835835 setWorkspaceActionsSettings: (actor, workspace, change) =>
836836 call("set_workspace_actions_settings", { actor, workspace, ...change }),
837+ workspaceManagedWorkflows: (workspace, viewer) => call("workspace_managed_workflows", { workspace, viewer }),
838+ setWorkspaceManagedWorkflow: (actor, workspace, name, change, create = false) =>
839+ call("set_workspace_managed_workflow", { actor, workspace, name, create, ...change }),
840+ removeWorkspaceManagedWorkflow: (actor, workspace, name) =>
841+ call("remove_workspace_managed_workflow", { actor, workspace, name }),
837842 environments: (repo, viewer) => call("environments", { repo, viewer }),
838843 setEnvironment: (actor, repo, name, change) => call("set_environment", { actor, repo, name, ...change }),
839844 deleteEnvironment: (actor, repo, name) => call("delete_environment", { actor, repo, name }),
+4−0
565565 // A workspace's policy for its repositories' tokens.
566566 ["get_workspace_workflow_permissions", "workspace:read"],
567567 ["set_workspace_workflow_permissions", "workspace:admin"],
568+ ["list_workspace_managed_workflows", "workspace:read"],
569+ ["create_workspace_managed_workflow", "workspace:admin"],
570+ ["update_workspace_managed_workflow", "workspace:admin"],
571+ ["delete_workspace_managed_workflow", "workspace:admin"],
568572 // A workspace's rules for personal access tokens, and its members' tokens.
569573 ["get_token_policy", "workspace:read"],
570574 ["set_token_policy", "workspace:admin"],
+28−0
1+-- A workspace's managed workflows (src/managed.rs): workflows its owners
2+-- add to its repositories, each run in every repository it targets as that
3+-- repository's own, under the path `managed/<name>.yml`. `source` is a copy
4+-- of the workflow file it was added from, taken when it was saved, so only
5+-- an owner changes what runs. `mode` is `required` (a repository's workflow
6+-- of the same `name:` is set aside, and no repository turns it off) or
7+-- `default` (a repository's workflow of the same name runs in its place,
8+-- and its admins can turn it off).
9+CREATE TABLE IF NOT EXISTS workspace_managed_workflows (
10+ namespace TEXT NOT NULL,
11+ name TEXT NOT NULL,
12+ -- `owner/repo/path@ref`, as given.
13+ source_ref TEXT NOT NULL,
14+ source_repo_id TEXT NOT NULL,
15+ source_sha TEXT,
16+ source_private INTEGER NOT NULL DEFAULT 0,
17+ source TEXT NOT NULL,
18+ -- Its `name:`, as repositories list it.
19+ workflow_name TEXT NOT NULL,
20+ mode TEXT NOT NULL,
21+ -- JSON: names and patterns; empty is every repository.
22+ repositories TEXT NOT NULL DEFAULT '[]',
23+ visibility TEXT NOT NULL DEFAULT 'all',
24+ created_at TEXT NOT NULL,
25+ updated_at TEXT NOT NULL,
26+ updated_by TEXT NOT NULL,
27+ PRIMARY KEY (namespace, name)
28+);
+4−0
256256 "set_managed_workflow" => reply(&service.set_managed_workflow(args(body)?).await?),
257257 "remove_managed_workflow" => reply(&service.remove_managed_workflow(args(body)?).await?),
258258 "set_managed_deploy" => reply(&service.set_managed_deploy(args(body)?).await?),
259+ // A workspace's managed workflows: its owners'.
260+ "workspace_managed_workflows" => reply(&service.workspace_managed_workflows(args(body)?).await?),
261+ "set_workspace_managed_workflow" => reply(&service.set_workspace_managed_workflow(args(body)?).await?),
262+ "remove_workspace_managed_workflow" => reply(&service.remove_workspace_managed_workflow(args(body)?).await?),
259263 "job_spec" => reply(&service.job_spec(args(body)?).await?),
260264 "job_auth" => reply(&service.job_auth(args(body)?).await?),
261265 "job_action" => reply(&service.job_action(args(body)?).await?),
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…