Merge branch 'main' into worktree-agent-a7dbe21737f9ad26a
# Conflicts: # apps/api/src/operations.rs # apps/web/app/routes/repo/deployments.tsx # apps/web/app/routes/repo/overview.tsx
| 1 | + | # Tests every pull request into main, so main can always be deployed. The | |
| 2 | + | # "Default branch" ruleset requires this workflow ("CI") to pass before a | |
| 3 | + | # pull request merges; people may still push to main directly, agents | |
| 4 | + | # may not. Deploy (deploy.yml) ships what lands on main. | |
| 5 | + | # | |
| 6 | + | # rust every crate's tests, natively | |
| 7 | + | # typescript type checks and tests of the apps and TS services, the | |
| 8 | + | # deploy and ops scripts, and the deploy manifest | |
| 9 | + | # build the site, sudo and the docs build as they deploy | |
| 10 | + | name: CI | |
| 11 | + | ||
| 12 | + | on: | |
| 13 | + | pull_request: | |
| 14 | + | branches: [main] | |
| 15 | + | workflow_dispatch: | |
| 16 | + | ||
| 17 | + | concurrency: | |
| 18 | + | group: ci-${{ github.ref }} | |
| 19 | + | cancel-in-progress: true | |
| 20 | + | ||
| 21 | + | env: | |
| 22 | + | CARGO_TERM_COLOR: never | |
| 23 | + | WRANGLER_SEND_METRICS: "false" | |
| 24 | + | ||
| 25 | + | jobs: | |
| 26 | + | rust: | |
| 27 | + | name: Rust | |
| 28 | + | runs-on: g1t-4core | |
| 29 | + | timeout-minutes: 45 | |
| 30 | + | steps: | |
| 31 | + | - uses: actions/checkout@v5 | |
| 32 | + | - name: Cache crates | |
| 33 | + | uses: actions/cache@v4 | |
| 34 | + | with: | |
| 35 | + | path: ~/.cargo/registry/cache | |
| 36 | + | key: cargo-crates-${{ runner.os }}-${{ hashFiles('Cargo.lock') }} | |
| 37 | + | restore-keys: cargo-crates-${{ runner.os }}- | |
| 38 | + | - name: Cache the test build | |
| 39 | + | uses: actions/cache@v4 | |
| 40 | + | with: | |
| 41 | + | path: | | |
| 42 | + | target/debug | |
| 43 | + | !target/debug/incremental | |
| 44 | + | key: cargo-test-${{ runner.os }}-${{ hashFiles('Cargo.lock', 'services/runner/base.json') }} | |
| 45 | + | restore-keys: cargo-test-${{ runner.os }}- | |
| 46 | + | - name: Tests | |
| 47 | + | run: cargo test --workspace --locked --quiet | |
| 48 | + | ||
| 49 | + | typescript: | |
| 50 | + | name: TypeScript | |
| 51 | + | runs-on: ubuntu-latest | |
| 52 | + | timeout-minutes: 30 | |
| 53 | + | steps: | |
| 54 | + | - uses: actions/checkout@v5 | |
| 55 | + | - name: Install | |
| 56 | + | run: npm ci --no-audit --no-fund | |
| 57 | + | - name: Type checks | |
| 58 | + | run: npm run typecheck | |
| 59 | + | - name: Tests | |
| 60 | + | run: npm test --workspaces --if-present | |
| 61 | + | - name: The deploy tool's tests | |
| 62 | + | run: npm run test:deploy | |
| 63 | + | - name: The ops scripts' tests | |
| 64 | + | run: npm run test:ops | |
| 65 | + | - name: The manifest matches every wrangler.jsonc | |
| 66 | + | run: node scripts/deploy.mjs manifest --check | |
| 67 | + | ||
| 68 | + | build: | |
| 69 | + | name: Build | |
| 70 | + | runs-on: ubuntu-latest | |
| 71 | + | timeout-minutes: 30 | |
| 72 | + | steps: | |
| 73 | + | - uses: actions/checkout@v5 | |
| 74 | + | - name: Install | |
| 75 | + | run: npm ci --no-audit --no-fund | |
| 76 | + | - name: The site, sudo and the docs | |
| 77 | + | run: node scripts/deploy.mjs build --only web,sudo,docs |
| 15 | 15 | mod oauth; | |
| 16 | 16 | mod openapi; | |
| 17 | 17 | mod pins; | |
| 18 | + | mod projects; | |
| 18 | 19 | mod operations; | |
| 19 | 20 | mod renamed; | |
| 20 | 21 | #[cfg(test)] |
| 47 | 47 | &[Op::ListPinnedProjects, Op::PinProject, Op::UnpinProject, Op::ReorderPinnedProjects], | |
| 48 | 48 | ), | |
| 49 | 49 | ( | |
| 50 | + | "Projects", | |
| 51 | + | "A project is what a workspace builds and runs, from a repository or a root directory in one. Each says what it is, where it runs and where to find it: its homepage, docs and other links.", | |
| 52 | + | &[Op::ListProjects, Op::GetProject, Op::UpdateProject], | |
| 53 | + | ), | |
| 54 | + | ( | |
| 50 | 55 | "Workspaces", | |
| 51 | 56 | "A workspace owns repositories and is the first part of their address. People and agents work in workspaces.", | |
| 52 | 57 | &[Op::GetWorkspace, Op::CreateWorkspace, Op::UpdateWorkspace, Op::DeleteWorkspace], | |
| ⋯ | |||
| 552 | 557 | Op::PinProject => "Pin a project", | |
| 553 | 558 | Op::UnpinProject => "Unpin a project", | |
| 554 | 559 | Op::ReorderPinnedProjects => "Reorder your pinned projects", | |
| 560 | + | Op::ListProjects => "List a workspace's projects", | |
| 561 | + | Op::GetProject => "Get a project", | |
| 562 | + | Op::UpdateProject => "Update a project", | |
| 555 | 563 | Op::ListTeams => "List teams", | |
| 556 | 564 | Op::GetTeam => "Get a team", | |
| 557 | 565 | Op::CreateTeam => "Create a team", | |
| 242 | 242 | PinProject, | |
| 243 | 243 | UnpinProject, | |
| 244 | 244 | ReorderPinnedProjects, | |
| 245 | + | ListProjects, | |
| 246 | + | GetProject, | |
| 247 | + | UpdateProject, | |
| 245 | 248 | ListTeams, | |
| 246 | 249 | GetTeam, | |
| 247 | 250 | CreateTeam, | |
| ⋯ | |||
| 635 | 638 | } | |
| 636 | 639 | ||
| 637 | 640 | impl Op { | |
| 638 | − | pub const ALL: [Op; 226] = [ | |
| 641 | + | pub const ALL: [Op; 229] = [ | |
| 639 | 642 | Op::Whoami, | |
| 640 | 643 | Op::GetWorkspace, | |
| 641 | 644 | Op::CreateWorkspace, | |
| ⋯ | |||
| 786 | 789 | Op::PinProject, | |
| 787 | 790 | Op::UnpinProject, | |
| 788 | 791 | Op::ReorderPinnedProjects, | |
| 792 | + | Op::ListProjects, | |
| 793 | + | Op::GetProject, | |
| 794 | + | Op::UpdateProject, | |
| 789 | 795 | Op::ListTeams, | |
| 790 | 796 | Op::GetTeam, | |
| 791 | 797 | Op::CreateTeam, | |
| ⋯ | |||
| 1021 | 1027 | Op::PinProject => "pin_project", | |
| 1022 | 1028 | Op::UnpinProject => "unpin_project", | |
| 1023 | 1029 | Op::ReorderPinnedProjects => "reorder_pinned_projects", | |
| 1030 | + | Op::ListProjects => "list_projects", | |
| 1031 | + | Op::GetProject => "get_project", | |
| 1032 | + | Op::UpdateProject => "update_project", | |
| 1024 | 1033 | Op::ListTeams => "list_teams", | |
| 1025 | 1034 | Op::GetTeam => "get_team", | |
| 1026 | 1035 | Op::CreateTeam => "create_team", | |
| ⋯ | |||
| 1470 | 1479 | Op::ReorderPinnedProjects => { | |
| 1471 | 1480 | "Put your pins in a workspace in a new order: `projects` names every pinned project's slug, once, in the order you want them. Returns your pins, in order." | |
| 1472 | 1481 | } | |
| 1482 | + | Op::ListProjects => { | |
| 1483 | + | "A workspace's projects that you can see, by name. A project is what a workspace builds and runs, from a repository or a root directory in one; every repository has a project of its own name. Each has what it is (`kind`: app, library, tool, docs or other) and why (`kind_reason`), where it runs (`runs`: `g1t` when g1t deploys it, `elsewhere` when it is deployed by other means, at `production_url`), and its `links`." | |
| 1484 | + | } | |
| 1485 | + | Op::GetProject => { | |
| 1486 | + | "A project: what it is (`kind`, and `kind_reason` saying why), where it runs (`runs` and `production_url`), what you set and what detection decides (`setting` and `detected`), its repository and `root_dir`, and its homepage, docs and other `links`. A private repository's project is found only by those who can see the repository." | |
| 1487 | + | } | |
| 1488 | + | Op::UpdateProject => { | |
| 1489 | + | "Change a project: its name, description, root directory, what it is, where it runs and its links. Only what you give changes. kind auto and runs auto leave each to detection. Setting runs makes it an app unless it is docs; making it a library, tool or other while Deployments are on is refused, so turn Deployments off first. Give description or homepage as null or \"\" to follow the repository's again, and production_url or docs_url as null or \"\" to clear it. links replaces its other links: at most 10, each a label of up to 40 characters and an http or https address (https:// is added when you leave the scheme out). Needs the Maintain role or higher on its repository." | |
| 1490 | + | } | |
| 1473 | 1491 | Op::ListTeams => { | |
| 1474 | 1492 | "A workspace's teams that you can see, yours first, then by name. A team is a group of the workspace's members, given roles on repositories together, mentioned as @workspace/team and asked to review together. A secret team is seen only by its own people and the workspace's owners. Each team has its `slug`, `name`, `description`, `visibility` (`visible` or `secret`), `parent`, whether its people are notified when it is mentioned (`notify`), its `review_assignment`, how many people, repositories and child teams it has (`members_count`, `repos_count`, `child_teams_count`), your own `viewer_role` in it, and whether you may change it (`can_manage`). `query` narrows them by name or slug. Members of the workspace only." | |
| 1475 | 1493 | } | |
| ⋯ | |||
| 2692 | 2710 | }), | |
| 2693 | 2711 | &["workspace", "projects"], | |
| 2694 | 2712 | ), | |
| 2713 | + | Op::ListProjects => object(json!({ "workspace": workspace_schema() }), &["workspace"]), | |
| 2714 | + | Op::GetProject => object( | |
| 2715 | + | json!({ | |
| 2716 | + | "workspace": workspace_schema(), | |
| 2717 | + | "project": { "type": "string", "description": "The project's slug, as in g1t.sh/{workspace}/{project}." }, | |
| 2718 | + | }), | |
| 2719 | + | &["workspace", "project"], | |
| 2720 | + | ), | |
| 2721 | + | Op::UpdateProject => object( | |
| 2722 | + | json!({ | |
| 2723 | + | "workspace": workspace_schema(), | |
| 2724 | + | "project": { "type": "string", "description": "The project's slug, as in g1t.sh/{workspace}/{project}." }, | |
| 2725 | + | "name": { "type": "string", "description": "Its name." }, | |
| 2726 | + | "description": { "type": ["string", "null"], "description": "Its own description. null or \"\" follows its repository's again." }, | |
| 2727 | + | "root_dir": { "type": "string", "description": "Where in the repository it lives, such as apps/web; \"\" for the whole repository." }, | |
| 2728 | + | "kind": { | |
| 2729 | + | "type": "string", | |
| 2730 | + | "enum": ["auto", "app", "library", "tool", "docs", "other"], | |
| 2731 | + | "description": "What it is. auto leaves it to detection. A library, tool or other runs nowhere.", | |
| 2732 | + | }, | |
| 2733 | + | "runs": { | |
| 2734 | + | "type": "string", | |
| 2735 | + | "enum": ["auto", "g1t", "elsewhere"], | |
| 2736 | + | "description": "Where it runs: g1t when g1t deploys it, elsewhere when it is deployed by other means. auto leaves it to Deployments.", | |
| 2737 | + | }, | |
| 2738 | + | "production_url": { "type": ["string", "null"], "description": "Production's address when it runs elsewhere. null or \"\" clears it." }, | |
| 2739 | + | "homepage": { "type": ["string", "null"], "description": "Its homepage. null or \"\" follows its repository's website again." }, | |
| 2740 | + | "docs_url": { "type": ["string", "null"], "description": "Where its documentation is read. null or \"\" clears it." }, | |
| 2741 | + | "links": { | |
| 2742 | + | "type": "array", | |
| 2743 | + | "maxItems": 10, | |
| 2744 | + | "items": { | |
| 2745 | + | "type": "object", | |
| 2746 | + | "properties": { | |
| 2747 | + | "label": { "type": "string", "maxLength": 40 }, | |
| 2748 | + | "url": { "type": "string", "description": "An http or https address; https:// is added when you leave the scheme out." }, | |
| 2749 | + | }, | |
| 2750 | + | "required": ["label", "url"], | |
| 2751 | + | }, | |
| 2752 | + | "description": "Its other links, replacing the ones it has. [] removes them all.", | |
| 2753 | + | }, | |
| 2754 | + | }), | |
| 2755 | + | &["workspace", "project"], | |
| 2756 | + | ), | |
| 2695 | 2757 | Op::ListTeams => object( | |
| 2696 | 2758 | json!({ | |
| 2697 | 2759 | "workspace": workspace_schema(), | |
| ⋯ | |||
| 2884 | 2946 | | Op::ListCheckNames | |
| 2885 | 2947 | | Op::GetMergeQueue | |
| 2886 | 2948 | | Op::GetCodeownersErrors | |
| 2949 | + | | Op::ListProjects | |
| 2950 | + | | Op::GetProject | |
| 2887 | 2951 | | Op::Rules(RulesOp::ListRepoRulesets | RulesOp::GetRepoRuleset | RulesOp::GetBranchRules) | |
| 2888 | 2952 | | Op::Deployments( | |
| 2889 | 2953 | DeploymentsOp::ListDeployments | |
| ⋯ | |||
| 2979 | 3043 | | Op::PinProject | |
| 2980 | 3044 | | Op::UnpinProject | |
| 2981 | 3045 | | Op::ReorderPinnedProjects | |
| 3046 | + | | Op::ListProjects | |
| 3047 | + | | Op::GetProject | |
| 3048 | + | | Op::UpdateProject | |
| 2982 | 3049 | | Op::ListTeams | |
| 2983 | 3050 | | Op::GetTeam | |
| 2984 | 3051 | | Op::CreateTeam | |
| ⋯ | |||
| 4867 | 4934 | Op::ListPinnedProjects | Op::PinProject | Op::UnpinProject | Op::ReorderPinnedProjects => { | |
| 4868 | 4935 | crate::pins::run(self, services, viewer, input).await | |
| 4869 | 4936 | } | |
| 4937 | + | // What a project is, where it runs and its links: the projects | |
| 4938 | + | // service keeps them and decides who may change them. | |
| 4939 | + | Op::ListProjects | Op::GetProject | Op::UpdateProject => { | |
| 4940 | + | crate::projects::run(self, services, viewer, input).await | |
| 4941 | + | } | |
| 4870 | 4942 | // The security suite: the security service decides, this gives | |
| 4871 | 4943 | // each answer its public shape. | |
| 4872 | 4944 | Op::Security(op) => crate::security::run(op, services, viewer, input).await, | |
| 1 | + | //! Projects: what a workspace builds and runs, where each runs, and its | |
| 2 | + | //! links. The projects service keeps them and decides who may see or change | |
| 3 | + | //! them; this is their public shape, in snake_case. | |
| 4 | + | ||
| 5 | + | use g1t_contracts::{FailureCode, Outcome, Viewer}; | |
| 6 | + | use serde_json::{Map, Value, json}; | |
| 7 | + | use worker::Result; | |
| 8 | + | ||
| 9 | + | use crate::operations::{Op, Services}; | |
| 10 | + | ||
| 11 | + | fn failed(code: FailureCode, message: &str) -> Result<Outcome<Value>> { | |
| 12 | + | Ok(Outcome::fail(code, message)) | |
| 13 | + | } | |
| 14 | + | ||
| 15 | + | fn slug(input: &Value, key: &str) -> Option<String> { | |
| 16 | + | input[key].as_str().map(str::trim).filter(|value| !value.is_empty()).map(str::to_lowercase) | |
| 17 | + | } | |
| 18 | + | ||
| 19 | + | /// A reason a kind was decided, as the API shows it. | |
| 20 | + | fn reason_json(reason: &Value) -> Value { | |
| 21 | + | json!({ "by": reason["by"], "detail": reason["detail"] }) | |
| 22 | + | } | |
| 23 | + | ||
| 24 | + | /// A project as the projects service answers (camelCase), as the API shows it. | |
| 25 | + | pub(crate) fn project_json(project: &Value, site: &str) -> Value { | |
| 26 | + | let workspace = project["workspace"].as_str().unwrap_or_default(); | |
| 27 | + | let slug = project["slug"].as_str().unwrap_or_default(); | |
| 28 | + | let source = &project["source"]; | |
| 29 | + | let repository = match (source["repo"]["namespace"].as_str(), source["repo"]["name"].as_str()) { | |
| 30 | + | (Some(namespace), Some(name)) => json!(format!("{namespace}/{name}")), | |
| 31 | + | _ => Value::Null, | |
| 32 | + | }; | |
| 33 | + | let links = &project["links"]; | |
| 34 | + | let custom: Vec<Value> = links["custom"] | |
| 35 | + | .as_array() | |
| 36 | + | .map(|items| items.iter().map(|link| json!({ "label": link["label"], "url": link["url"] })).collect()) | |
| 37 | + | .unwrap_or_default(); | |
| 38 | + | json!({ | |
| 39 | + | "id": project["id"], | |
| 40 | + | "workspace": workspace, | |
| 41 | + | "slug": slug, | |
| 42 | + | "name": project["name"], | |
| 43 | + | "description": project["description"], | |
| 44 | + | "description_inherited": project["descriptionInherited"].as_bool().unwrap_or(false), | |
| 45 | + | "url": format!("{}/{workspace}/{slug}", site.trim_end_matches('/')), | |
| 46 | + | "repository": repository, | |
| 47 | + | "root_dir": source["rootDir"].as_str().unwrap_or_default(), | |
| 48 | + | "default_branch": source["defaultBranch"], | |
| 49 | + | "private": project["private"], | |
| 50 | + | "archived": project["archived"], | |
| 51 | + | "primary": project["primary"], | |
| 52 | + | "kind": project["kind"], | |
| 53 | + | "kind_reason": reason_json(&project["kindReason"]), | |
| 54 | + | "runs": project["runs"], | |
| 55 | + | "production_url": project["productionUrl"], | |
| 56 | + | "setting": { "kind": project["setting"]["kind"], "runs": project["setting"]["runs"] }, | |
| 57 | + | "detected": { | |
| 58 | + | "kind": project["detected"]["kind"], | |
| 59 | + | "reason": reason_json(&project["detected"]["reason"]), | |
| 60 | + | }, | |
| 61 | + | "ecosystem": project["ecosystem"], | |
| 62 | + | "links": { | |
| 63 | + | "homepage": links["homepage"], | |
| 64 | + | "homepage_inherited": links["homepageInherited"].as_bool().unwrap_or(false), | |
| 65 | + | "docs": links["docs"], | |
| 66 | + | "custom": custom, | |
| 67 | + | }, | |
| 68 | + | "created_at": project["createdAt"], | |
| 69 | + | "updated_at": project["updatedAt"], | |
| 70 | + | "pushed_at": project["pushedAt"], | |
| 71 | + | }) | |
| 72 | + | } | |
| 73 | + | ||
| 74 | + | /// The fields a change may set: (input, service, whether null clears it). | |
| 75 | + | const CHANGES: &[(&str, &str, bool)] = &[ | |
| 76 | + | ("name", "name", false), | |
| 77 | + | ("description", "description", true), | |
| 78 | + | ("root_dir", "rootDir", false), | |
| 79 | + | ("kind", "kind", false), | |
| 80 | + | ("runs", "runs", false), | |
| 81 | + | ("production_url", "productionUrl", true), | |
| 82 | + | ("homepage", "homepage", true), | |
| 83 | + | ("docs_url", "docsUrl", true), | |
| 84 | + | ]; | |
| 85 | + | ||
| 86 | + | /// The change `input` asks for, in the service's spelling: only what it | |
| 87 | + | /// gives, with null passed on for what null clears. | |
| 88 | + | pub(crate) fn changes(input: &Value) -> std::result::Result<Value, String> { | |
| 89 | + | let mut out = Map::new(); | |
| 90 | + | for (key, field, clearable) in CHANGES { | |
| 91 | + | match input.get(*key) { | |
| 92 | + | None => {} | |
| 93 | + | Some(Value::Null) if *clearable => { | |
| 94 | + | out.insert((*field).to_owned(), Value::Null); | |
| 95 | + | } | |
| 96 | + | Some(Value::Null) => {} | |
| 97 | + | Some(Value::String(text)) => { | |
| 98 | + | out.insert((*field).to_owned(), json!(text)); | |
| 99 | + | } | |
| 100 | + | Some(_) => { | |
| 101 | + | let kind = if *clearable { "a string or null" } else { "a string" }; | |
| 102 | + | return Err(format!("{key} must be {kind}.")); | |
| 103 | + | } | |
| 104 | + | } | |
| 105 | + | } | |
| 106 | + | match input.get("links") { | |
| 107 | + | None | Some(Value::Null) => {} | |
| 108 | + | Some(Value::Array(items)) => { | |
| 109 | + | let mut links = Vec::with_capacity(items.len()); | |
| 110 | + | for item in items { | |
| 111 | + | match (item["label"].as_str(), item["url"].as_str()) { | |
| 112 | + | (Some(label), Some(url)) => links.push(json!({ "label": label, "url": url })), | |
| 113 | + | _ => return Err("Each of links needs a label and a url.".to_owned()), | |
| 114 | + | } | |
| 115 | + | } | |
| 116 | + | out.insert("links".to_owned(), Value::Array(links)); | |
| 117 | + | } | |
| 118 | + | Some(_) => return Err("links must be a list of { label, url }.".to_owned()), | |
| 119 | + | } | |
| 120 | + | Ok(Value::Object(out)) | |
| 121 | + | } | |
| 122 | + | ||
| 123 | + | /// Runs one of the project operations. | |
| 124 | + | pub async fn run(op: Op, services: &Services, viewer: &Viewer, input: &Value) -> Result<Outcome<Value>> { | |
| 125 | + | let Some(workspace) = slug(input, "workspace") else { | |
| 126 | + | return failed(FailureCode::Invalid, "Give the workspace's slug."); | |
| 127 | + | }; | |
| 128 | + | let site = services.addresses.site.as_str(); | |
| 129 | + | let one = |outcome: Outcome<Value>| -> Outcome<Value> { | |
| 130 | + | match outcome { | |
| 131 | + | Outcome::Ok(project) => Outcome::Ok(project_json(&project, site)), | |
| 132 | + | Outcome::Fail(failure) => Outcome::Fail(failure), | |
| 133 | + | } | |
| 134 | + | }; | |
| 135 | + | if op == Op::ListProjects { | |
| 136 | + | let listed: Outcome<Vec<Value>> = | |
| 137 | + | g1t_kit::call(&services.projects, "list", &json!({ "workspace": workspace, "viewer": viewer })).await?; | |
| 138 | + | return Ok(match listed { | |
| 139 | + | Outcome::Ok(projects) => { | |
| 140 | + | Outcome::Ok(Value::Array(projects.iter().map(|project| project_json(project, site)).collect())) | |
| 141 | + | } | |
| 142 | + | Outcome::Fail(failure) => Outcome::Fail(failure), | |
| 143 | + | }); | |
| 144 | + | } | |
| 145 | + | let Some(project) = slug(input, "project") else { | |
| 146 | + | return failed(FailureCode::Invalid, "Give the project's slug."); | |
| 147 | + | }; | |
| 148 | + | match op { | |
| 149 | + | Op::GetProject => Ok(one( | |
| 150 | + | g1t_kit::call( | |
| 151 | + | &services.projects, | |
| 152 | + | "get", | |
| 153 | + | &json!({ "workspace": workspace, "slug": project, "viewer": viewer }), | |
| 154 | + | ) | |
| 155 | + | .await?, | |
| 156 | + | )), | |
| 157 | + | Op::UpdateProject => { | |
| 158 | + | let Some(actor) = viewer else { | |
| 159 | + | return failed(FailureCode::Unauthenticated, "This needs a g1t access token."); | |
| 160 | + | }; | |
| 161 | + | let changes = match changes(input) { | |
| 162 | + | Ok(changes) => changes, | |
| 163 | + | Err(message) => return failed(FailureCode::Invalid, &message), | |
| 164 | + | }; | |
| 165 | + | Ok(one( | |
| 166 | + | g1t_kit::call( | |
| 167 | + | &services.projects, | |
| 168 | + | "update", | |
| 169 | + | &json!({ "actor": actor, "workspace": workspace, "slug": project, "changes": changes }), | |
| 170 | + | ) | |
| 171 | + | .await?, | |
| 172 | + | )) | |
| 173 | + | } | |
| 174 | + | _ => failed(FailureCode::Invalid, "Not a project operation."), | |
| 175 | + | } | |
| 176 | + | } | |
| 177 | + | ||
| 178 | + | #[cfg(test)] | |
| 179 | + | mod tests { | |
| 180 | + | use super::*; | |
| 181 | + | use g1t_kit::wire; | |
| 182 | + | ||
| 183 | + | fn project() -> Value { | |
| 184 | + | json!({ | |
| 185 | + | "id": "prj_1", "workspace": "flagon-io", "slug": "g1t", "name": "g1t", | |
| 186 | + | "description": "Git hosting for people and agents.", "descriptionInherited": true, | |
| 187 | + | "source": { | |
| 188 | + | "kind": "hosted", "repoId": "repo_1", "repo": { "namespace": "flagon-io", "name": "g1t" }, | |
| 189 | + | "rootDir": "", "defaultBranch": "main", | |
| 190 | + | }, | |
| 191 | + | "private": false, "archived": false, "primary": true, | |
| 192 | + | "setting": { "kind": "app", "runs": "elsewhere" }, | |
| 193 | + | "kind": "app", "kindReason": { "by": "set", "detail": "Set to an app." }, | |
| 194 | + | "runs": "elsewhere", "productionUrl": "https://g1t.sh", | |
| 195 | + | "detected": { "kind": "app", "reason": { "by": "files", "detail": "It has a wrangler.jsonc." } }, | |
| 196 | + | "ecosystem": null, | |
| 197 | + | "links": { | |
| 198 | + | "homepage": "https://g1t.sh", "homepageInherited": true, "docs": "https://docs.g1t.sh", | |
| 199 | + | "custom": [{ "label": "Status", "url": "https://status.g1t.sh" }], | |
| 200 | + | }, | |
| 201 | + | "createdBy": "usr_1", "createdAt": "2026-09-01T10:00:00.000Z", "updatedAt": "2026-10-07T10:00:00.000Z", | |
| 202 | + | "pushedAt": "2026-10-07T09:00:00.000Z", "activity": 12.5, | |
| 203 | + | }) | |
| 204 | + | } | |
| 205 | + | ||
| 206 | + | #[test] | |
| 207 | + | fn a_project_is_snake_case_with_its_address() { | |
| 208 | + | let shown = project_json(&project(), "https://g1t.sh/"); | |
| 209 | + | assert!(wire::camel_case_keys(&shown).is_empty(), "{:?}", wire::camel_case_keys(&shown)); | |
| 210 | + | assert_eq!(shown["url"], "https://g1t.sh/flagon-io/g1t"); | |
| 211 | + | assert_eq!(shown["repository"], "flagon-io/g1t"); | |
| 212 | + | assert_eq!(shown["root_dir"], ""); | |
| 213 | + | assert_eq!(shown["default_branch"], "main"); | |
| 214 | + | assert_eq!(shown["description_inherited"], true); | |
| 215 | + | assert_eq!(shown["kind_reason"]["by"], "set"); | |
| 216 | + | assert_eq!(shown["production_url"], "https://g1t.sh"); | |
| 217 | + | assert_eq!(shown["setting"]["runs"], "elsewhere"); | |
| 218 | + | assert_eq!(shown["detected"]["reason"]["by"], "files"); | |
| 219 | + | assert_eq!(shown["links"]["homepage_inherited"], true); | |
| 220 | + | assert_eq!(shown["links"]["custom"][0]["label"], "Status"); | |
| 221 | + | assert_eq!(shown["pushed_at"], "2026-10-07T09:00:00.000Z"); | |
| 222 | + | // What is the service's own business stays there. | |
| 223 | + | assert!(shown.get("source").is_none()); | |
| 224 | + | assert!(shown.get("activity").is_none()); | |
| 225 | + | assert!(shown.get("created_by").is_none()); | |
| 226 | + | } | |
| 227 | + | ||
| 228 | + | #[test] | |
| 229 | + | fn a_change_is_only_what_is_given_in_the_service_s_spelling() { | |
| 230 | + | let asked = json!({ | |
| 231 | + | "workspace": "flagon-io", "project": "g1t", | |
| 232 | + | "kind": "app", "runs": "elsewhere", "production_url": "https://g1t.sh", | |
| 233 | + | "root_dir": "apps/web", "docs_url": "docs.g1t.sh", | |
| 234 | + | "links": [{ "label": "Status", "url": "https://status.g1t.sh", "extra": 1 }], | |
| 235 | + | }); | |
| 236 | + | let sent = changes(&asked).unwrap(); | |
| 237 | + | assert_eq!( | |
| 238 | + | sent, | |
| 239 | + | json!({ | |
| 240 | + | "kind": "app", "runs": "elsewhere", "productionUrl": "https://g1t.sh", "rootDir": "apps/web", | |
| 241 | + | "docsUrl": "docs.g1t.sh", "links": [{ "label": "Status", "url": "https://status.g1t.sh" }], | |
| 242 | + | }) | |
| 243 | + | ); | |
| 244 | + | assert!(sent.get("workspace").is_none()); | |
| 245 | + | assert!(sent.get("name").is_none()); | |
| 246 | + | } | |
| 247 | + | ||
| 248 | + | #[test] | |
| 249 | + | fn null_clears_what_it_can_and_absent_changes_nothing() { | |
| 250 | + | let sent = changes(&json!({ "description": null, "homepage": null, "production_url": null, "docs_url": null, "name": null, "kind": null })).unwrap(); | |
| 251 | + | assert_eq!(sent, json!({ "description": null, "homepage": null, "productionUrl": null, "docsUrl": null })); | |
| 252 | + | assert_eq!(changes(&json!({})).unwrap(), json!({})); | |
| 253 | + | assert_eq!(changes(&json!({ "links": [] })).unwrap(), json!({ "links": [] })); | |
| 254 | + | } | |
| 255 | + | ||
| 256 | + | #[test] | |
| 257 | + | fn a_change_of_the_wrong_type_is_refused() { | |
| 258 | + | assert!(changes(&json!({ "kind": 3 })).is_err()); | |
| 259 | + | assert!(changes(&json!({ "links": "https://g1t.sh" })).is_err()); | |
| 260 | + | assert!(changes(&json!({ "links": [{ "url": "https://g1t.sh" }] })).is_err()); | |
| 261 | + | } | |
| 262 | + | } |
| 6317 | 6317 | ], | |
| 6318 | 6318 | "notes": "Name every pinned project once; anything else is refused with `422 invalid`." | |
| 6319 | 6319 | }, | |
| 6320 | + | "list_projects": { | |
| 6321 | + | "params": { | |
| 6322 | + | "workspace": "flagon-io" | |
| 6323 | + | }, | |
| 6324 | + | "response": [ | |
| 6325 | + | { | |
| 6326 | + | "id": "prj_01kkp3m8w2f6t9qh4c7d1r5n0x", | |
| 6327 | + | "workspace": "flagon-io", | |
| 6328 | + | "slug": "g1t", | |
| 6329 | + | "name": "g1t", | |
| 6330 | + | "description": "Git hosting where people and agents work together.", | |
| 6331 | + | "description_inherited": true, | |
| 6332 | + | "url": "https://g1t.sh/flagon-io/g1t", | |
| 6333 | + | "repository": "flagon-io/g1t", | |
| 6334 | + | "root_dir": "", | |
| 6335 | + | "default_branch": "main", | |
| 6336 | + | "private": false, | |
| 6337 | + | "archived": false, | |
| 6338 | + | "primary": true, | |
| 6339 | + | "kind": "app", | |
| 6340 | + | "kind_reason": { | |
| 6341 | + | "by": "set", | |
| 6342 | + | "detail": "Set to an app that runs elsewhere." | |
| 6343 | + | }, | |
| 6344 | + | "runs": "elsewhere", | |
| 6345 | + | "production_url": "https://g1t.sh", | |
| 6346 | + | "setting": { | |
| 6347 | + | "kind": "app", | |
| 6348 | + | "runs": "elsewhere" | |
| 6349 | + | }, | |
| 6350 | + | "detected": { | |
| 6351 | + | "kind": "app", | |
| 6352 | + | "reason": { | |
| 6353 | + | "by": "files", | |
| 6354 | + | "detail": "wrangler.jsonc at its root makes it an app." | |
| 6355 | + | } | |
| 6356 | + | }, | |
| 6357 | + | "ecosystem": null, | |
| 6358 | + | "links": { | |
| 6359 | + | "homepage": "https://g1t.sh", | |
| 6360 | + | "homepage_inherited": true, | |
| 6361 | + | "docs": "https://docs.g1t.sh", | |
| 6362 | + | "custom": [ | |
| 6363 | + | { | |
| 6364 | + | "label": "Status", | |
| 6365 | + | "url": "https://status.g1t.sh" | |
| 6366 | + | } | |
| 6367 | + | ] | |
| 6368 | + | }, | |
| 6369 | + | "created_at": "2026-09-01T10:00:00.000Z", | |
| 6370 | + | "updated_at": "2026-10-07T11:20:00.000Z", | |
| 6371 | + | "pushed_at": "2026-10-07T10:00:00.000Z" | |
| 6372 | + | }, | |
| 6373 | + | { | |
| 6374 | + | "id": "prj_01kkp3n2a7e5v8sk3b6g9j4m1y", | |
| 6375 | + | "workspace": "flagon-io", | |
| 6376 | + | "slug": "hello", | |
| 6377 | + | "name": "hello", | |
| 6378 | + | "description": "Greets people from the command line.", | |
| 6379 | + | "description_inherited": false, | |
| 6380 | + | "url": "https://g1t.sh/flagon-io/hello", | |
| 6381 | + | "repository": "flagon-io/hello", | |
| 6382 | + | "root_dir": "", | |
| 6383 | + | "default_branch": "main", | |
| 6384 | + | "private": false, | |
| 6385 | + | "archived": false, | |
| 6386 | + | "primary": true, | |
| 6387 | + | "kind": "library", | |
| 6388 | + | "kind_reason": { | |
| 6389 | + | "by": "files", | |
| 6390 | + | "detail": "composer.json says it is a library." | |
| 6391 | + | }, | |
| 6392 | + | "runs": null, | |
| 6393 | + | "production_url": null, | |
| 6394 | + | "setting": { | |
| 6395 | + | "kind": null, | |
| 6396 | + | "runs": null | |
| 6397 | + | }, | |
| 6398 | + | "detected": { | |
| 6399 | + | "kind": "library", | |
| 6400 | + | "reason": { | |
| 6401 | + | "by": "files", | |
| 6402 | + | "detail": "composer.json says it is a library." | |
| 6403 | + | } | |
| 6404 | + | }, | |
| 6405 | + | "ecosystem": "composer", | |
| 6406 | + | "links": { | |
| 6407 | + | "homepage": null, | |
| 6408 | + | "homepage_inherited": false, | |
| 6409 | + | "docs": null, | |
| 6410 | + | "custom": [] | |
| 6411 | + | }, | |
| 6412 | + | "created_at": "2026-09-12T14:30:00.000Z", | |
| 6413 | + | "updated_at": "2026-10-01T09:12:00.000Z", | |
| 6414 | + | "pushed_at": null | |
| 6415 | + | } | |
| 6416 | + | ], | |
| 6417 | + | "notes": "By name. Projects whose repository you cannot see are left out; without a token you see public ones only. `kind` is what the project is, from `setting` where a person set it and from detection otherwise (`detected`, shown beside it); `kind_reason` says why. `runs` is null for what is not deployed. `homepage_inherited` is true while the homepage is its repository's website. `pushed_at` is null until its repository is pushed to. See [Projects](/guides/projects/)." | |
| 6418 | + | }, | |
| 6419 | + | "get_project": { | |
| 6420 | + | "params": { | |
| 6421 | + | "workspace": "flagon-io", | |
| 6422 | + | "project": "g1t" | |
| 6423 | + | }, | |
| 6424 | + | "response": { | |
| 6425 | + | "id": "prj_01kkp3m8w2f6t9qh4c7d1r5n0x", | |
| 6426 | + | "workspace": "flagon-io", | |
| 6427 | + | "slug": "g1t", | |
| 6428 | + | "name": "g1t", | |
| 6429 | + | "description": "Git hosting where people and agents work together.", | |
| 6430 | + | "description_inherited": true, | |
| 6431 | + | "url": "https://g1t.sh/flagon-io/g1t", | |
| 6432 | + | "repository": "flagon-io/g1t", | |
| 6433 | + | "root_dir": "", | |
| 6434 | + | "default_branch": "main", | |
| 6435 | + | "private": false, | |
| 6436 | + | "archived": false, | |
| 6437 | + | "primary": true, | |
| 6438 | + | "kind": "app", | |
| 6439 | + | "kind_reason": { | |
| 6440 | + | "by": "set", | |
| 6441 | + | "detail": "Set to an app that runs elsewhere." | |
| 6442 | + | }, | |
| 6443 | + | "runs": "elsewhere", | |
| 6444 | + | "production_url": "https://g1t.sh", | |
| 6445 | + | "setting": { | |
| 6446 | + | "kind": "app", | |
| 6447 | + | "runs": "elsewhere" | |
| 6448 | + | }, | |
| 6449 | + | "detected": { | |
| 6450 | + | "kind": "app", | |
| 6451 | + | "reason": { | |
| 6452 | + | "by": "files", | |
| 6453 | + | "detail": "wrangler.jsonc at its root makes it an app." | |
| 6454 | + | } | |
| 6455 | + | }, | |
| 6456 | + | "ecosystem": null, | |
| 6457 | + | "links": { | |
| 6458 | + | "homepage": "https://g1t.sh", | |
| 6459 | + | "homepage_inherited": true, | |
| 6460 | + | "docs": "https://docs.g1t.sh", | |
| 6461 | + | "custom": [ | |
| 6462 | + | { | |
| 6463 | + | "label": "Status", | |
| 6464 | + | "url": "https://status.g1t.sh" | |
| 6465 | + | } | |
| 6466 | + | ] | |
| 6467 | + | }, | |
| 6468 | + | "created_at": "2026-09-01T10:00:00.000Z", | |
| 6469 | + | "updated_at": "2026-10-07T11:20:00.000Z", | |
| 6470 | + | "pushed_at": "2026-10-07T10:00:00.000Z" | |
| 6471 | + | }, | |
| 6472 | + | "notes": "`404` for a project that does not exist or whose repository you cannot see. `setting` holds what a person set, each part null while it is left to detection. `ecosystem` is where a library's files say it is published (`composer`, `npm`, `cargo`, `go` or `python`), or null. See [What a project is](/guides/projects/#what-a-project-is)." | |
| 6473 | + | }, | |
| 6474 | + | "update_project": { | |
| 6475 | + | "params": { | |
| 6476 | + | "workspace": "flagon-io", | |
| 6477 | + | "project": "g1t" | |
| 6478 | + | }, | |
| 6479 | + | "request": { | |
| 6480 | + | "kind": "app", | |
| 6481 | + | "runs": "elsewhere", | |
| 6482 | + | "production_url": "https://g1t.sh", | |
| 6483 | + | "docs_url": "https://docs.g1t.sh", | |
| 6484 | + | "links": [ | |
| 6485 | + | { | |
| 6486 | + | "label": "Status", | |
| 6487 | + | "url": "https://status.g1t.sh" | |
| 6488 | + | } | |
| 6489 | + | ] | |
| 6490 | + | }, | |
| 6491 | + | "response": { | |
| 6492 | + | "id": "prj_01kkp3m8w2f6t9qh4c7d1r5n0x", | |
| 6493 | + | "workspace": "flagon-io", | |
| 6494 | + | "slug": "g1t", | |
| 6495 | + | "name": "g1t", | |
| 6496 | + | "description": "Git hosting where people and agents work together.", | |
| 6497 | + | "description_inherited": true, | |
| 6498 | + | "url": "https://g1t.sh/flagon-io/g1t", | |
| 6499 | + | "repository": "flagon-io/g1t", | |
| 6500 | + | "root_dir": "", | |
| 6501 | + | "default_branch": "main", | |
| 6502 | + | "private": false, | |
| 6503 | + | "archived": false, | |
| 6504 | + | "primary": true, | |
| 6505 | + | "kind": "app", | |
| 6506 | + | "kind_reason": { | |
| 6507 | + | "by": "set", | |
| 6508 | + | "detail": "Set to an app that runs elsewhere." | |
| 6509 | + | }, | |
| 6510 | + | "runs": "elsewhere", | |
| 6511 | + | "production_url": "https://g1t.sh", | |
| 6512 | + | "setting": { | |
| 6513 | + | "kind": "app", | |
| 6514 | + | "runs": "elsewhere" | |
| 6515 | + | }, | |
| 6516 | + | "detected": { | |
| 6517 | + | "kind": "app", | |
| 6518 | + | "reason": { | |
| 6519 | + | "by": "files", | |
| 6520 | + | "detail": "wrangler.jsonc at its root makes it an app." | |
| 6521 | + | } | |
| 6522 | + | }, | |
| 6523 | + | "ecosystem": null, | |
| 6524 | + | "links": { | |
| 6525 | + | "homepage": "https://g1t.sh", | |
| 6526 | + | "homepage_inherited": true, | |
| 6527 | + | "docs": "https://docs.g1t.sh", | |
| 6528 | + | "custom": [ | |
| 6529 | + | { | |
| 6530 | + | "label": "Status", | |
| 6531 | + | "url": "https://status.g1t.sh" | |
| 6532 | + | } | |
| 6533 | + | ] | |
| 6534 | + | }, | |
| 6535 | + | "created_at": "2026-09-01T10:00:00.000Z", | |
| 6536 | + | "updated_at": "2026-10-07T11:20:00.000Z", | |
| 6537 | + | "pushed_at": "2026-10-07T10:00:00.000Z" | |
| 6538 | + | }, | |
| 6539 | + | "notes": "Only the fields given change. `kind` and `runs` take `auto` to go back to detection. Setting `runs` makes the project an app unless it is docs. Making it a `library`, `tool` or `other` while [Deployments](/guides/deployments/) are on is refused with `409 conflict`: turn them off first. `description` and `homepage` given as null or `\"\"` follow the repository's again; `production_url` and `docs_url` given as null or `\"\"` are cleared. `links` replaces the project's other links: at most 10, each with a `label` of up to 40 characters and an http or https `url` (`https://` is added when you leave the scheme out); anything else is refused with `422 invalid`. Needs the Maintain role or higher on the project's repository. Recorded in the [audit log](/guides/audit-log/). See [Settings](/guides/projects/#settings)." | |
| 6540 | + | }, | |
| 6320 | 6541 | "list_secret_scanning_alerts": { | |
| 6321 | 6542 | "params": { | |
| 6322 | 6543 | "owner": "flagon-io", |
| 91 | 91 | route("PATCH", "/user/repository_invitations/:id", Op::AcceptRepoInvitation, &[]), | |
| 92 | 92 | route("DELETE", "/user/repository_invitations/:id", Op::DeclineRepoInvitation, &[]), | |
| 93 | 93 | route("PATCH", "/workspaces/:workspace", Op::UpdateWorkspace, &[]), | |
| 94 | + | // A workspace's projects: what each is, where it runs, its links. | |
| 95 | + | route("GET", "/workspaces/:workspace/projects", Op::ListProjects, &[]), | |
| 96 | + | route("GET", "/workspaces/:workspace/projects/:project", Op::GetProject, &[]), | |
| 97 | + | route("PATCH", "/workspaces/:workspace/projects/:project", Op::UpdateProject, &[]), | |
| 94 | 98 | route("PUT", "/workspaces/:workspace/base_permission", Op::SetBasePermission, &[]), | |
| 95 | 99 | route( | |
| 96 | 100 | "GET", |
| 283 | 283 | Tool { | |
| 284 | 284 | name: "workspace", | |
| 285 | 285 | title: "Workspaces", | |
| 286 | − | description: "Workspaces own repositories (g1t.sh/{workspace}/{repo}): create, update or delete one, invite members, connect integrations and model providers, set rulesets that hold across its repositories, and keep your own pinned projects at the top of its sidebar.", | |
| 286 | + | description: "Workspaces own repositories (g1t.sh/{workspace}/{repo}): create, update or delete one, invite members, connect integrations and model providers, set rulesets that hold across its repositories, read and change its projects (what each is, where it runs, its links), and keep your own pinned projects at the top of its sidebar.", | |
| 287 | 287 | default_action: None, | |
| 288 | 288 | actions: &[ | |
| 289 | 289 | a("get", Op::GetWorkspace, "A workspace's details and settings"), | |
| ⋯ | |||
| 299 | 299 | a("test_integration", Op::TestIntegration, "Check its credentials"), | |
| 300 | 300 | a("get_model_routes", Op::GetModelRoutes, "Where each kind of work's model requests go"), | |
| 301 | 301 | a("set_model_routes", Op::SetModelRoutes, "Replace them"), | |
| 302 | + | a("list_projects", Op::ListProjects, "Its projects you can see: what each is, where it runs, its links"), | |
| 303 | + | a("get_project", Op::GetProject, "One project"), | |
| 304 | + | a("update_project", Op::UpdateProject, "Change a project's name, description, kind, where it runs or its links"), | |
| 302 | 305 | a("list_pinned_projects", Op::ListPinnedProjects, "Your pinned projects in it, in your order"), | |
| 303 | 306 | a("pin_project", Op::PinProject, "Pin a project, at a position or the end"), | |
| 304 | 307 | a("unpin_project", Op::UnpinProject, "Unpin a project"), | |
| 51 | 51 | nothing and costs nothing, and the next visit wakes it in milliseconds. | |
| 52 | 52 | ||
| 53 | 53 | Deployments are part of the [g1t plan](/guides/usage-and-billing/#the-g1t-plan), | |
| 54 | − | and are opt-in per project. A project with deployments off says | |
| 55 | − | **Deployments are off** on its overview, with **Turn on deployments** for | |
| 54 | + | and are opt-in per project. A project set to deploy on g1t, with | |
| 55 | + | deployments off, says **Deployments are off** on its overview, with **Turn on deployments** for | |
| 56 | 56 | people with the Admin [role](/guides/access-and-roles/) on its repository. Nothing builds or runs until you turn them on, and one click turns | |
| 57 | 57 | them off again. | |
| 58 | 58 | ||
| ⋯ | |||
| 72 | 72 | Production starts building at once from the default branch. Every pull | |
| 73 | 73 | request opened or pushed to from then on gets a preview. | |
| 74 | 74 | ||
| 75 | − | A project g1t takes for a library or a tool, such as a Composer package, | |
| 76 | − | does not offer to deploy on its overview; it shows its packages instead. | |
| 77 | − | Its **Deployments** page still turns them on, and doing so makes it an | |
| 78 | − | app. A project set to **Doesn't deploy** cannot have deployments turned | |
| 79 | − | on until the setting changes. See | |
| 80 | − | [apps and libraries](/guides/projects/#apps-and-libraries). | |
| 75 | + | Deployments are for projects g1t runs. Not every project is one: a | |
| 76 | + | library, a tool or documentation is published rather than deployed, and | |
| 77 | + | an app may be deployed by its own pipeline somewhere else. Only a project | |
| 78 | + | that is [an app or site deployed on g1t](/guides/projects/#what-a-project-is) | |
| 79 | + | is asked to turn deployments on. Any other project's **Deployments** page | |
| 80 | + | still turns them on; doing so makes it an app deployed on g1t, including | |
| 81 | + | one that was deployed elsewhere. A project set to be a library, a tool or | |
| 82 | + | something else cannot have deployments turned on until that setting | |
| 83 | + | changes. | |
| 81 | 84 | ||
| 82 | 85 | While payments on g1t are in test mode, no real card is charged: use the | |
| 83 | 86 | test card `4242 4242 4242 4242` with any future date and any code. | |
| 25 | 25 | ||
| 26 | 26 | On g1t's models you do not have to pick a model. **Auto**, the default, | |
| 27 | 27 | sends each job to the least costly model that can do it, from three tiers: | |
| 28 | − | **fast** (Claude Haiku 4.5 today), **standard** (Claude Sonnet 5.5) and | |
| 28 | + | **fast** (Claude Haiku 5.5 today), **standard** (Claude Sonnet 5.5) and | |
| 29 | 29 | **most capable** (Claude Opus 5.5). It decides by the kind of job, the | |
| 30 | 30 | size of the change it reads, the issue's labels, whether the last attempt | |
| 31 | 31 | at the same work failed, and what has worked in the repository before: | |
| 32 | 32 | ||
| 33 | − | - Catching up, answering a question and reviewing a small change that | |
| 34 | − | touches no sensitive path start on the fast model. | |
| 35 | − | - Making and revising changes, planning, and most reviews start on the | |
| 36 | − | standard model. | |
| 33 | + | - Catching up, answering a question, planning, and reviewing a small | |
| 34 | + | change that touches no sensitive path start on the fast model. | |
| 35 | + | - Making and revising changes, and most reviews, start on the standard | |
| 36 | + | model. | |
| 37 | + | - Planning runs at high effort (the model thinks longer before it | |
| 38 | + | answers), answering at medium and catching up at low, on models that | |
| 39 | + | take an effort level. | |
| 37 | 40 | - A review of a very large change, work on an issue labelled | |
| 38 | 41 | `architecture`, and work that failed twice in a row go to the most | |
| 39 | 42 | capable model. One failure moves the next attempt up one tier. | |
| ⋯ | |||
| 42 | 45 | keeps failing, up. | |
| 43 | 46 | ||
| 44 | 47 | Every run says which model it used and why, in one line on its run and in | |
| 45 | − | its pull request's session, such as *Used a fast model (Claude Haiku 4.5): | |
| 48 | + | its pull request's session, such as *Used a fast model (Claude Haiku 5.5): | |
| 46 | 49 | small change, 3 files and 80 lines.* The full rules are in | |
| 47 | 50 | [which model runs](/guides/working-with-g1t/#which-model-runs). | |
| 48 | 51 | ||
| 35 | 35 | ||
| 36 | 36 | | Page | Address | | | |
| 37 | 37 | | --- | --- | --- | | |
| 38 | − | | **Overview** | `g1t.sh/<workspace>/<project>` | Production with a screenshot, or for a library its packages; the steps left to get to production or to a first release; what is in progress, active branches, live previews, recent builds, and the source. See [the overview](#the-overview). | | |
| 38 | + | | **Overview** | `g1t.sh/<workspace>/<project>` | What it is and its links; production wherever it is deployed, or for a library its packages; the steps that apply to it; what is in progress, active branches, the latest commits, and its About. See [the overview](#the-overview). | | |
| 39 | 39 | | **Code** | `…/code` | The repository's files, commits and branches. | | |
| 40 | 40 | | **Issues**, **Pull requests**, **Merge queue**, **Plan** | `…/issues` and so on | As they always were. | | |
| 41 | 41 | | **Actions** | `…/actions` | [GitHub Actions workflows](/guides/actions/). | | |
| ⋯ | |||
| 53 | 53 | ||
| 54 | 54 | | Part | What it shows | | |
| 55 | 55 | | --- | --- | | |
| 56 | − | | **Production** | For an app: a screenshot of the live site, which opens it; its address, the commit it runs and when it went up; **Visit** and **Redeploy**. See [the production screenshot](/guides/deployments/#the-production-screenshot). | | |
| 57 | − | | **Packages** | For a [library or a tool](#apps-and-libraries), in place of Production: the packages its repository publishes, each with its latest version and how to install it, or how to publish a first one. | | |
| 58 | − | | **Get to production**, or **Ship a release** for a library | The checklists below, until every step is done or you dismiss it. | | |
| 56 | + | | **What it is** | A badge, such as **App, deployed elsewhere** or **Library**, beside its homepage and docs. People who can change its settings open the badge to change [what it is](#what-a-project-is) in one click. | | |
| 57 | + | | **Production** | For an app [deployed on g1t](/guides/deployments/): a screenshot of the live site, which opens it; its address, the commit it runs and when it went up; **Visit** and **Redeploy**. For an app deployed elsewhere: production's address and **Visit**, with the default branch's latest commit. For an app nobody has placed yet: one question, where it runs. | | |
| 58 | + | | **Packages** | For a library or a tool, in place of Production: the packages its repository publishes, each with its latest version and how to install it, or how to publish a first one. | | |
| 59 | + | | **Documentation**, **Homepage** | For docs, where they are read; for anything else, its homepage. | | |
| 60 | + | | **Get to production**, **Ship a release** or **Get started** | The [checklist](#the-checklist) for what the project is, until every step is done or you dismiss it. | | |
| 59 | 61 | | **Right now** | Agents at work, and open pull requests moving from working to landed. | | |
| 60 | 62 | | **Needs you** | What is waiting on a person: a failed production build, a pull request to merge or review, a stuck run. | | |
| 61 | 63 | | **Active branches** | Branches other than the default, newest first. See [active branches](#active-branches). | | |
| 62 | − | | **Recent changes**, **Activity**, **Previews** | What landed, everything that happened, and the previews that are up. | | |
| 64 | + | | **Recent changes**, **Latest on main**, **Activity**, **Previews** | What landed, the default branch's latest commits, everything that happened, and the previews that are up. | | |
| 65 | + | | **About** | Its description, its [links](#links), what it is, and its latest release (its newest tag). People who can change its settings edit the description and links from here. | | |
| 63 | 66 | | **Health**, **Dependencies**, **Clone** | How often checks pass, recent builds and open issues by age; what it uses and what uses it; the clone address. | | |
| 64 | 67 | ||
| 68 | + | ### The checklist | |
| 69 | + | ||
| 70 | + | People with a role on its repository see a card that counts what the | |
| 71 | + | project has done, such as **3/5**. Its steps follow | |
| 72 | + | [what the project is](#what-a-project-is): only an app deployed on g1t is | |
| 73 | + | asked to deploy, add a domain or open a preview. Each step is worked out | |
| 74 | + | from the project itself, and each links to where you do it. The card goes | |
| 75 | + | away when every step is done. To hide it sooner, choose **×** on it. That | |
| 76 | + | hides it for this project in this browser only. | |
| 77 | + | ||
| 78 | + | | What it is | Card | Steps | | |
| 79 | + | | --- | --- | --- | | |
| 80 | + | | App or site, deployed on g1t | **Get to production** | [Below](#get-to-production). | | |
| 81 | + | | Library, tool | **Ship a release** | [Below](#ship-a-release). | | |
| 82 | + | | App, deployed elsewhere | **Get started** | Push code; add production's address; add checks on pull requests; repository instructions; a first issue for g1t. | | |
| 83 | + | | App nobody has placed yet | **Get started** | Push code; say where it runs; add checks on pull requests; repository instructions; a first issue for g1t. | | |
| 84 | + | | Documentation | **Get started** | Push code; add where its docs are read (a docs or homepage address); repository instructions; a first issue for g1t. | | |
| 85 | + | | Something else | **Get started** | Push code; add its links; repository instructions; a first issue for g1t. | | |
| 86 | + | ||
| 65 | 87 | ### Get to production | |
| 66 | 88 | ||
| 67 | − | People with a role on its repository see a card that counts what the project has done towards | |
| 68 | − | production, such as **3/6**. Each step is worked out from the project | |
| 69 | − | itself, and each links to where you do it: | |
| 89 | + | For an app or site deployed on g1t: | |
| 70 | 90 | ||
| 71 | 91 | | Step | Done when | Links to | | |
| 72 | 92 | | --- | --- | --- | | |
| ⋯ | |||
| 77 | 97 | | Set up repository instructions | `AGENTS.md` or `CLAUDE.md` is at the root of the default branch. | The instructions on the **Agents** page. See [repository instructions](/guides/working-with-g1t/#repository-instructions). | | |
| 78 | 98 | | Assign a first issue to g1t | g1t has had a run, a pull request or an issue here. | A new issue. | | |
| 79 | 99 | ||
| 80 | − | The card goes away when every step is done. To hide it sooner, choose | |
| 81 | − | **×** on it. That hides it for this project in this browser only. | |
| 82 | − | ||
| 83 | 100 | ### Ship a release | |
| 84 | 101 | ||
| 85 | − | A [library or a tool](#apps-and-libraries) does not deploy, so its card | |
| 86 | − | counts the steps to a first release instead: | |
| 102 | + | A library or a tool does not deploy, so its card counts the steps to a | |
| 103 | + | first release instead: | |
| 87 | 104 | ||
| 88 | 105 | | Step | Done when | Links to | | |
| 89 | 106 | | --- | --- | --- | | |
| ⋯ | |||
| 93 | 110 | | Set up repository instructions | As above. | The **Agents** page. | | |
| 94 | 111 | | Assign a first issue to g1t | As above. | A new issue. | | |
| 95 | 112 | ||
| 96 | − | ## Apps and libraries | |
| 113 | + | ## What a project is | |
| 97 | 114 | ||
| 98 | − | Every project is either an **app**, which deploys, or a **library or a | |
| 99 | − | tool**, which is published and installed. An app's overview shows | |
| 100 | − | production and the steps to get there; a library's shows its packages | |
| 101 | − | and the steps to a first release, and never offers to turn on | |
| 102 | − | deployments. Its **Deployments** page stays in the sidebar either way. | |
| 115 | + | A project is one of these, and its overview follows: | |
| 103 | 116 | ||
| 104 | − | g1t works it out for itself, in this order: | |
| 117 | + | | What it is | Its overview shows | | |
| 118 | + | | --- | --- | | |
| 119 | + | | **App or site, deployed on g1t** | Production on g1t.page, with previews and domains, and **Get to production**. | | |
| 120 | + | | **App or site, deployed elsewhere** | Production at the address you give, deployed by your own pipeline, with **Visit**. It is never asked to turn on Deployments; Deployment settings stay one link away. | | |
| 121 | + | | **Library or package** | The packages its repository publishes and how to install them, and **Ship a release**. | | |
| 122 | + | | **Tool or CLI** | The same as a library: its packages and releases. | | |
| 123 | + | | **Documentation** | Where its docs are read. Docs can be published on g1t.page too, by turning on Deployments. | | |
| 124 | + | | **Something else** | Its homepage and links. | | |
| 105 | 125 | ||
| 106 | − | 1. If [Deployments](/guides/deployments/) are on for the project, it is an app. | |
| 126 | + | Its **Deployments** page stays in the sidebar whatever it is. | |
| 127 | + | ||
| 128 | + | ### How g1t works it out | |
| 129 | + | ||
| 130 | + | Until you say, g1t works it out for itself, in this order: | |
| 131 | + | ||
| 132 | + | 1. If [Deployments](/guides/deployments/) are on for the project, it is | |
| 133 | + | an app deployed on g1t. | |
| 107 | 134 | 2. If its repository publishes a package other than a container image, | |
| 108 | 135 | such as a [Composer](/guides/composer/) or [npm](/guides/npm/) | |
| 109 | 136 | package, it is a library. | |
| 110 | 137 | 3. If the files at the root of its default branch (or of its root | |
| 111 | − | directory) say it is a library, it is one: | |
| 138 | + | directory) say what it is, it is that: | |
| 112 | 139 | ||
| 113 | − | | File | A library when | | |
| 140 | + | | File | Says | | |
| 114 | 141 | | --- | --- | | |
| 115 | − | | `composer.json` | Its `type` is anything but `project`, such as `library`; or it has no `type`, has `autoload`, and has no `public/index.php`. | | |
| 116 | − | | `Cargo.toml` | It builds a library (`[lib]` or `src/lib.rs`) and no binary (`[[bin]]` or `src/main.rs`). | | |
| 117 | − | | `go.mod` | No `.go` file at the root is `package main`. | | |
| 118 | − | | `pyproject.toml` | It has a build backend and depends on no app framework, such as Django, Flask or FastAPI. | | |
| 119 | − | | `package.json` | It has `main`, `exports`, `module`, `files` or `bin`, no `start` or `dev` script, and no app framework such as Next.js, Astro, Nuxt, Remix or SvelteKit. | | |
| 142 | + | | `wrangler.jsonc`, `wrangler.json` or `wrangler.toml` | An app. | | |
| 143 | + | | `mkdocs.yml`, `book.toml`, `docusaurus.config.js` (or `.ts`, `.mjs`) or `antora.yml` | Documentation. | | |
| 144 | + | | `composer.json` | A library when its `type` is anything but `project`, such as `library`; or it has no `type`, has `autoload`, and has no `public/index.php`. | | |
| 145 | + | | `Cargo.toml` | A library when it builds a library (`[lib]` or `src/lib.rs`) and no binary (`[[bin]]` or `src/main.rs`). | | |
| 146 | + | | `go.mod` | A library when no `.go` file at the root is `package main`. | | |
| 147 | + | | `pyproject.toml` | A library when it has a build backend and depends on no app framework, such as Django, Flask or FastAPI. | | |
| 148 | + | | `package.json` | A tool when it has `bin` and nothing to import (no `main`, `module` or `exports`); a library when it has `main`, `exports`, `module`, `files` or `bin`; either way only with no `start` or `dev` script and no app framework such as Next.js, Astro, Nuxt, Remix or SvelteKit. | | |
| 120 | 149 | ||
| 121 | 150 | The language's own manifest is read before `package.json`, which many | |
| 122 | − | projects carry only for tooling. A Workers config (`wrangler.jsonc`, | |
| 123 | − | `wrangler.json` or `wrangler.toml`) or an `index.html` at the root | |
| 124 | − | makes it an app. | |
| 151 | + | projects carry only for tooling. An `index.html` at the root makes it | |
| 152 | + | an app. | |
| 125 | 153 | 4. Anything else is an app. | |
| 126 | 154 | ||
| 127 | 155 | The files are read again on every push to the default branch. | |
| 128 | 156 | ||
| 129 | − | To decide for yourself, open **Settings**, then **General**, and under | |
| 130 | − | **Deployments for this project** choose: | |
| 157 | + | An app found this way runs on g1t only while Deployments are on. Until | |
| 158 | + | you say where it runs, its overview asks, in place of production: | |
| 159 | + | **Deploy on g1t**, **It's deployed elsewhere** (with production's | |
| 160 | + | address), or **It isn't deployed** (a library, a tool, documentation or | |
| 161 | + | something else). | |
| 162 | + | ||
| 163 | + | ### Choosing it yourself | |
| 164 | + | ||
| 165 | + | From the overview, open the badge beside its name, such as **App**, and | |
| 166 | + | choose one. Or open **Settings**, then **General**, and under **What it | |
| 167 | + | is** choose **Detect automatically**, which shows what was detected and | |
| 168 | + | why, or one of the kinds above. **App or site, deployed elsewhere** asks | |
| 169 | + | for production's address. | |
| 170 | + | ||
| 171 | + | Choosing a library, a tool or something else while Deployments are on is | |
| 172 | + | refused: turn them off first under **Settings**, then **Deployments**; | |
| 173 | + | g1t does not turn them off for you. While it is set that way, Deployments | |
| 174 | + | cannot be turned on. Turning Deployments on for an app deployed elsewhere | |
| 175 | + | makes it an app deployed on g1t. | |
| 131 | 176 | ||
| 132 | − | - **Detect automatically**: the rules above. It shows what was detected | |
| 133 | − | and why. | |
| 134 | − | - **Deploys**: an app, whatever its files say. | |
| 135 | − | - **Doesn't deploy**: a library or a tool. Its overview offers no | |
| 136 | − | deploying. If Deployments are on for it, turn them off first under | |
| 137 | − | **Settings**, then **Deployments**; g1t does not turn them off for you. | |
| 138 | − | While it is set this way, Deployments cannot be turned on. | |
| 177 | + | Changing what it is needs the role that changes the project's settings. | |
| 139 | 178 | ||
| 140 | 179 | ### Active branches | |
| 141 | 180 | ||
| ⋯ | |||
| 152 | 191 | are read, those with open pull requests first; a project with more says | |
| 153 | 192 | how many it has. | |
| 154 | 193 | ||
| 194 | + | ## Links | |
| 195 | + | ||
| 196 | + | A project has a **homepage**, a **docs** address, and up to 10 other | |
| 197 | + | links, each a label and an address, such as a status page or its listing | |
| 198 | + | in a registry. They show in its overview's About and beside what it is, | |
| 199 | + | on its card on its workspace's page, in its workspace's Projects, and on | |
| 200 | + | Explore for a public project. | |
| 201 | + | ||
| 202 | + | To set them, open **Settings**, then **General**, and fill in **Links**; | |
| 203 | + | or choose the pencil on the overview's **About**. Each is an http or | |
| 204 | + | https address; `https://` is added when you leave it out. A label left | |
| 205 | + | empty is the address's host name. Removing every row of other links | |
| 206 | + | clears them. | |
| 207 | + | ||
| 208 | + | A repository's own project shows the repository's website as its | |
| 209 | + | homepage until you give the project one of its own. For an app deployed | |
| 210 | + | on g1t, its g1t.page address still shows as production; set the homepage | |
| 211 | + | to production's own domain if you have one. | |
| 212 | + | ||
| 155 | 213 | ## Settings | |
| 156 | 214 | ||
| 157 | 215 | A project's **Settings** has a tab for each part: | |
| 158 | 216 | ||
| 159 | 217 | | Tab | What it holds | | |
| 160 | 218 | | --- | --- | | |
| 161 | − | | **General** | The project's name and description, its source, its **root directory**, and whether it deploys (see [apps and libraries](#apps-and-libraries)). A project shows its repository's description, and follows it as it changes, until you give the project one of its own; **Use the repository's description** goes back. | | |
| 219 | + | | **General** | The project's name and description, its source, its **root directory**, [what it is](#what-a-project-is), and its [links](#links). A project shows its repository's description, and follows it as it changes, until you give the project one of its own; **Use the repository's description** goes back. | | |
| 162 | 220 | | **Deployments** | Production, previews, build command, output directory and idle days. See [Deployments](/guides/deployments/#settings). | | |
| 163 | 221 | | **Domains** | Custom domains for production. See [custom domains](/guides/deployments/#custom-domains). | | |
| 164 | 222 | | **Dependencies** | The projects this one uses, and the ones that use it. See [Dependencies](#dependencies). | | |
| ⋯ | |||
| 259 | 317 | Projects keep their repository's routes: `/repos/{workspace}/{project}/…` | |
| 260 | 318 | reaches the project's repository, and its secrets and variables are the | |
| 261 | 319 | project's. See the [API reference](/reference/api/). | |
| 320 | + | ||
| 321 | + | What a project is, where it runs and its links have routes of their own, | |
| 322 | + | and actions on the [MCP](/reference/mcp/) `workspace` tool: | |
| 323 | + | ||
| 324 | + | | Route | MCP action | | | |
| 325 | + | | --- | --- | --- | | |
| 326 | + | | `GET /workspaces/{workspace}/projects` | `list_projects` | The workspace's projects you can see. | | |
| 327 | + | | `GET /workspaces/{workspace}/projects/{project}` | `get_project` | One project, with `kind`, `runs`, `production_url`, `setting`, `detected` and `links`. | | |
| 328 | + | | `PATCH /workspaces/{workspace}/projects/{project}` | `update_project` | Changes `kind`, `runs`, `production_url`, `homepage`, `docs_url`, `links`, or its name, description or root directory. Needs `repo:write`. | | |
| 329 | + | ||
| 330 | + | To say an app is deployed by your own pipeline, with its docs: | |
| 331 | + | ||
| 332 | + | ```sh | |
| 333 | + | curl -X PATCH https://api.g1t.sh/workspaces/acme/projects/web \ | |
| 334 | + | -H "Authorization: Bearer $G1T_TOKEN" \ | |
| 335 | + | -H "Content-Type: application/json" \ | |
| 336 | + | -d '{"runs": "elsewhere", "production_url": "https://acme.dev", "docs_url": "https://docs.acme.dev"}' | |
| 337 | + | ``` | |
| 446 | 446 | ||
| 447 | 447 | | Tier | Model today | For | | |
| 448 | 448 | | --- | --- | --- | | |
| 449 | − | | Fast | Claude Haiku 4.5 | Small, well-bounded work | | |
| 449 | + | | Fast | Claude Haiku 5.5 | Small, well-bounded work, and plans | | |
| 450 | 450 | | Standard | Claude Sonnet 5.5 | Most changes and reviews | | |
| 451 | 451 | | Most capable | Claude Opus 5.5 | Hard work, and work that failed on the standard model | | |
| 452 | 452 | ||
| ⋯ | |||
| 460 | 460 | | Reviewing a pull request that changes more than 60 files or 3,000 lines | Most capable | | |
| 461 | 461 | | Reviewing any other pull request, or one whose changed files g1t does not know yet | Standard | | |
| 462 | 462 | | Catching up with the base branch and resolving conflicts | Fast | | |
| 463 | − | | Planning an outcome | Standard | | |
| 463 | + | | Planning an outcome | Fast, at high effort | | |
| 464 | 464 | ||
| 465 | 465 | Then, in this order: | |
| 466 | 466 | ||
| ⋯ | |||
| 486 | 486 | ||
| 487 | 487 | The agent's own small background steps run on the fast tier. | |
| 488 | 488 | ||
| 489 | + | Some work also has an effort level, how long the model thinks before it | |
| 490 | + | answers: planning runs at high effort, answering a question at medium and | |
| 491 | + | catching up at low, whichever tier the work lands on. Other work runs at | |
| 492 | + | the model's default. A workspace's own provider is sent no effort level. | |
| 493 | + | ||
| 489 | 494 | Every run says which model it used and why, in one line: as the first | |
| 490 | 495 | step on its run, and at the top of its pull request's session. For | |
| 491 | − | example, *Used a fast model (Claude Haiku 4.5): small change, 3 files and | |
| 496 | + | example, *Used a fast model (Claude Haiku 5.5): small change, 3 files and | |
| 492 | 497 | 80 lines.* An agent's review also says which model wrote it. When a better | |
| 493 | 498 | model for a tier appears, g1t changes the route and nothing you have set | |
| 494 | 499 | up needs to change. | |
| 347 | 347 | - **Find a project** matches every word you type in a project's name, its | |
| 348 | 348 | address or its description. Press <kbd>/</kbd> anywhere on the page to | |
| 349 | 349 | start typing. | |
| 350 | − | - **Filters**: public or private; apps or libraries; the language its | |
| 350 | + | - **Filters**: public or private; [what it is](/guides/projects/#what-a-project-is) | |
| 351 | + | (apps, libraries, and tools, docs or other when the workspace has any); the language its | |
| 351 | 352 | manifests say it is written in; only projects with | |
| 352 | 353 | [Deployments](/guides/deployments/) on; and archived projects, which are | |
| 353 | 354 | left out unless you ask for them. Each choice shows how many projects it |
| 541 | 541 | ## `workspace` | |
| 542 | 542 | ||
| 543 | 543 | Workspaces own repositories: create, update or delete one, invite members, | |
| 544 | − | connect [integrations](/guides/integrations/) and model providers, and keep | |
| 545 | − | your own [pinned projects](/guides/workspaces/#pinned-and-recent-projects) | |
| 546 | − | at the top of its sidebar. See [workspaces](/guides/workspaces/). | |
| 544 | + | connect [integrations](/guides/integrations/) and model providers, read and | |
| 545 | + | change its [projects](/guides/projects/) (what each is, where it runs, its | |
| 546 | + | links), and keep your own | |
| 547 | + | [pinned projects](/guides/workspaces/#pinned-and-recent-projects) at the top | |
| 548 | + | of its sidebar. See [workspaces](/guides/workspaces/). | |
| 547 | 549 | ||
| 548 | 550 | | Action | What it does | Required | Scope | | |
| 549 | 551 | | --- | --- | --- | --- | | |
| ⋯ | |||
| 560 | 562 | | [`test_integration`](/reference/api/integrations/test-integration/) | Check its credentials against the system it connects to. Owners only. | `workspace`, `id` | `workspace:admin` | | |
| 561 | 563 | | [`get_model_routes`](/reference/api/integrations/get-model-routes/) | Which provider and model each kind of work goes to. Members only. | `workspace` | `workspace:read` | | |
| 562 | 564 | | [`set_model_routes`](/reference/api/integrations/set-model-routes/) | Replace them: each route has `task`, `connection_id` (null for g1t's models) and `model` (on g1t's models: `small`, `large`, `frontier`, or null for Auto). Owners only. | `workspace`, `routes` | `workspace:admin` | | |
| 565 | + | | [`list_projects`](/reference/api/projects/list-projects/) | Its projects you can see, by name: each with its `kind` and `kind_reason`, where it `runs` and its `production_url`, its repository and `root_dir`, and its `links`. | `workspace` | `repo:read` | | |
| 566 | + | | [`get_project`](/reference/api/projects/get-project/) | One project, with what a person set (`setting`) and what detection decides (`detected`). | `workspace`, `project` | `repo:read` | | |
| 567 | + | | [`update_project`](/reference/api/projects/update-project/) | Change its `name`, `description`, `root_dir`, `kind`, `runs`, `production_url`, `homepage`, `docs_url` or `links`; only what you give changes. `auto` leaves `kind` or `runs` to detection, null clears a link or goes back to the repository's, and `links` replaces its other links (at most 10). Maintain role or higher. | `workspace`, `project` | `repo:write` | | |
| 563 | 568 | | [`list_pinned_projects`](/reference/api/pinned-projects/list-pinned-projects/) | Your pinned projects in it, in your order, each with its `position`. Your own: a personal token or an OAuth sign-in. | `workspace` | `account:read` | | |
| 564 | 569 | | [`pin_project`](/reference/api/pinned-projects/pin-project/) | Pin a project you can see, at `position` (0 first) or at the end; at most 8 a workspace. Returns your pins. | `workspace`, `project` | `account:write` | | |
| 565 | 570 | | [`unpin_project`](/reference/api/pinned-projects/unpin-project/) | Unpin it. Returns your pins. | `workspace`, `project` | `account:write` | | |
Binary or large file; its contents are not shown.
| 736 | 736 | <Section | |
| 737 | 737 | id="reset" | |
| 738 | 738 | title="Reset billing (testing)" | |
| 739 | − | description="Wipes this workspace's billing: its ledger and balance, plan, limits, trial, invoices, holds, signals and cost rows. The workspace, its members and its repositories stay. Only while billing is on Stripe's test key." | |
| 739 | + | description="Wipes this workspace's billing: its ledger and balance, plan, limits, trial, invoices, holds, signals and cost rows. What its usage cost g1t is kept, and counted on Costs & margin as given away (testing resets). The workspace, its members and its repositories stay. Only while billing is on Stripe's test key." | |
| 740 | 740 | > | |
| 741 | 741 | <form method="post" action={`${pathname}#reset`} className="space-y-4"> | |
| 742 | 742 | <input type="hidden" name="intent" value="reset" /> |
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
This change is too large to show in full.