Skip to content

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

g1t.sh

Commit

Merge main into open-signups: the mirroring, org chart, get-started and boards removals and the runner changes, under open sign-ups. The API's operation count and its OpenAPI document are taken from both.

syntaqxcommitted Parents8f63c2e14f96e9Browse files
222 files+63−852206 files not counted0/222 viewed
+3−1
1818 # api.expo.dev | mobile-build.yml, mobile-release.yml
1919 # expo.dev | mobile-build.yml, mobile-release.yml
2020 # storage.googleapis.com | mobile-build.yml, mobile-release.yml
21+# *.eascdn.net | mobile-build.yml, mobile-release.yml
2122 #
22−# (the API, its artifacts, and where EAS uploads the project's archive).
23+# (the API, its artifact links, where EAS uploads the project's archive,
24+# and where an artifact link sends the APK's download).
2325 name: Mobile build
2426
2527 on:
+4−0
44 fetch-retries=5
55 fetch-retry-mintimeout=5000
66 fetch-retry-maxtimeout=60000
7+# Fewer connections at once than npm's 15: on g1t's cloud runners each one
8+# passes through the runner Worker, and bursts of parallel tarball
9+# downloads are when a body broke off midway.
10+maxsockets=8
+1−1
11211121
11221122 [[package]]
11231123 name = "g1t-runner"
1124−version = "0.3.1"
1124+version = "0.3.2"
11251125 dependencies = [
11261126 "anyhow",
11271127 "base64 0.22.1",
+2−2
3737 | Chat: channels, direct messages and threads, live, with people and agents as members | Live |
3838 | Notifications: mentions, reviews, approvals and alerts, with browser push | Live |
3939 | People and teams: members, roles, nested teams, invites | Live |
40−| A directory of people and agents, profiles, org chart, teams with agent members | Coming |
40+| A directory of people and agents, profiles, teams with agent members | Coming |
4141 | Artifacts: documents written together, live, by people and agents | Live |
4242 | Slides, designs and dashboards as artifacts | Coming |
4343 | Scratchpads: tools, reports and agents anyone can build on a copy of the data, promoted after review | Coming |
7777 | Packages: npm, Cargo, Composer, Maven, NuGet, RubyGems, Go modules, container images | Live |
7878 | Secret push protection, history scanning, dependency upgrade pull requests | Live |
7979 | A preview of every pull request on `g1t.page`, production on merge, custom domains | Live |
80−| GitHub import, mirroring and pushing back | Live |
80+| GitHub import: every branch and tag, and issues | Live |
8181 | A copy of the data for every preview, instant rollback, container apps kept warm | Coming |
8282 | Git over SSH (waits on inbound TCP on Workers; use HTTPS) | Coming |
8383
+7−3
1919 mod deploy_keys;
2020 mod limits;
2121 mod logs;
22−mod mirrors;
2322 mod mcp;
2423 mod notifications;
2524 mod oauth;
769768 _ => (rest.to_owned(), "job_report"),
770769 };
771770 let body = json_body(&mut request).await;
771+ // `rotate`: a harness that takes a new token with the spec and
772+ // lets go of the one it asked with (plan.rs `job_spec`).
773+ let rotate = method == "job_spec" && body["rotate"].as_bool() == Some(true);
772774 let answered: Outcome<Value> = g1t_kit::call(
773775 &services.actions,
774776 method,
775− &json!({ "job": job, "token": body["token"], "report": body["report"] }),
777+ &json!({ "job": job, "token": body["token"], "report": body["report"], "rotate": rotate }),
776778 )
777779 .await?;
778780 return match answered {
942944 #[test]
943945 fn a_job_spec_gets_the_toolkits_variables() {
944946 // As the actions service sends it, converted as the API does.
945− let sent = json!({ "variables": { "GITHUB_SHA": "abc" }, "runtime": { "token": "h.p.s", "idToken": true } });
947+ let sent = json!({ "variables": { "GITHUB_SHA": "abc" }, "runtime": { "token": "h.p.s", "idToken": true }, "nextToken": "n" });
946948 let mut spec = g1t_kit::wire::snake_case_keeping(sent.clone(), super::JOB_SPEC_AS_GIVEN);
947949 super::with_runtime(&mut spec, "https://api.g1t.sh", true);
948950 assert!(spec.get("runtime").is_none(), "the runner never sees it");
951+ // The token that replaces the one the harness asked with.
952+ assert_eq!(spec["next_token"], "n");
949953 let vars = &spec["variables"];
950954 assert_eq!(vars["GITHUB_SHA"], "abc");
951955 assert_eq!(vars["ACTIONS_RUNTIME_TOKEN"], "h.p.s");
+0−376
1−//! Mirroring over REST and MCP: a repository's links to copies of it on
2−//! other hosts (see `g1t_contracts::mirrors`), at
3−//! `/repos/{owner}/{name}/mirror…`, and as the MCP `mirror` tool.
4−//!
5−//! The integrations service keeps the links and decides who may change
6−//! them: the Admin role on the repository; syncing needs push. Agents never
7−//! move a repository to g1t or handle a remote's token.
8−
9−use g1t_contracts::mirrors::{
10− MirrorActArgs, MirrorAddArgs, MirrorCiArgs, MirrorHandBackArgs, MirrorMoveInArgs, MirrorRemoveArgs, MirrorSettings,
11− MirrorSettingsArgs, MirrorViewArgs, RefDecision, RemoteProvider, RemoteRole,
12−};
13−use g1t_contracts::repos::{GetArgs, Repo};
14−use g1t_contracts::{FailureCode, Outcome, Viewer};
15−use serde_json::{Value, json};
16−use std::collections::BTreeMap;
17−use worker::Result;
18−
19−use crate::operations::{Services, repo_path};
20−
21−/// One operation on a repository's mirroring.
22−#[derive(Clone, Copy, Debug, PartialEq, Eq)]
23−pub enum MirrorsOp {
24− GetMirror,
25− GetHandBackPlan,
26− TakeOver,
27− SetCiFailover,
28− HandBack,
29− MoveToG1t,
30− SyncMirror,
31− AddRemote,
32− UpdateRemote,
33− RemoveRemote,
34−}
35−
36−impl MirrorsOp {
37− /// Every one: `Op::ALL` lists each as `Op::Mirrors(…)`, which a test
38− /// checks against this.
39− #[cfg(test)]
40− pub const ALL: [MirrorsOp; 10] = [
41− MirrorsOp::GetMirror,
42− MirrorsOp::GetHandBackPlan,
43− MirrorsOp::TakeOver,
44− MirrorsOp::SetCiFailover,
45− MirrorsOp::HandBack,
46− MirrorsOp::MoveToG1t,
47− MirrorsOp::SyncMirror,
48− MirrorsOp::AddRemote,
49− MirrorsOp::UpdateRemote,
50− MirrorsOp::RemoveRemote,
51− ];
52−
53− pub fn name(self) -> &'static str {
54− match self {
55− MirrorsOp::GetMirror => "get_mirror",
56− MirrorsOp::GetHandBackPlan => "get_hand_back_plan",
57− MirrorsOp::TakeOver => "take_over_mirror",
58− MirrorsOp::SetCiFailover => "set_ci_failover",
59− MirrorsOp::HandBack => "hand_back_mirror",
60− MirrorsOp::MoveToG1t => "move_mirror_to_g1t",
61− MirrorsOp::SyncMirror => "sync_mirror",
62− MirrorsOp::AddRemote => "add_mirror_remote",
63− MirrorsOp::UpdateRemote => "update_mirror_remote",
64− MirrorsOp::RemoveRemote => "remove_mirror_remote",
65− }
66− }
67−
68− /// For the API reference.
69− pub fn title(self) -> &'static str {
70− match self {
71− MirrorsOp::GetMirror => "Get a repository's mirroring",
72− MirrorsOp::GetHandBackPlan => "Get the hand-back plan",
73− MirrorsOp::TakeOver => "Take over a mirror",
74− MirrorsOp::SetCiFailover => "Start or end CI failover",
75− MirrorsOp::HandBack => "Hand a takeover back",
76− MirrorsOp::MoveToG1t => "Move a mirror to g1t",
77− MirrorsOp::SyncMirror => "Sync a repository's remotes",
78− MirrorsOp::AddRemote => "Add a remote",
79− MirrorsOp::UpdateRemote => "Update a remote's settings",
80− MirrorsOp::RemoveRemote => "Remove a remote",
81− }
82− }
83−
84− pub fn description(self) -> &'static str {
85− match self {
86− MirrorsOp::GetMirror => "A repository's links to other hosts. `remotes` lists each with its `role` (`leader`: the remote leads and this repository is its mirror; `follower`: g1t leads and the remote is kept in step), `provider` (`github`, `g1t` or `git`), `name` (`github.com/acme/web`), `state` (a mirror is `standby`, `ci`, `takeover` or `handing_back`; a follower is `following` or `stuck`), `reachable` and `unreachable_since`, `synced_at`, `last_error`, `checks_problem` (why g1t could not report checks to the remote, such as the GitHub App lacking the Commit statuses permission; null once a report goes through) and its `settings`. `can_manage` says whether you may change them. A mirror standing by is read-only on g1t and runs nothing. Needs read access.",
87− MirrorsOp::GetHandBackPlan => "What handing a takeover back would do, ref by ref, in `plan.refs`: each with `base` (where it was when the takeover began), `ours`, `theirs` and `action`: `push` (only g1t moved), `fetch` (only the remote moved), `pull_request` (the remote protects the branch: g1t's commits go to `g1t/handback/<branch>` there as a pull request, and g1t follows the remote's branch) or `diverged` (both moved: it needs a decision). `plan.reachable` is false while the remote does not answer; `plan.ready` once it answers and every diverged ref has a decision. Asks the remote, so it takes a moment. Needs the Admin role.",
88− MirrorsOp::TakeOver => "Take over a mirror: g1t leads it for now, so pushes, pull requests, issues, agents and workflows work on g1t, whether or not the remote is answering. Each ref's commit is recorded as where the takeover began. Workflows that deploy wait for approval, unless the link's settings say otherwise. End it with hand_back_mirror, or keep it with move_mirror_to_g1t. Needs the Admin role.",
89− MirrorsOp::SetCiFailover => "Start (`on: true`) or end (`on: false`) CI failover on a mirror standing by: the remote keeps the code, and g1t runs its workflows, `.github/workflows` as well as `.g1t/workflows`, on each push copied in. Workflows that deploy wait for approval unless the link's settings say otherwise. Needs the Admin role.",
90− MirrorsOp::HandBack => "Hand a takeover back to the remote, as get_hand_back_plan describes, with `decisions` for diverged refs: an object from ref (`refs/heads/docs`) to `keep_ours` (g1t's commit is pushed over the remote's), `keep_theirs` (the remote's is taken; g1t's is kept under `refs/g1t/replaced/`) or `pull_request`. Refused while the remote does not answer or a diverged ref has no decision. The repository is read-only while it goes back; if anything is refused, g1t keeps the lead and says why. On success the mirror stands by again, and `notes` lists the pull requests opened. Needs the Admin role.",
91− MirrorsOp::MoveToG1t => "Move a mirror to g1t for good: it stops being a mirror and g1t no longer tracks the remote, so pushes made there no longer come here. Allowed while it stands by, in CI failover, or during a takeover, whose work stays as it is. With `keep_remote_updated: true` the remote becomes a follower instead of being unlinked, and g1t pushes to it from then on. Needs the Admin role; agents' tokens are refused.",
92− MirrorsOp::SyncMirror => "Bring a repository's remotes in step now: a mirror standing by or in CI failover copies the remote's branches and tags in; each follower is pushed g1t's (which also clears a follower that was stuck by pushing over what changed there). Needs push access.",
93− MirrorsOp::AddRemote => "Link a repository to a remote on another g1t or any git host over HTTPS (GitHub repositories are linked by importing them through g1t's GitHub App). `role` `follower`: g1t leads and pushes to it. `role` `leader`: this repository becomes its mirror; only an empty repository can, and it is filled from the remote. `url` is its https clone address, `token` a token that can read and push (kept sealed, never returned), `username` the user it is sent as. Needs the Admin role; agents' tokens are refused.",
94− MirrorsOp::UpdateRemote => "Change a remote's `settings`, all optional: `notify` (`banner`, or `inbox` to also tell the workspace's owners), `take_over_after` (minutes, 5 to 1440, after which g1t takes over on its own while the remote does not answer; null for never), `hand_back` (`when_clean`: an automatic takeover goes back on its own once every ref goes back without a decision; `ask`), `keep_ci_warm` (run `.g1t/workflows` on pushes copied in while standing by), `github_workflows` (run `.github/workflows` in CI failover and takeovers), `hold_deploys`, `report_checks` (a GitHub mirror: report the statuses g1t sets on a commit, such as workflow runs and deploys, to the commit on GitHub as `g1t / <context>` while g1t runs its workflows; on by default), and for a follower `remote_pushes` (`adopt` fast-forwards made there, or `overwrite` them). Needs the Admin role; agents' tokens are refused.",
95− MirrorsOp::RemoveRemote => "Unlink a remote. A mirror becomes an ordinary repository with what it has; a follower is no longer pushed to. Refused during a takeover: hand it back or move it to g1t first. Needs the Admin role; agents' tokens are refused.",
96− }
97− }
98−
99− pub fn input(self) -> Value {
100− let repo = json!({ "type": "string", "description": "Repository as \"owner/name\", e.g. \"flagon-io/hello\"." });
101− let id = json!({ "type": "string", "description": "The remote's id (rmt_…), from get_mirror." });
102− let (properties, required): (Value, &[&str]) = match self {
103− MirrorsOp::GetMirror | MirrorsOp::GetHandBackPlan | MirrorsOp::TakeOver | MirrorsOp::SyncMirror => {
104− (json!({ "repo": repo }), &["repo"])
105− }
106− MirrorsOp::SetCiFailover => (
107− json!({ "repo": repo, "on": { "type": "boolean", "description": "True to start CI failover, false to end it." } }),
108− &["repo", "on"],
109− ),
110− MirrorsOp::HandBack => (
111− json!({
112− "repo": repo,
113− "decisions": {
114− "type": "object",
115− "description": "For each diverged ref, by its full name: keep_ours, keep_theirs or pull_request.",
116− "additionalProperties": { "type": "string", "enum": ["keep_ours", "keep_theirs", "pull_request"] },
117− },
118− }),
119− &["repo"],
120− ),
121− MirrorsOp::MoveToG1t => (
122− json!({
123− "repo": repo,
124− "keep_remote_updated": { "type": "boolean", "description": "True to keep pushing to the remote from g1t; false (the default) to stop tracking it." },
125− }),
126− &["repo"],
127− ),
128− MirrorsOp::AddRemote => (
129− json!({
130− "repo": repo,
131− "provider": { "type": "string", "enum": ["g1t", "git"], "description": "Another g1t (g1t.sh or your own), or any git host." },
132− "role": { "type": "string", "enum": ["follower", "leader"], "description": "follower: g1t leads and pushes to it. leader: this empty repository becomes its mirror." },
133− "url": { "type": "string", "description": "Its https clone address, such as https://g1t.sh/acme/web.git." },
134− "username": { "type": "string", "description": "The user the token is sent as. x-access-token when left out." },
135− "token": { "type": "string", "description": "A token that can read and push. Kept sealed; never returned." },
136− }),
137− &["repo", "provider", "role", "url", "token"],
138− ),
139− MirrorsOp::UpdateRemote => (
140− json!({
141− "repo": repo,
142− "id": id,
143− "notify": { "type": "string", "enum": ["banner", "inbox"] },
144− "take_over_after": { "type": ["integer", "null"], "minimum": 5, "maximum": 1440 },
145− "hand_back": { "type": "string", "enum": ["when_clean", "ask"] },
146− "keep_ci_warm": { "type": "boolean" },
147− "github_workflows": { "type": "boolean" },
148− "hold_deploys": { "type": "boolean" },
149− "report_checks": { "type": "boolean" },
150− "remote_pushes": { "type": "string", "enum": ["adopt", "overwrite"] },
151− }),
152− &["repo", "id"],
153− ),
154− MirrorsOp::RemoveRemote => (json!({ "repo": repo, "id": id }), &["repo", "id"]),
155− };
156− json!({ "type": "object", "properties": properties, "required": required })
157− }
158−}
159−
160−fn text(input: &Value, key: &str) -> String {
161− input[key].as_str().map(str::trim).unwrap_or_default().to_owned()
162−}
163−
164−/// A boolean as sent: JSON, or a word from a form.
165−fn flag(value: &Value) -> Option<bool> {
166− match value {
167− Value::Bool(flag) => Some(*flag),
168− Value::String(word) => match word.trim().to_ascii_lowercase().as_str() {
169− "true" | "1" => Some(true),
170− "false" | "0" => Some(false),
171− _ => None,
172− },
173− _ => None,
174− }
175−}
176−
177−/// `settings` with what `input` changes, in `snake_case` as sent.
178−pub fn settings_from(mut settings: MirrorSettings, input: &Value) -> std::result::Result<MirrorSettings, String> {
179− let word = |key: &str| input.get(key).filter(|value| !value.is_null()).map(|value| value.as_str().unwrap_or_default());
180− if let Some(notify) = word("notify") {
181− settings.notify = serde_json::from_value(json!(notify)).map_err(|_| "notify is banner or inbox.")?;
182− }
183− if let Some(value) = input.get("take_over_after") {
184− settings.take_over_after = match value {
185− Value::Null => None,
186− value => Some(value.as_u64().map(|minutes| minutes as u32).ok_or("take_over_after is a number of minutes, or null.")?),
187− };
188− }
189− if let Some(hand_back) = word("hand_back") {
190− settings.hand_back = serde_json::from_value(json!(hand_back)).map_err(|_| "hand_back is when_clean or ask.")?;
191− }
192− if let Some(remote_pushes) = word("remote_pushes") {
193− settings.remote_pushes = serde_json::from_value(json!(remote_pushes)).map_err(|_| "remote_pushes is adopt or overwrite.")?;
194− }
195− for (key, field) in [
196− ("keep_ci_warm", &mut settings.keep_ci_warm),
197− ("github_workflows", &mut settings.github_workflows),
198− ("hold_deploys", &mut settings.hold_deploys),
199− ("report_checks", &mut settings.report_checks),
200− ] {
201− if let Some(value) = input.get(key).filter(|value| !value.is_null()) {
202− *field = flag(value).ok_or_else(|| format!("{key} is true or false."))?;
203− }
204− }
205− Ok(settings)
206−}
207−
208−/// `decisions` as sent: ref to `keep_ours`, `keep_theirs` or `pull_request`.
209−pub fn decisions_from(input: &Value) -> std::result::Result<BTreeMap<String, RefDecision>, String> {
210− let Some(decisions) = input.get("decisions").filter(|value| !value.is_null()) else {
211− return Ok(BTreeMap::new());
212− };
213− let Some(decisions) = decisions.as_object() else {
214− return Err("decisions is an object from ref to keep_ours, keep_theirs or pull_request.".to_owned());
215− };
216− decisions
217− .iter()
218− .map(|(git_ref, decision)| {
219− let decision = serde_json::from_value(decision.clone())
220− .map_err(|_| format!("{git_ref}: say keep_ours, keep_theirs or pull_request."))?;
221− let git_ref = if git_ref.starts_with("refs/") { git_ref.clone() } else { format!("refs/heads/{git_ref}") };
222− Ok((git_ref, decision))
223− })
224− .collect()
225−}
226−
227−pub async fn run(op: MirrorsOp, services: &Services, viewer: &Viewer, input: &Value) -> Result<Outcome<Value>> {
228− let Some(path) = repo_path(input) else {
229− return Ok(Outcome::fail(FailureCode::Invalid, "Give the repository as \"owner/name\"."));
230− };
231− let found: Outcome<Repo> = g1t_kit::call(&services.repos, "get", &GetArgs { path, viewer: viewer.clone() }).await?;
232− let repo = match found {
233− Outcome::Ok(repo) => repo,
234− Outcome::Fail(failure) => return Ok(Outcome::Fail(failure)),
235− };
236− let integrations = &services.integrations;
237− if op == MirrorsOp::GetMirror {
238− return g1t_kit::call(integrations, "mirror_view", &MirrorViewArgs { viewer: viewer.clone(), repo_id: repo.id }).await;
239− }
240− let Some(actor) = viewer.clone() else {
241− return Ok(Outcome::fail(FailureCode::Unauthenticated, "Sign in to change a repository's mirroring."));
242− };
243− let act = || MirrorActArgs { actor: actor.clone(), repo_id: repo.id.clone() };
244− let id = text(input, "id");
245− if matches!(op, MirrorsOp::UpdateRemote | MirrorsOp::RemoveRemote) && id.is_empty() {
246− return Ok(Outcome::fail(FailureCode::Invalid, "Name the remote by its id (rmt_…), from get_mirror."));
247− }
248− match op {
249− MirrorsOp::GetMirror => unreachable!("answered above"),
250− MirrorsOp::GetHandBackPlan => g1t_kit::call(integrations, "mirror_hand_back_plan", &act()).await,
251− MirrorsOp::TakeOver => g1t_kit::call(integrations, "mirror_take_over", &act()).await,
252− MirrorsOp::SyncMirror => g1t_kit::call(integrations, "mirror_sync", &act()).await,
253− MirrorsOp::SetCiFailover => {
254− let Some(on) = flag(&input["on"]) else {
255− return Ok(Outcome::fail(FailureCode::Invalid, "Say on: true to start CI failover, or false to end it."));
256− };
257− g1t_kit::call(integrations, "mirror_ci", &MirrorCiArgs { actor, repo_id: repo.id, on }).await
258− }
259− MirrorsOp::HandBack => {
260− let decisions = match decisions_from(input) {
261− Ok(decisions) => decisions,
262− Err(problem) => return Ok(Outcome::fail(FailureCode::Invalid, problem)),
263− };
264− g1t_kit::call(integrations, "mirror_hand_back", &MirrorHandBackArgs { actor, repo_id: repo.id, decisions }).await
265− }
266− MirrorsOp::MoveToG1t => {
267− let keep_remote_updated = match &input["keep_remote_updated"] {
268− Value::Null => false,
269− value => match flag(value) {
270− Some(keep) => keep,
271− None => return Ok(Outcome::fail(FailureCode::Invalid, "keep_remote_updated is true or false.")),
272− },
273− };
274− g1t_kit::call(integrations, "mirror_move_in", &MirrorMoveInArgs { actor, repo_id: repo.id, keep_remote_updated }).await
275− }
276− MirrorsOp::AddRemote => {
277− let provider = match text(input, "provider").as_str() {
278− "g1t" => RemoteProvider::G1t,
279− "git" => RemoteProvider::Git,
280− "github" => {
281− return Ok(Outcome::fail(
282− FailureCode::Invalid,
283− "GitHub repositories are linked by importing them through g1t's GitHub App.",
284− ));
285− }
286− _ => return Ok(Outcome::fail(FailureCode::Invalid, "provider is g1t or git.")),
287− };
288− let role = match text(input, "role").as_str() {
289− "leader" => RemoteRole::Leader,
290− "follower" => RemoteRole::Follower,
291− _ => return Ok(Outcome::fail(FailureCode::Invalid, "role is follower (g1t leads) or leader (the remote leads).")),
292− };
293− let username = Some(text(input, "username")).filter(|name| !name.is_empty());
294− let token = Some(text(input, "token")).filter(|token| !token.is_empty());
295− let args = MirrorAddArgs { actor, repo_id: repo.id, provider, role, url: text(input, "url"), username, token };
296− g1t_kit::call(integrations, "mirror_add", &args).await
297− }
298− MirrorsOp::UpdateRemote => {
299− let view: Outcome<g1t_contracts::mirrors::MirrorView> =
300− g1t_kit::call(integrations, "mirror_view", &MirrorViewArgs { viewer: viewer.clone(), repo_id: repo.id }).await?;
301− let view = match view {
302− Outcome::Ok(view) => view,
303− Outcome::Fail(failure) => return Ok(Outcome::Fail(failure)),
304− };
305− let Some(remote) = view.remotes.into_iter().find(|remote| remote.id == id) else {
306− return Ok(Outcome::fail(FailureCode::NotFound, "That remote is not one of this repository's."));
307− };
308− let settings = match settings_from(remote.settings, input) {
309− Ok(settings) => settings,
310− Err(problem) => return Ok(Outcome::fail(FailureCode::Invalid, problem)),
311− };
312− g1t_kit::call(integrations, "mirror_settings", &MirrorSettingsArgs { actor, remote_id: id, settings }).await
313− }
314− MirrorsOp::RemoveRemote => {
315− let view: Outcome<g1t_contracts::mirrors::MirrorView> =
316− g1t_kit::call(integrations, "mirror_view", &MirrorViewArgs { viewer: viewer.clone(), repo_id: repo.id }).await?;
317− if !matches!(&view, Outcome::Ok(view) if view.remotes.iter().any(|remote| remote.id == id)) {
318− return Ok(Outcome::fail(FailureCode::NotFound, "That remote is not one of this repository's."));
319− }
320− g1t_kit::call(integrations, "mirror_remove", &MirrorRemoveArgs { actor, remote_id: id }).await
321− }
322− }
323−}
324−
325−#[cfg(test)]
326−mod tests {
327− use super::*;
328− use g1t_contracts::credentials::NEVER;
329− use g1t_contracts::mirrors::{HandBack, Notify, RemotePushes};
330− use g1t_contracts::scopes::{Level, scope_for};
331−
332− #[test]
333− fn each_operation_is_described_and_scoped() {
334− for op in MirrorsOp::ALL {
335− assert!(crate::operations::Op::ALL.contains(&crate::operations::Op::Mirrors(op)), "{}", op.name());
336− assert!(!op.title().is_empty() && op.description().len() > 40, "{}", op.name());
337− assert!(op.input()["required"].as_array().unwrap().contains(&json!("repo")), "{}", op.name());
338− let level = scope_for(op.name()).unwrap_or_else(|| panic!("{} has no scope", op.name())).level();
339− let expected = match op {
340− MirrorsOp::GetMirror => Level::Read,
341− MirrorsOp::SyncMirror => Level::Write,
342− _ => Level::Admin,
343− };
344− assert_eq!(level, expected, "{}", op.name());
345− }
346− for op in [MirrorsOp::MoveToG1t, MirrorsOp::AddRemote, MirrorsOp::UpdateRemote, MirrorsOp::RemoveRemote] {
347− assert!(NEVER.contains(&op.name()), "agents never {}", op.name());
348− }
349− }
350−
351− #[test]
352− fn settings_change_only_what_is_sent() {
353− let base = MirrorSettings::default();
354− let changed = settings_from(base.clone(), &json!({ "notify": "inbox", "take_over_after": 30, "keep_ci_warm": "true" })).unwrap();
355− assert_eq!(changed.notify, Notify::Inbox);
356− assert_eq!(changed.take_over_after, Some(30));
357− assert!(changed.keep_ci_warm);
358− assert_eq!(changed.hand_back, HandBack::WhenClean);
359− assert_eq!(changed.remote_pushes, RemotePushes::Adopt);
360− assert!(changed.report_checks, "checks go to GitHub unless turned off");
361− assert!(!settings_from(base.clone(), &json!({ "report_checks": false })).unwrap().report_checks);
362− let cleared = settings_from(changed, &json!({ "take_over_after": null })).unwrap();
363− assert_eq!(cleared.take_over_after, None);
364− assert!(settings_from(base.clone(), &json!({ "notify": "pager" })).is_err());
365− assert!(settings_from(base, &json!({ "hold_deploys": "maybe" })).is_err());
366− }
367−
368− #[test]
369− fn decisions_name_refs_or_branches() {
370− let decisions = decisions_from(&json!({ "decisions": { "docs": "keep_theirs", "refs/tags/v1": "keep_ours" } })).unwrap();
371− assert_eq!(decisions.get("refs/heads/docs"), Some(&RefDecision::KeepTheirs));
372− assert_eq!(decisions.get("refs/tags/v1"), Some(&RefDecision::KeepOurs));
373− assert!(decisions_from(&json!({ "decisions": { "docs": "merge" } })).is_err());
374− assert!(decisions_from(&json!({})).unwrap().is_empty());
375− }
376−}
+0−18
1212 use crate::archives::ArchiveOp;
1313 use crate::customize::CustomizeOp;
1414 use crate::deploy_keys::DeployKeysOp;
15−use crate::mirrors::MirrorsOp;
1615 use crate::deployments::DeploymentsOp;
1716 use crate::telemetry::TelemetryOp;
1817 use crate::packages::PackagesOp;
189188 Op::DeployKeys(DeployKeysOp::GetDeployKey),
190189 Op::DeployKeys(DeployKeysOp::CreateDeployKey),
191190 Op::DeployKeys(DeployKeysOp::DeleteDeployKey),
192− ],
193− ),
194− (
195− "Mirroring",
196− "A repository's links to copies of it on other hosts: a mirror stands by as a read-only copy of a remote that leads, until someone takes over on g1t and later hands back, or moves it to g1t for good. A repository g1t leads can be mirrored to other hosts.",
197− &[
198− Op::Mirrors(MirrorsOp::GetMirror),
199− Op::Mirrors(MirrorsOp::GetHandBackPlan),
200− Op::Mirrors(MirrorsOp::TakeOver),
201− Op::Mirrors(MirrorsOp::SetCiFailover),
202− Op::Mirrors(MirrorsOp::HandBack),
203− Op::Mirrors(MirrorsOp::MoveToG1t),
204− Op::Mirrors(MirrorsOp::SyncMirror),
205− Op::Mirrors(MirrorsOp::AddRemote),
206− Op::Mirrors(MirrorsOp::UpdateRemote),
207− Op::Mirrors(MirrorsOp::RemoveRemote),
208191 ],
209192 ),
210193 (
867850 Op::Artifacts(op) => op.title(),
868851 Op::Archives(op) => op.title(),
869852 Op::DeployKeys(op) => op.title(),
870− Op::Mirrors(op) => op.title(),
871853 Op::Packages(op) => op.title(),
872854 Op::Folios(op) => op.title(),
873855 Op::Customize(op) => op.title(),
+15−23
3232 use crate::archives::ArchiveOp;
3333 use crate::customize::CustomizeOp;
3434 use crate::deploy_keys::DeployKeysOp;
35−use crate::mirrors::MirrorsOp;
3635 use crate::deployments::DeploymentsOp;
3736 use crate::telemetry::TelemetryOp;
3837 use crate::packages::PackagesOp;
340339 Archives(ArchiveOp),
341340 /// A repository's deploy keys: deploy_keys.rs.
342341 DeployKeys(DeployKeysOp),
343− /// A repository's mirroring: its remotes, takeovers and hand-backs:
344− /// mirrors.rs.
345− Mirrors(MirrorsOp),
346342 /// A workspace's packages, their versions, deleting and restoring
347343 /// them, and who may use them: packages.rs.
348344 Packages(PackagesOp),
721717 }
722718
723719 impl Op {
724− pub const ALL: [Op; 405] = [
720+ pub const ALL: [Op; 395] = [
725721 Op::Whoami,
726722 Op::GetWorkspace,
727723 Op::CreateWorkspace,
10591055 Op::Customize(CustomizeOp::GrantConnector),
10601056 Op::Customize(CustomizeOp::RevokeConnector),
10611057 Op::Customize(CustomizeOp::Bundle),
1062− Op::Mirrors(MirrorsOp::GetMirror),
1063− Op::Mirrors(MirrorsOp::GetHandBackPlan),
1064− Op::Mirrors(MirrorsOp::TakeOver),
1065− Op::Mirrors(MirrorsOp::SetCiFailover),
1066− Op::Mirrors(MirrorsOp::HandBack),
1067− Op::Mirrors(MirrorsOp::MoveToG1t),
1068− Op::Mirrors(MirrorsOp::SyncMirror),
1069− Op::Mirrors(MirrorsOp::AddRemote),
1070− Op::Mirrors(MirrorsOp::UpdateRemote),
1071− Op::Mirrors(MirrorsOp::RemoveRemote),
10721058 Op::Protection(ProtectionOp::UpdateEnvironment),
10731059 Op::Protection(ProtectionOp::DeleteEnvironment),
10741060 Op::Protection(ProtectionOp::GetPendingDeployments),
13431329 Op::Artifacts(op) => op.name(),
13441330 Op::Archives(op) => op.name(),
13451331 Op::DeployKeys(op) => op.name(),
1346− Op::Mirrors(op) => op.name(),
13471332 Op::Packages(op) => op.name(),
13481333 Op::Folios(op) => op.name(),
13491334 Op::Customize(op) => op.name(),
16691654 }
16701655 Op::UpdateWorkflow => "Turn a workflow on or off without changing its file. Needs the Maintain role or higher.",
16711656 Op::ListActionsSecrets => {
1672− "The secrets of a repository (with the workspace's rows that reach it) or of a workspace: each row's key, the environments it applies to, and whether workflows (`secrets.NAME`), deployments, or both read it. Values are never returned. A repository's need the Admin role on it; a workspace's, a member."
1657+ "The secrets of a repository (with the workspace's rows that reach it) or of a workspace: each row's key, the environments it applies to, whether workflows (`secrets.NAME`), deployments, or both read it, and for a brokered secret the hosts g1t fills it in for (`brokered_hosts`). Values are never returned. A repository's need the Admin role on it; a workspace's, a member."
16731658 }
16741659 Op::SetActionsSecret => {
1675− "Add or change a secret's row. Without `id` or `environments`, the key's row for every environment, as GitHub's API addresses a secret. `available_to` is workflows and/or deployments (both, for a new row); `environments` limits it to some, such as production or preview, so a key can hold a value per environment. A variable's row can become a secret this way; a secret never becomes a variable. A repository's need the Admin role on it; a workspace's, an owner. Workspace tokens, G1T_TOKEN included, cannot change them."
1660+ "Add or change a secret's row. Without `id` or `environments`, the key's row for every environment, as GitHub's API addresses a secret. `available_to` is workflows and/or deployments (both, for a new row); `environments` limits it to some, such as production or preview, so a key can hold a value per environment. A variable's row can become a secret this way; a secret never becomes a variable. `brokered_hosts` brokers the secret: workflow jobs on g1t's machines get a stand-in in place of its value, and g1t puts the real value on the job's requests to those hosts (in headers, Basic auth included, and the query string) as they leave its sandbox, so the value never enters it. A brokered secret is read by workflows only and its value is at least 8 characters; an empty list stops brokering it. A repository's need the Admin role on it; a workspace's, an owner. Workspace tokens, G1T_TOKEN included, cannot change them."
16761661 }
16771662 Op::DeleteActionsSecret => "Remove a secret: one row by `id`, or every row of the key.",
16781663 Op::ListActionsVariables => {
19151900 Op::Artifacts(op) => op.description(),
19161901 Op::Archives(op) => op.description(),
19171902 Op::DeployKeys(op) => op.description(),
1918− Op::Mirrors(op) => op.description(),
19191903 Op::Packages(op) => op.description(),
19201904 Op::Folios(op) => op.description(),
19211905 Op::Customize(op) => op.description(),
28592843 "description": "A workspace's row: the projects it reaches, by slug. Empty is every one."
28602844 },
28612845 "note": { "type": "string", "description": "Where to rotate it, or who to ask." },
2846+ "brokered_hosts": {
2847+ "type": "array",
2848+ "items": { "type": "string" },
2849+ "description": "A secret's: the hosts g1t fills it in for, such as api.example.com or *.example.com. Jobs get a stand-in in place of its value. Empty stops brokering it; left out, unchanged."
2850+ },
28622851 })),
28632852 &["setting"],
28642853 ),
34173406 Op::Artifacts(op) => op.input(),
34183407 Op::Archives(op) => op.input(),
34193408 Op::DeployKeys(op) => op.input(),
3420− Op::Mirrors(op) => op.input(),
34213409 Op::Packages(op) => op.input(),
34223410 Op::Folios(op) => op.input(),
34233411 Op::Customize(op) => op.input(),
45614549 is_private: input["private"].as_bool() == Some(true),
45624550 import_url: optional_text(input, "import_url"),
45634551 import_token: None,
4564− mirror: None,
45654552 },
45664553 )
45674554 .await
51155102 }
51165103 // Request bodies arrive in snake_case; the actions service
51175104 // takes `availableTo`.
5118− for (key, to) in [("available_to", "availableTo"), ("environments", "environments"), ("repositories", "projects"), ("projects", "projects")] {
5105+ for (key, to) in [
5106+ ("available_to", "availableTo"),
5107+ ("environments", "environments"),
5108+ ("repositories", "projects"),
5109+ ("projects", "projects"),
5110+ ("brokered_hosts", "brokeredHosts"),
5111+ ] {
51195112 if let Some(list) = strings(input, key) {
51205113 args[to] = json!(list);
51215114 }
58075800 Op::Artifacts(op) => crate::run_artifacts::run(op, services, viewer, input).await,
58085801 Op::Archives(op) => crate::archives::run(op, services, viewer, input).await,
58095802 Op::DeployKeys(op) => crate::deploy_keys::run(op, services, viewer, input).await,
5810− Op::Mirrors(op) => crate::mirrors::run(op, services, viewer, input).await,
58115803 Op::Packages(op) => crate::packages::run(op, services, viewer, input).await,
58125804 Op::Folios(op) => crate::folios::run(op, services, viewer, input).await,
58135805 Op::Customize(op) => crate::customize::run(op, services, viewer, input).await,
+21−395
30723072 "environments": [],
30733073 "projects": [],
30743074 "note": null,
3075− "updated_by": "syntaqx"
3075+ "updated_by": "syntaqx",
3076+ "brokered_hosts": [
3077+ "registry.npmjs.org"
3078+ ]
30763079 },
30773080 {
30783081 "id": "set_01kpv9q4r7s0t3v6w9x2y5z8ac",
30903093 ],
30913094 "projects": [],
30923095 "note": "Rotate in the Stripe dashboard.",
3093− "updated_by": "syntaqx"
3096+ "updated_by": "syntaqx",
3097+ "brokered_hosts": []
30943098 }
30953099 ],
3096− "notes": "Values are never returned. A repository's list includes the workspace's rows that reach it, with `scope` set to `workspace`. Empty `environments` means every environment."
3100+ "notes": "Values are never returned. A repository's list includes the workspace's rows that reach it, with `scope` set to `workspace`. Empty `environments` means every environment; empty `brokered_hosts`, a secret that is not brokered."
30973101 },
30983102 "list_actions_secrets_for_workspace": {
30993103 "params": {
31133117 "environments": [],
31143118 "projects": [],
31153119 "note": null,
3116− "updated_by": "syntaqx"
3120+ "updated_by": "syntaqx",
3121+ "brokered_hosts": [
3122+ "registry.npmjs.org"
3123+ ]
31173124 }
31183125 ],
3119− "notes": "Values are never returned. A repository's list includes the workspace's rows that reach it, with `scope` set to `workspace`. Empty `environments` means every environment."
3126+ "notes": "Values are never returned. A repository's list includes the workspace's rows that reach it, with `scope` set to `workspace`. Empty `environments` means every environment; empty `brokered_hosts`, a secret that is not brokered."
31203127 },
31213128 "set_actions_secret": {
31223129 "params": {
31483155 ],
31493156 "projects": [],
31503157 "note": null,
3151− "updated_by": "syntaqx"
3158+ "updated_by": "syntaqx",
3159+ "brokered_hosts": []
31523160 },
31533161 "notes": "Keys are uppercased. See [secrets and variables](/guides/secrets-and-variables/)."
31543162 },
31613169 "value": "npm_…",
31623170 "projects": [
31633171 "hello"
3172+ ],
3173+ "brokered_hosts": [
3174+ "registry.npmjs.org"
31643175 ]
31653176 },
31663177 "response": {
31783189 "hello"
31793190 ],
31803191 "note": null,
3181− "updated_by": "syntaqx"
3192+ "updated_by": "syntaqx",
3193+ "brokered_hosts": [
3194+ "registry.npmjs.org"
3195+ ]
31823196 }
31833197 },
31843198 "delete_actions_secret": {
1374913763 "role": "write",
1375013764 "linked": true,
1375113765 "created_at": null
13752− }
13753− ]
13754− }
13755− },
13756− "get_mirror": {
13757− "params": {
13758− "owner": "acme",
13759− "name": "web"
13760− },
13761− "response": {
13762− "remotes": [
13763− {
13764− "id": "rmt_01kp4a2b3c4d5e6f7g8h9j0k1m",
13765− "repo_id": "rep_01kp49z8y7x6w5v4t3s2r1q0pn",
13766− "repo": "acme/web",
13767− "provider": "github",
13768− "role": "leader",
13769− "name": "github.com/acme/web",
13770− "url": "https://github.com/acme/web",
13771− "state": "standby",
13772− "state_since": "2026-10-08T13:58:00.000Z",
13773− "state_by": "syntaqx",
13774− "reachable": true,
13775− "unreachable_since": null,
13776− "synced_at": "2026-10-08T13:57:42.000Z",
13777− "last_error": null,
13778− "checks_problem": null,
13779− "settings": {
13780− "notify": "banner",
13781− "take_over_after": null,
13782− "hand_back": "when_clean",
13783− "keep_ci_warm": false,
13784− "github_workflows": true,
13785− "hold_deploys": true,
13786− "report_checks": true,
13787− "remote_pushes": "adopt"
13788− },
13789− "created_at": "2026-10-05T10:00:00.000Z"
13790− }
13791− ],
13792− "plan": null,
13793− "can_manage": true
13794− },
13795− "notes": "A repository that leads lists its followers instead, each `role: follower` with `state` `following` or `stuck`. A mirror standing by is read-only on g1t: pushes, merges, issues, pull requests and agents are refused, and nothing runs. `reachable` is false once the remote's host has failed three checks over two minutes; by default that is only shown, and nothing happens on its own."
13796− },
13797− "get_hand_back_plan": {
13798− "params": {
13799− "owner": "acme",
13800− "name": "web"
13801− },
13802− "response": {
13803− "remotes": [
13804− {
13805− "id": "rmt_01kp4a2b3c4d5e6f7g8h9j0k1m",
13806− "repo_id": "rep_01kp49z8y7x6w5v4t3s2r1q0pn",
13807− "repo": "acme/web",
13808− "provider": "github",
13809− "role": "leader",
13810− "name": "github.com/acme/web",
13811− "url": "https://github.com/acme/web",
13812− "state": "takeover",
13813− "state_since": "2026-10-08T13:58:00.000Z",
13814− "state_by": "syntaqx",
13815− "reachable": true,
13816− "unreachable_since": null,
13817− "synced_at": "2026-10-08T13:57:42.000Z",
13818− "last_error": null,
13819− "checks_problem": null,
13820− "settings": {
13821− "notify": "banner",
13822− "take_over_after": null,
13823− "hand_back": "when_clean",
13824− "keep_ci_warm": false,
13825− "github_workflows": true,
13826− "hold_deploys": true,
13827− "report_checks": true,
13828− "remote_pushes": "adopt"
13829− },
13830− "created_at": "2026-10-05T10:00:00.000Z"
13831− }
13832− ],
13833− "plan": {
13834− "refs": [
13835− {
13836− "ref": "refs/heads/fix/login-timeout",
13837− "base": "9d0e1aa4c8f2b7e6d5a4c3b2a1f0e9d8c7b6a5f4",
13838− "ours": "c47f2b8e1d0c9b8a7f6e5d4c3b2a1f0e9d8c7b6a",
13839− "theirs": "9d0e1aa4c8f2b7e6d5a4c3b2a1f0e9d8c7b6a5f4",
13840− "action": "push",
13841− "decision": null
13842− },
13843− {
13844− "ref": "refs/heads/main",
13845− "base": "41c9e02b7a6f5e4d3c2b1a0f9e8d7c6b5a4f3e2d",
13846− "ours": "e01d9a37f6e5d4c3b2a1f0e9d8c7b6a5f4e3d2c1",
13847− "theirs": "41c9e02b7a6f5e4d3c2b1a0f9e8d7c6b5a4f3e2d",
13848− "action": "pull_request",
13849− "decision": null
13850− },
13851− {
13852− "ref": "refs/heads/docs/readme",
13853− "base": "7f3a1c2d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b",
13854− "ours": "5b6c7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c",
13855− "theirs": "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0",
13856− "action": "diverged",
13857− "decision": null
13858− }
13859− ],
13860− "reachable": true,
13861− "ready": false
13862− },
13863− "can_manage": true
13864− },
13865− "notes": "Refs that did not move on either side are left out. `ready` stays false until `docs/readme` has a decision."
13866− },
13867− "take_over_mirror": {
13868− "params": {
13869− "owner": "acme",
13870− "name": "web"
13871− },
13872− "response": {
13873− "remotes": [
13874− {
13875− "id": "rmt_01kp4a2b3c4d5e6f7g8h9j0k1m",
13876− "repo_id": "rep_01kp49z8y7x6w5v4t3s2r1q0pn",
13877− "repo": "acme/web",
13878− "provider": "github",
13879− "role": "leader",
13880− "name": "github.com/acme/web",
13881− "url": "https://github.com/acme/web",
13882− "state": "takeover",
13883− "state_since": "2026-10-08T13:58:00.000Z",
13884− "state_by": "syntaqx",
13885− "reachable": false,
13886− "unreachable_since": "2026-10-08T14:04:00.000Z",
13887− "synced_at": "2026-10-08T13:57:42.000Z",
13888− "last_error": null,
13889− "checks_problem": null,
13890− "settings": {
13891− "notify": "banner",
13892− "take_over_after": null,
13893− "hand_back": "when_clean",
13894− "keep_ci_warm": false,
13895− "github_workflows": true,
13896− "hold_deploys": true,
13897− "report_checks": true,
13898− "remote_pushes": "adopt"
13899− },
13900− "created_at": "2026-10-05T10:00:00.000Z"
13901− }
13902− ],
13903− "plan": null,
13904− "can_manage": true
13905− }
13906− },
13907− "set_ci_failover": {
13908− "params": {
13909− "owner": "acme",
13910− "name": "web"
13911− },
13912− "request": {
13913− "on": true
13914− },
13915− "response": {
13916− "remotes": [
13917− {
13918− "id": "rmt_01kp4a2b3c4d5e6f7g8h9j0k1m",
13919− "repo_id": "rep_01kp49z8y7x6w5v4t3s2r1q0pn",
13920− "repo": "acme/web",
13921− "provider": "github",
13922− "role": "leader",
13923− "name": "github.com/acme/web",
13924− "url": "https://github.com/acme/web",
13925− "state": "ci",
13926− "state_since": "2026-10-08T13:58:00.000Z",
13927− "state_by": "syntaqx",
13928− "reachable": true,
13929− "unreachable_since": null,
13930− "synced_at": "2026-10-08T13:57:42.000Z",
13931− "last_error": null,
13932− "checks_problem": null,
13933− "settings": {
13934− "notify": "banner",
13935− "take_over_after": null,
13936− "hand_back": "when_clean",
13937− "keep_ci_warm": false,
13938− "github_workflows": true,
13939− "hold_deploys": true,
13940− "report_checks": true,
13941− "remote_pushes": "adopt"
13942− },
13943− "created_at": "2026-10-05T10:00:00.000Z"
13944− }
13945− ],
13946− "plan": null,
13947− "can_manage": true
13948− }
13949− },
13950− "hand_back_mirror": {
13951− "params": {
13952− "owner": "acme",
13953− "name": "web"
13954− },
13955− "request": {
13956− "decisions": {
13957− "refs/heads/docs/readme": "pull_request"
13958− }
13959− },
13960− "response": {
13961− "remotes": [
13962− {
13963− "id": "rmt_01kp4a2b3c4d5e6f7g8h9j0k1m",
13964− "repo_id": "rep_01kp49z8y7x6w5v4t3s2r1q0pn",
13965− "repo": "acme/web",
13966− "provider": "github",
13967− "role": "leader",
13968− "name": "github.com/acme/web",
13969− "url": "https://github.com/acme/web",
13970− "state": "standby",
13971− "state_since": "2026-10-08T13:58:00.000Z",
13972− "state_by": "syntaqx",
13973− "reachable": true,
13974− "unreachable_since": null,
13975− "synced_at": "2026-10-08T13:57:42.000Z",
13976− "last_error": null,
13977− "checks_problem": null,
13978− "settings": {
13979− "notify": "banner",
13980− "take_over_after": null,
13981− "hand_back": "when_clean",
13982− "keep_ci_warm": false,
13983− "github_workflows": true,
13984− "hold_deploys": true,
13985− "report_checks": true,
13986− "remote_pushes": "adopt"
13987− },
13988− "created_at": "2026-10-05T10:00:00.000Z"
1398913766 }
13990− ],
13991− "plan": null,
13992− "can_manage": true,
13993− "notes": [
13994− "main: opened https://github.com/acme/web/pull/1290",
13995− "docs/readme: opened https://github.com/acme/web/pull/1291"
1399613767 ]
13997− },
13998− "notes": "Branch names work as keys too: `{ \"docs/readme\": \"keep_theirs\" }`."
13999− },
14000− "move_mirror_to_g1t": {
14001− "params": {
14002− "owner": "acme",
14003− "name": "web"
14004− },
14005− "request": {
14006− "keep_remote_updated": false
14007− },
14008− "response": {
14009− "remotes": [],
14010− "plan": null,
14011− "can_manage": true
14012− },
14013− "notes": "With `keep_remote_updated: true` the remote stays in `remotes` as a follower, and g1t pushes to it from then on."
14014− },
14015− "sync_mirror": {
14016− "params": {
14017− "owner": "acme",
14018− "name": "web"
14019− },
14020− "response": {
14021− "remotes": [
14022− {
14023− "id": "rmt_01kp4a2b3c4d5e6f7g8h9j0k1m",
14024− "repo_id": "rep_01kp49z8y7x6w5v4t3s2r1q0pn",
14025− "repo": "acme/web",
14026− "provider": "github",
14027− "role": "leader",
14028− "name": "github.com/acme/web",
14029− "url": "https://github.com/acme/web",
14030− "state": "standby",
14031− "state_since": "2026-10-08T13:58:00.000Z",
14032− "state_by": "syntaqx",
14033− "reachable": true,
14034− "unreachable_since": null,
14035− "synced_at": "2026-10-08T13:57:42.000Z",
14036− "last_error": null,
14037− "checks_problem": null,
14038− "settings": {
14039− "notify": "banner",
14040− "take_over_after": null,
14041− "hand_back": "when_clean",
14042− "keep_ci_warm": false,
14043− "github_workflows": true,
14044− "hold_deploys": true,
14045− "report_checks": true,
14046− "remote_pushes": "adopt"
14047− },
14048− "created_at": "2026-10-05T10:00:00.000Z"
14049− }
14050− ],
14051− "plan": null,
14052− "can_manage": true
14053− }
14054− },
14055− "add_mirror_remote": {
14056− "params": {
14057− "owner": "acme",
14058− "name": "web"
14059− },
14060− "request": {
14061− "provider": "g1t",
14062− "role": "follower",
14063− "url": "https://git.acme.internal/acme/web.git",
14064− "username": "deploy",
14065− "token": "g1t_…"
14066− },
14067− "response": {
14068− "id": "rmt_01kp4b3c4d5e6f7g8h9j0k1m2n",
14069− "repo_id": "rep_01kp49z8y7x6w5v4t3s2r1q0pn",
14070− "repo": "acme/web",
14071− "provider": "g1t",
14072− "role": "follower",
14073− "name": "git.acme.internal/acme/web",
14074− "url": "https://git.acme.internal/acme/web",
14075− "state": "following",
14076− "state_since": "2026-10-08T13:58:00.000Z",
14077− "state_by": "syntaqx",
14078− "reachable": true,
14079− "unreachable_since": null,
14080− "synced_at": "2026-10-08T13:57:42.000Z",
14081− "last_error": null,
14082− "checks_problem": null,
14083− "settings": {
14084− "notify": "banner",
14085− "take_over_after": null,
14086− "hand_back": "when_clean",
14087− "keep_ci_warm": false,
14088− "github_workflows": true,
14089− "hold_deploys": true,
14090− "report_checks": true,
14091− "remote_pushes": "adopt"
14092− },
14093− "created_at": "2026-10-05T10:00:00.000Z"
14094− },
14095− "notes": "The token is never returned. A remote that refused it, or could not be reached, is still added, with `last_error` saying why."
14096− },
14097− "update_mirror_remote": {
14098− "params": {
14099− "owner": "acme",
14100− "name": "web",
14101− "id": "rmt_01kp4a2b3c4d5e6f7g8h9j0k1m"
14102− },
14103− "request": {
14104− "notify": "inbox",
14105− "take_over_after": 30
14106− },
14107− "response": {
14108− "id": "rmt_01kp4a2b3c4d5e6f7g8h9j0k1m",
14109− "repo_id": "rep_01kp49z8y7x6w5v4t3s2r1q0pn",
14110− "repo": "acme/web",
14111− "provider": "github",
14112− "role": "leader",
14113− "name": "github.com/acme/web",
14114− "url": "https://github.com/acme/web",
14115− "state": "standby",
14116− "state_since": "2026-10-08T13:58:00.000Z",
14117− "state_by": "syntaqx",
14118− "reachable": true,
14119− "unreachable_since": null,
14120− "synced_at": "2026-10-08T13:57:42.000Z",
14121− "last_error": null,
14122− "checks_problem": null,
14123− "settings": {
14124− "notify": "inbox",
14125− "take_over_after": 30,
14126− "hand_back": "when_clean",
14127− "keep_ci_warm": false,
14128− "github_workflows": true,
14129− "hold_deploys": true,
14130− "report_checks": true,
14131− "remote_pushes": "adopt"
14132− },
14133− "created_at": "2026-10-05T10:00:00.000Z"
1413413768 }
14135− },
14136− "remove_mirror_remote": {
14137− "params": {
14138− "owner": "acme",
14139− "name": "web",
14140− "id": "rmt_01kp4b3c4d5e6f7g8h9j0k1m2n"
14141− },
14142− "response": true
1414313769 },
1414413770 "list_workspace_artifacts": {
1414513771 "params": {
+0−12
77 use crate::archives::ArchiveOp;
88 use crate::customize::CustomizeOp;
99 use crate::deploy_keys::DeployKeysOp;
10−use crate::mirrors::MirrorsOp;
1110 use crate::deployments::DeploymentsOp;
1211 use crate::telemetry::TelemetryOp;
1312 use crate::packages::PackagesOp;
8685 route("POST", "/repos/:owner/:name/keys", Op::DeployKeys(DeployKeysOp::CreateDeployKey), &[]),
8786 route("GET", "/repos/:owner/:name/keys/:id", Op::DeployKeys(DeployKeysOp::GetDeployKey), &[]),
8887 route("DELETE", "/repos/:owner/:name/keys/:id", Op::DeployKeys(DeployKeysOp::DeleteDeployKey), &[]),
89− // Mirroring: a repository's remotes, takeovers and hand-backs.
90− route("GET", "/repos/:owner/:name/mirror", Op::Mirrors(MirrorsOp::GetMirror), &[]),
91− route("GET", "/repos/:owner/:name/mirror/hand-back", Op::Mirrors(MirrorsOp::GetHandBackPlan), &[]),
92− route("POST", "/repos/:owner/:name/mirror/take-over", Op::Mirrors(MirrorsOp::TakeOver), &[]),
93− route("POST", "/repos/:owner/:name/mirror/ci", Op::Mirrors(MirrorsOp::SetCiFailover), &[]),
94− route("POST", "/repos/:owner/:name/mirror/hand-back", Op::Mirrors(MirrorsOp::HandBack), &[]),
95− route("POST", "/repos/:owner/:name/mirror/move-to-g1t", Op::Mirrors(MirrorsOp::MoveToG1t), &[]),
96− route("POST", "/repos/:owner/:name/mirror/sync", Op::Mirrors(MirrorsOp::SyncMirror), &[]),
97− route("POST", "/repos/:owner/:name/mirror/remotes", Op::Mirrors(MirrorsOp::AddRemote), &[]),
98− route("PATCH", "/repos/:owner/:name/mirror/remotes/:id", Op::Mirrors(MirrorsOp::UpdateRemote), &[]),
99− route("DELETE", "/repos/:owner/:name/mirror/remotes/:id", Op::Mirrors(MirrorsOp::RemoveRemote), &[]),
10088 // Your notifications: threads, marking them, and what you subscribe
10189 // to and watch. GitHub's addresses, with g1t's saved and snoozed.
10290 route("GET", "/notifications", Op::ListNotifications, &[("all", "all"), ("participating", "participating"), ("view", "view"), ("reason", "reason"), ("severity", "severity"), ("since", "since"), ("before", "before"), ("cursor", "cursor"), ("per_page", "per_page")]),
+2−2
625625 }
626626
627627 /// The repository a project's permission comes from, as the deployments
628−/// service reads it: a mirrored project has none, and its workspace's base
629−/// permission decides.
628+/// service reads it: a project without a hosted source has none, and its
629+/// workspace's base permission decides.
630630 fn repo_of(project: &Value) -> (String, String, bool) {
631631 let source = &project["source"];
632632 let private = project["private"].as_bool().unwrap_or(true);
+1−12
2323 use crate::archives::ArchiveOp;
2424 use crate::customize::CustomizeOp;
2525 use crate::deploy_keys::DeployKeysOp;
26−use crate::mirrors::MirrorsOp;
2726 use crate::deployments::DeploymentsOp;
2827 use crate::telemetry::TelemetryOp;
2928 use crate::packages::PackagesOp;
7271 Tool {
7372 name: "repository",
7473 title: "Repositories",
75− description: "Repositories: find, read and create them, change their settings and rulesets (what may happen to branches and tags, and what a pull request needs to merge), check their CODEOWNERS file, manage their labels and milestones, see and dismiss their security alerts (secrets and vulnerable dependencies), read what their default branch says (languages, contributors, license), star them, publish releases, and look after their mirroring (take a mirror over, hand it back, or move it to g1t for good). Name one as \"owner/name\". Deleting, transferring and changing visibility need `confirm`.",
74+ description: "Repositories: find, read and create them, change their settings and rulesets (what may happen to branches and tags, and what a pull request needs to merge), check their CODEOWNERS file, manage their labels and milestones, see and dismiss their security alerts (secrets and vulnerable dependencies), read what their default branch says (languages, contributors, license), star them and publish releases. Name one as \"owner/name\". Deleting, transferring and changing visibility need `confirm`.",
7675 default_action: None,
7776 actions: &[
7877 a("list", Op::ListRepos, "Repositories you can see"),
123122 a("delete_branch", Op::DeleteBranch, "Delete a branch; make it again at its commit to restore it"),
124123 a("rename", Op::RenameRepo, "Rename it; old addresses redirect"),
125124 a("transfer", Op::TransferRepo, "Move it to another workspace you own"),
126− a("mirror", Op::Mirrors(MirrorsOp::GetMirror), "Its remotes: what it mirrors or is mirrored to"),
127− a("mirror_hand_back_plan", Op::Mirrors(MirrorsOp::GetHandBackPlan), "What handing a takeover back would do, ref by ref"),
128− a("mirror_take_over", Op::Mirrors(MirrorsOp::TakeOver), "Make g1t lead a mirror for now"),
129− a("mirror_ci", Op::Mirrors(MirrorsOp::SetCiFailover), "Run a mirror's workflows on g1t (on), or stop (off)"),
130− a("mirror_hand_back", Op::Mirrors(MirrorsOp::HandBack), "Send a takeover back, deciding diverged refs"),
131− a("mirror_move_to_g1t", Op::Mirrors(MirrorsOp::MoveToG1t), "Stop tracking the remote; g1t leads for good"),
132− a("mirror_sync", Op::Mirrors(MirrorsOp::SyncMirror), "Bring its remotes in step now"),
133− a("mirror_add_remote", Op::Mirrors(MirrorsOp::AddRemote), "Link another g1t or git host"),
134− a("mirror_update_remote", Op::Mirrors(MirrorsOp::UpdateRemote), "Change a remote's settings"),
135− a("mirror_remove_remote", Op::Mirrors(MirrorsOp::RemoveRemote), "Unlink a remote"),
136125 a("archive", Op::ArchiveRepo, "Make it read-only"),
137126 a("unarchive", Op::UnarchiveRepo, "Make it writable again"),
138127 a("set_visibility", Op::SetRepoVisibility, "Make it public or private"),
+1−1
13301330
13311331 [[package]]
13321332 name = "g1t-desktop"
1333−version = "0.3.4"
1333+version = "0.3.5"
13341334 dependencies = [
13351335 "base64 0.22.1",
13361336 "ed25519-dalek",
+1−1
11 [package]
22 name = "g1t-desktop"
3−version = "0.3.4"
3+version = "0.3.5"
44 edition = "2024"
55 license = "MIT"
66 repository = "https://g1t.sh/flagon-io/g1t"
+1−1
1414 '/guides/g1t-agents/': '/guides/working-with-g1t/',
1515 '/guides/inbox/': '/guides/notifications/',
1616 '/guides/marketplace/': '/guides/customize/',
17+ '/guides/mirroring/': '/guides/github/',
1718 '/guides/today/': '/guides/home/',
1819 },
1920 integrations: [
181182 items: [
182183 { label: 'Accounts and sign-in', slug: 'guides/authentication' },
183184 { label: 'GitHub', slug: 'guides/github' },
184− { label: 'Mirroring', slug: 'guides/mirroring' },
185185 { label: 'Workspaces and tokens', slug: 'guides/workspaces' },
186186 { label: 'Access and roles', slug: 'guides/access-and-roles' },
187187 { label: 'Permissions', slug: 'guides/permissions' },
+4−4
5959 `main`. Each push sends only what the last did not.
6060 - **Status.** Depends on Cloudflare.
6161
62−### Imports and mirrors up to 40 MB
62+### Imports up to 40 MB
6363
64−Importing or mirroring a repository copies it in one piece of at most
65−40 MB, after compression.
64+Importing a repository copies it in one piece of at most 40 MB, after
65+compression.
6666
6767 - **Why.** The copy is held in memory while it moves, and a Worker has
6868 128 MB for everything it is doing at once.
6969 - **Instead.** Clone the repository yourself and push it to g1t, in steps if
70− it is over 100 MB. See [Import, mirror or move a repository](/guides/github/#what-comes-across).
70+ it is over 100 MB. See [Import a repository](/guides/github/#what-comes-across).
7171 - **Status.** Streaming the copy, so size stops mattering, is planned.
7272
7373 ### Partial clone works, but is not promised
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…

Comments

Sign in to comment.