An agent can turn on a project's g1t.page deployments, and have them from the moment it makes the repository: an agent (or Claude Code through the g1t MCP) could create a repository but not make it deploy, so flagon-io/session-lab needed a person to click Turn on deployments under Settings → Deployments, because only the site (that page, and Ask's new-repository card) called the deployments service's update_settings, and update_project's runs does not turn them on. Two operations now read and change a project's deployment settings as that page does, through the same service call: get_deployment_settings (GET /workspaces/{workspace}/projects/{project}/deployments/settings, deployments:read, the Read role as the service checks) and update_deployment_settings (PATCH there, repo:admin, the Admin role the page and the service require), in snake_case: enabled, production, previews, build_as, build_command, output_dir, dockerfile, instance_type, idle_days, via_workflow, with production_url, primary_domain, detected and framework read back. enabled true builds production at once; without the g1t plan it is refused with 402 payment_required saying where an owner starts it, without Admin with 403, and for a library, tool or other with 409, as the site says; bad values are refused with 422 before the service is asked. They are actions of the MCP workflow tool next to the deployment actions, and the CLI has g1t deploy settings (flags such as --previews off and --build-command change them), g1t deploy enable and g1t deploy disable, which find the current repository's project as g1t logs does (logs' resolve_project). create_repo takes deploy (REST, the repository tool's create, g1t repo create --deploy): once the repository is made its project's deployments are turned on as Ask's card does it, as its creator, and the response adds deployments (the settings) and deploy_error (why not); the repository is made either way, and deploy needs no scope beyond repo:create since the creator is its admin. Run credentials never change deployment settings (NEVER), and get_deployment_settings is a read for the audit log. The site keeps its own calls (ask.ts and the Settings page call the same service method the API now does). The deployments guide (Without the site, Settings), the deployments API guide, projects, the CLI guide, the MCP reference, reference.json and the regenerated openapi.json, CLI ops table and CLI reference say how. Deploy order: api, then the docs; the CLI commands ship with the next CLI release; no service or migration changes. Checked: cargo test -p g1t-api (211; openapi regenerated with G1T_WRITE_OPENAPI), -p g1t-contracts (316), -p g1t (110 + 51; ops_table and cli_reference regenerated with G1T_WRITE_OPS and G1T_WRITE_DOCS); cargo check -p g1t-api --target wasm32-unknown-unknown; node --test apps/web/app/lib/token-scopes.test.ts.
- G1t-Session:
- ses_01m4z48a8be8mbhv0qt30jyg0j
22 files+530−126 files not counted0/22 viewed
| 7 | 7 | //! them (Read to see, Write to report) and keeps them; deployments travel | |
| 8 | 8 | //! in `snake_case` between services too, so a request's fields reach it | |
| 9 | 9 | //! as they are, and a deployment's `payload` comes back as it was given. | |
| 10 | + | //! | |
| 11 | + | //! A project's g1t.page settings (whether it deploys at all, and how it | |
| 12 | + | //! builds) are here too, named by workspace and project as the project | |
| 13 | + | //! routes are: the deployments service keeps them in `camelCase`, as the | |
| 14 | + | //! site reads them, so they are shaped here. | |
| 10 | 15 | ||
| 11 | 16 | use g1t_contracts::{FailureCode, Outcome, Viewer}; | |
| 12 | 17 | use serde_json::{Map, Value, json}; | |
| ⋯ | |||
| 25 | 30 | ListEnvironments, | |
| 26 | 31 | GetEnvironment, | |
| 27 | 32 | StartDeploymentBuild, | |
| 33 | + | GetDeploymentSettings, | |
| 34 | + | UpdateDeploymentSettings, | |
| 28 | 35 | } | |
| 29 | 36 | ||
| 30 | 37 | /// The states a deployment's status can have. | |
| 31 | 38 | const STATES: [&str; 6] = ["queued", "in_progress", "success", "failure", "error", "inactive"]; | |
| 32 | 39 | ||
| 40 | + | /// What a project may be built as (`BUILD_AS` in @g1t/contracts). | |
| 41 | + | const BUILD_AS: [&str; 4] = ["auto", "static", "workers", "docker"]; | |
| 42 | + | ||
| 43 | + | /// The sizes a container app runs at (`APP_INSTANCES` in @g1t/contracts). | |
| 44 | + | const INSTANCE_TYPES: [&str; 6] = ["lite", "basic", "standard-1", "standard-2", "standard-3", "standard-4"]; | |
| 45 | + | ||
| 33 | 46 | impl DeploymentsOp { | |
| 34 | 47 | /// Every one: `Op::ALL` lists each as `Op::Deployments(…)`, which a | |
| 35 | 48 | /// test checks against this. | |
| 36 | 49 | #[cfg(test)] | |
| 37 | − | pub const ALL: [DeploymentsOp; 8] = [ | |
| 50 | + | pub const ALL: [DeploymentsOp; 10] = [ | |
| 38 | 51 | DeploymentsOp::ListDeployments, | |
| 39 | 52 | DeploymentsOp::GetDeployment, | |
| 40 | 53 | DeploymentsOp::CreateDeployment, | |
| ⋯ | |||
| 43 | 56 | DeploymentsOp::ListEnvironments, | |
| 44 | 57 | DeploymentsOp::GetEnvironment, | |
| 45 | 58 | DeploymentsOp::StartDeploymentBuild, | |
| 59 | + | DeploymentsOp::GetDeploymentSettings, | |
| 60 | + | DeploymentsOp::UpdateDeploymentSettings, | |
| 46 | 61 | ]; | |
| 47 | 62 | ||
| 48 | 63 | pub fn name(self) -> &'static str { | |
| ⋯ | |||
| 55 | 70 | DeploymentsOp::ListEnvironments => "list_environments", | |
| 56 | 71 | DeploymentsOp::GetEnvironment => "get_environment", | |
| 57 | 72 | DeploymentsOp::StartDeploymentBuild => "start_deployment_build", | |
| 73 | + | DeploymentsOp::GetDeploymentSettings => "get_deployment_settings", | |
| 74 | + | DeploymentsOp::UpdateDeploymentSettings => "update_deployment_settings", | |
| 58 | 75 | } | |
| 59 | 76 | } | |
| 60 | 77 | ||
| ⋯ | |||
| 69 | 86 | DeploymentsOp::ListEnvironments => "List environments", | |
| 70 | 87 | DeploymentsOp::GetEnvironment => "Get an environment", | |
| 71 | 88 | DeploymentsOp::StartDeploymentBuild => "Start a g1t.page build from a workflow", | |
| 89 | + | DeploymentsOp::GetDeploymentSettings => "Get a project's deployment settings", | |
| 90 | + | DeploymentsOp::UpdateDeploymentSettings => "Update a project's deployment settings", | |
| 72 | 91 | } | |
| 73 | 92 | } | |
| 74 | 93 | ||
| ⋯ | |||
| 82 | 101 | DeploymentsOp::ListEnvironments => "List the environments a repository's deployments went to, those people use directly first (production by name before others), then the most recently deployed, and after them those with protection rules but no deployment yet. Each has its name, url (where its current deployment is served), production_environment, transient_environment, deployments_count, latest (its newest deployment, whatever its state), current (its newest successful deployment that is still active) and updated_at, and, when it has rules, protection_rules (required_reviewers, wait_timer, branch_policy), deployment_branch_policy, branch_policies and can_admins_bypass. total_count counts deployments across every environment. Needs the Read role.", | |
| 83 | 102 | DeploymentsOp::GetEnvironment => "Get one environment by name, matched without regard to case, with its current and latest deployments and its protection rules (see update_environment). An environment with rules but no deployment yet is found too. A name with slashes is URL-encoded in the path. Needs the Read role.", | |
| 84 | 103 | DeploymentsOp::StartDeploymentBuild => "From a running g1t Actions job, open a g1t.page build of one of the repository's projects, which the job then builds in its own checkout with `g1t-runner deploy` (the g1t/deploy action does both). Only the job's own token (G1T_TOKEN) may call it, with `deployments: write` (or `pages: write`) in its `permissions:`. project is the project's slug; it may be left out when the repository has one project with Deployments on. The run decides what is built: a run on the project's production branch (a push, a dispatch or a schedule there) builds production, a pull request's run builds that pull request's preview, and a run on another branch builds that branch's preview; a tag's run is refused. sha is the commit built, the run's unless you say. A pull request from a fork or from someone without the Write role, and a run that had to be approved, get no build secrets. The workspace's plan must include Deployments and its spend limit must not be reached; the build's time is the job's Actions time and is not charged again. Returns deploy_id and token (DEPLOY_ID and DEPLOY_TOKEN for `g1t-runner deploy`, the token good for this build alone until it finishes), kind (production or preview), project, branch, number, commit, url (the app's address), trusted, the project's root_dir, build_command and output_dir, build_as (auto, static, workers or docker) and dockerfile (its Dockerfile's path, or null to find `Dockerfile` or `Containerfile`), and build_env and build_secrets (the project's variables and secrets for builds of that kind). The commit's `g1t / deploy` check is pending until the build reports.", | |
| 104 | + | DeploymentsOp::GetDeploymentSettings => "A project's g1t.page deployment settings, as its Settings → Deployments page shows them: whether Deployments are on (`enabled`; off for every project until someone turns them on), whether the default branch deploys to production on every push (`production`) and every pull request gets a preview (`previews`), production's address (`production_url`, and `primary_domain` once a custom domain serves it), how it builds (`build_as`: auto, static, workers or docker; `build_command`, `output_dir`, `dockerfile` and `instance_type` for a container; `via_workflow` to build through the deploy workflow g1t manages), how many days an unvisited preview stays up (`idle_days`), and what its last finished build found it to be (`detected`: workers, static, html or container, with `framework`), null before one has. A repository's own project has its name. Needs the Read role on the project's repository.", | |
| 105 | + | DeploymentsOp::UpdateDeploymentSettings => "Change a project's g1t.page deployment settings; only what you give changes. `enabled: true` turns Deployments on, as Turn on deployments under Settings → Deployments does: production starts building from the default branch at once, and every pull request gets a preview. That needs the g1t plan on the workspace (refused with payment_required otherwise; an owner starts it under Billing), and is refused for a project set to be a library, a tool or other until update_project makes it an app. `enabled: false` takes production and every preview down now and stops building; turning `production` or `previews` off takes those apps down. build_command, output_dir and dockerfile take null or \"\" to go back to what is found by itself; idle_days is 1 to 90; build_as is auto, static, workers or docker; instance_type is a container size (lite, basic, standard-1 to standard-4). Needs the Admin role on the project's repository. Returns the settings as get_deployment_settings does.", | |
| 85 | 106 | } | |
| 86 | 107 | } | |
| 87 | 108 | ||
| ⋯ | |||
| 89 | 110 | pub fn writes(self) -> bool { | |
| 90 | 111 | matches!( | |
| 91 | 112 | self, | |
| 92 | − | DeploymentsOp::CreateDeployment | DeploymentsOp::CreateDeploymentStatus | DeploymentsOp::StartDeploymentBuild | |
| 113 | + | DeploymentsOp::CreateDeployment | |
| 114 | + | | DeploymentsOp::CreateDeploymentStatus | |
| 115 | + | | DeploymentsOp::StartDeploymentBuild | |
| 116 | + | | DeploymentsOp::UpdateDeploymentSettings | |
| 93 | 117 | ) | |
| 94 | 118 | } | |
| 95 | 119 | ||
| 120 | + | /// Whether it is about one repository, named by `repo`: a project's | |
| 121 | + | /// settings are named by its workspace and slug instead. | |
| 122 | + | pub fn needs_repo(self) -> bool { | |
| 123 | + | !matches!(self, DeploymentsOp::GetDeploymentSettings | DeploymentsOp::UpdateDeploymentSettings) | |
| 124 | + | } | |
| 125 | + | ||
| 96 | 126 | pub fn input(self) -> Value { | |
| 97 | 127 | let repo = json!({ "type": "string", "description": "Repository as \"owner/name\", e.g. \"flagon-io/hello\"." }); | |
| 98 | 128 | let id = json!({ "type": "string", "description": "The deployment's id: dep_… for a reported one, dpl_… for a g1t.page build." }); | |
| 99 | 129 | let state = |what: &str| json!({ "type": "string", "enum": STATES, "description": what }); | |
| 100 | 130 | let address = |what: &str| json!({ "type": "string", "description": what }); | |
| 131 | + | let workspace = json!({ "type": "string", "description": "The workspace's slug, e.g. \"flagon-io\"." }); | |
| 132 | + | let project = json!({ "type": "string", "description": "The project's slug, as in g1t.sh/{workspace}/{project}; a repository's own project has its name." }); | |
| 101 | 133 | let (properties, required): (Value, &[&str]) = match self { | |
| 102 | 134 | DeploymentsOp::ListDeployments => ( | |
| 103 | 135 | json!({ | |
| ⋯ | |||
| 160 | 192 | }), | |
| 161 | 193 | &["repo", "environment"], | |
| 162 | 194 | ), | |
| 195 | + | DeploymentsOp::GetDeploymentSettings => (json!({ "workspace": workspace, "project": project }), &["workspace", "project"]), | |
| 196 | + | DeploymentsOp::UpdateDeploymentSettings => ( | |
| 197 | + | json!({ | |
| 198 | + | "workspace": workspace, | |
| 199 | + | "project": project, | |
| 200 | + | "enabled": { "type": "boolean", "description": "Turn Deployments on (production builds at once; needs the g1t plan) or off (every app comes down)." }, | |
| 201 | + | "production": { "type": "boolean", "description": "Deploy the default branch to production on every push." }, | |
| 202 | + | "previews": { "type": "boolean", "description": "A preview for every branch with an open pull request." }, | |
| 203 | + | "build_as": { "type": "string", "enum": BUILD_AS, "description": "What it is built as; auto finds it from its files." }, | |
| 204 | + | "build_command": { "type": ["string", "null"], "description": "Runs instead of the project's own build script; null or \"\" for its own." }, | |
| 205 | + | "output_dir": { "type": ["string", "null"], "description": "What a static site serves; null or \"\" to find it by itself." }, | |
| 206 | + | "dockerfile": { "type": ["string", "null"], "description": "For a container: its Dockerfile's path inside the project; null or \"\" for Dockerfile or Containerfile." }, | |
| 207 | + | "instance_type": { "type": "string", "enum": INSTANCE_TYPES, "description": "For a container: the size it runs at." }, | |
| 208 | + | "idle_days": { "type": "integer", "minimum": 1, "maximum": 90, "description": "A preview no one visits for this many days comes down." }, | |
| 209 | + | "via_workflow": { "type": "boolean", "description": "Build through the deploy workflow g1t manages for the project, listed under Actions." }, | |
| 210 | + | }), | |
| 211 | + | &["workspace", "project"], | |
| 212 | + | ), | |
| 163 | 213 | }; | |
| 164 | 214 | json!({ "type": "object", "properties": properties, "required": required }) | |
| 165 | 215 | } | |
| ⋯ | |||
| 267 | 317 | out.insert("name".into(), name.into()); | |
| 268 | 318 | "get_environment" | |
| 269 | 319 | } | |
| 320 | + | // Named by workspace and project: see `settings_args`. | |
| 321 | + | DeploymentsOp::GetDeploymentSettings | DeploymentsOp::UpdateDeploymentSettings => { | |
| 322 | + | return Err("Name the workspace and the project.".to_owned()); | |
| 323 | + | } | |
| 270 | 324 | }; | |
| 271 | 325 | Ok((method, out)) | |
| 272 | 326 | } | |
| 273 | 327 | ||
| 328 | + | /// The project a settings operation names, as the deployments service | |
| 329 | + | /// takes it. | |
| 330 | + | fn project_ref(input: &Value) -> std::result::Result<Value, String> { | |
| 331 | + | let slug = |key: &str| input[key].as_str().map(str::trim).filter(|value| !value.is_empty()).map(str::to_lowercase); | |
| 332 | + | let workspace = slug("workspace").ok_or("Give the workspace's slug.")?; | |
| 333 | + | let project = slug("project").ok_or("Give the project's slug.")?; | |
| 334 | + | Ok(json!({ "workspace": workspace, "slug": project })) | |
| 335 | + | } | |
| 336 | + | ||
| 337 | + | /// The change an update asks for, in the deployments service's spelling: | |
| 338 | + | /// only what it gives. `Err` says what is wrong. | |
| 339 | + | pub(crate) fn settings_changes(input: &Value) -> std::result::Result<Map<String, Value>, String> { | |
| 340 | + | let mut out = Map::new(); | |
| 341 | + | for (key, field) in [("enabled", "enabled"), ("production", "production"), ("previews", "previews"), ("via_workflow", "viaWorkflow")] { | |
| 342 | + | match input.get(key) { | |
| 343 | + | None | Some(Value::Null) => {} | |
| 344 | + | Some(_) => { | |
| 345 | + | let value = flag(input, key).ok_or(format!("{key} must be true or false."))?; | |
| 346 | + | out.insert(field.into(), value.into()); | |
| 347 | + | } | |
| 348 | + | } | |
| 349 | + | } | |
| 350 | + | // Text that "" or null puts back to what is found by itself. | |
| 351 | + | for (key, field) in [("build_command", "buildCommand"), ("output_dir", "outputDir"), ("dockerfile", "dockerfile")] { | |
| 352 | + | match input.get(key) { | |
| 353 | + | None => {} | |
| 354 | + | Some(Value::Null) => { | |
| 355 | + | out.insert(field.into(), Value::Null); | |
| 356 | + | } | |
| 357 | + | Some(Value::String(text)) if text.trim().chars().count() > 500 => { | |
| 358 | + | return Err(format!("{key} is at most 500 characters.")); | |
| 359 | + | } | |
| 360 | + | Some(Value::String(text)) if text.trim().is_empty() => { | |
| 361 | + | out.insert(field.into(), Value::Null); | |
| 362 | + | } | |
| 363 | + | Some(Value::String(text)) => { | |
| 364 | + | out.insert(field.into(), text.trim().into()); | |
| 365 | + | } | |
| 366 | + | Some(_) => return Err(format!("{key} must be a string or null.")), | |
| 367 | + | } | |
| 368 | + | } | |
| 369 | + | let word = |key: &str, allowed: &[&str]| -> std::result::Result<Option<String>, String> { | |
| 370 | + | match input.get(key) { | |
| 371 | + | None | Some(Value::Null) => Ok(None), | |
| 372 | + | Some(Value::String(text)) if allowed.contains(&text.trim()) => Ok(Some(text.trim().to_owned())), | |
| 373 | + | Some(_) => Err(format!("{key} must be one of {}.", allowed.join(", "))), | |
| 374 | + | } | |
| 375 | + | }; | |
| 376 | + | if let Some(build_as) = word("build_as", &BUILD_AS)? { | |
| 377 | + | out.insert("buildAs".into(), build_as.into()); | |
| 378 | + | } | |
| 379 | + | if let Some(instance_type) = word("instance_type", &INSTANCE_TYPES)? { | |
| 380 | + | out.insert("instanceType".into(), instance_type.into()); | |
| 381 | + | } | |
| 382 | + | match input.get("idle_days") { | |
| 383 | + | None | Some(Value::Null) => {} | |
| 384 | + | Some(_) => match number(input, "idle_days") { | |
| 385 | + | Some(days @ 1..=90) => { | |
| 386 | + | out.insert("idleDays".into(), days.into()); | |
| 387 | + | } | |
| 388 | + | _ => return Err("idle_days must be a whole number from 1 to 90.".to_owned()), | |
| 389 | + | }, | |
| 390 | + | } | |
| 391 | + | Ok(out) | |
| 392 | + | } | |
| 393 | + | ||
| 394 | + | /// A project's deployment settings as the deployments service answers | |
| 395 | + | /// (`camelCase`), as the API shows them. | |
| 396 | + | pub(crate) fn settings_json(settings: &Value, project: &Value) -> Value { | |
| 397 | + | json!({ | |
| 398 | + | "workspace": project["workspace"], | |
| 399 | + | "project": project["slug"], | |
| 400 | + | "enabled": settings["enabled"].as_bool().unwrap_or(false), | |
| 401 | + | "production": settings["production"].as_bool().unwrap_or(true), | |
| 402 | + | "previews": settings["previews"].as_bool().unwrap_or(true), | |
| 403 | + | "production_url": settings["productionUrl"], | |
| 404 | + | "primary_domain": settings["primaryDomain"], | |
| 405 | + | "build_as": settings["buildAs"].as_str().unwrap_or("auto"), | |
| 406 | + | "build_command": settings["buildCommand"], | |
| 407 | + | "output_dir": settings["outputDir"], | |
| 408 | + | "dockerfile": settings["dockerfile"], | |
| 409 | + | "instance_type": settings["instanceType"].as_str().unwrap_or(INSTANCE_TYPES[0]), | |
| 410 | + | "idle_days": settings["idleDays"], | |
| 411 | + | "via_workflow": settings["viaWorkflow"].as_bool().unwrap_or(false), | |
| 412 | + | "detected": settings["detected"], | |
| 413 | + | "framework": settings["framework"], | |
| 414 | + | }) | |
| 415 | + | } | |
| 416 | + | ||
| 417 | + | /// Reads or changes a project's deployment settings. | |
| 418 | + | async fn run_settings(op: DeploymentsOp, services: &Services, viewer: &Viewer, input: &Value) -> Result<Outcome<Value>> { | |
| 419 | + | let project = match project_ref(input) { | |
| 420 | + | Ok(project) => project, | |
| 421 | + | Err(message) => return Ok(Outcome::fail(FailureCode::Invalid, message)), | |
| 422 | + | }; | |
| 423 | + | let answered: Outcome<Value> = if op == DeploymentsOp::GetDeploymentSettings { | |
| 424 | + | g1t_kit::call(&services.deployments, "settings", &json!({ "project": project, "viewer": viewer })).await? | |
| 425 | + | } else { | |
| 426 | + | let Some(actor) = viewer else { | |
| 427 | + | return Ok(Outcome::fail(FailureCode::Unauthenticated, "Changing deployment settings needs a g1t access token.")); | |
| 428 | + | }; | |
| 429 | + | let changes = match settings_changes(input) { | |
| 430 | + | Ok(changes) => changes, | |
| 431 | + | Err(message) => return Ok(Outcome::fail(FailureCode::Invalid, message)), | |
| 432 | + | }; | |
| 433 | + | g1t_kit::call(&services.deployments, "update_settings", &json!({ "actor": actor, "project": project, "changes": changes })).await? | |
| 434 | + | }; | |
| 435 | + | Ok(match answered { | |
| 436 | + | Outcome::Ok(settings) => Outcome::Ok(settings_json(&settings, &project)), | |
| 437 | + | Outcome::Fail(failure) => Outcome::Fail(failure), | |
| 438 | + | }) | |
| 439 | + | } | |
| 440 | + | ||
| 441 | + | /// For create_repo with `deploy`: turns Deployments on for the new | |
| 442 | + | /// repository's own project (named as the repository is), as the site's | |
| 443 | + | /// new-repository card does, as the person who made it. Never fails the | |
| 444 | + | /// creation: what it answers is `deployments` (the settings) or | |
| 445 | + | /// `deploy_error` (why they are not on: no g1t plan, a role too low). | |
| 446 | + | pub(crate) async fn turn_on_for_new_repo(services: &Services, actor: &Value, namespace: &str, name: &str) -> (Value, Value) { | |
| 447 | + | let project = json!({ "workspace": namespace.to_lowercase(), "slug": name.to_lowercase() }); | |
| 448 | + | let asked = json!({ "actor": actor, "project": project, "changes": { "enabled": true } }); | |
| 449 | + | let answered: Result<Outcome<Value>> = g1t_kit::call(&services.deployments, "update_settings", &asked).await; | |
| 450 | + | match answered { | |
| 451 | + | Ok(Outcome::Ok(settings)) => (settings_json(&settings, &project), Value::Null), | |
| 452 | + | Ok(Outcome::Fail(failure)) => (Value::Null, failure.message.into()), | |
| 453 | + | Err(error) => { | |
| 454 | + | worker::console_error!("created {namespace}/{name} without deployments: {error}"); | |
| 455 | + | ( | |
| 456 | + | Value::Null, | |
| 457 | + | "Deployments could not be turned on. Try again with update_deployment_settings, or from the project's Settings → Deployments.".into(), | |
| 458 | + | ) | |
| 459 | + | } | |
| 460 | + | } | |
| 461 | + | } | |
| 462 | + | ||
| 274 | 463 | pub async fn run(op: DeploymentsOp, services: &Services, viewer: &Viewer, input: &Value) -> Result<Outcome<Value>> { | |
| 464 | + | if !op.needs_repo() { | |
| 465 | + | return run_settings(op, services, viewer, input).await; | |
| 466 | + | } | |
| 275 | 467 | let (method, mut args) = match args(op, input) { | |
| 276 | 468 | Ok(found) => found, | |
| 277 | 469 | Err(message) => return Ok(Outcome::fail(FailureCode::Invalid, message)), | |
| ⋯ | |||
| 331 | 523 | fn each_operation_is_described_with_a_schema() { | |
| 332 | 524 | for op in DeploymentsOp::ALL { | |
| 333 | 525 | assert!(!op.title().is_empty() && op.description().len() > 40, "{}", op.name()); | |
| 334 | − | assert!(op.input()["required"].as_array().unwrap().contains(&json!("repo")), "{}", op.name()); | |
| 526 | + | let required = op.input()["required"].as_array().unwrap().clone(); | |
| 527 | + | if op.needs_repo() { | |
| 528 | + | assert!(required.contains(&json!("repo")), "{}", op.name()); | |
| 529 | + | } else { | |
| 530 | + | assert_eq!(required, vec![json!("workspace"), json!("project")], "{}", op.name()); | |
| 531 | + | } | |
| 335 | 532 | } | |
| 533 | + | assert!(DeploymentsOp::UpdateDeploymentSettings.writes()); | |
| 534 | + | assert!(!DeploymentsOp::GetDeploymentSettings.writes()); | |
| 535 | + | } | |
| 536 | + | ||
| 537 | + | #[test] | |
| 538 | + | fn a_settings_change_is_only_what_is_given_in_the_service_s_spelling() { | |
| 539 | + | let sent = settings_changes(&json!({ | |
| 540 | + | "workspace": "flagon-io", "project": "web", "enabled": true, "previews": "false", | |
| 541 | + | "build_command": " npm run build ", "output_dir": "", "dockerfile": null, | |
| 542 | + | "build_as": "static", "instance_type": "basic", "idle_days": "14", "ignored": 1, | |
| 543 | + | })) | |
| 544 | + | .unwrap(); | |
| 545 | + | assert_eq!( | |
| 546 | + | Value::Object(sent), | |
| 547 | + | json!({ | |
| 548 | + | "enabled": true, "previews": false, "buildCommand": "npm run build", "outputDir": null, | |
| 549 | + | "dockerfile": null, "buildAs": "static", "instanceType": "basic", "idleDays": 14, | |
| 550 | + | }) | |
| 551 | + | ); | |
| 552 | + | assert!(settings_changes(&json!({})).unwrap().is_empty()); | |
| 553 | + | assert_eq!(Value::Object(settings_changes(&json!({ "enabled": false })).unwrap()), json!({ "enabled": false })); | |
| 554 | + | for bad in [ | |
| 555 | + | json!({ "enabled": "yes" }), | |
| 556 | + | json!({ "build_as": "container" }), | |
| 557 | + | json!({ "instance_type": "huge" }), | |
| 558 | + | json!({ "idle_days": 0 }), | |
| 559 | + | json!({ "idle_days": 91 }), | |
| 560 | + | json!({ "build_command": 3 }), | |
| 561 | + | json!({ "output_dir": "x".repeat(501) }), | |
| 562 | + | ] { | |
| 563 | + | assert!(settings_changes(&bad).is_err(), "{bad}"); | |
| 564 | + | } | |
| 565 | + | assert!(project_ref(&json!({ "workspace": "acme" })).is_err()); | |
| 566 | + | assert_eq!(project_ref(&json!({ "workspace": " Acme ", "project": "Web" })).unwrap(), json!({ "workspace": "acme", "slug": "web" })); | |
| 567 | + | } | |
| 568 | + | ||
| 569 | + | #[test] | |
| 570 | + | fn settings_are_snake_case() { | |
| 571 | + | let shown = settings_json( | |
| 572 | + | &json!({ | |
| 573 | + | "enabled": true, "previews": true, "production": true, "buildCommand": null, "outputDir": "dist", | |
| 574 | + | "idleDays": 7, "viaWorkflow": false, "productionUrl": "https://web-acme.g1t.page", "primaryDomain": null, | |
| 575 | + | "detected": "static", "framework": "astro", "buildAs": "auto", "dockerfile": null, "instanceType": "lite", | |
| 576 | + | "containers": { "available": true, "sleepMinutes": 5, "prices": [] }, | |
| 577 | + | }), | |
| 578 | + | &json!({ "workspace": "acme", "slug": "web" }), | |
| 579 | + | ); | |
| 580 | + | assert!(g1t_kit::wire::camel_case_keys(&shown).is_empty(), "{:?}", g1t_kit::wire::camel_case_keys(&shown)); | |
| 581 | + | assert_eq!(shown["production_url"], "https://web-acme.g1t.page"); | |
| 582 | + | assert_eq!(shown["output_dir"], "dist"); | |
| 583 | + | assert_eq!((shown["workspace"].clone(), shown["project"].clone()), (json!("acme"), json!("web"))); | |
| 584 | + | assert!(shown.get("containers").is_none()); | |
| 336 | 585 | } | |
| 337 | 586 | } | |
| 505 | 505 | Op::Deployments(DeploymentsOp::ListEnvironments), | |
| 506 | 506 | Op::Deployments(DeploymentsOp::GetEnvironment), | |
| 507 | 507 | Op::Deployments(DeploymentsOp::StartDeploymentBuild), | |
| 508 | + | Op::Deployments(DeploymentsOp::GetDeploymentSettings), | |
| 509 | + | Op::Deployments(DeploymentsOp::UpdateDeploymentSettings), | |
| 508 | 510 | ], | |
| 509 | 511 | ), | |
| 510 | 512 | ( |
| 812 | 812 | } | |
| 813 | 813 | ||
| 814 | 814 | impl Op { | |
| 815 | − | pub const ALL: [Op; 406] = [ | |
| 815 | + | pub const ALL: [Op; 408] = [ | |
| 816 | 816 | Op::Whoami, | |
| 817 | 817 | Op::GetWorkspace, | |
| 818 | 818 | Op::CreateWorkspace, | |
| ⋯ | |||
| 1108 | 1108 | Op::Deployments(DeploymentsOp::ListEnvironments), | |
| 1109 | 1109 | Op::Deployments(DeploymentsOp::GetEnvironment), | |
| 1110 | 1110 | Op::Deployments(DeploymentsOp::StartDeploymentBuild), | |
| 1111 | + | Op::Deployments(DeploymentsOp::GetDeploymentSettings), | |
| 1112 | + | Op::Deployments(DeploymentsOp::UpdateDeploymentSettings), | |
| 1111 | 1113 | Op::Telemetry(TelemetryOp::GetOverview), | |
| 1112 | 1114 | Op::Telemetry(TelemetryOp::ListRequests), | |
| 1113 | 1115 | Op::Telemetry(TelemetryOp::GetRequest), | |
| ⋯ | |||
| 1599 | 1601 | "A repository's merge queue: the pull requests waiting to land, in order, each with the state it is being tested in (the default branch with the pull requests ahead of it landed in turn, each by its merge method) and how that went; then those that recently landed or left. With the queue on, merging a pull request adds it here." | |
| 1600 | 1602 | } | |
| 1601 | 1603 | Op::CreateRepo => { | |
| 1602 | − | "Create a repository in one of your workspaces, empty or as a copy of a public git repository elsewhere. It gets a project of its own name; description is that project's." | |
| 1604 | + | "Create a repository in one of your workspaces, empty or as a copy of a public git repository elsewhere. It gets a project of its own name; description is that project's. With deploy true, its project's g1t.page Deployments are turned on as soon as it is made, as update_deployment_settings with enabled true does: its first push to the default branch deploys production. The repository is made either way: the response has `deployments` (the settings, with production_url) when they are on, or `deploy_error` saying why not (the workspace has no g1t plan, or your role is too low)." | |
| 1603 | 1605 | } | |
| 1604 | 1606 | Op::ListIssues => { | |
| 1605 | 1607 | "Issues on a repository, newest first. An issue is something that should change: a bug, a feature, a question. Pull requests are made against it. Filter by state, by a label's name, or by a milestone's number." | |
| ⋯ | |||
| 2607 | 2609 | "type": "string", | |
| 2608 | 2610 | "description": "Copy the default branch of a public git repository at this https address, e.g. https://github.com/owner/repo.", | |
| 2609 | 2611 | }, | |
| 2612 | + | "deploy": { | |
| 2613 | + | "type": "boolean", | |
| 2614 | + | "description": "Turn on its project's g1t.page Deployments once it is made, so its first push deploys production. Needs the g1t plan on the workspace; when they cannot be turned on, it is still made and deploy_error says why. False unless you say.", | |
| 2615 | + | }, | |
| 2610 | 2616 | }), | |
| 2611 | 2617 | &["name"], | |
| 2612 | 2618 | ), | |
| ⋯ | |||
| 3826 | 3832 | if let Op::Telemetry(_) = self { | |
| 3827 | 3833 | return false; | |
| 3828 | 3834 | } | |
| 3835 | + | // So do its deployment settings. | |
| 3836 | + | if let Op::Deployments(op) = self { | |
| 3837 | + | return op.needs_repo(); | |
| 3838 | + | } | |
| 3829 | 3839 | if let Op::About(op) = self { | |
| 3830 | 3840 | return op.needs_repo(); | |
| 3831 | 3841 | } | |
| ⋯ | |||
| 4922 | 4932 | Err(error) => worker::console_error!("created {} without its description: {error}", repo["id"]), | |
| 4923 | 4933 | } | |
| 4924 | 4934 | } | |
| 4925 | − | Ok(created) | |
| 4935 | + | // Its deployments on from the start, when asked; the | |
| 4936 | + | // repository stays made whether or not they can be. | |
| 4937 | + | match created { | |
| 4938 | + | Outcome::Ok(mut repo) if yes(input, "deploy") == Some(true) => { | |
| 4939 | + | let namespace = repo["namespace"].as_str().unwrap_or_default().to_owned(); | |
| 4940 | + | let name = repo["name"].as_str().unwrap_or_default().to_owned(); | |
| 4941 | + | let (deployments, deploy_error) = | |
| 4942 | + | crate::deployments::turn_on_for_new_repo(services, &serde_json::to_value(&owner)?, &namespace, &name).await; | |
| 4943 | + | if let Some(fields) = repo.as_object_mut() { | |
| 4944 | + | fields.insert("deployments".to_owned(), deployments); | |
| 4945 | + | fields.insert("deploy_error".to_owned(), deploy_error); | |
| 4946 | + | } | |
| 4947 | + | Ok(Outcome::Ok(repo)) | |
| 4948 | + | } | |
| 4949 | + | created => Ok(created), | |
| 4950 | + | } | |
| 4926 | 4951 | } | |
| 4927 | 4952 | Op::ListIssues => { | |
| 4928 | 4953 | pass( | |
| 662 | 662 | "website": "https://hello.g1t.page", | |
| 663 | 663 | "archived_at": null | |
| 664 | 664 | }, | |
| 665 | − | "notes": "`owner_id` is the id of the person who created the repository. With `import_url`, `default_branch` is the default branch of the repository copied." | |
| 665 | + | "notes": "`owner_id` is the id of the person who created the repository. With `import_url`, `default_branch` is the default branch of the repository copied. With `deploy: true`, its project's [Deployments](/guides/deployments/) are turned on once it is made, as [`update_deployment_settings`](/reference/api/deployments/update-deployment-settings/) with `enabled: true` does, and the response adds `deployments` (those settings, with `production_url`) and `deploy_error` (null). When they cannot be turned on (the workspace has no g1t plan, or your role there is below Admin) the repository is still made: `deployments` is null and `deploy_error` says why, and they can be turned on later. Turning them on here needs no scope beyond `repo:create`: you are the new repository's admin." | |
| 666 | 666 | }, | |
| 667 | 667 | "get_repo": { | |
| 668 | 668 | "response": { | |
| ⋯ | |||
| 8076 | 8076 | }, | |
| 8077 | 8077 | "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)." | |
| 8078 | 8078 | }, | |
| 8079 | + | "get_deployment_settings": { | |
| 8080 | + | "params": { | |
| 8081 | + | "workspace": "flagon-io", | |
| 8082 | + | "project": "session-lab" | |
| 8083 | + | }, | |
| 8084 | + | "response": { | |
| 8085 | + | "workspace": "flagon-io", | |
| 8086 | + | "project": "session-lab", | |
| 8087 | + | "enabled": false, | |
| 8088 | + | "production": true, | |
| 8089 | + | "previews": true, | |
| 8090 | + | "production_url": "https://session-lab-flagonio.g1t.page", | |
| 8091 | + | "primary_domain": null, | |
| 8092 | + | "build_as": "auto", | |
| 8093 | + | "build_command": null, | |
| 8094 | + | "output_dir": null, | |
| 8095 | + | "dockerfile": null, | |
| 8096 | + | "instance_type": "lite", | |
| 8097 | + | "idle_days": 7, | |
| 8098 | + | "via_workflow": false, | |
| 8099 | + | "detected": null, | |
| 8100 | + | "framework": null | |
| 8101 | + | }, | |
| 8102 | + | "notes": "A repository's own project has the repository's name, so `flagon-io/session-lab`'s settings are at `/workspaces/flagon-io/projects/session-lab/deployments/settings`. `production_url` is where production is served once Deployments are on; `primary_domain` is the first active custom domain that serves it. `detected` and `framework` are what the last finished build found, and null before one has. Needs the Read role on the project's repository (`404` to anyone who cannot see it). See [Deployments](/guides/deployments/#settings)." | |
| 8103 | + | }, | |
| 8104 | + | "update_deployment_settings": { | |
| 8105 | + | "params": { | |
| 8106 | + | "workspace": "flagon-io", | |
| 8107 | + | "project": "session-lab" | |
| 8108 | + | }, | |
| 8109 | + | "request": { | |
| 8110 | + | "enabled": true | |
| 8111 | + | }, | |
| 8112 | + | "response": { | |
| 8113 | + | "workspace": "flagon-io", | |
| 8114 | + | "project": "session-lab", | |
| 8115 | + | "enabled": true, | |
| 8116 | + | "production": true, | |
| 8117 | + | "previews": true, | |
| 8118 | + | "production_url": "https://session-lab-flagonio.g1t.page", | |
| 8119 | + | "primary_domain": null, | |
| 8120 | + | "build_as": "auto", | |
| 8121 | + | "build_command": null, | |
| 8122 | + | "output_dir": null, | |
| 8123 | + | "dockerfile": null, | |
| 8124 | + | "instance_type": "lite", | |
| 8125 | + | "idle_days": 7, | |
| 8126 | + | "via_workflow": false, | |
| 8127 | + | "detected": null, | |
| 8128 | + | "framework": null | |
| 8129 | + | }, | |
| 8130 | + | "notes": "Only the fields given change. `enabled: true` turns Deployments on: production starts building from the default branch at once (an empty repository's first push to it deploys), and every pull request gets a preview. It is refused with `402 payment_required` when the workspace does not have the [g1t plan](/guides/usage-and-billing/#the-g1t-plan), saying where an owner starts it, and with `409 conflict` for a project set to be a library, a tool or other (change `kind` with [`update_project`](/reference/api/projects/update-project/) first). `enabled: false` takes production and every preview down at once. `build_command`, `output_dir` and `dockerfile` given as null or `\"\"` go back to what is found by itself; `idle_days` is 1 to 90; anything else is refused with `422 invalid`. Needs the Admin role on the project's repository (`403` otherwise, saying so) and a token with `repo:admin`. To turn them on as a repository is made, give `deploy: true` to [`create_repo`](/reference/api/repositories/create-repo/). See [Turn on deployments](/guides/deployments/#turn-on-deployments)." | |
| 8131 | + | }, | |
| 8079 | 8132 | "list_secret_scanning_alerts": { | |
| 8080 | 8133 | "params": { | |
| 8081 | 8134 | "owner": "flagon-io", | |
| 161 | 161 | ), | |
| 162 | 162 | route("GET", "/workspaces/:workspace/projects/:project", Op::GetProject, &[]), | |
| 163 | 163 | route("PATCH", "/workspaces/:workspace/projects/:project", Op::UpdateProject, &[]), | |
| 164 | + | // Its g1t.page deployments: whether it deploys, and how it builds. | |
| 165 | + | route( | |
| 166 | + | "GET", | |
| 167 | + | "/workspaces/:workspace/projects/:project/deployments/settings", | |
| 168 | + | Op::Deployments(DeploymentsOp::GetDeploymentSettings), | |
| 169 | + | &[], | |
| 170 | + | ), | |
| 171 | + | route( | |
| 172 | + | "PATCH", | |
| 173 | + | "/workspaces/:workspace/projects/:project/deployments/settings", | |
| 174 | + | Op::Deployments(DeploymentsOp::UpdateDeploymentSettings), | |
| 175 | + | &[], | |
| 176 | + | ), | |
| 164 | 177 | // A project's observability: what its software did, and how it is collected. | |
| 165 | 178 | route( | |
| 166 | 179 | "GET", |
| 85 | 85 | a("contents", Op::GetContents, "A file's text, or a directory's entries and README, at a branch, tag or commit"), | |
| 86 | 86 | a("tarball", Op::Archives(ArchiveOp::DownloadTarball), "A 10-minute link to a branch, tag or commit as a .tar.gz"), | |
| 87 | 87 | a("zipball", Op::Archives(ArchiveOp::DownloadZipball), "A 10-minute link to a branch, tag or commit as a .zip"), | |
| 88 | − | a("create", Op::CreateRepo, "Create one, empty or copied from a public git URL; private unless private is false"), | |
| 88 | + | a("create", Op::CreateRepo, "Create one, empty or copied from a public git URL; private unless private is false; deploy true turns its g1t.page deployments on"), | |
| 89 | 89 | a("update", Op::UpdateRepo, "Change description (its project's), website, topics, default branch, protection, forking"), | |
| 90 | 90 | a("get_settings", Op::GetRepoSettings, "How pull requests merge, and the default branch's protection as its rules stack"), | |
| 91 | 91 | a("update_settings", Op::UpdateRepoSettings, "Change how pull requests merge and the default branch protection ruleset"), | |
| ⋯ | |||
| 267 | 267 | a("list_environments", Op::Deployments(DeploymentsOp::ListEnvironments), "Environments with their current and latest deployments"), | |
| 268 | 268 | a("get_environment", Op::Deployments(DeploymentsOp::GetEnvironment), "One environment by name, with its protection rules"), | |
| 269 | 269 | a("start_deployment_build", Op::Deployments(DeploymentsOp::StartDeploymentBuild), "From a workflow job: open a g1t.page build it runs itself"), | |
| 270 | + | a("get_deployment_settings", Op::Deployments(DeploymentsOp::GetDeploymentSettings), "A project's g1t.page settings: whether it deploys, production's address, how it builds"), | |
| 271 | + | a("update_deployment_settings", Op::Deployments(DeploymentsOp::UpdateDeploymentSettings), "Turn a project's g1t.page deployments on (enabled true) or off, or change how it builds"), | |
| 270 | 272 | a("update_environment", Op::Protection(ProtectionOp::UpdateEnvironment), "Set an environment's reviewers, wait timer and branches"), | |
| 271 | 273 | a("delete_environment", Op::Protection(ProtectionOp::DeleteEnvironment), "Remove an environment's protection rules"), | |
| 272 | 274 | a("pending_deployments", Op::Protection(ProtectionOp::GetPendingDeployments), "The environments holding a run's jobs"), | |
| 204 | 204 | ```sh | |
| 205 | 205 | g1t repo create my-project --source . --remote g1t # make it, add it as a remote, push | |
| 206 | 206 | g1t repo create acme/web --public --clone | |
| 207 | + | g1t repo create acme/site --deploy # its g1t.page deployments on from the start | |
| 207 | 208 | g1t repo list acme | |
| 208 | 209 | g1t repo view acme/web --web | |
| 209 | 210 | g1t repo clone acme/web | |
| ⋯ | |||
| 341 | 342 | ```sh | |
| 342 | 343 | g1t deploy list --env production | |
| 343 | 344 | g1t deploy view dep_01kq8m3t5v7x9z1b3d5f7h9k2m | |
| 345 | + | g1t deploy enable # turn on this project's g1t.page deployments | |
| 346 | + | g1t deploy settings --project acme/web --previews off --build-command "npm run build:site" | |
| 344 | 347 | ||
| 345 | 348 | g1t logs --since 1h --level error,fatal | |
| 346 | 349 | g1t logs -q '"payment failed"' --follow | |
| 422 | 422 | | `get_environment` | `GET /repos/{owner}/{name}/environments/{environment}` | | |
| 423 | 423 | | `update_environment` | `PUT /repos/{owner}/{name}/environments/{environment}` | | |
| 424 | 424 | | `delete_environment` | `DELETE /repos/{owner}/{name}/environments/{environment}` | | |
| 425 | + | | `get_deployment_settings` | `GET /workspaces/{workspace}/projects/{project}/deployments/settings` | | |
| 426 | + | | `update_deployment_settings` | `PATCH /workspaces/{workspace}/projects/{project}/deployments/settings` | | |
| 425 | 427 | ||
| 426 | 428 | They take the repository as `repo`, written `owner/name`, and the same | |
| 427 | − | fields as the routes. | |
| 429 | + | fields as the routes; a project's g1t.page settings take `workspace` and | |
| 430 | + | `project` instead, and `update_deployment_settings` with `enabled: true` | |
| 431 | + | [turns its deployments on](/guides/deployments/#without-the-site). | |
| 428 | 432 | ||
| 429 | 433 | ## On the site | |
| 430 | 434 |
| 78 | 78 | Production starts building at once from the default branch. Every pull | |
| 79 | 79 | request opened or pushed to from then on gets a preview. | |
| 80 | 80 | ||
| 81 | + | ### Without the site | |
| 82 | + | ||
| 83 | + | An agent, a script or the CLI turns them on the same way, as someone with | |
| 84 | + | the Admin role on the repository; the workspace still needs the g1t plan. | |
| 85 | + | ||
| 86 | + | - **As you make the repository.** Give `deploy` when you create it: | |
| 87 | + | `g1t repo create acme/web --deploy`, `deploy: true` on the | |
| 88 | + | [MCP](/reference/mcp/) `repository` tool's `create`, or on | |
| 89 | + | [`POST /repos`](/reference/api/repositories/create-repo/). Its first | |
| 90 | + | push to the default branch then deploys production. The repository is | |
| 91 | + | made even when deployments can't be turned on: the response has | |
| 92 | + | `deployments` (the settings, with `production_url`) when they are on, or | |
| 93 | + | `deploy_error` saying why not, such as the workspace having no g1t plan. | |
| 94 | + | - **For a project that exists.** `g1t deploy enable` in its checkout | |
| 95 | + | (`--project acme/web` from anywhere), the MCP `workflow` tool's | |
| 96 | + | `update_deployment_settings` with `enabled: true`, or: | |
| 97 | + | ||
| 98 | + | ```sh | |
| 99 | + | curl -X PATCH https://api.g1t.sh/workspaces/acme/projects/web/deployments/settings \ | |
| 100 | + | -H "Authorization: Bearer $G1T_TOKEN" \ | |
| 101 | + | -H "Content-Type: application/json" \ | |
| 102 | + | -d '{"enabled": true}' | |
| 103 | + | ``` | |
| 104 | + | ||
| 105 | + | It answers with the project's settings, `production_url` among them. | |
| 106 | + | Without the g1t plan it is refused with `402 payment_required`, saying | |
| 107 | + | where an owner starts it; without the Admin role, with `403`. Turning them | |
| 108 | + | on for an existing project needs a token with `repo:admin`; turning them | |
| 109 | + | on as you make a repository needs only `repo:create`, which the | |
| 110 | + | [Agent preset](/guides/authentication/#presets) gives, since you are the | |
| 111 | + | new repository's admin. `g1t deploy disable` (or `enabled: false`) turns | |
| 112 | + | them off. The other [settings](#settings) change the same way: | |
| 113 | + | `g1t deploy settings` shows them, and takes flags such as | |
| 114 | + | `--build-command` and `--previews off`. | |
| 115 | + | ||
| 81 | 116 | Deployments are for projects g1t.page hosts. Not every project is one: a | |
| 82 | 117 | library, a tool or documentation is published rather than deployed, and | |
| 83 | 118 | an app may deploy itself, with [a workflow](#deploying-from-a-workflow) | |
| ⋯ | |||
| 725 | 760 | | Idle days | 7 | 1 to 90. A preview no one visits this long comes down. | | |
| 726 | 761 | | Build with a workflow | Off | Build through the deploy workflow g1t manages for the project; see [build with a workflow](#build-with-a-workflow). | | |
| 727 | 762 | ||
| 763 | + | The same settings are read and changed with | |
| 764 | + | [`get_deployment_settings`](/reference/api/deployments/get-deployment-settings/) and | |
| 765 | + | [`update_deployment_settings`](/reference/api/deployments/update-deployment-settings/) | |
| 766 | + | (`GET` and `PATCH /workspaces/{workspace}/projects/{project}/deployments/settings`, | |
| 767 | + | the MCP `workflow` tool's actions of those names, and `g1t deploy settings`), as | |
| 768 | + | `production`, `previews`, `build_as`, `dockerfile`, `instance_type`, | |
| 769 | + | `build_command`, `output_dir`, `idle_days` and `via_workflow`. | |
| 770 | + | ||
| 728 | 771 | ## Logs, traces and errors | |
| 729 | 772 | ||
| 730 | 773 | Every request an app answers shows under the project's | |
| 301 | 301 | | `GET /workspaces/{workspace}/projects/{project}` | `get_project` | One project, with `kind`, `runs`, `production_url`, `setting`, `detected` and `links`. | | |
| 302 | 302 | | `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`. | | |
| 303 | 303 | ||
| 304 | + | Its g1t.page deployment settings, on the `workflow` tool, are at | |
| 305 | + | `GET` and `PATCH /workspaces/{workspace}/projects/{project}/deployments/settings` | |
| 306 | + | (`get_deployment_settings`, `update_deployment_settings`): `enabled: true` | |
| 307 | + | turns its deployments on. See [Turn on deployments](/guides/deployments/#without-the-site). | |
| 308 | + | ||
| 304 | 309 | To say an app is deployed by your own pipeline, with its docs: | |
| 305 | 310 | ||
| 306 | 311 | ```sh |
| 160 | 160 | | `-c, --clone` | Clone it once it is made. | | |
| 161 | 161 | | `--source <DIR>` | Add it as a remote of this directory's repository and push it there. | | |
| 162 | 162 | | `--remote <REMOTE>` | The remote's name, with --source. Default: `origin`. | | |
| 163 | + | | `--deploy` | Turn on its g1t.page deployments once it is made, so its first push deploys production. | | |
| 163 | 164 | ||
| 164 | 165 | Calls [`create_repo`](/reference/api/repositories/create-repo/). | |
| 165 | 166 | ||
| ⋯ | |||
| 905 | 906 | ||
| 906 | 907 | ## g1t deploy | |
| 907 | 908 | ||
| 908 | − | Deployments: list and view. | |
| 909 | + | Deployments: list and view, and turn a project's g1t.page deployments on or off. | |
| 909 | 910 | ||
| 910 | 911 | ### g1t deploy list | |
| 911 | 912 | ||
| ⋯ | |||
| 939 | 940 | ||
| 940 | 941 | Calls [`get_deployment`](/reference/api/deployments/get-deployment/). | |
| 941 | 942 | ||
| 943 | + | ### g1t deploy settings | |
| 944 | + | ||
| 945 | + | Show a project's g1t.page deployment settings, or change them with flags. | |
| 946 | + | ||
| 947 | + | ```text | |
| 948 | + | g1t deploy settings [OPTIONS] | |
| 949 | + | ``` | |
| 950 | + | ||
| 951 | + | | Argument or flag | | | |
| 952 | + | | --- | --- | | |
| 953 | + | | `-p, --project <PROJECT>` | The project, as SLUG or WORKSPACE/SLUG; the current repository's when left out. | | |
| 954 | + | | `--production <SWITCH>` | Deploy the default branch on every push. One of `on`, `off`. | | |
| 955 | + | | `--previews <SWITCH>` | A preview for every branch with an open pull request. One of `on`, `off`. | | |
| 956 | + | | `--build-as <KIND>` | auto, static, workers or docker. | | |
| 957 | + | | `--build-command <COMMAND>` | Runs instead of the project's own build script; "" for its own. | | |
| 958 | + | | `--output-dir <DIR>` | What a static site serves; "" to find it by itself. | | |
| 959 | + | | `--dockerfile <PATH>` | A container's Dockerfile, inside the project; "" for Dockerfile or Containerfile. | | |
| 960 | + | | `--instance-type <SIZE>` | A container's size: lite, basic, standard-1 to standard-4. | | |
| 961 | + | | `--idle-days <DAYS>` | A preview no one visits this many days comes down: 1 to 90. | | |
| 962 | + | | `--via-workflow <SWITCH>` | Build through the deploy workflow g1t manages for the project. One of `on`, `off`. | | |
| 963 | + | ||
| 964 | + | Calls [`get_deployment_settings`](/reference/api/deployments/get-deployment-settings/), [`update_deployment_settings`](/reference/api/deployments/update-deployment-settings/). | |
| 965 | + | ||
| 966 | + | ### g1t deploy enable | |
| 967 | + | ||
| 968 | + | Turn on a project's g1t.page deployments: production builds from the default branch at once. | |
| 969 | + | ||
| 970 | + | ```text | |
| 971 | + | g1t deploy enable [OPTIONS] | |
| 972 | + | ``` | |
| 973 | + | ||
| 974 | + | | Argument or flag | | | |
| 975 | + | | --- | --- | | |
| 976 | + | | `-p, --project <PROJECT>` | The project, as SLUG or WORKSPACE/SLUG; the current repository's when left out. | | |
| 977 | + | ||
| 978 | + | Calls [`update_deployment_settings`](/reference/api/deployments/update-deployment-settings/). | |
| 979 | + | ||
| 980 | + | ### g1t deploy disable | |
| 981 | + | ||
| 982 | + | Turn off a project's g1t.page deployments: production and every preview come down. | |
| 983 | + | ||
| 984 | + | ```text | |
| 985 | + | g1t deploy disable [OPTIONS] | |
| 986 | + | ``` | |
| 987 | + | ||
| 988 | + | | Argument or flag | | | |
| 989 | + | | --- | --- | | |
| 990 | + | | `-p, --project <PROJECT>` | The project, as SLUG or WORKSPACE/SLUG; the current repository's when left out. | | |
| 991 | + | ||
| 992 | + | Calls [`update_deployment_settings`](/reference/api/deployments/update-deployment-settings/). | |
| 993 | + | ||
| 942 | 994 | ## g1t logs | |
| 943 | 995 | ||
| 944 | 996 | A project's logs and errors. | |
| 259 | 259 | | [`contents`](/reference/api/repositories/get-contents/) | What it holds at `path` (the root when left out) at `ref` (the default branch when left out): a file's `text` (null for a binary one; at most 200,000 characters), or a directory's `entries` and its README. | `repo` | `code:read` | | |
| 260 | 260 | | [`tarball`](/reference/api/repositories/download-tarball-archive/) | A link to a branch, tag or commit (`ref`, the default branch when left out) as a `.tar.gz` that needs no token, good for 10 minutes, as `url`, with the `commit` it is of. | `repo` | `code:read` | | |
| 261 | 261 | | [`zipball`](/reference/api/repositories/download-zipball-archive/) | The same as a `.zip`. | `repo` | `code:read` | | |
| 262 | − | | [`create`](/reference/api/repositories/create-repo/) | Create a repository in one of your workspaces, empty or as a copy of a public git repository (`import_url`). `description` becomes its [project's](/guides/projects/), as a repository has none of its own. `workspace` may be left out if you belong to exactly one. It is private unless `private` is false (or `visibility` is `public`); the [REST API](/reference/api/repositories/create-repo/) makes one public unless `private` is true. | `name` | `repo:create`, which `repo:write` includes and the [Agent preset](/guides/authentication/#presets) gives | | |
| 262 | + | | [`create`](/reference/api/repositories/create-repo/) | Create a repository in one of your workspaces, empty or as a copy of a public git repository (`import_url`). `description` becomes its [project's](/guides/projects/), as a repository has none of its own. `workspace` may be left out if you belong to exactly one. It is private unless `private` is false (or `visibility` is `public`); the [REST API](/reference/api/repositories/create-repo/) makes one public unless `private` is true. `deploy: true` turns its [deployments](/guides/deployments/#without-the-site) on once it is made: the answer has `deployments` with `production_url`, or `deploy_error` saying why not (the repository is made either way). | `name` | `repo:create`, which `repo:write` includes and the [Agent preset](/guides/authentication/#presets) gives | | |
| 263 | 263 | | [`update`](/reference/api/repositories/update-repo/) | Change its `website`, `topics` and `default_branch`, whether its default branch is `protected`, whether it is `private`, and whether a private repository may be forked (`allow_forking`). Maintain role; `protected`, `private`, `allow_forking` and `default_branch` need Admin, and `private` the [member privileges](/guides/workspaces/#member-privileges) unless you are an owner. | `repo` | `repo:write` | | |
| 264 | 264 | | [`get_settings`](/reference/api/repositories/get-repo-settings/) | How it handles pull requests: how they merge, and the default branch's required checks, approvals, bypassing checks, being up to date and merge queue as its [rulesets](/guides/rules/) stack. | `repo` | `repo:read` | | |
| 265 | 265 | | [`update_settings`](/reference/api/repositories/update-repo-settings/) | Change those settings. Only the fields given change; `required_checks` replaces the whole list. Maintain role. | `repo` | `repo:write` | | |
| ⋯ | |||
| 478 | 478 | | [`list_environments`](/reference/api/deployments/list-environments/) | Environments with their address, current and latest deployments, production first. | `repo` | `deployments:read` | | |
| 479 | 479 | | [`get_environment`](/reference/api/deployments/get-environment/) | One environment by name, with its protection rules. | `repo`, `environment` | `deployments:read` | | |
| 480 | 480 | | [`start_deployment_build`](/reference/api/deployments/start-deployment-build/) | From a running workflow job, with the job's own token only: open a g1t.page build of a `project` (optional when the repository has one with Deployments on) that the job builds itself with `g1t-runner deploy`, optionally at `sha`. Production for a run on the production branch, a preview otherwise. Returns `deploy_id`, `token`, `kind`, `url`, `build_env` and `build_secrets`. See [deploying from a workflow](/guides/deployments/#deploying-from-a-workflow). | `repo` | `deployments:write` | | |
| 481 | + | | [`get_deployment_settings`](/reference/api/deployments/get-deployment-settings/) | A project's g1t.page settings: `enabled`, `production`, `previews`, `production_url`, `primary_domain`, `build_as`, `build_command`, `output_dir`, `dockerfile`, `instance_type`, `idle_days`, `via_workflow`, and what its last build `detected`. A repository's own project has its name. Read role. | `workspace`, `project` | `deployments:read` | | |
| 482 | + | | [`update_deployment_settings`](/reference/api/deployments/update-deployment-settings/) | Change them; only what you give changes. `enabled: true` [turns deployments on](/guides/deployments/#turn-on-deployments) (production builds at once; refused without the g1t plan), `false` takes every app down. Admin role. | `workspace`, `project` | `repo:admin` | | |
| 481 | 483 | | [`update_environment`](/reference/api/run-protection/update-environment/) | Set an environment's [protection rules](/guides/actions/#environments): `reviewers` (up to 6, `{"type": "User" or "Team", "name"}`), `prevent_self_review`, `wait_timer` (minutes), `deployment_branch_policy`, `branch_policies` and `can_admins_bypass`. Admin role. | `repo`, `environment` | `repo:admin` | | |
| 482 | 484 | | [`delete_environment`](/reference/api/run-protection/delete-environment/) | Remove an environment's protection rules; its secrets and deployments stay. Admin role. | `repo`, `environment` | `repo:admin` | | |
| 483 | 485 | | [`pending_deployments`](/reference/api/run-protection/get-pending-deployments/) | The environments holding a run's jobs: their state, reviewers, wait timer, jobs, and whether you may approve. | `repo`, `id` | `workflows:read` | | |
Binary or large file; its contents are not shown.
| 271 | 271 | "list_commit_comments", | |
| 272 | 272 | "list_projects", | |
| 273 | 273 | "get_project", | |
| 274 | + | "get_deployment_settings", | |
| 274 | 275 | "list_deleted_repos", | |
| 275 | 276 | "list_collaborators", | |
| 276 | 277 | "get_collaborator_permission", | |
| ⋯ | |||
| 377 | 378 | "detach_fork", | |
| 378 | 379 | "update_repo", | |
| 379 | 380 | "update_project", | |
| 381 | + | // Turning a project's deployments on starts paid work; off, takes it down. | |
| 382 | + | "update_deployment_settings", | |
| 380 | 383 | "delete_repo", | |
| 381 | 384 | "list_deleted_repos", | |
| 382 | 385 | "restore_repo", | |
| 1242 | 1242 | ("create_deployment", Scope::DeploymentsWrite), | |
| 1243 | 1243 | ("create_deployment_status", Scope::DeploymentsWrite), | |
| 1244 | 1244 | ("start_deployment_build", Scope::DeploymentsWrite), | |
| 1245 | + | // A project's g1t.page settings: reading them is reading its | |
| 1246 | + | // deployments; turning them on or off, or changing how it builds, | |
| 1247 | + | // takes the Admin role on the site and here. | |
| 1248 | + | ("get_deployment_settings", Scope::DeploymentsRead), | |
| 1249 | + | ("update_deployment_settings", Scope::RepoAdmin), | |
| 1245 | 1250 | // Observability: what a project's software did, and how it is collected. | |
| 1246 | 1251 | ("get_observability_overview", Scope::ObservabilityRead), | |
| 1247 | 1252 | ("list_requests", Scope::ObservabilityRead), |
| 101 | 101 | /// The repository's actions/cache entries: list, usage, delete. | |
| 102 | 102 | #[command(subcommand)] | |
| 103 | 103 | Cache(CacheCommand), | |
| 104 | − | /// Deployments: list and view. | |
| 104 | + | /// Deployments: list and view, and turn a project's g1t.page deployments on or off. | |
| 105 | 105 | #[command(subcommand)] | |
| 106 | 106 | Deploy(DeployCommand), | |
| 107 | 107 | /// A project's logs and errors. | |
| ⋯ | |||
| 318 | 318 | /// The remote's name, with --source. | |
| 319 | 319 | #[arg(long, default_value = "origin")] | |
| 320 | 320 | pub remote: String, | |
| 321 | + | /// Turn on its g1t.page deployments once it is made, so its first push deploys production. | |
| 322 | + | #[arg(long)] | |
| 323 | + | pub deploy: bool, | |
| 321 | 324 | } | |
| 322 | 325 | ||
| 323 | 326 | #[derive(Debug, Args)] | |
| ⋯ | |||
| 978 | 981 | List(DeployListArgs), | |
| 979 | 982 | /// Show a deployment and its statuses. | |
| 980 | 983 | View(DeployViewArgs), | |
| 984 | + | /// Show a project's g1t.page deployment settings, or change them with flags. | |
| 985 | + | Settings(DeploySettingsArgs), | |
| 986 | + | /// Turn on a project's g1t.page deployments: production builds from the default branch at once. | |
| 987 | + | Enable(DeployProjectArgs), | |
| 988 | + | /// Turn off a project's g1t.page deployments: production and every preview come down. | |
| 989 | + | Disable(DeployProjectArgs), | |
| 990 | + | } | |
| 991 | + | ||
| 992 | + | /// A setting turned on or off. | |
| 993 | + | #[derive(Clone, Copy, Debug, PartialEq, Eq, ValueEnum)] | |
| 994 | + | pub enum Switch { | |
| 995 | + | On, | |
| 996 | + | Off, | |
| 997 | + | } | |
| 998 | + | ||
| 999 | + | #[derive(Debug, Args)] | |
| 1000 | + | pub struct DeployProjectArgs { | |
| 1001 | + | /// The project, as SLUG or WORKSPACE/SLUG; the current repository's when left out. | |
| 1002 | + | #[arg(short, long)] | |
| 1003 | + | pub project: Option<String>, | |
| 1004 | + | } | |
| 1005 | + | ||
| 1006 | + | #[derive(Debug, Args)] | |
| 1007 | + | pub struct DeploySettingsArgs { | |
| 1008 | + | /// The project, as SLUG or WORKSPACE/SLUG; the current repository's when left out. | |
| 1009 | + | #[arg(short, long)] | |
| 1010 | + | pub project: Option<String>, | |
| 1011 | + | /// Deploy the default branch on every push. | |
| 1012 | + | #[arg(long, value_name = "SWITCH")] | |
| 1013 | + | pub production: Option<Switch>, | |
| 1014 | + | /// A preview for every branch with an open pull request. | |
| 1015 | + | #[arg(long, value_name = "SWITCH")] | |
| 1016 | + | pub previews: Option<Switch>, | |
| 1017 | + | /// auto, static, workers or docker. | |
| 1018 | + | #[arg(long, value_name = "KIND")] | |
| 1019 | + | pub build_as: Option<String>, | |
| 1020 | + | /// Runs instead of the project's own build script; "" for its own. | |
| 1021 | + | #[arg(long, value_name = "COMMAND")] | |
| 1022 | + | pub build_command: Option<String>, | |
| 1023 | + | /// What a static site serves; "" to find it by itself. | |
| 1024 | + | #[arg(long, value_name = "DIR")] | |
| 1025 | + | pub output_dir: Option<String>, | |
| 1026 | + | /// A container's Dockerfile, inside the project; "" for Dockerfile or Containerfile. | |
| 1027 | + | #[arg(long, value_name = "PATH")] | |
| 1028 | + | pub dockerfile: Option<String>, | |
| 1029 | + | /// A container's size: lite, basic, standard-1 to standard-4. | |
| 1030 | + | #[arg(long, value_name = "SIZE")] | |
| 1031 | + | pub instance_type: Option<String>, | |
| 1032 | + | /// A preview no one visits this many days comes down: 1 to 90. | |
| 1033 | + | #[arg(long, value_name = "DAYS")] | |
| 1034 | + | pub idle_days: Option<u32>, | |
| 1035 | + | /// Build through the deploy workflow g1t manages for the project. | |
| 1036 | + | #[arg(long, value_name = "SWITCH")] | |
| 1037 | + | pub via_workflow: Option<Switch>, | |
| 981 | 1038 | } | |
| 982 | 1039 | ||
| 983 | 1040 | #[derive(Debug, Args)] | |
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Comments
Sign in to comment.