Deployments wherever they run: API, g1t Actions environments, and a Deployments section
- Deployments service: reported deployments, statuses and environments (migration 0010), read together with g1t.page builds in one model; each status sets the commit check deploy / <environment>; events deployment.created and deployment_status.created. - g1t Actions: jobs with environment: make one deployment per run, attempt and environment (environment.url, deployment: false). - API and MCP: /repos/{owner}/{name}/deployments, statuses, environments; workflow tool actions; deployments:read/write scopes; OpenAPI. - Rules: required_deployments is met by deploy / <environment>. - Site: Deployments page, detail with status timeline, panel on the code page's About and the overview; production deployed elsewhere. - deploy.yml reports g1t.sh as production. Docs: Deployments API guide.
| 8 | 8 | # core, edge, front the units of each stage, in jobs that share a build; | |
| 9 | 9 | # a stage starts only when the one before it succeeded | |
| 10 | 10 | # | |
| 11 | + | # Each run that deploys is one production deployment of g1t.sh, made by the | |
| 12 | + | # jobs that name `environment: production` (one per run, however many jobs): | |
| 13 | + | # in progress when the first starts, then a success or a failure when the | |
| 14 | + | # run ends. It shows on the project's Deployments page and as the commit's | |
| 15 | + | # `deploy / production` check. The plan job reads production's secrets | |
| 16 | + | # with `deployment: false`, so a dry run or a change that deploys nothing | |
| 17 | + | # makes no deployment. | |
| 18 | + | # | |
| 11 | 19 | # Needs the repository secret CLOUDFLARE_API_TOKEN (a Production row), the | |
| 12 | 20 | # variable CLOUDFLARE_ACCOUNT_ID, and api.cloudflare.com among the project's | |
| 13 | 21 | # workflow-only domains for deploy.yml in production (Settings, Guardrails), | |
| ⋯ | |||
| 61 | 69 | name: Plan | |
| 62 | 70 | needs: check | |
| 63 | 71 | runs-on: ubuntu-latest | |
| 64 | − | environment: production | |
| 72 | + | # Production's secrets, without a deployment: planning deploys nothing. | |
| 73 | + | environment: | |
| 74 | + | name: production | |
| 75 | + | deployment: false | |
| 65 | 76 | timeout-minutes: 15 | |
| 66 | 77 | outputs: | |
| 67 | 78 | migrate: ${{ steps.plan.outputs.migrate }} | |
| ⋯ | |||
| 96 | 107 | needs: plan | |
| 97 | 108 | if: ${{ needs.plan.outputs.migrate == 'true' && inputs.dry_run != true }} | |
| 98 | 109 | runs-on: ubuntu-latest | |
| 99 | − | environment: production | |
| 110 | + | environment: | |
| 111 | + | name: production | |
| 112 | + | url: https://g1t.sh | |
| 100 | 113 | timeout-minutes: 20 | |
| 101 | 114 | steps: | |
| 102 | 115 | - uses: actions/checkout@v5 | |
| ⋯ | |||
| 115 | 128 | if: ${{ !failure() && !cancelled() && needs.plan.outputs.has_core == 'true' && inputs.dry_run != true }} | |
| 116 | 129 | # Rust builds get 4 vCPUs; everything else the standard machine. | |
| 117 | 130 | runs-on: ${{ matrix.rust && 'g1t-4core' || 'ubuntu-latest' }} | |
| 118 | − | environment: production | |
| 131 | + | environment: | |
| 132 | + | name: production | |
| 133 | + | url: https://g1t.sh | |
| 119 | 134 | timeout-minutes: 60 | |
| 120 | 135 | strategy: | |
| 121 | 136 | # A deploy cut off halfway is worse than one that finishes: the other | |
| ⋯ | |||
| 182 | 197 | needs: [plan, migrate, core] | |
| 183 | 198 | if: ${{ !failure() && !cancelled() && needs.plan.outputs.has_edge == 'true' && inputs.dry_run != true }} | |
| 184 | 199 | runs-on: ${{ matrix.rust && 'g1t-4core' || 'ubuntu-latest' }} | |
| 185 | − | environment: production | |
| 200 | + | environment: | |
| 201 | + | name: production | |
| 202 | + | url: https://g1t.sh | |
| 186 | 203 | timeout-minutes: 60 | |
| 187 | 204 | strategy: | |
| 188 | 205 | fail-fast: false | |
| ⋯ | |||
| 195 | 212 | needs: [plan, migrate, core, edge] | |
| 196 | 213 | if: ${{ !failure() && !cancelled() && needs.plan.outputs.has_front == 'true' && inputs.dry_run != true }} | |
| 197 | 214 | runs-on: ${{ matrix.rust && 'g1t-4core' || 'ubuntu-latest' }} | |
| 198 | − | environment: production | |
| 215 | + | environment: | |
| 216 | + | name: production | |
| 217 | + | url: https://g1t.sh | |
| 199 | 218 | timeout-minutes: 60 | |
| 200 | 219 | strategy: | |
| 201 | 220 | fail-fast: false | |
| 1 | + | //! Deployments over REST and MCP: a repository's deployments wherever they | |
| 2 | + | //! run, their statuses, and its environments. | |
| 3 | + | //! | |
| 4 | + | //! Any CI reports a deployment and its statuses here; a g1t Actions job | |
| 5 | + | //! with an `environment:` makes them itself, and g1t.page builds are read | |
| 6 | + | //! in alongside. The deployments service decides who may see and report | |
| 7 | + | //! them (Read to see, Write to report) and keeps them; deployments travel | |
| 8 | + | //! in `snake_case` between services too, so a request's fields reach it | |
| 9 | + | //! as they are, and a deployment's `payload` comes back as it was given. | |
| 10 | + | ||
| 11 | + | use g1t_contracts::{FailureCode, Outcome, Viewer}; | |
| 12 | + | use serde_json::{Map, Value, json}; | |
| 13 | + | use worker::Result; | |
| 14 | + | ||
| 15 | + | use crate::operations::{Services, repo_path}; | |
| 16 | + | ||
| 17 | + | /// One operation on deployments. | |
| 18 | + | #[derive(Clone, Copy, Debug, PartialEq, Eq)] | |
| 19 | + | pub enum DeploymentsOp { | |
| 20 | + | ListDeployments, | |
| 21 | + | GetDeployment, | |
| 22 | + | CreateDeployment, | |
| 23 | + | ListDeploymentStatuses, | |
| 24 | + | CreateDeploymentStatus, | |
| 25 | + | ListEnvironments, | |
| 26 | + | GetEnvironment, | |
| 27 | + | } | |
| 28 | + | ||
| 29 | + | /// The states a deployment's status can have. | |
| 30 | + | const STATES: [&str; 6] = ["queued", "in_progress", "success", "failure", "error", "inactive"]; | |
| 31 | + | ||
| 32 | + | impl DeploymentsOp { | |
| 33 | + | /// Every one: `Op::ALL` lists each as `Op::Deployments(…)`, which a | |
| 34 | + | /// test checks against this. | |
| 35 | + | #[cfg(test)] | |
| 36 | + | pub const ALL: [DeploymentsOp; 7] = [ | |
| 37 | + | DeploymentsOp::ListDeployments, | |
| 38 | + | DeploymentsOp::GetDeployment, | |
| 39 | + | DeploymentsOp::CreateDeployment, | |
| 40 | + | DeploymentsOp::ListDeploymentStatuses, | |
| 41 | + | DeploymentsOp::CreateDeploymentStatus, | |
| 42 | + | DeploymentsOp::ListEnvironments, | |
| 43 | + | DeploymentsOp::GetEnvironment, | |
| 44 | + | ]; | |
| 45 | + | ||
| 46 | + | pub fn name(self) -> &'static str { | |
| 47 | + | match self { | |
| 48 | + | DeploymentsOp::ListDeployments => "list_deployments", | |
| 49 | + | DeploymentsOp::GetDeployment => "get_deployment", | |
| 50 | + | DeploymentsOp::CreateDeployment => "create_deployment", | |
| 51 | + | DeploymentsOp::ListDeploymentStatuses => "list_deployment_statuses", | |
| 52 | + | DeploymentsOp::CreateDeploymentStatus => "create_deployment_status", | |
| 53 | + | DeploymentsOp::ListEnvironments => "list_environments", | |
| 54 | + | DeploymentsOp::GetEnvironment => "get_environment", | |
| 55 | + | } | |
| 56 | + | } | |
| 57 | + | ||
| 58 | + | /// For the API reference: "List deployments". | |
| 59 | + | pub fn title(self) -> &'static str { | |
| 60 | + | match self { | |
| 61 | + | DeploymentsOp::ListDeployments => "List deployments", | |
| 62 | + | DeploymentsOp::GetDeployment => "Get a deployment", | |
| 63 | + | DeploymentsOp::CreateDeployment => "Create a deployment", | |
| 64 | + | DeploymentsOp::ListDeploymentStatuses => "List deployment statuses", | |
| 65 | + | DeploymentsOp::CreateDeploymentStatus => "Create a deployment status", | |
| 66 | + | DeploymentsOp::ListEnvironments => "List environments", | |
| 67 | + | DeploymentsOp::GetEnvironment => "Get an environment", | |
| 68 | + | } | |
| 69 | + | } | |
| 70 | + | ||
| 71 | + | pub fn description(self) -> &'static str { | |
| 72 | + | match self { | |
| 73 | + | DeploymentsOp::ListDeployments => "List a repository's deployments wherever they run, newest first: those reported through this API, those g1t Actions made for jobs with an `environment:`, and g1t.page builds (production and previews). Each has its environment, ref and sha, task, description, payload, transient_environment and production_environment, its latest state (queued, in_progress, success, failure, error or inactive), environment_url and log_url, creator, and source (api, actions or g1t_page), with run_id and run_url for g1t Actions and project and number for g1t.page. Filter by environment, ref, sha (or a prefix), task, state, source and creator; page with page and per_page (30 by default, at most 100). total_count counts every match. Needs the Read role; a public repository's are open to anyone.", | |
| 74 | + | DeploymentsOp::GetDeployment => "Get one deployment by id (dep_… for a reported one, dpl_… for a g1t.page build), with every status it has had, oldest first. Needs the Read role.", | |
| 75 | + | DeploymentsOp::CreateDeployment => "Report a deployment of a commit to an environment, from any CI or script. ref is the branch, tag or commit deployed; sha is resolved from it unless you give the whole commit id. environment is production unless you say (any name up to 255 characters, such as staging or review/feature-x; names are matched without regard to case, and the first spelling is kept). task is deploy unless you say; payload is any JSON object, returned as given. production_environment is true for an environment named production unless you say; transient_environment marks one that goes away, such as a review app. Its first status is state (queued unless you say), with environment_url and log_url. Each status also shows on the commit as the check `deploy / <environment>`, which a ruleset's required_deployments rule can require. Needs the Write role. Returns the deployment with its statuses.", | |
| 76 | + | DeploymentsOp::ListDeploymentStatuses => "List a deployment's statuses, newest first: each with its state, description, environment_url, log_url, creator and created_at. A g1t.page build's are read from the build itself. Needs the Read role.", | |
| 77 | + | DeploymentsOp::CreateDeploymentStatus => "Add a status to a reported deployment: state (queued, in_progress, success, failure, error or inactive), description, environment_url (where it is served) and log_url (where its output can be read). The deployment takes its state, and any address it gives. A success with auto_inactive (true unless you say) makes the environment's older successful deployments inactive. The commit's `deploy / <environment>` check follows: pending while queued or in progress, then success, failure or error. A g1t.page build's statuses come from the build and cannot be added to. Needs the Write role.", | |
| 78 | + | 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. 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. total_count counts deployments across every environment. Needs the Read role.", | |
| 79 | + | DeploymentsOp::GetEnvironment => "Get one environment by name, matched without regard to case, with its current and latest deployments. A name with slashes is URL-encoded in the path. Needs the Read role.", | |
| 80 | + | } | |
| 81 | + | } | |
| 82 | + | ||
| 83 | + | /// Whether it changes anything. | |
| 84 | + | pub fn writes(self) -> bool { | |
| 85 | + | matches!(self, DeploymentsOp::CreateDeployment | DeploymentsOp::CreateDeploymentStatus) | |
| 86 | + | } | |
| 87 | + | ||
| 88 | + | pub fn input(self) -> Value { | |
| 89 | + | let repo = json!({ "type": "string", "description": "Repository as \"owner/name\", e.g. \"flagon-io/hello\"." }); | |
| 90 | + | let id = json!({ "type": "string", "description": "The deployment's id: dep_… for a reported one, dpl_… for a g1t.page build." }); | |
| 91 | + | let state = |what: &str| json!({ "type": "string", "enum": STATES, "description": what }); | |
| 92 | + | let address = |what: &str| json!({ "type": "string", "description": what }); | |
| 93 | + | let (properties, required): (Value, &[&str]) = match self { | |
| 94 | + | DeploymentsOp::ListDeployments => ( | |
| 95 | + | json!({ | |
| 96 | + | "repo": repo, | |
| 97 | + | "environment": { "type": "string", "description": "Only this environment's, matched without regard to case." }, | |
| 98 | + | "ref": { "type": "string", "description": "Only deployments of this branch, tag or commit as it was given." }, | |
| 99 | + | "sha": { "type": "string", "description": "Only deployments of this commit, or of commits starting with it." }, | |
| 100 | + | "task": { "type": "string", "description": "Only this task's, such as deploy." }, | |
| 101 | + | "state": state("Only deployments whose latest status has this state."), | |
| 102 | + | "source": { "type": "string", "enum": ["api", "actions", "g1t_page"], "description": "Only those reported through the API, made by g1t Actions, or built on g1t.page." }, | |
| 103 | + | "creator": { "type": "string", "description": "Only those this username (or g1t) made." }, | |
| 104 | + | "page": { "type": "integer", "description": "Which page, from 1." }, | |
| 105 | + | "per_page": { "type": "integer", "description": "How many a page holds, 1 to 100; 30 by default." }, | |
| 106 | + | }), | |
| 107 | + | &["repo"], | |
| 108 | + | ), | |
| 109 | + | DeploymentsOp::GetDeployment | DeploymentsOp::ListDeploymentStatuses => (json!({ "repo": repo, "id": id }), &["repo", "id"]), | |
| 110 | + | DeploymentsOp::CreateDeployment => ( | |
| 111 | + | json!({ | |
| 112 | + | "repo": repo, | |
| 113 | + | "ref": { "type": "string", "description": "The branch, tag or commit deployed, such as main or v1.4.0." }, | |
| 114 | + | "sha": { "type": "string", "description": "The commit deployed; resolved from ref when left out." }, | |
| 115 | + | "environment": { "type": "string", "description": "Where it went, such as production, staging or review/feature-x; production unless you say." }, | |
| 116 | + | "task": { "type": "string", "description": "What kind of deployment, such as deploy or deploy:migrations; deploy unless you say." }, | |
| 117 | + | "description": { "type": "string", "description": "A short note, at most 1,000 characters." }, | |
| 118 | + | "payload": { "type": "object", "description": "Anything else to keep with it, as a JSON object (a JSON string of one is read too), at most 64 KB. Returned as given." }, | |
| 119 | + | "production_environment": { "type": "boolean", "description": "Whether people use this environment directly. True for production unless you say." }, | |
| 120 | + | "transient_environment": { "type": "boolean", "description": "Whether the environment goes away, such as a review app. False unless you say." }, | |
| 121 | + | "state": state("Its first status: queued unless you say. Report in_progress, then success or failure, as it goes."), | |
| 122 | + | "environment_url": address("Where it is served, an http(s) address."), | |
| 123 | + | "log_url": address("Where its output can be read, an http(s) address."), | |
| 124 | + | }), | |
| 125 | + | &["repo", "ref"], | |
| 126 | + | ), | |
| 127 | + | DeploymentsOp::CreateDeploymentStatus => ( | |
| 128 | + | json!({ | |
| 129 | + | "repo": repo, | |
| 130 | + | "id": id, | |
| 131 | + | "state": state("Where it is now."), | |
| 132 | + | "description": { "type": "string", "description": "A short note, at most 1,000 characters." }, | |
| 133 | + | "environment_url": address("Where it is served, an http(s) address."), | |
| 134 | + | "log_url": address("Where its output can be read, an http(s) address."), | |
| 135 | + | "auto_inactive": { "type": "boolean", "description": "On a success, make the environment's older successful deployments inactive. True unless you say." }, | |
| 136 | + | }), | |
| 137 | + | &["repo", "id", "state"], | |
| 138 | + | ), | |
| 139 | + | DeploymentsOp::ListEnvironments => (json!({ "repo": repo }), &["repo"]), | |
| 140 | + | DeploymentsOp::GetEnvironment => ( | |
| 141 | + | json!({ | |
| 142 | + | "repo": repo, | |
| 143 | + | "environment": { "type": "string", "description": "The environment's name, such as production." }, | |
| 144 | + | }), | |
| 145 | + | &["repo", "environment"], | |
| 146 | + | ), | |
| 147 | + | }; | |
| 148 | + | json!({ "type": "object", "properties": properties, "required": required }) | |
| 149 | + | } | |
| 150 | + | } | |
| 151 | + | ||
| 152 | + | /// The fields of `input` named in `keys` that were given, as they were. | |
| 153 | + | fn given(input: &Value, keys: &[&str]) -> Map<String, Value> { | |
| 154 | + | keys.iter() | |
| 155 | + | .filter_map(|key| input.get(*key).filter(|value| !value.is_null()).map(|value| ((*key).to_owned(), value.clone()))) | |
| 156 | + | .collect() | |
| 157 | + | } | |
| 158 | + | ||
| 159 | + | /// A query parameter's number, given as a number or as text. | |
| 160 | + | fn number(input: &Value, key: &str) -> Option<u64> { | |
| 161 | + | match &input[key] { | |
| 162 | + | Value::Number(number) => number.as_u64(), | |
| 163 | + | Value::String(digits) => digits.trim().parse().ok(), | |
| 164 | + | _ => None, | |
| 165 | + | } | |
| 166 | + | } | |
| 167 | + | ||
| 168 | + | /// A flag given as a boolean or as text. | |
| 169 | + | fn flag(input: &Value, key: &str) -> Option<bool> { | |
| 170 | + | match &input[key] { | |
| 171 | + | Value::Bool(value) => Some(*value), | |
| 172 | + | Value::String(text) => match text.trim() { | |
| 173 | + | "true" | "1" => Some(true), | |
| 174 | + | "false" | "0" => Some(false), | |
| 175 | + | _ => None, | |
| 176 | + | }, | |
| 177 | + | _ => None, | |
| 178 | + | } | |
| 179 | + | } | |
| 180 | + | ||
| 181 | + | /// The arguments the deployments service takes for `op`, from the | |
| 182 | + | /// operation's input; `Err` says what is missing or wrong. | |
| 183 | + | pub(crate) fn args(op: DeploymentsOp, input: &Value) -> std::result::Result<(&'static str, Map<String, Value>), String> { | |
| 184 | + | let mut out = Map::new(); | |
| 185 | + | let repo = repo_path(input).ok_or("Give the repository as \"owner/name\".")?; | |
| 186 | + | out.insert("repo".into(), json!({ "namespace": repo.namespace, "name": repo.name })); | |
| 187 | + | let id = || input["id"].as_str().map(str::trim).filter(|id| !id.is_empty()).map(str::to_owned).ok_or("Give the deployment's id."); | |
| 188 | + | if let Some(state) = input["state"].as_str() | |
| 189 | + | && !STATES.contains(&state) | |
| 190 | + | { | |
| 191 | + | return Err(format!("{state} is not a state: use queued, in_progress, success, failure, error or inactive.")); | |
| 192 | + | } | |
| 193 | + | let method = match op { | |
| 194 | + | DeploymentsOp::ListDeployments => { | |
| 195 | + | out.extend(given(input, &["environment", "ref", "sha", "task", "state", "source", "creator"])); | |
| 196 | + | if let Some(page) = number(input, "page") { | |
| 197 | + | out.insert("page".into(), page.into()); | |
| 198 | + | } | |
| 199 | + | if let Some(per_page) = number(input, "per_page") { | |
| 200 | + | out.insert("per_page".into(), per_page.into()); | |
| 201 | + | } | |
| 202 | + | "list_deployments" | |
| 203 | + | } | |
| 204 | + | DeploymentsOp::GetDeployment => { | |
| 205 | + | out.insert("id".into(), id()?.into()); | |
| 206 | + | "get_deployment" | |
| 207 | + | } | |
| 208 | + | DeploymentsOp::ListDeploymentStatuses => { | |
| 209 | + | out.insert("id".into(), id()?.into()); | |
| 210 | + | "list_deployment_statuses" | |
| 211 | + | } | |
| 212 | + | DeploymentsOp::CreateDeployment => { | |
| 213 | + | if input["ref"].as_str().is_none_or(|text| text.trim().is_empty()) && input["sha"].as_str().is_none() { | |
| 214 | + | return Err("Give the ref deployed: a branch, a tag or a commit.".to_owned()); | |
| 215 | + | } | |
| 216 | + | out.extend(given( | |
| 217 | + | input, | |
| 218 | + | &["ref", "sha", "environment", "task", "description", "payload", "state", "environment_url", "log_url"], | |
| 219 | + | )); | |
| 220 | + | for key in ["production_environment", "transient_environment"] { | |
| 221 | + | if let Some(value) = flag(input, key) { | |
| 222 | + | out.insert(key.into(), value.into()); | |
| 223 | + | } | |
| 224 | + | } | |
| 225 | + | "create_deployment" | |
| 226 | + | } | |
| 227 | + | DeploymentsOp::CreateDeploymentStatus => { | |
| 228 | + | out.insert("id".into(), id()?.into()); | |
| 229 | + | if input["state"].as_str().is_none() { | |
| 230 | + | return Err("Give the status's state: queued, in_progress, success, failure, error or inactive.".to_owned()); | |
| 231 | + | } | |
| 232 | + | out.extend(given(input, &["state", "description", "environment_url", "log_url"])); | |
| 233 | + | if let Some(value) = flag(input, "auto_inactive") { | |
| 234 | + | out.insert("auto_inactive".into(), value.into()); | |
| 235 | + | } | |
| 236 | + | "create_deployment_status" | |
| 237 | + | } | |
| 238 | + | DeploymentsOp::ListEnvironments => "list_environments", | |
| 239 | + | DeploymentsOp::GetEnvironment => { | |
| 240 | + | let name = input["environment"].as_str().map(str::trim).filter(|name| !name.is_empty()).ok_or("Name the environment.")?; | |
| 241 | + | out.insert("name".into(), name.into()); | |
| 242 | + | "get_environment" | |
| 243 | + | } | |
| 244 | + | }; | |
| 245 | + | Ok((method, out)) | |
| 246 | + | } | |
| 247 | + | ||
| 248 | + | pub async fn run(op: DeploymentsOp, services: &Services, viewer: &Viewer, input: &Value) -> Result<Outcome<Value>> { | |
| 249 | + | let (method, mut args) = match args(op, input) { | |
| 250 | + | Ok(found) => found, | |
| 251 | + | Err(message) => return Ok(Outcome::fail(FailureCode::Invalid, message)), | |
| 252 | + | }; | |
| 253 | + | if op.writes() { | |
| 254 | + | let Some(actor) = viewer else { | |
| 255 | + | return Ok(Outcome::fail(FailureCode::Unauthenticated, "Reporting a deployment needs a g1t access token.")); | |
| 256 | + | }; | |
| 257 | + | args.insert("actor".into(), serde_json::to_value(actor)?); | |
| 258 | + | } else { | |
| 259 | + | args.insert("viewer".into(), serde_json::to_value(viewer)?); | |
| 260 | + | } | |
| 261 | + | g1t_kit::call(&services.deployments, method, &Value::Object(args)).await | |
| 262 | + | } | |
| 263 | + | ||
| 264 | + | #[cfg(test)] | |
| 265 | + | mod tests { | |
| 266 | + | use super::*; | |
| 267 | + | ||
| 268 | + | #[test] | |
| 269 | + | fn a_request_becomes_the_services_arguments() { | |
| 270 | + | let (method, args) = super::args( | |
| 271 | + | DeploymentsOp::CreateDeployment, | |
| 272 | + | &json!({ "repo": "acme/web", "ref": "main", "environment": "staging", "payload": { "buildId": 7 }, "production_environment": "false", "ignored": 1 }), | |
| 273 | + | ) | |
| 274 | + | .unwrap(); | |
| 275 | + | assert_eq!(method, "create_deployment"); | |
| 276 | + | assert_eq!(args["repo"], json!({ "namespace": "acme", "name": "web" })); | |
| 277 | + | assert_eq!(args["payload"], json!({ "buildId": 7 }), "a payload passes through as given"); | |
| 278 | + | assert_eq!(args["production_environment"], json!(false)); | |
| 279 | + | assert!(!args.contains_key("ignored")); | |
| 280 | + | let (method, args) = super::args(DeploymentsOp::ListDeployments, &json!({ "repo": "acme/web", "page": "2", "state": "failure" })).unwrap(); | |
| 281 | + | assert_eq!(method, "list_deployments"); | |
| 282 | + | assert_eq!(args["page"], json!(2)); | |
| 283 | + | assert!(super::args(DeploymentsOp::ListDeployments, &json!({ "repo": "acme/web", "state": "done" })).is_err()); | |
| 284 | + | assert!(super::args(DeploymentsOp::CreateDeployment, &json!({ "repo": "acme/web" })).is_err()); | |
| 285 | + | assert!(super::args(DeploymentsOp::CreateDeploymentStatus, &json!({ "repo": "acme/web", "id": "dep_1" })).is_err()); | |
| 286 | + | let (method, args) = super::args(DeploymentsOp::GetEnvironment, &json!({ "repo": "acme/web", "environment": "review/x" })).unwrap(); | |
| 287 | + | assert_eq!((method, args["name"].clone()), ("get_environment", json!("review/x"))); | |
| 288 | + | } | |
| 289 | + | ||
| 290 | + | #[test] | |
| 291 | + | fn each_operation_is_described_with_a_schema() { | |
| 292 | + | for op in DeploymentsOp::ALL { | |
| 293 | + | assert!(!op.title().is_empty() && op.description().len() > 40, "{}", op.name()); | |
| 294 | + | assert!(op.input()["required"].as_array().unwrap().contains(&json!("repo")), "{}", op.name()); | |
| 295 | + | } | |
| 296 | + | } | |
| 297 | + | } |
| 9 | 9 | mod audit; | |
| 10 | 10 | mod billing; | |
| 11 | 11 | mod blobs; | |
| 12 | + | mod deployments; | |
| 12 | 13 | mod mcp; | |
| 13 | 14 | mod notifications; | |
| 14 | 15 | mod oauth; |
| 7 | 7 | use g1t_contracts::scopes::scope_for; | |
| 8 | 8 | use serde_json::{Map, Value, json}; | |
| 9 | 9 | ||
| 10 | + | use crate::deployments::DeploymentsOp; | |
| 10 | 11 | use crate::operations::Op; | |
| 11 | 12 | use crate::rules::RulesOp; | |
| 12 | 13 | use crate::security::SecurityOp; | |
| ⋯ | |||
| 314 | 315 | ], | |
| 315 | 316 | ), | |
| 316 | 317 | ( | |
| 318 | + | "Deployments", | |
| 319 | + | "A repository's deployments wherever they run: reported from any CI with these routes, made by g1t Actions jobs with an `environment:`, or built on g1t.page. Each has statuses, shows on its commit as the check `deploy / <environment>`, and belongs to an environment.", | |
| 320 | + | &[ | |
| 321 | + | Op::Deployments(DeploymentsOp::ListDeployments), | |
| 322 | + | Op::Deployments(DeploymentsOp::CreateDeployment), | |
| 323 | + | Op::Deployments(DeploymentsOp::GetDeployment), | |
| 324 | + | Op::Deployments(DeploymentsOp::ListDeploymentStatuses), | |
| 325 | + | Op::Deployments(DeploymentsOp::CreateDeploymentStatus), | |
| 326 | + | Op::Deployments(DeploymentsOp::ListEnvironments), | |
| 327 | + | Op::Deployments(DeploymentsOp::GetEnvironment), | |
| 328 | + | ], | |
| 329 | + | ), | |
| 330 | + | ( | |
| 317 | 331 | "Secrets and variables", | |
| 318 | 332 | "Values that workflows and deployments read, per repository or for a whole workspace, with a row per environment.", | |
| 319 | 333 | &[ | |
| ⋯ | |||
| 557 | 571 | Op::GetCodeownersErrors => "List CODEOWNERS errors", | |
| 558 | 572 | Op::Security(op) => op.title(), | |
| 559 | 573 | Op::Rules(op) => op.title(), | |
| 574 | + | Op::Deployments(op) => op.title(), | |
| 560 | 575 | } | |
| 561 | 576 | } | |
| 562 | 577 | ||
| 26 | 26 | }; | |
| 27 | 27 | ||
| 28 | 28 | use crate::alerts::{AlertKind, SecurityAlert}; | |
| 29 | + | use crate::deployments::DeploymentsOp; | |
| 29 | 30 | use crate::rules::RulesOp; | |
| 30 | 31 | use crate::security::SecurityOp; | |
| 31 | 32 | use g1t_contracts::inbox::{Reason, Severity, WATCH_EVENTS, WatchLevel}; | |
| ⋯ | |||
| 55 | 56 | pub security: Fetcher, | |
| 56 | 57 | /// Projects: a person's pinned ones. | |
| 57 | 58 | pub projects: Fetcher, | |
| 59 | + | /// Deployments wherever they run, and environments. | |
| 60 | + | pub deployments: Fetcher, | |
| 58 | 61 | /// Where the request came in, for its audit entries. | |
| 59 | 62 | pub audit: crate::audit::AuditContext, | |
| 60 | 63 | /// Set for a request made with an agent's token: all it may do. | |
| ⋯ | |||
| 79 | 82 | search: env.service("SEARCH")?, | |
| 80 | 83 | security: env.service("SECURITY")?, | |
| 81 | 84 | projects: env.service("PROJECTS")?, | |
| 85 | + | deployments: env.service("DEPLOYMENTS")?, | |
| 82 | 86 | scope: None, | |
| 83 | 87 | audit: crate::audit::AuditContext::default(), | |
| 84 | 88 | addresses: crate::addresses::Addresses::from_env(env), | |
| ⋯ | |||
| 267 | 271 | Security(SecurityOp), | |
| 268 | 272 | /// Rulesets: rules.rs. | |
| 269 | 273 | Rules(RulesOp), | |
| 274 | + | /// Deployments wherever they run, and environments: deployments.rs. | |
| 275 | + | Deployments(DeploymentsOp), | |
| 270 | 276 | } | |
| 271 | 277 | ||
| 272 | 278 | fn failed(code: FailureCode, message: &str) -> Result<Outcome<Value>> { | |
| ⋯ | |||
| 629 | 635 | } | |
| 630 | 636 | ||
| 631 | 637 | impl Op { | |
| 632 | − | pub const ALL: [Op; 219] = [ | |
| 638 | + | pub const ALL: [Op; 226] = [ | |
| 633 | 639 | Op::Whoami, | |
| 634 | 640 | Op::GetWorkspace, | |
| 635 | 641 | Op::CreateWorkspace, | |
| ⋯ | |||
| 849 | 855 | Op::Rules(RulesOp::UpdateWorkspaceRuleset), | |
| 850 | 856 | Op::Rules(RulesOp::DeleteWorkspaceRuleset), | |
| 851 | 857 | Op::Rules(RulesOp::ListWorkspaceRuleEvaluations), | |
| 858 | + | Op::Deployments(DeploymentsOp::ListDeployments), | |
| 859 | + | Op::Deployments(DeploymentsOp::CreateDeployment), | |
| 860 | + | Op::Deployments(DeploymentsOp::GetDeployment), | |
| 861 | + | Op::Deployments(DeploymentsOp::ListDeploymentStatuses), | |
| 862 | + | Op::Deployments(DeploymentsOp::CreateDeploymentStatus), | |
| 863 | + | Op::Deployments(DeploymentsOp::ListEnvironments), | |
| 864 | + | Op::Deployments(DeploymentsOp::GetEnvironment), | |
| 852 | 865 | ]; | |
| 853 | 866 | ||
| 854 | 867 | pub fn by_name(name: &str) -> Option<Op> { | |
| ⋯ | |||
| 1035 | 1048 | Op::GetCodeownersErrors => "get_codeowners_errors", | |
| 1036 | 1049 | Op::Security(op) => op.name(), | |
| 1037 | 1050 | Op::Rules(op) => op.name(), | |
| 1051 | + | Op::Deployments(op) => op.name(), | |
| 1038 | 1052 | } | |
| 1039 | 1053 | } | |
| 1040 | 1054 | ||
| ⋯ | |||
| 1533 | 1547 | } | |
| 1534 | 1548 | Op::Security(op) => op.description(), | |
| 1535 | 1549 | Op::Rules(op) => op.description(), | |
| 1550 | + | Op::Deployments(op) => op.description(), | |
| 1536 | 1551 | } | |
| 1537 | 1552 | } | |
| 1538 | 1553 | ||
| ⋯ | |||
| 2843 | 2858 | ), | |
| 2844 | 2859 | Op::Security(op) => op.input(), | |
| 2845 | 2860 | Op::Rules(op) => op.input(), | |
| 2861 | + | Op::Deployments(op) => op.input(), | |
| 2846 | 2862 | } | |
| 2847 | 2863 | } | |
| 2848 | 2864 | ||
| ⋯ | |||
| 2869 | 2885 | | Op::GetMergeQueue | |
| 2870 | 2886 | | Op::GetCodeownersErrors | |
| 2871 | 2887 | | Op::Rules(RulesOp::ListRepoRulesets | RulesOp::GetRepoRuleset | RulesOp::GetBranchRules) | |
| 2888 | + | | Op::Deployments( | |
| 2889 | + | DeploymentsOp::ListDeployments | |
| 2890 | + | | DeploymentsOp::GetDeployment | |
| 2891 | + | | DeploymentsOp::ListDeploymentStatuses | |
| 2892 | + | | DeploymentsOp::ListEnvironments | |
| 2893 | + | | DeploymentsOp::GetEnvironment | |
| 2894 | + | ) | |
| 2872 | 2895 | ) | |
| 2873 | 2896 | } | |
| 2874 | 2897 | ||
| ⋯ | |||
| 4848 | 4871 | // each answer its public shape. | |
| 4849 | 4872 | Op::Security(op) => crate::security::run(op, services, viewer, input).await, | |
| 4850 | 4873 | Op::Rules(op) => crate::rules::run(op, services, viewer, input).await, | |
| 4874 | + | Op::Deployments(op) => crate::deployments::run(op, services, viewer, input).await, | |
| 4851 | 4875 | Op::ReopenSecurityAlert => { | |
| 4852 | 4876 | let changed: Outcome<AlertChange> = call( | |
| 4853 | 4877 | &services.security, | |
| 9278 | 9278 | ] | |
| 9279 | 9279 | } | |
| 9280 | 9280 | } | |
| 9281 | + | }, | |
| 9282 | + | "list_deployments": { | |
| 9283 | + | "params": { | |
| 9284 | + | "owner": "flagon-io", | |
| 9285 | + | "name": "g1t" | |
| 9286 | + | }, | |
| 9287 | + | "query": { | |
| 9288 | + | "environment": "production", | |
| 9289 | + | "per_page": "2" | |
| 9290 | + | }, | |
| 9291 | + | "response": { | |
| 9292 | + | "deployments": [ | |
| 9293 | + | { | |
| 9294 | + | "id": "dep_01kq8m3t5v7x9z1b3d5f7h9k2m", | |
| 9295 | + | "environment": "production", | |
| 9296 | + | "ref": "main", | |
| 9297 | + | "sha": "7c1e9a4b2d6f80135ac9e2b7d4f6a8c0e1b3d5f7", | |
| 9298 | + | "task": "deploy", | |
| 9299 | + | "description": "Deploy", | |
| 9300 | + | "payload": {}, | |
| 9301 | + | "transient_environment": false, | |
| 9302 | + | "production_environment": true, | |
| 9303 | + | "state": "success", | |
| 9304 | + | "environment_url": "https://g1t.sh", | |
| 9305 | + | "log_url": "https://g1t.sh/flagon-io/g1t/actions/runs/run_01kq8m2r4t6v8x0z2b4d6f8h0k", | |
| 9306 | + | "creator": "syntaqx", | |
| 9307 | + | "source": "actions", | |
| 9308 | + | "run_id": "run_01kq8m2r4t6v8x0z2b4d6f8h0k", | |
| 9309 | + | "run_url": "https://g1t.sh/flagon-io/g1t/actions/runs/run_01kq8m2r4t6v8x0z2b4d6f8h0k", | |
| 9310 | + | "project": null, | |
| 9311 | + | "number": null, | |
| 9312 | + | "created_at": "2026-10-07T18:02:11.204Z", | |
| 9313 | + | "updated_at": "2026-10-07T18:19:47.881Z" | |
| 9314 | + | } | |
| 9315 | + | ], | |
| 9316 | + | "total_count": 304, | |
| 9317 | + | "page": 1, | |
| 9318 | + | "per_page": 2 | |
| 9319 | + | }, | |
| 9320 | + | "notes": "Every deployment of the repository in one list, wherever it ran: `source` is `api` for those reported with [`POST /repos/{owner}/{name}/deployments`](/reference/api/deployments/create-deployment/), `actions` for those a g1t Actions job with an `environment:` made, and `g1t_page` for [g1t.page](/guides/deployments/) builds, whose ids start `dpl_`. `state` is the latest status's. See [Deployments API](/guides/deployments-api/)." | |
| 9321 | + | }, | |
| 9322 | + | "create_deployment": { | |
| 9323 | + | "params": { | |
| 9324 | + | "owner": "flagon-io", | |
| 9325 | + | "name": "g1t" | |
| 9326 | + | }, | |
| 9327 | + | "request": { | |
| 9328 | + | "ref": "main", | |
| 9329 | + | "environment": "staging", | |
| 9330 | + | "description": "Deployed by the release pipeline", | |
| 9331 | + | "payload": { | |
| 9332 | + | "pipeline": 4182, | |
| 9333 | + | "region": "us-east" | |
| 9334 | + | }, | |
| 9335 | + | "state": "in_progress", | |
| 9336 | + | "log_url": "https://ci.example.com/pipelines/4182" | |
| 9337 | + | }, | |
| 9338 | + | "response": { | |
| 9339 | + | "id": "dep_01kq7z9a1c3e5g7j9m1p3r5t7v", | |
| 9340 | + | "environment": "staging", | |
| 9341 | + | "ref": "main", | |
| 9342 | + | "sha": "4b8d0f2a6c1e3579bd02468ace13579bdf02468a", | |
| 9343 | + | "task": "deploy", | |
| 9344 | + | "description": "Deployed by the release pipeline", | |
| 9345 | + | "payload": { | |
| 9346 | + | "pipeline": 4182, | |
| 9347 | + | "region": "us-east" | |
| 9348 | + | }, | |
| 9349 | + | "transient_environment": false, | |
| 9350 | + | "production_environment": false, | |
| 9351 | + | "state": "in_progress", | |
| 9352 | + | "environment_url": null, | |
| 9353 | + | "log_url": "https://ci.example.com/pipelines/4182", | |
| 9354 | + | "creator": "flagon-io", | |
| 9355 | + | "source": "api", | |
| 9356 | + | "run_id": null, | |
| 9357 | + | "run_url": null, | |
| 9358 | + | "project": null, | |
| 9359 | + | "number": null, | |
| 9360 | + | "created_at": "2026-10-06T21:40:03.512Z", | |
| 9361 | + | "updated_at": "2026-10-06T21:40:03.512Z", | |
| 9362 | + | "statuses": [ | |
| 9363 | + | { | |
| 9364 | + | "id": "dst_01kq7z9a1d4f6h8k0m2p4r6t8v", | |
| 9365 | + | "deployment_id": "dep_01kq7z9a1c3e5g7j9m1p3r5t7v", | |
| 9366 | + | "state": "in_progress", | |
| 9367 | + | "description": "Deployed by the release pipeline", | |
| 9368 | + | "environment_url": null, | |
| 9369 | + | "log_url": "https://ci.example.com/pipelines/4182", | |
| 9370 | + | "creator": "flagon-io", | |
| 9371 | + | "created_at": "2026-10-06T21:40:03.512Z" | |
| 9372 | + | } | |
| 9373 | + | ] | |
| 9374 | + | }, | |
| 9375 | + | "notes": "Report from any CI with an access token that has `deployments:write` (the CI preset has it) and the Write role. Then report each step with [`POST …/deployments/{id}/statuses`](/reference/api/deployments/create-deployment-status/). Each status sets the check `deploy / <environment>` on the commit. A g1t Actions job with an `environment:` does all of this itself. See [Deployments API](/guides/deployments-api/)." | |
| 9376 | + | }, | |
| 9377 | + | "get_deployment": { | |
| 9378 | + | "params": { | |
| 9379 | + | "owner": "flagon-io", | |
| 9380 | + | "name": "g1t", | |
| 9381 | + | "id": "dep_01kq8m3t5v7x9z1b3d5f7h9k2m" | |
| 9382 | + | }, | |
| 9383 | + | "response": { | |
| 9384 | + | "id": "dep_01kq8m3t5v7x9z1b3d5f7h9k2m", | |
| 9385 | + | "environment": "production", | |
| 9386 | + | "ref": "main", | |
| 9387 | + | "sha": "7c1e9a4b2d6f80135ac9e2b7d4f6a8c0e1b3d5f7", | |
| 9388 | + | "task": "deploy", | |
| 9389 | + | "description": "Deploy", | |
| 9390 | + | "payload": {}, | |
| 9391 | + | "transient_environment": false, | |
| 9392 | + | "production_environment": true, | |
| 9393 | + | "state": "success", | |
| 9394 | + | "environment_url": "https://g1t.sh", | |
| 9395 | + | "log_url": "https://g1t.sh/flagon-io/g1t/actions/runs/run_01kq8m2r4t6v8x0z2b4d6f8h0k", | |
| 9396 | + | "creator": "syntaqx", | |
| 9397 | + | "source": "actions", | |
| 9398 | + | "run_id": "run_01kq8m2r4t6v8x0z2b4d6f8h0k", | |
| 9399 | + | "run_url": "https://g1t.sh/flagon-io/g1t/actions/runs/run_01kq8m2r4t6v8x0z2b4d6f8h0k", | |
| 9400 | + | "project": null, | |
| 9401 | + | "number": null, | |
| 9402 | + | "created_at": "2026-10-07T18:02:11.204Z", | |
| 9403 | + | "updated_at": "2026-10-07T18:19:47.881Z", | |
| 9404 | + | "statuses": [ | |
| 9405 | + | { | |
| 9406 | + | "id": "dst_01kq8m3t5w0a2c4e6g8j0m2p4r", | |
| 9407 | + | "deployment_id": "dep_01kq8m3t5v7x9z1b3d5f7h9k2m", | |
| 9408 | + | "state": "in_progress", | |
| 9409 | + | "description": "Deploy is deploying", | |
| 9410 | + | "environment_url": "https://g1t.sh", | |
| 9411 | + | "log_url": "https://g1t.sh/flagon-io/g1t/actions/runs/run_01kq8m2r4t6v8x0z2b4d6f8h0k", | |
| 9412 | + | "creator": "syntaqx", | |
| 9413 | + | "created_at": "2026-10-07T18:02:11.204Z" | |
| 9414 | + | }, | |
| 9415 | + | { | |
| 9416 | + | "id": "dst_01kq8n4v6x8z0b2d4f6h8k0m2p", | |
| 9417 | + | "deployment_id": "dep_01kq8m3t5v7x9z1b3d5f7h9k2m", | |
| 9418 | + | "state": "success", | |
| 9419 | + | "description": "Deploy deployed", | |
| 9420 | + | "environment_url": "https://g1t.sh", | |
| 9421 | + | "log_url": "https://g1t.sh/flagon-io/g1t/actions/runs/run_01kq8m2r4t6v8x0z2b4d6f8h0k", | |
| 9422 | + | "creator": "syntaqx", | |
| 9423 | + | "created_at": "2026-10-07T18:19:47.881Z" | |
| 9424 | + | } | |
| 9425 | + | ] | |
| 9426 | + | }, | |
| 9427 | + | "notes": "`statuses` are oldest first. A g1t.page build's (`dpl_…`) are read from the build: queued, building, how it ended, and `inactive` once a newer build replaced it or it was taken down." | |
| 9428 | + | }, | |
| 9429 | + | "list_deployment_statuses": { | |
| 9430 | + | "params": { | |
| 9431 | + | "owner": "flagon-io", | |
| 9432 | + | "name": "g1t", | |
| 9433 | + | "id": "dep_01kq8m3t5v7x9z1b3d5f7h9k2m" | |
| 9434 | + | }, | |
| 9435 | + | "response": [ | |
| 9436 | + | { | |
| 9437 | + | "id": "dst_01kq8n4v6x8z0b2d4f6h8k0m2p", | |
| 9438 | + | "deployment_id": "dep_01kq8m3t5v7x9z1b3d5f7h9k2m", | |
| 9439 | + | "state": "success", | |
| 9440 | + | "description": "Deploy deployed", | |
| 9441 | + | "environment_url": "https://g1t.sh", | |
| 9442 | + | "log_url": "https://g1t.sh/flagon-io/g1t/actions/runs/run_01kq8m2r4t6v8x0z2b4d6f8h0k", | |
| 9443 | + | "creator": "syntaqx", | |
| 9444 | + | "created_at": "2026-10-07T18:19:47.881Z" | |
| 9445 | + | }, | |
| 9446 | + | { | |
| 9447 | + | "id": "dst_01kq8m3t5w0a2c4e6g8j0m2p4r", | |
| 9448 | + | "deployment_id": "dep_01kq8m3t5v7x9z1b3d5f7h9k2m", | |
| 9449 | + | "state": "in_progress", | |
| 9450 | + | "description": "Deploy is deploying", | |
| 9451 | + | "environment_url": "https://g1t.sh", | |
| 9452 | + | "log_url": "https://g1t.sh/flagon-io/g1t/actions/runs/run_01kq8m2r4t6v8x0z2b4d6f8h0k", | |
| 9453 | + | "creator": "syntaqx", | |
| 9454 | + | "created_at": "2026-10-07T18:02:11.204Z" | |
| 9455 | + | } | |
| 9456 | + | ] | |
| 9457 | + | }, | |
| 9458 | + | "create_deployment_status": { | |
| 9459 | + | "params": { | |
| 9460 | + | "owner": "flagon-io", | |
| 9461 | + | "name": "g1t", | |
| 9462 | + | "id": "dep_01kq7z9a1c3e5g7j9m1p3r5t7v" | |
| 9463 | + | }, | |
| 9464 | + | "request": { | |
| 9465 | + | "state": "failure", | |
| 9466 | + | "description": "Smoke tests failed", | |
| 9467 | + | "log_url": "https://ci.example.com/pipelines/4182" | |
| 9468 | + | }, | |
| 9469 | + | "response": { | |
| 9470 | + | "id": "dst_01kq7zc2e4g6j8m0p2r4t6v8x0", | |
| 9471 | + | "deployment_id": "dep_01kq7z9a1c3e5g7j9m1p3r5t7v", | |
| 9472 | + | "state": "failure", | |
| 9473 | + | "description": "Smoke tests failed", | |
| 9474 | + | "environment_url": null, | |
| 9475 | + | "log_url": "https://ci.example.com/pipelines/4182", | |
| 9476 | + | "creator": "flagon-io", | |
| 9477 | + | "created_at": "2026-10-06T21:44:58.020Z" | |
| 9478 | + | }, | |
| 9479 | + | "notes": "`queued` and `in_progress` set the commit's `deploy / <environment>` check pending; `success`, `failure` and `error` settle it; `inactive` leaves it. A success makes the environment's older successful deployments `inactive` unless `auto_inactive` is false. A g1t.page build (`dpl_…`) answers `409`: its statuses come from the build." | |
| 9480 | + | }, | |
| 9481 | + | "list_environments": { | |
| 9482 | + | "params": { | |
| 9483 | + | "owner": "flagon-io", | |
| 9484 | + | "name": "g1t" | |
| 9485 | + | }, | |
| 9486 | + | "response": { | |
| 9487 | + | "total_count": 304, | |
| 9488 | + | "environments": [ | |
| 9489 | + | { | |
| 9490 | + | "name": "production", | |
| 9491 | + | "url": "https://g1t.sh", | |
| 9492 | + | "production_environment": true, | |
| 9493 | + | "transient_environment": false, | |
| 9494 | + | "deployments_count": 281, | |
| 9495 | + | "latest": { | |
| 9496 | + | "id": "dep_01kq8m3t5v7x9z1b3d5f7h9k2m", | |
| 9497 | + | "environment": "production", | |
| 9498 | + | "ref": "main", | |
| 9499 | + | "sha": "7c1e9a4b2d6f80135ac9e2b7d4f6a8c0e1b3d5f7", | |
| 9500 | + | "task": "deploy", | |
| 9501 | + | "description": "Deploy", | |
| 9502 | + | "payload": {}, | |
| 9503 | + | "transient_environment": false, | |
| 9504 | + | "production_environment": true, | |
| 9505 | + | "state": "success", | |
| 9506 | + | "environment_url": "https://g1t.sh", | |
| 9507 | + | "log_url": "https://g1t.sh/flagon-io/g1t/actions/runs/run_01kq8m2r4t6v8x0z2b4d6f8h0k", | |
| 9508 | + | "creator": "syntaqx", | |
| 9509 | + | "source": "actions", | |
| 9510 | + | "run_id": "run_01kq8m2r4t6v8x0z2b4d6f8h0k", | |
| 9511 | + | "run_url": "https://g1t.sh/flagon-io/g1t/actions/runs/run_01kq8m2r4t6v8x0z2b4d6f8h0k", | |
| 9512 | + | "project": null, | |
| 9513 | + | "number": null, | |
| 9514 | + | "created_at": "2026-10-07T18:02:11.204Z", | |
| 9515 | + | "updated_at": "2026-10-07T18:19:47.881Z" | |
| 9516 | + | }, | |
| 9517 | + | "current": { | |
| 9518 | + | "id": "dep_01kq8m3t5v7x9z1b3d5f7h9k2m", | |
| 9519 | + | "environment": "production", | |
| 9520 | + | "ref": "main", | |
| 9521 | + | "sha": "7c1e9a4b2d6f80135ac9e2b7d4f6a8c0e1b3d5f7", | |
| 9522 | + | "task": "deploy", | |
| 9523 | + | "description": "Deploy", | |
| 9524 | + | "payload": {}, | |
| 9525 | + | "transient_environment": false, | |
| 9526 | + | "production_environment": true, | |
| 9527 | + | "state": "success", | |
| 9528 | + | "environment_url": "https://g1t.sh", | |
| 9529 | + | "log_url": "https://g1t.sh/flagon-io/g1t/actions/runs/run_01kq8m2r4t6v8x0z2b4d6f8h0k", | |
| 9530 | + | "creator": "syntaqx", | |
| 9531 | + | "source": "actions", | |
| 9532 | + | "run_id": "run_01kq8m2r4t6v8x0z2b4d6f8h0k", | |
| 9533 | + | "run_url": "https://g1t.sh/flagon-io/g1t/actions/runs/run_01kq8m2r4t6v8x0z2b4d6f8h0k", | |
| 9534 | + | "project": null, | |
| 9535 | + | "number": null, | |
| 9536 | + | "created_at": "2026-10-07T18:02:11.204Z", | |
| 9537 | + | "updated_at": "2026-10-07T18:19:47.881Z" | |
| 9538 | + | }, | |
| 9539 | + | "updated_at": "2026-10-07T18:19:47.881Z" | |
| 9540 | + | }, | |
| 9541 | + | { | |
| 9542 | + | "name": "staging", | |
| 9543 | + | "url": null, | |
| 9544 | + | "production_environment": false, | |
| 9545 | + | "transient_environment": false, | |
| 9546 | + | "deployments_count": 19, | |
| 9547 | + | "latest": { | |
| 9548 | + | "id": "dep_01kq7z9a1c3e5g7j9m1p3r5t7v", | |
| 9549 | + | "environment": "staging", | |
| 9550 | + | "ref": "main", | |
| 9551 | + | "sha": "4b8d0f2a6c1e3579bd02468ace13579bdf02468a", | |
| 9552 | + | "task": "deploy", | |
| 9553 | + | "description": "Deployed by the release pipeline", | |
| 9554 | + | "payload": { | |
| 9555 | + | "pipeline": 4182, | |
| 9556 | + | "region": "us-east" | |
| 9557 | + | }, | |
| 9558 | + | "transient_environment": false, | |
| 9559 | + | "production_environment": false, | |
| 9560 | + | "state": "failure", | |
| 9561 | + | "environment_url": null, | |
| 9562 | + | "log_url": "https://ci.example.com/pipelines/4182", | |
| 9563 | + | "creator": "flagon-io", | |
| 9564 | + | "source": "api", | |
| 9565 | + | "run_id": null, | |
| 9566 | + | "run_url": null, | |
| 9567 | + | "project": null, | |
| 9568 | + | "number": null, | |
| 9569 | + | "created_at": "2026-10-06T21:40:03.512Z", | |
| 9570 | + | "updated_at": "2026-10-06T21:44:58.020Z" | |
| 9571 | + | }, | |
| 9572 | + | "current": null, | |
| 9573 | + | "updated_at": "2026-10-06T21:44:58.020Z" | |
| 9574 | + | } | |
| 9575 | + | ] | |
| 9576 | + | }, | |
| 9577 | + | "notes": "Production comes first, then the most recently deployed. `latest` is the newest deployment whatever its state; `current` is the newest that succeeded and is still active, and `url` is where it is served." | |
| 9578 | + | }, | |
| 9579 | + | "get_environment": { | |
| 9580 | + | "params": { | |
| 9581 | + | "owner": "flagon-io", | |
| 9582 | + | "name": "docs", | |
| 9583 | + | "environment": "preview" | |
| 9584 | + | }, | |
| 9585 | + | "response": { | |
| 9586 | + | "name": "preview", | |
| 9587 | + | "url": "https://docs-git-docs-deployments-flagon-io.g1t.page", | |
| 9588 | + | "production_environment": false, | |
| 9589 | + | "transient_environment": true, | |
| 9590 | + | "deployments_count": 87, | |
| 9591 | + | "latest": { | |
| 9592 | + | "id": "dpl_01kq8p2b4d6f8h0k2m4p6r8t0v", | |
| 9593 | + | "environment": "preview", | |
| 9594 | + | "ref": "docs-deployments", | |
| 9595 | + | "sha": "e2f4a6c8b0d1e3f5a7c9b1d3e5f7a9c1b3d5e7f9", | |
| 9596 | + | "task": "deploy", | |
| 9597 | + | "description": "Preview of docs-deployments on g1t.page", | |
| 9598 | + | "payload": {}, | |
| 9599 | + | "transient_environment": true, | |
| 9600 | + | "production_environment": false, | |
| 9601 | + | "state": "success", | |
| 9602 | + | "environment_url": "https://docs-git-docs-deployments-flagon-io.g1t.page", | |
| 9603 | + | "log_url": "https://g1t.sh/flagon-io/docs/deployments/dpl_01kq8p2b4d6f8h0k2m4p6r8t0v", | |
| 9604 | + | "creator": "syntaqx", | |
| 9605 | + | "source": "g1t_page", | |
| 9606 | + | "run_id": null, | |
| 9607 | + | "run_url": null, | |
| 9608 | + | "project": "docs", | |
| 9609 | + | "number": 412, | |
| 9610 | + | "created_at": "2026-10-08T01:12:44.630Z", | |
| 9611 | + | "updated_at": "2026-10-08T01:13:52.101Z" | |
| 9612 | + | }, | |
| 9613 | + | "current": { | |
| 9614 | + | "id": "dpl_01kq8p2b4d6f8h0k2m4p6r8t0v", | |
| 9615 | + | "environment": "preview", | |
| 9616 | + | "ref": "docs-deployments", | |
| 9617 | + | "sha": "e2f4a6c8b0d1e3f5a7c9b1d3e5f7a9c1b3d5e7f9", | |
| 9618 | + | "task": "deploy", | |
| 9619 | + | "description": "Preview of docs-deployments on g1t.page", | |
| 9620 | + | "payload": {}, | |
| 9621 | + | "transient_environment": true, | |
| 9622 | + | "production_environment": false, | |
| 9623 | + | "state": "success", | |
| 9624 | + | "environment_url": "https://docs-git-docs-deployments-flagon-io.g1t.page", | |
| 9625 | + | "log_url": "https://g1t.sh/flagon-io/docs/deployments/dpl_01kq8p2b4d6f8h0k2m4p6r8t0v", | |
| 9626 | + | "creator": "syntaqx", | |
| 9627 | + | "source": "g1t_page", | |
| 9628 | + | "run_id": null, | |
| 9629 | + | "run_url": null, | |
| 9630 | + | "project": "docs", | |
| 9631 | + | "number": 412, | |
| 9632 | + | "created_at": "2026-10-08T01:12:44.630Z", | |
| 9633 | + | "updated_at": "2026-10-08T01:13:52.101Z" | |
| 9634 | + | }, | |
| 9635 | + | "updated_at": "2026-10-08T01:13:52.101Z" | |
| 9636 | + | } | |
| 9281 | 9637 | } | |
| 9282 | 9638 | } |
| 109 | 109 | } | |
| 110 | 110 | // Built by the API itself. | |
| 111 | 111 | Op::Rules(RulesOp::DeleteRepoRuleset | RulesOp::DeleteWorkspaceRuleset) => return as_is, | |
| 112 | + | // Deployments travel in `snake_case` between services too. | |
| 113 | + | Op::Deployments(_) => return as_is, | |
| 112 | 114 | // Built by the API itself, in `snake_case`. | |
| 113 | 115 | Op::ListSecurityAlerts => return through::<Vec<crate::alerts::SecurityAlert>>(op, as_is), | |
| 114 | 116 | Op::DismissSecurityAlert | Op::ReopenSecurityAlert => { |
| 2 | 2 | ||
| 3 | 3 | use serde_json::{Map, Value}; | |
| 4 | 4 | ||
| 5 | + | use crate::deployments::DeploymentsOp; | |
| 5 | 6 | use crate::operations::Op; | |
| 6 | 7 | use crate::rules::RulesOp; | |
| 7 | 8 | use crate::security::SecurityOp; | |
| ⋯ | |||
| 262 | 263 | Op::Rules(RulesOp::ListWorkspaceRuleEvaluations), | |
| 263 | 264 | &[("ruleset_id", "ruleset_id"), ("verdict", "verdict"), ("problems_only", "problems_only"), ("before", "before"), ("limit", "limit")], | |
| 264 | 265 | ), | |
| 266 | + | // Deployments wherever they run, their statuses, and environments. | |
| 267 | + | route( | |
| 268 | + | "GET", | |
| 269 | + | "/repos/:owner/:name/deployments", | |
| 270 | + | Op::Deployments(DeploymentsOp::ListDeployments), | |
| 271 | + | &[("environment", "environment"), ("ref", "ref"), ("sha", "sha"), ("task", "task"), ("state", "state"), ("source", "source"), ("creator", "creator"), ("page", "page"), ("per_page", "per_page")], | |
| 272 | + | ), | |
| 273 | + | route("POST", "/repos/:owner/:name/deployments", Op::Deployments(DeploymentsOp::CreateDeployment), &[]), | |
| 274 | + | route("GET", "/repos/:owner/:name/deployments/:id", Op::Deployments(DeploymentsOp::GetDeployment), &[]), | |
| 275 | + | route("GET", "/repos/:owner/:name/deployments/:id/statuses", Op::Deployments(DeploymentsOp::ListDeploymentStatuses), &[]), | |
| 276 | + | route("POST", "/repos/:owner/:name/deployments/:id/statuses", Op::Deployments(DeploymentsOp::CreateDeploymentStatus), &[]), | |
| 277 | + | route("GET", "/repos/:owner/:name/environments", Op::Deployments(DeploymentsOp::ListEnvironments), &[]), | |
| 278 | + | route("GET", "/repos/:owner/:name/environments/:environment", Op::Deployments(DeploymentsOp::GetEnvironment), &[]), | |
| 265 | 279 | route("GET", "/repos/:owner/:name/queue", Op::GetMergeQueue, &[]), | |
| 266 | 280 | route( | |
| 267 | 281 | "POST", | |
| ⋯ | |||
| 861 | 875 | if let Some(branch) = param("branch") { | |
| 862 | 876 | input.insert("branch".to_owned(), Value::String(percent_decoded(branch))); | |
| 863 | 877 | } | |
| 878 | + | // An environment's name may hold slashes and spaces, URL-encoded. | |
| 879 | + | if let Some(environment) = param("environment") { | |
| 880 | + | input.insert("environment".to_owned(), Value::String(percent_decoded(environment))); | |
| 881 | + | } | |
| 864 | 882 | if let Some(label) = param("label") { | |
| 865 | 883 | input.insert("label".to_owned(), Value::String(percent_decoded(label))); | |
| 866 | 884 | } | |
| 18 | 18 | use g1t_contracts::scopes::{Level, NO_SCOPE, TokenAccess, scope_for}; | |
| 19 | 19 | use serde_json::{Map, Value, json}; | |
| 20 | 20 | ||
| 21 | + | use crate::deployments::DeploymentsOp; | |
| 21 | 22 | use crate::operations::Op; | |
| 22 | 23 | use crate::rules::RulesOp; | |
| 23 | 24 | use crate::security::SecurityOp; | |
| ⋯ | |||
| 182 | 183 | Tool { | |
| 183 | 184 | name: "workflow", | |
| 184 | 185 | title: "Workflows", | |
| 185 | − | description: "GitHub Actions workflows from .g1t/workflows: their runs, jobs and logs, and running, cancelling or rerunning them. Also the self-hosted runners they run on: a workspace's (`workspace`) or a repository's own (`repo`), their groups, and where agent work runs.", | |
| 186 | + | description: "GitHub Actions workflows from .g1t/workflows: their runs, jobs and logs, and running, cancelling or rerunning them. Deployments wherever they run (reported from any CI, made by jobs with an `environment:`, or built on g1t.page), their statuses and environments, and reporting your own. Also the self-hosted runners they run on: a workspace's (`workspace`) or a repository's own (`repo`), their groups, and where agent work runs.", | |
| 186 | 187 | default_action: None, | |
| 187 | 188 | actions: &[ | |
| 188 | 189 | a("list", Op::ListWorkflows, "Workflows on the default branch"), | |
| ⋯ | |||
| 193 | 194 | a("cancel", Op::CancelWorkflowRun, "Cancel a run"), | |
| 194 | 195 | a("rerun", Op::RerunWorkflowRun, "Run a finished run again"), | |
| 195 | 196 | a("update", Op::UpdateWorkflow, "Turn a workflow on or off"), | |
| 197 | + | a("list_deployments", Op::Deployments(DeploymentsOp::ListDeployments), "Deployments wherever they run, newest first, filtered"), | |
| 198 | + | a("get_deployment", Op::Deployments(DeploymentsOp::GetDeployment), "One deployment with every status it has had"), | |
| 199 | + | a("create_deployment", Op::Deployments(DeploymentsOp::CreateDeployment), "Report a deployment of a ref to an environment"), | |
| 200 | + | a("deployment_statuses", Op::Deployments(DeploymentsOp::ListDeploymentStatuses), "A deployment's statuses, newest first"), | |
| 201 | + | a("create_deployment_status", Op::Deployments(DeploymentsOp::CreateDeploymentStatus), "Report where a deployment is: in_progress, success, failure"), | |
| 202 | + | a("list_environments", Op::Deployments(DeploymentsOp::ListEnvironments), "Environments with their current and latest deployments"), | |
| 203 | + | a("get_environment", Op::Deployments(DeploymentsOp::GetEnvironment), "One environment by name"), | |
| 196 | 204 | a("list_runners", Op::ListRunners, "Self-hosted runners, with status, labels and what each is doing"), | |
| 197 | 205 | a("create_runner_token", Op::CreateRunnerRegistrationToken, "A one-hour token for g1t-runner register"), | |
| 198 | 206 | a("remove_runner", Op::RemoveRunner, "Remove a self-hosted runner"), | |
| 74 | 74 | items: [ | |
| 75 | 75 | { label: 'Projects', slug: 'guides/projects' }, | |
| 76 | 76 | { label: 'Deployments', slug: 'guides/deployments' }, | |
| 77 | + | { label: 'Deployments API', slug: 'guides/deployments-api' }, | |
| 77 | 78 | { label: 'Packages', slug: 'guides/packages' }, | |
| 78 | 79 | { label: 'Container images', slug: 'guides/containers' }, | |
| 79 | 80 | { label: 'npm', slug: 'guides/npm' }, |
| 42 | 42 | | `GITHUB_OUTPUT`, `GITHUB_ENV`, `GITHUB_PATH`, `GITHUB_STATE`, `GITHUB_STEP_SUMMARY` | The same. | | |
| 43 | 43 | | `::error::`, `::warning::`, `::notice::`, `::group::`, `::add-mask::` | The same: errors and warnings become annotations on the run. | | |
| 44 | 44 | | `secrets.*`, `vars.*`, `secrets.GITHUB_TOKEN` | The same. `secrets.G1T_TOKEN` is the workspace's own token for the run; `GITHUB_TOKEN` is its alias. | | |
| 45 | − | | `environment:` on a job | The job reads each key's row for that environment, as GitHub's environment secrets work. | | |
| 45 | + | | `environment:` on a job | The job reads each key's row for that environment, as GitHub's environment secrets work, and the run records a [deployment](/guides/deployments-api/#deployments-from-g1t-actions) to it. `url` gives the deployment its address; `deployment: false` reads the environment's values without making one. | | |
| 46 | 46 | | `actions/upload-artifact`, `actions/download-artifact` | Kept with the run for 14 days, passed between its jobs, and downloadable from the run's page. Up to 60 MB each. | | |
| 47 | 47 | | `actions/cache`, `actions/cache/restore`, `actions/cache/save` | Kept per repository, found by `key` or the newest under a `restore-keys` prefix. `path` takes globs and `!` exclusions. Up to 2 GiB each; see [the cache](#the-cache). | | |
| 48 | 48 | ||
| ⋯ | |||
| 66 | 66 | - **Environments' protection rules** (required reviewers, wait timers, | |
| 67 | 67 | branch limits). A job with `environment:` gets that environment's | |
| 68 | 68 | [values](/guides/secrets-and-variables/#a-value-per-environment), and runs | |
| 69 | − | without waiting. | |
| 69 | + | without waiting. It still records a | |
| 70 | + | [deployment](/guides/deployments-api/#deployments-from-g1t-actions) | |
| 71 | + | unless it says `deployment: false`. | |
| 70 | 72 | ||
| 71 | 73 | Why each of these is missing, and what to use instead, is on | |
| 72 | 74 | [What g1t can't do yet](/about/limitations/#actions-and-runners). | |
| ⋯ | |||
| 264 | 266 | Secrets are read as `${{ secrets.KEY }}` and config as `${{ vars.KEY }}`, | |
| 265 | 267 | from the rows under **Settings → Secrets and variables** that are | |
| 266 | 268 | available to Workflows. A job with `environment: production` reads each | |
| 267 | − | key's Production row; other jobs read the rows for all environments. See | |
| 269 | + | key's Production row; other jobs read the rows for all environments. A | |
| 270 | + | job with an `environment:` also makes a deployment to it; see | |
| 271 | + | [deployments from g1t Actions](/guides/deployments-api/#deployments-from-g1t-actions). See | |
| 268 | 272 | [Secrets and variables](/guides/secrets-and-variables/) for how rows, | |
| 269 | 273 | environments and the workspace's rows work. | |
| 270 | 274 | ||
| 321 | 321 | | Issues & pull requests | `issues:read`, `issues:write`, `pull_requests:read`, `pull_requests:write` | | |
| 322 | 322 | | Agents | `agents:run` | | |
| 323 | 323 | | Workflows | `workflows:read`, `workflows:write` | | |
| 324 | + | | Deployments | `deployments:read`, `deployments:write` | | |
| 324 | 325 | | Memory & search | `memory:read`, `memory:write` | | |
| 325 | 326 | | Account | `account:read`, `account:write` | | |
| 326 | 327 | | Notifications | `notifications:read`, `notifications:write` | | |
| ⋯ | |||
| 354 | 355 | | `agents:run` | Put g1t to work and message it, which uses the workspace's money | | |
| 355 | 356 | | `workflows:read` | Read workflows, runs and logs | | |
| 356 | 357 | | `workflows:write` | Run, cancel, rerun and turn workflows on or off | | |
| 358 | + | | `deployments:read` | See [deployments](/guides/deployments-api/), their statuses and environments | | |
| 359 | + | | `deployments:write` | Report deployments and their statuses, from any CI | | |
| 357 | 360 | | `memory:read` | Recall memory and search the workspace's context | | |
| 358 | 361 | | `memory:write` | Save memory for the next agent | | |
| 359 | 362 | | `account:read` | Read your email addresses, invites, invitations and pinned projects | | |
| ⋯ | |||
| 411 | 414 | | --- | --- | | |
| 412 | 415 | | Read only | Every `read` scope. Changes nothing. | | |
| 413 | 416 | | Agent | Every `read` scope except `runners:read`, and `code:write`, `issues:write`, `pull_requests:write`, `agents:run`, `memory:write` and `notifications:write`. Reads everything, works on issues and pull requests, pushes code, puts g1t to work, and answers your inbox. No admin scope. | | |
| 414 | − | | CI | `repo:read`, `code:read`, `code:write`, `packages:read`, `packages:write`, `workflows:read` and `workflows:write`. Clones and pushes code, pushes and pulls packages, and runs workflows. | | |
| 417 | + | | CI | `repo:read`, `code:read`, `code:write`, `packages:read`, `packages:write`, `workflows:read`, `workflows:write`, `deployments:read` and `deployments:write`. Clones and pushes code, pushes and pulls packages, runs workflows and reports deployments. | | |
| 415 | 418 | | Full access | Everything you can do, including deleting repositories and changing who has access. Marked **Dangerous**. | | |
| 416 | 419 | ||
| 417 | 420 | Admin scopes change things that are hard to undo, or decide who can reach | |
| 1 | + | --- | |
| 2 | + | title: Deployments API | |
| 3 | + | description: Report deployments from any CI, see every deployment of a repository and its environments in one place, require them before a merge, and hear of them by webhook. | |
| 4 | + | --- | |
| 5 | + | ||
| 6 | + | A repository keeps one list of its deployments, wherever they ran. A | |
| 7 | + | deployment is one commit sent to one environment, such as `production` or | |
| 8 | + | `staging`. An environment is the place it went: a name, and the address it | |
| 9 | + | is served at. Each deployment has a list of statuses that say how it went, | |
| 10 | + | and its latest status is its state. | |
| 11 | + | ||
| 12 | + | Deployments reach that list three ways. Each deployment says which in its | |
| 13 | + | `source`: | |
| 14 | + | ||
| 15 | + | | `source` | Made by | Ids | | |
| 16 | + | | --- | --- | --- | | |
| 17 | + | | `api` | Any CI or script, through the routes on this page, with an access token. | `dep_…` | | |
| 18 | + | | `actions` | A [g1t Actions](/guides/actions/) job with an `environment:`, by itself. | `dep_…` | | |
| 19 | + | | `g1t_page` | A [g1t.page](/guides/deployments/) build of a project, to the `production` or `preview` environment. | `dpl_…` | | |
| 20 | + | ||
| 21 | + | A status's id starts `dst_`. Every deployment, whatever its source, shows | |
| 22 | + | on the repository's **Deployments** page, sets a check on its commit, and | |
| 23 | + | sends [webhooks](#webhooks). | |
| 24 | + | ||
| 25 | + | ## Report a deployment from any CI | |
| 26 | + | ||
| 27 | + | You report a deployment by creating it, then adding a status each time it | |
| 28 | + | moves on. The base URL is `https://api.g1t.sh`, and every request carries | |
| 29 | + | an access token as `Authorization: Bearer g1t_…`. | |
| 30 | + | ||
| 31 | + | ### Make a token | |
| 32 | + | ||
| 33 | + | 1. Open your **Settings → Access tokens**, or a workspace's | |
| 34 | + | **Settings → Access tokens** for a token that belongs to the workspace. | |
| 35 | + | 2. Give the token a name, such as `release-pipeline`. | |
| 36 | + | 3. Select the **CI** preset. It includes `deployments:read` and | |
| 37 | + | `deployments:write`. To report deployments and nothing else, tick only | |
| 38 | + | `deployments:write` under **Deployments**. | |
| 39 | + | 4. Choose an expiry and select **Create token**. Copy the token now: it is | |
| 40 | + | not shown again. | |
| 41 | + | 5. Store it in your CI as a secret named `G1T_TOKEN`. | |
| 42 | + | ||
| 43 | + | Reporting also needs the Write [role](/guides/access-and-roles/) on the | |
| 44 | + | repository. See [scopes](/guides/authentication/#scopes). | |
| 45 | + | ||
| 46 | + | ### Create the deployment | |
| 47 | + | ||
| 48 | + | `POST /repos/{owner}/{name}/deployments` creates a deployment. Give it | |
| 49 | + | `state: "in_progress"` when the deploy has started: | |
| 50 | + | ||
| 51 | + | ```sh | |
| 52 | + | curl -X POST https://api.g1t.sh/repos/acme/web/deployments \ | |
| 53 | + | -H "Authorization: Bearer $G1T_TOKEN" \ | |
| 54 | + | -H "Content-Type: application/json" \ | |
| 55 | + | -d '{ | |
| 56 | + | "ref": "main", | |
| 57 | + | "environment": "staging", | |
| 58 | + | "description": "Deployed by the release pipeline", | |
| 59 | + | "state": "in_progress", | |
| 60 | + | "log_url": "https://ci.example.com/pipelines/4182" | |
| 61 | + | }' | |
| 62 | + | ``` | |
| 63 | + | ||
| 64 | + | The answer is the deployment, with its first status in `statuses`. Keep | |
| 65 | + | its `id` for the next call. | |
| 66 | + | ||
| 67 | + | | Field | Default | | | |
| 68 | + | | --- | --- | --- | | |
| 69 | + | | `ref` | Required | The branch, tag or commit deployed, such as `main` or `v1.4.0`. | | |
| 70 | + | | `sha` | Resolved from `ref` | The commit deployed. Give the whole commit id to skip resolving `ref`. | | |
| 71 | + | | `environment` | `production` | Any name up to 255 characters, such as `staging` or `review/feature-x`. Names are matched without regard to case, and the first spelling is kept. | | |
| 72 | + | | `task` | `deploy` | What kind of deployment, such as `deploy:migrations`. Up to 100 characters. | | |
| 73 | + | | `description` | None | A short note, up to 1,000 characters. | | |
| 74 | + | | `payload` | `{}` | Anything else to keep with it: a JSON object, or a JSON string of one, up to 64 KB. Returned as given. | | |
| 75 | + | | `production_environment` | `true` for `production`, else `false` | Whether people use this environment directly. | | |
| 76 | + | | `transient_environment` | `false` | Whether the environment goes away, such as a review app. | | |
| 77 | + | | `state` | `queued` | Its first status. See [statuses](#statuses). | | |
| 78 | + | | `environment_url` | None | Where it is served, an `http` or `https` address. | | |
| 79 | + | | `log_url` | None | Where its output can be read, an `http` or `https` address. | | |
| 80 | + | ||
| 81 | + | ### Report how it went | |
| 82 | + | ||
| 83 | + | `POST /repos/{owner}/{name}/deployments/{id}/statuses` adds a status. When | |
| 84 | + | the deploy succeeds, give the address it is served at: | |
| 85 | + | ||
| 86 | + | ```sh | |
| 87 | + | curl -X POST https://api.g1t.sh/repos/acme/web/deployments/dep_01kq7z9a1c3e5g7j9m1p3r5t7v/statuses \ | |
| 88 | + | -H "Authorization: Bearer $G1T_TOKEN" \ | |
| 89 | + | -H "Content-Type: application/json" \ | |
| 90 | + | -d '{ | |
| 91 | + | "state": "success", | |
| 92 | + | "environment_url": "https://staging.example.com", | |
| 93 | + | "log_url": "https://ci.example.com/pipelines/4182" | |
| 94 | + | }' | |
| 95 | + | ``` | |
| 96 | + | ||
| 97 | + | When it fails: | |
| 98 | + | ||
| 99 | + | ```sh | |
| 100 | + | curl -X POST https://api.g1t.sh/repos/acme/web/deployments/dep_01kq7z9a1c3e5g7j9m1p3r5t7v/statuses \ | |
| 101 | + | -H "Authorization: Bearer $G1T_TOKEN" \ | |
| 102 | + | -H "Content-Type: application/json" \ | |
| 103 | + | -d '{ | |
| 104 | + | "state": "failure", | |
| 105 | + | "description": "Smoke tests failed", | |
| 106 | + | "log_url": "https://ci.example.com/pipelines/4182" | |
| 107 | + | }' | |
| 108 | + | ``` | |
| 109 | + | ||
| 110 | + | | Field | Default | | | |
| 111 | + | | --- | --- | --- | | |
| 112 | + | | `state` | Required | `queued`, `in_progress`, `success`, `failure`, `error` or `inactive`. | | |
| 113 | + | | `description` | None | A short note, up to 1,000 characters. | | |
| 114 | + | | `environment_url` | None | Where it is served, an `http` or `https` address. | | |
| 115 | + | | `log_url` | None | Where its output can be read, an `http` or `https` address. | | |
| 116 | + | | `auto_inactive` | `true` | On a `success`, give the environment's older successful deployments an `inactive` status. See [auto_inactive](#auto_inactive). | | |
| 117 | + | ||
| 118 | + | The deployment takes the status's state, and any address the status gives. | |
| 119 | + | ||
| 120 | + | ### A script for any CI | |
| 121 | + | ||
| 122 | + | This script wraps a deploy command. It needs `curl`, `jq`, and three | |
| 123 | + | variables: `G1T_TOKEN`, `G1T_REPO` (such as `acme/web`) and `GIT_COMMIT` | |
| 124 | + | (the commit being deployed). Change `ENVIRONMENT`, `ENVIRONMENT_URL` and | |
| 125 | + | the deploy command to yours. | |
| 126 | + | ||
| 127 | + | ```sh | |
| 128 | + | #!/bin/sh | |
| 129 | + | set -eu | |
| 130 | + | ||
| 131 | + | API="https://api.g1t.sh/repos/$G1T_REPO/deployments" | |
| 132 | + | ENVIRONMENT="staging" | |
| 133 | + | ENVIRONMENT_URL="https://staging.example.com" | |
| 134 | + | ||
| 135 | + | report() { | |
| 136 | + | curl -fsS -X POST "$1" \ | |
| 137 | + | -H "Authorization: Bearer $G1T_TOKEN" \ | |
| 138 | + | -H "Content-Type: application/json" \ | |
| 139 | + | -d "$2" | |
| 140 | + | } | |
| 141 | + | ||
| 142 | + | # 1. Create the deployment, in progress. | |
| 143 | + | ID=$(report "$API" "$(jq -n \ | |
| 144 | + | --arg ref "$GIT_COMMIT" \ | |
| 145 | + | --arg environment "$ENVIRONMENT" \ | |
| 146 | + | '{ref: $ref, environment: $environment, state: "in_progress"}')" | jq -r .id) | |
| 147 | + | ||
| 148 | + | # 2. Deploy, then report how it went. | |
| 149 | + | if ./deploy.sh; then | |
| 150 | + | report "$API/$ID/statuses" "$(jq -n --arg url "$ENVIRONMENT_URL" \ | |
| 151 | + | '{state: "success", environment_url: $url}')" > /dev/null | |
| 152 | + | else | |
| 153 | + | report "$API/$ID/statuses" '{"state": "failure", "description": "The deploy command failed"}' > /dev/null | |
| 154 | + | exit 1 | |
| 155 | + | fi | |
| 156 | + | ``` | |
| 157 | + | ||
| 158 | + | Give `log_url` in both calls to link the deployment to your CI's page for | |
| 159 | + | the job. | |
| 160 | + | ||
| 161 | + | ## Statuses | |
| 162 | + | ||
| 163 | + | | State | Means | The commit's check | | |
| 164 | + | | --- | --- | --- | | |
| 165 | + | | `queued` | It is waiting to start. | Pending | | |
| 166 | + | | `in_progress` | It is deploying. | Pending | | |
| 167 | + | | `success` | It is live. | Success | | |
| 168 | + | | `failure` | It did not go live. | Failure | | |
| 169 | + | | `error` | Something went wrong around it, such as a cancelled run. | Error | | |
| 170 | + | | `inactive` | It is no longer what the environment serves. | Left as it was | | |
| 171 | + | ||
| 172 | + | A g1t.page build's statuses are read from the build itself: `queued`, then | |
| 173 | + | `in_progress` while it builds, then how it ended, and `inactive` once a | |
| 174 | + | newer build replaced it or it was taken down. You cannot add statuses to a | |
| 175 | + | g1t.page build: `POST …/deployments/dpl_…/statuses` answers `409`. | |
| 176 | + | ||
| 177 | + | ### auto_inactive | |
| 178 | + | ||
| 179 | + | An environment serves one deployment at a time. When a deployment | |
| 180 | + | succeeds, the environment's older successful deployments each get an | |
| 181 | + | `inactive` status, so only the newest stays current. To keep them as they | |
| 182 | + | are, for example when several deployments are live side by side, send | |
| 183 | + | `"auto_inactive": false` with the `success`. | |
| 184 | + | ||
| 185 | + | ## Environments | |
| 186 | + | ||
| 187 | + | An environment exists once something deploys to it. You do not create | |
| 188 | + | environments first. | |
| 189 | + | ||
| 190 | + | - **Production environments** are those people use directly. An | |
| 191 | + | environment named `production` is one unless the deployment says | |
| 192 | + | `"production_environment": false`. Set it to `true` for others, such as | |
| 193 | + | `live`. | |
| 194 | + | - **Transient environments** go away, such as a review app for one pull | |
| 195 | + | request. Set `"transient_environment": true` on their deployments. | |
| 196 | + | ||
| 197 | + | `GET /repos/{owner}/{name}/environments` lists them: `production` first, | |
| 198 | + | then other production environments, then the rest, most recently deployed | |
| 199 | + | first. Each has: | |
| 200 | + | ||
| 201 | + | | Field | | | |
| 202 | + | | --- | --- | | |
| 203 | + | | `name` | The environment's name, as first spelled. | | |
| 204 | + | | `url` | Where it is served: `current`'s `environment_url`. | | |
| 205 | + | | `production_environment`, `transient_environment` | As its deployments said. | | |
| 206 | + | | `deployments_count` | How many deployments went to it. | | |
| 207 | + | | `latest` | Its newest deployment, whatever its state. | | |
| 208 | + | | `current` | Its newest successful deployment that is still active. Null when none is. | | |
| 209 | + | | `updated_at` | When it last changed. | | |
| 210 | + | ||
| 211 | + | `total_count` counts deployments across every environment. | |
| 212 | + | `GET /repos/{owner}/{name}/environments/{environment}` returns one, matched | |
| 213 | + | without regard to case. URL-encode a name with slashes: | |
| 214 | + | `…/environments/review%2Ffeature-x`. | |
| 215 | + | ||
| 216 | + | ## Read deployments | |
| 217 | + | ||
| 218 | + | | Route | What it returns | | |
| 219 | + | | --- | --- | | |
| 220 | + | | `GET /repos/{owner}/{name}/deployments` | Deployments, newest first, as `{ deployments, total_count, page, per_page }`. | | |
| 221 | + | | `GET /repos/{owner}/{name}/deployments/{id}` | One deployment, with every status in `statuses`, oldest first. | | |
| 222 | + | | `GET /repos/{owner}/{name}/deployments/{id}/statuses` | Its statuses, newest first. | | |
| 223 | + | | `GET /repos/{owner}/{name}/environments` | The environments, as above. | | |
| 224 | + | | `GET /repos/{owner}/{name}/environments/{environment}` | One environment. | | |
| 225 | + | ||
| 226 | + | The list takes these filters: | |
| 227 | + | ||
| 228 | + | | Query | | | |
| 229 | + | | --- | --- | | |
| 230 | + | | `environment` | Only this environment's, matched without regard to case. | | |
| 231 | + | | `ref` | Only deployments of this branch, tag or commit, as it was given. | | |
| 232 | + | | `sha` | Only deployments of this commit, or of commits starting with it. | | |
| 233 | + | | `task` | Only this task's. | | |
| 234 | + | | `state` | Only deployments whose latest status has this state. | | |
| 235 | + | | `source` | `api`, `actions` or `g1t_page`. | | |
| 236 | + | | `creator` | Only those this username made, or `g1t`. | | |
| 237 | + | | `page`, `per_page` | Which page, from 1, and how many on it: 30 unless you say, at most 100. | | |
| 238 | + | ||
| 239 | + | ```sh | |
| 240 | + | curl "https://api.g1t.sh/repos/acme/web/deployments?environment=production&state=success&per_page=5" \ | |
| 241 | + | -H "Authorization: Bearer $G1T_TOKEN" | |
| 242 | + | ``` | |
| 243 | + | ||
| 244 | + | A deployment has these fields: | |
| 245 | + | ||
| 246 | + | | Field | | | |
| 247 | + | | --- | --- | | |
| 248 | + | | `id` | `dep_…`, or `dpl_…` for a g1t.page build. | | |
| 249 | + | | `environment`, `ref`, `sha`, `task`, `description`, `payload` | As reported. | | |
| 250 | + | | `production_environment`, `transient_environment` | As reported. | | |
| 251 | + | | `state` | Its latest status's state. | | |
| 252 | + | | `environment_url`, `log_url` | The latest addresses it was given. | | |
| 253 | + | | `creator` | The username of whoever reported it or started its run, or `g1t`. | | |
| 254 | + | | `source` | `api`, `actions` or `g1t_page`. | | |
| 255 | + | | `run_id`, `run_url` | The g1t Actions run that made it. Null for other sources. | | |
| 256 | + | | `project`, `number` | For a g1t.page build: the project, and for a preview its pull request's number. Null for other sources. | | |
| 257 | + | | `created_at`, `updated_at` | When it was made, and when it last changed. | | |
| 258 | + | ||
| 259 | + | Reading needs the `deployments:read` scope and the Read role. A public | |
| 260 | + | repository's deployments can be read by anyone. Reporting needs | |
| 261 | + | `deployments:write` and the Write role, and an | |
| 262 | + | [archived](/guides/managing-repositories/) repository refuses it with | |
| 263 | + | `409`. Every route, with its example, is in the | |
| 264 | + | [API reference](/reference/api/deployments/list-deployments/). | |
| 265 | + | ||
| 266 | + | ## The commit's check | |
| 267 | + | ||
| 268 | + | Every status sets a check on the deployment's commit, named | |
| 269 | + | `deploy / <environment>`, such as `deploy / staging`. It links to the | |
| 270 | + | deployment's page, `https://g1t.sh/{owner}/{name}/deployments/{id}`. The | |
| 271 | + | [statuses table](#statuses) says which state the check takes. A pull | |
| 272 | + | request whose head was deployed shows the check with its other checks. | |
| 273 | + | ||
| 274 | + | g1t.page builds keep their own check, `g1t / deploy`. See | |
| 275 | + | [previews of branches](/guides/deployments/#previews-of-branches). | |
| 276 | + | ||
| 277 | + | ### Require a deployment before merging | |
| 278 | + | ||
| 279 | + | A ruleset's **Require deployments to succeed** rule | |
| 280 | + | (`required_deployments`) holds a pull request until its head has deployed | |
| 281 | + | successfully to each environment it names. A successful | |
| 282 | + | `deploy / <environment>` check meets it, so any environment you report to, | |
| 283 | + | from anywhere, can be required: | |
| 284 | + | ||
| 285 | + | ```sh | |
| 286 | + | curl -X POST https://api.g1t.sh/repos/acme/web/rulesets \ | |
| 287 | + | -H "Authorization: Bearer $G1T_TOKEN" \ | |
| 288 | + | -H "Content-Type: application/json" \ | |
| 289 | + | -d '{ | |
| 290 | + | "ruleset_name": "Staging first", | |
| 291 | + | "conditions": { "ref_name": { "include": ["~DEFAULT_BRANCH"] } }, | |
| 292 | + | "rules": [ | |
| 293 | + | { "type": "required_deployments", "parameters": { "environments": ["staging"] } } | |
| 294 | + | ] | |
| 295 | + | }' | |
| 296 | + | ``` | |
| 297 | + | ||
| 298 | + | For this to work, your CI deploys each pull request's head to `staging` | |
| 299 | + | and reports it. See [rules](/guides/rules/#pull-requests-and-checks). | |
| 300 | + | ||
| 301 | + | ## Deployments from g1t Actions | |
| 302 | + | ||
| 303 | + | A [g1t Actions](/guides/actions/) job with an `environment:` makes a | |
| 304 | + | deployment by itself. You do not call the API. | |
| 305 | + | ||
| 306 | + | ```yaml | |
| 307 | + | jobs: | |
| 308 | + | deploy: | |
| 309 | + | runs-on: ubuntu-latest | |
| 310 | + | environment: production | |
| 311 | + | steps: | |
| 312 | + | - uses: actions/checkout@v4 | |
| 313 | + | - run: ./deploy.sh | |
| 314 | + | ``` | |
| 315 | + | ||
| 316 | + | Give the environment's address with `url`. Its expressions are filled in | |
| 317 | + | from the `github`, `inputs` and `matrix` contexts, and it must be an | |
| 318 | + | `http` or `https` address: | |
| 319 | + | ||
| 320 | + | ```yaml | |
| 321 | + | jobs: | |
| 322 | + | deploy: | |
| 323 | + | runs-on: ubuntu-latest | |
| 324 | + | environment: | |
| 325 | + | name: staging | |
| 326 | + | url: https://staging.example.com/${{ github.sha }} | |
| 327 | + | steps: | |
| 328 | + | - uses: actions/checkout@v4 | |
| 329 | + | - run: ./deploy.sh staging | |
| 330 | + | ``` | |
| 331 | + | ||
| 332 | + | A run makes one deployment per environment, however many of its jobs name | |
| 333 | + | it. A matrix that deploys to three regions makes one `production` | |
| 334 | + | deployment, not three: | |
| 335 | + | ||
| 336 | + | ```yaml | |
| 337 | + | jobs: | |
| 338 | + | deploy: | |
| 339 | + | runs-on: ubuntu-latest | |
| 340 | + | strategy: | |
| 341 | + | matrix: | |
| 342 | + | region: [us-east, eu-west, ap-south] | |
| 343 | + | environment: production | |
| 344 | + | steps: | |
| 345 | + | - uses: actions/checkout@v4 | |
| 346 | + | - run: ./deploy.sh ${{ matrix.region }} | |
| 347 | + | ``` | |
| 348 | + | ||
| 349 | + | How the deployment goes: | |
| 350 | + | ||
| 351 | + | 1. It is made, `in_progress`, when the first job that names the | |
| 352 | + | environment starts. | |
| 353 | + | 2. If one of those jobs fails, it is marked `failure` at once, unless the | |
| 354 | + | job has `continue-on-error`. | |
| 355 | + | 3. When the run finishes, it settles: `failure` if any of those jobs | |
| 356 | + | failed, `error` if the run was cancelled, and `success` otherwise. Jobs | |
| 357 | + | that were skipped do not count. If none of them ran, no deployment is | |
| 358 | + | made. | |
| 359 | + | ||
| 360 | + | Its `ref` is the run's branch, `sha` the run's commit, `creator` whoever | |
| 361 | + | started the run, and `log_url` and `run_url` the run's page. A run | |
| 362 | + | attempted again makes a deployment of its own. Jobs on | |
| 363 | + | [self-hosted runners](/guides/self-hosted-runners/) make deployments the | |
| 364 | + | same way. | |
| 365 | + | ||
| 366 | + | A job that needs an environment's | |
| 367 | + | [secrets and variables](/guides/secrets-and-variables/#a-value-per-environment) | |
| 368 | + | but does not deploy, such as one that plans a change, says | |
| 369 | + | `deployment: false`: | |
| 370 | + | ||
| 371 | + | ```yaml | |
| 372 | + | jobs: | |
| 373 | + | plan: | |
| 374 | + | runs-on: ubuntu-latest | |
| 375 | + | environment: | |
| 376 | + | name: production | |
| 377 | + | deployment: false | |
| 378 | + | steps: | |
| 379 | + | - uses: actions/checkout@v4 | |
| 380 | + | - run: ./plan.sh | |
| 381 | + | ``` | |
| 382 | + | ||
| 383 | + | A job's `G1T_TOKEN` has full access, so a step can also report deployments | |
| 384 | + | of its own with the API. | |
| 385 | + | ||
| 386 | + | ## Webhooks | |
| 387 | + | ||
| 388 | + | [Webhooks](/guides/webhooks/) send two events for deployments of every | |
| 389 | + | source, g1t.page builds included: | |
| 390 | + | ||
| 391 | + | | Event | When | `data` | | |
| 392 | + | | --- | --- | --- | | |
| 393 | + | | `deployment.created` | A deployment was made. | `repo_id`, `deployment` (without `payload`) | | |
| 394 | + | | `deployment_status.created` | A deployment got a status. | `repo_id`, `deployment` (without `payload`), `deployment_status` | | |
| 395 | + | ||
| 396 | + | `deployment.succeeded` and `deployment.failed` are sent for g1t.page builds | |
| 397 | + | only. | |
| 398 | + | ||
| 399 | + | ## MCP | |
| 400 | + | ||
| 401 | + | The [`workflow` tool](/reference/mcp/#workflow) has an action for each route: | |
| 402 | + | ||
| 403 | + | | Action | Route | | |
| 404 | + | | --- | --- | | |
| 405 | + | | `list_deployments` | `GET /repos/{owner}/{name}/deployments` | | |
| 406 | + | | `get_deployment` | `GET /repos/{owner}/{name}/deployments/{id}` | | |
| 407 | + | | `create_deployment` | `POST /repos/{owner}/{name}/deployments` | | |
| 408 | + | | `deployment_statuses` | `GET /repos/{owner}/{name}/deployments/{id}/statuses` | | |
| 409 | + | | `create_deployment_status` | `POST /repos/{owner}/{name}/deployments/{id}/statuses` | | |
| 410 | + | | `list_environments` | `GET /repos/{owner}/{name}/environments` | | |
| 411 | + | | `get_environment` | `GET /repos/{owner}/{name}/environments/{environment}` | | |
| 412 | + | ||
| 413 | + | They take the repository as `repo`, written `owner/name`, and the same | |
| 414 | + | fields as the routes. | |
| 415 | + | ||
| 416 | + | ## On the site | |
| 417 | + | ||
| 418 | + | - **The Deployments page**, `g1t.sh/<owner>/<project>/deployments`, shows | |
| 419 | + | a card for each environment: its state, address, commit, ref, who | |
| 420 | + | deployed it and from where, when, and how many deployments it has had. | |
| 421 | + | Below is every deployment, newest first, filtered by **Environment**, | |
| 422 | + | **State**, **Source**, **Creator** and **Ref**. Projects deployed to | |
| 423 | + | g1t.page manage their apps under **On g1t.page** on the same page. | |
| 424 | + | - **A deployment's page**, `g1t.sh/<owner>/<project>/deployments/<id>`, | |
| 425 | + | shows its statuses in order, links to its log and run, and its payload. | |
| 426 | + | - **The repository's code page** and **the project's overview** show a | |
| 427 | + | **Deployments** panel with how many there are, and each environment's | |
| 428 | + | latest deployment and when. It links to the Deployments page. | |
| 429 | + | - **The project's overview** shows a production environment deployed | |
| 430 | + | elsewhere in its production card: its address, state, commit and when it | |
| 431 | + | went up. |
| 41 | 41 | is built again from the new default branch. An | |
| 42 | 42 | [archived](/guides/managing-repositories/) repository's apps keep serving. | |
| 43 | 43 | ||
| 44 | + | Every build is also a deployment in the repository's one list of | |
| 45 | + | deployments, beside those reported from any CI and those g1t Actions | |
| 46 | + | jobs make, with the source `g1t_page` and the environment `production` or | |
| 47 | + | `preview`. See [Deployments API](/guides/deployments-api/) to read them, | |
| 48 | + | report your own, and hear of them by webhook. | |
| 49 | + | ||
| 44 | 50 | An app runs only while it answers a request. One nobody visits runs | |
| 45 | 51 | nothing and costs nothing, and the next visit wakes it in milliseconds. | |
| 46 | 52 |
| 139 | 139 | | Require a pull request before merging | `pull_request` | Pushes straight to the branch are refused. A pull request into it needs what the parameters say. | | |
| 140 | 140 | | Require status checks to pass | `required_status_checks` | These checks must pass on a pull request's head before it merges. | | |
| 141 | 141 | | Require the merge queue | `merge_queue` | Merging into the default branch adds the pull request to the [merge queue](/guides/merge-queue/), with these settings. On other branches it merges directly. | | |
| 142 | − | | Require deployments to succeed | `required_deployments` | A pull request's head must have deployed successfully to these environments. | | |
| 142 | + | | Require deployments to succeed | `required_deployments` | A pull request's head must have deployed successfully to these environments: on g1t.page, or anywhere it was reported. | | |
| 143 | 143 | ||
| 144 | 144 | `pull_request` parameters: | |
| 145 | 145 | ||
| ⋯ | |||
| 173 | 173 | ||
| 174 | 174 | `required_deployments` takes `environments`. `preview` is a pull request's | |
| 175 | 175 | [preview deployment](/guides/deployments/). A project's slug is that | |
| 176 | − | project's deployment, when a repository has several. | |
| 176 | + | project's deployment, when a repository has several. Any other name is an | |
| 177 | + | environment [deployments are reported to](/guides/deployments-api/), from | |
| 178 | + | any CI or by a g1t Actions job with an `environment:`: a successful | |
| 179 | + | `deploy / <environment>` check on the head meets it, such as | |
| 180 | + | `deploy / staging` for `staging`. Names are matched without regard to case. | |
| 177 | 181 | ||
| 178 | 182 | ### Commits | |
| 179 | 183 | ||
| 94 | 94 | | `checks.completed` | A pull request's checks finished: every status on its head has reported and none is still pending, or the merge queue took it out. `data.number`, `data.commit`, and `data.status`, `passed` or `failed`. | | |
| 95 | 95 | | `review.completed` | g1t reviewed a pull request. `data.verdict`. | | |
| 96 | 96 | | `workflow.completed` | A [workflow](/guides/actions/) run finished. `data.workflow`, `data.conclusion`, `data.run_id`, `data.sha`, `data.pull`. | | |
| 97 | − | | `deployment.succeeded`, `deployment.failed` | A build of a [project](/guides/deployments/) finished, for production or a pull request's preview. `data.deployment_id`, `data.project`, `data.kind` (`production` or `preview`), `data.number` for a preview, `data.commit`, `data.path`, `data.error` on failure, and `data.recovered` when a success follows a failure. | | |
| 97 | + | | `deployment.succeeded`, `deployment.failed` | A g1t.page build of a [project](/guides/deployments/) finished, for production or a pull request's preview. `data.deployment_id`, `data.project`, `data.kind` (`production` or `preview`), `data.number` for a preview, `data.commit`, `data.path`, `data.error` on failure, and `data.recovered` when a success follows a failure. | | |
| 98 | + | | `deployment.created` | A deployment was made, from any source: reported through the [API](/guides/deployments-api/), made by a g1t Actions job with an `environment:`, or a g1t.page build. `data.repo_id` and `data.deployment`, without its `payload`. | | |
| 99 | + | | `deployment_status.created` | A deployment got a status, from any source. `data.repo_id`, `data.deployment` without its `payload`, and `data.deployment_status`. | | |
| 98 | 100 | | `queue.changed` | The merge queue gained, lost or settled an entry. | | |
| 99 | 101 | | `session.appended` | An agent's session grew. Busy: choose it only if you need it. | | |
| 100 | 102 | | `agent.asked` | An agent asked the agent on another pull request a question, or handed it work, while that one was not at work; g1t wakes it to answer. | |
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.