Skip to content

Commit

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.

syntaqxcommitted Parent73e9cacBrowse files
53 files+1205−140/53 viewed
+24−5
88 # core, edge, front the units of each stage, in jobs that share a build;
99 # a stage starts only when the one before it succeeded
1010 #
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+#
1119 # Needs the repository secret CLOUDFLARE_API_TOKEN (a Production row), the
1220 # variable CLOUDFLARE_ACCOUNT_ID, and api.cloudflare.com among the project's
1321 # workflow-only domains for deploy.yml in production (Settings, Guardrails),
6169 name: Plan
6270 needs: check
6371 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
6576 timeout-minutes: 15
6677 outputs:
6778 migrate: ${{ steps.plan.outputs.migrate }}
96107 needs: plan
97108 if: ${{ needs.plan.outputs.migrate == 'true' && inputs.dry_run != true }}
98109 runs-on: ubuntu-latest
99− environment: production
110+ environment:
111+ name: production
112+ url: https://g1t.sh
100113 timeout-minutes: 20
101114 steps:
102115 - uses: actions/checkout@v5
115128 if: ${{ !failure() && !cancelled() && needs.plan.outputs.has_core == 'true' && inputs.dry_run != true }}
116129 # Rust builds get 4 vCPUs; everything else the standard machine.
117130 runs-on: ${{ matrix.rust && 'g1t-4core' || 'ubuntu-latest' }}
118− environment: production
131+ environment:
132+ name: production
133+ url: https://g1t.sh
119134 timeout-minutes: 60
120135 strategy:
121136 # A deploy cut off halfway is worse than one that finishes: the other
182197 needs: [plan, migrate, core]
183198 if: ${{ !failure() && !cancelled() && needs.plan.outputs.has_edge == 'true' && inputs.dry_run != true }}
184199 runs-on: ${{ matrix.rust && 'g1t-4core' || 'ubuntu-latest' }}
185− environment: production
200+ environment:
201+ name: production
202+ url: https://g1t.sh
186203 timeout-minutes: 60
187204 strategy:
188205 fail-fast: false
195212 needs: [plan, migrate, core, edge]
196213 if: ${{ !failure() && !cancelled() && needs.plan.outputs.has_front == 'true' && inputs.dry_run != true }}
197214 runs-on: ${{ matrix.rust && 'g1t-4core' || 'ubuntu-latest' }}
198− environment: production
215+ environment:
216+ name: production
217+ url: https://g1t.sh
199218 timeout-minutes: 60
200219 strategy:
201220 fail-fast: false
+297−0
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+}
+1−0
99 mod audit;
1010 mod billing;
1111 mod blobs;
12+mod deployments;
1213 mod mcp;
1314 mod notifications;
1415 mod oauth;
+15−0
77 use g1t_contracts::scopes::scope_for;
88 use serde_json::{Map, Value, json};
99
10+use crate::deployments::DeploymentsOp;
1011 use crate::operations::Op;
1112 use crate::rules::RulesOp;
1213 use crate::security::SecurityOp;
314315 ],
315316 ),
316317 (
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+ (
317331 "Secrets and variables",
318332 "Values that workflows and deployments read, per repository or for a whole workspace, with a row per environment.",
319333 &[
557571 Op::GetCodeownersErrors => "List CODEOWNERS errors",
558572 Op::Security(op) => op.title(),
559573 Op::Rules(op) => op.title(),
574+ Op::Deployments(op) => op.title(),
560575 }
561576 }
562577
+25−1
2626 };
2727
2828 use crate::alerts::{AlertKind, SecurityAlert};
29+use crate::deployments::DeploymentsOp;
2930 use crate::rules::RulesOp;
3031 use crate::security::SecurityOp;
3132 use g1t_contracts::inbox::{Reason, Severity, WATCH_EVENTS, WatchLevel};
5556 pub security: Fetcher,
5657 /// Projects: a person's pinned ones.
5758 pub projects: Fetcher,
59+ /// Deployments wherever they run, and environments.
60+ pub deployments: Fetcher,
5861 /// Where the request came in, for its audit entries.
5962 pub audit: crate::audit::AuditContext,
6063 /// Set for a request made with an agent's token: all it may do.
7982 search: env.service("SEARCH")?,
8083 security: env.service("SECURITY")?,
8184 projects: env.service("PROJECTS")?,
85+ deployments: env.service("DEPLOYMENTS")?,
8286 scope: None,
8387 audit: crate::audit::AuditContext::default(),
8488 addresses: crate::addresses::Addresses::from_env(env),
267271 Security(SecurityOp),
268272 /// Rulesets: rules.rs.
269273 Rules(RulesOp),
274+ /// Deployments wherever they run, and environments: deployments.rs.
275+ Deployments(DeploymentsOp),
270276 }
271277
272278 fn failed(code: FailureCode, message: &str) -> Result<Outcome<Value>> {
629635 }
630636
631637 impl Op {
632− pub const ALL: [Op; 219] = [
638+ pub const ALL: [Op; 226] = [
633639 Op::Whoami,
634640 Op::GetWorkspace,
635641 Op::CreateWorkspace,
849855 Op::Rules(RulesOp::UpdateWorkspaceRuleset),
850856 Op::Rules(RulesOp::DeleteWorkspaceRuleset),
851857 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),
852865 ];
853866
854867 pub fn by_name(name: &str) -> Option<Op> {
10351048 Op::GetCodeownersErrors => "get_codeowners_errors",
10361049 Op::Security(op) => op.name(),
10371050 Op::Rules(op) => op.name(),
1051+ Op::Deployments(op) => op.name(),
10381052 }
10391053 }
10401054
15331547 }
15341548 Op::Security(op) => op.description(),
15351549 Op::Rules(op) => op.description(),
1550+ Op::Deployments(op) => op.description(),
15361551 }
15371552 }
15381553
28432858 ),
28442859 Op::Security(op) => op.input(),
28452860 Op::Rules(op) => op.input(),
2861+ Op::Deployments(op) => op.input(),
28462862 }
28472863 }
28482864
28692885 | Op::GetMergeQueue
28702886 | Op::GetCodeownersErrors
28712887 | 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+ )
28722895 )
28732896 }
28742897
48484871 // each answer its public shape.
48494872 Op::Security(op) => crate::security::run(op, services, viewer, input).await,
48504873 Op::Rules(op) => crate::rules::run(op, services, viewer, input).await,
4874+ Op::Deployments(op) => crate::deployments::run(op, services, viewer, input).await,
48514875 Op::ReopenSecurityAlert => {
48524876 let changed: Outcome<AlertChange> = call(
48534877 &services.security,
+356−0
92789278 ]
92799279 }
92809280 }
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+ }
92819637 }
92829638 }
+2−0
109109 }
110110 // Built by the API itself.
111111 Op::Rules(RulesOp::DeleteRepoRuleset | RulesOp::DeleteWorkspaceRuleset) => return as_is,
112+ // Deployments travel in `snake_case` between services too.
113+ Op::Deployments(_) => return as_is,
112114 // Built by the API itself, in `snake_case`.
113115 Op::ListSecurityAlerts => return through::<Vec<crate::alerts::SecurityAlert>>(op, as_is),
114116 Op::DismissSecurityAlert | Op::ReopenSecurityAlert => {
+18−0
22
33 use serde_json::{Map, Value};
44
5+use crate::deployments::DeploymentsOp;
56 use crate::operations::Op;
67 use crate::rules::RulesOp;
78 use crate::security::SecurityOp;
262263 Op::Rules(RulesOp::ListWorkspaceRuleEvaluations),
263264 &[("ruleset_id", "ruleset_id"), ("verdict", "verdict"), ("problems_only", "problems_only"), ("before", "before"), ("limit", "limit")],
264265 ),
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), &[]),
265279 route("GET", "/repos/:owner/:name/queue", Op::GetMergeQueue, &[]),
266280 route(
267281 "POST",
861875 if let Some(branch) = param("branch") {
862876 input.insert("branch".to_owned(), Value::String(percent_decoded(branch)));
863877 }
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+ }
864882 if let Some(label) = param("label") {
865883 input.insert("label".to_owned(), Value::String(percent_decoded(label)));
866884 }
+9−1
1818 use g1t_contracts::scopes::{Level, NO_SCOPE, TokenAccess, scope_for};
1919 use serde_json::{Map, Value, json};
2020
21+use crate::deployments::DeploymentsOp;
2122 use crate::operations::Op;
2223 use crate::rules::RulesOp;
2324 use crate::security::SecurityOp;
182183 Tool {
183184 name: "workflow",
184185 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.",
186187 default_action: None,
187188 actions: &[
188189 a("list", Op::ListWorkflows, "Workflows on the default branch"),
193194 a("cancel", Op::CancelWorkflowRun, "Cancel a run"),
194195 a("rerun", Op::RerunWorkflowRun, "Run a finished run again"),
195196 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"),
196204 a("list_runners", Op::ListRunners, "Self-hosted runners, with status, labels and what each is doing"),
197205 a("create_runner_token", Op::CreateRunnerRegistrationToken, "A one-hour token for g1t-runner register"),
198206 a("remove_runner", Op::RemoveRunner, "Remove a self-hosted runner"),
+1−0
7474 items: [
7575 { label: 'Projects', slug: 'guides/projects' },
7676 { label: 'Deployments', slug: 'guides/deployments' },
77+ { label: 'Deployments API', slug: 'guides/deployments-api' },
7778 { label: 'Packages', slug: 'guides/packages' },
7879 { label: 'Container images', slug: 'guides/containers' },
7980 { label: 'npm', slug: 'guides/npm' },
+7−3
4242 | `GITHUB_OUTPUT`, `GITHUB_ENV`, `GITHUB_PATH`, `GITHUB_STATE`, `GITHUB_STEP_SUMMARY` | The same. |
4343 | `::error::`, `::warning::`, `::notice::`, `::group::`, `::add-mask::` | The same: errors and warnings become annotations on the run. |
4444 | `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. |
4646 | `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. |
4747 | `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). |
4848
6666 - **Environments' protection rules** (required reviewers, wait timers,
6767 branch limits). A job with `environment:` gets that environment's
6868 [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`.
7072
7173 Why each of these is missing, and what to use instead, is on
7274 [What g1t can't do yet](/about/limitations/#actions-and-runners).
264266 Secrets are read as `${{ secrets.KEY }}` and config as `${{ vars.KEY }}`,
265267 from the rows under **Settings → Secrets and variables** that are
266268 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
268272 [Secrets and variables](/guides/secrets-and-variables/) for how rows,
269273 environments and the workspace's rows work.
270274
+4−1
321321 | Issues & pull requests | `issues:read`, `issues:write`, `pull_requests:read`, `pull_requests:write` |
322322 | Agents | `agents:run` |
323323 | Workflows | `workflows:read`, `workflows:write` |
324+| Deployments | `deployments:read`, `deployments:write` |
324325 | Memory & search | `memory:read`, `memory:write` |
325326 | Account | `account:read`, `account:write` |
326327 | Notifications | `notifications:read`, `notifications:write` |
354355 | `agents:run` | Put g1t to work and message it, which uses the workspace's money |
355356 | `workflows:read` | Read workflows, runs and logs |
356357 | `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 |
357360 | `memory:read` | Recall memory and search the workspace's context |
358361 | `memory:write` | Save memory for the next agent |
359362 | `account:read` | Read your email addresses, invites, invitations and pinned projects |
411414 | --- | --- |
412415 | Read only | Every `read` scope. Changes nothing. |
413416 | 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. |
415418 | Full access | Everything you can do, including deleting repositories and changing who has access. Marked **Dangerous**. |
416419
417420 Admin scopes change things that are hard to undo, or decide who can reach
+431−0
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.
+6−0
4141 is built again from the new default branch. An
4242 [archived](/guides/managing-repositories/) repository's apps keep serving.
4343
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+
4450 An app runs only while it answers a request. One nobody visits runs
4551 nothing and costs nothing, and the next visit wakes it in milliseconds.
4652
+6−2
139139 | 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. |
140140 | Require status checks to pass | `required_status_checks` | These checks must pass on a pull request's head before it merges. |
141141 | 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. |
143143
144144 `pull_request` parameters:
145145
173173
174174 `required_deployments` takes `environments`. `preview` is a pull request's
175175 [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.
177181
178182 ### Commits
179183
+3−1
9494 | `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`. |
9595 | `review.completed` | g1t reviewed a pull request. `data.verdict`. |
9696 | `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`. |
98100 | `queue.changed` | The merge queue gained, lost or settled an entry. |
99101 | `session.appended` | An agent's session grew. Busy: choose it only if you need it. |
100102 | `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. |
+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

This change is too large to show in full.