Skip to content
136 linesCodeBlameRaw

Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.

Merge GitHub pull request #8 (claude/cool-newton-7nm8r9): Docker deployments on g1t.page behind a switch that stays off until Cloudflare's container APIs are checked live, workflows a workspace manages in its repositories, tarball and zipball downloads, refs/pull/* kept to g1t alone, framework detection for g1t.page builds, and a workflow's event payload matching GitHub's.1//! A repository's files at a branch, tag or commit as one archive, at
2//! GitHub's addresses: `GET /repos/{owner}/{repo}/tarball/{ref}` and
3//! `/zipball/{ref}` (and without a ref, the default branch).
4//!
5//! As GitHub's redirect to codeload, these answer `302 Found` with a link to
6//! the site's archive route (apps/web routes/repo/archive.ts) for the commit
7//! the ref names, carrying a token the repos service made for whoever asked
8//! (services/repos archive_links.rs). The link is good for 10 minutes and
9//! needs no token of its own, so a client following the redirect, which
10//! drops its `Authorization` header on the way, gets a private
11//! repository's files too. The archive holds one folder,
12//! `<owner>-<repo>-<short sha>/`, as GitHub's does: `actions/checkout`
13//! downloads one this way in a job container without git, and expects it.
14
15use g1t_contracts::repos::{ArchiveLink, ArchiveLinkArgs};
16use g1t_contracts::{FailureCode, Outcome, Viewer};
17use serde_json::{Value, json};
18use worker::Result;
19
20use crate::operations::{Services, repo_path};
21
22#[derive(Clone, Copy, Debug, PartialEq, Eq)]
23pub enum ArchiveOp {
24 DownloadTarball,
25 DownloadZipball,
26}
27
28impl ArchiveOp {
29 #[cfg(test)]
30 pub const ALL: [ArchiveOp; 2] = [ArchiveOp::DownloadTarball, ArchiveOp::DownloadZipball];
31
32 pub fn name(self) -> &'static str {
33 match self {
34 ArchiveOp::DownloadTarball => "download_tarball_archive",
35 ArchiveOp::DownloadZipball => "download_zipball_archive",
36 }
37 }
38
39 /// For the API reference.
40 pub fn title(self) -> &'static str {
41 match self {
42 ArchiveOp::DownloadTarball => "Download a repository archive (tar)",
43 ArchiveOp::DownloadZipball => "Download a repository archive (zip)",
44 }
45 }
46
47 pub fn description(self) -> &'static str {
48 match self {
49 ArchiveOp::DownloadTarball => "A link to download a branch, tag or commit as a gzipped tar archive (the default branch without ref), good for 10 minutes and needing no token. Over REST, GET …/tarball/{ref} answers 302 with the link in Location, as GitHub does: `curl -L` follows it. Over MCP the link is returned as url, with commit and expires_at. The archive holds one folder, <owner>-<repo>-<first 7 of the commit>/, with executable files and symlinks as git has them and the commit in its pax header. Up to 10,000 files and 24 MB; clone a larger repository. Needs the Read role; a public repository's are open to anyone.",
50 ArchiveOp::DownloadZipball => "A link to download a branch, tag or commit as a zip archive (the default branch without ref), good for 10 minutes and needing no token. Over REST, GET …/zipball/{ref} answers 302 with the link in Location, as GitHub does: `curl -L` follows it. Over MCP the link is returned as url, with commit and expires_at. The archive holds one folder, <owner>-<repo>-<first 7 of the commit>/. Up to 10,000 files and 24 MB; clone a larger repository. Needs the Read role; a public repository's are open to anyone.",
51 }
52 }
53
54 fn suffix(self) -> &'static str {
55 match self {
56 ArchiveOp::DownloadTarball => "tar.gz",
57 ArchiveOp::DownloadZipball => "zip",
58 }
59 }
60
61 pub fn input(self) -> Value {
62 json!({
63 "type": "object",
64 "properties": {
65 "repo": { "type": "string", "description": "Repository as \"owner/name\", e.g. \"flagon-io/hello\"." },
66 "ref": { "type": "string", "description": "A branch, tag or commit; the default branch when left out. A branch with slashes is sent URL-encoded as one path segment." },
67 },
68 "required": ["repo"],
69 })
70 }
71}
72
73/// Where the site serves the archive of `link`.
74pub fn archive_url(site: &str, link: &ArchiveLink, op: ArchiveOp) -> String {
75 format!(
76 "{site}/{}/{}/archive/{}.{}?token={}",
77 link.repo.namespace,
78 link.repo.name,
79 link.commit,
80 op.suffix(),
81 link.token
82 )
83}
84
85pub async fn run(op: ArchiveOp, services: &Services, viewer: &Viewer, input: &Value) -> Result<Outcome<Value>> {
86 let Some(path) = repo_path(input) else {
87 return Ok(Outcome::fail(FailureCode::Invalid, "Give the repository as \"owner/name\"."));
88 };
89 let git_ref = input["ref"].as_str().map(str::trim).filter(|git_ref| !git_ref.is_empty()).map(str::to_owned);
90 let found: Outcome<ArchiveLink> = g1t_kit::call(&services.repos, "archive_link", &ArchiveLinkArgs { path, viewer: viewer.clone(), git_ref }).await?;
91 Ok(match found {
92 Outcome::Ok(link) => Outcome::Ok(json!({
93 "url": archive_url(&services.addresses.site, &link, op),
94 "commit": link.commit,
95 "expires_at": link.expires_at,
96 })),
97 Outcome::Fail(refused) => Outcome::Fail(refused),
98 })
99}
100
101#[cfg(test)]
102mod tests {
103 use super::*;
104
105 fn link() -> ArchiveLink {
106 let repo: g1t_contracts::repos::Repo = serde_json::from_value(json!({
107 "id": "repo_1", "namespace": "acme", "name": "web", "description": null, "isPrivate": true,
108 "ownerId": "ws_1", "defaultBranch": "main", "forkOf": null, "createdAt": "2026-10-01T00:00:00.000Z",
109 }))
110 .unwrap();
111 ArchiveLink { repo, commit: "4807077b296e6edbf410d55e72749d3e1170c291".into(), token: "abc_-123".into(), expires_at: "2026-10-12T00:10:00.000Z".into() }
112 }
113
114 #[test]
115 fn the_link_is_the_sites_archive_of_the_commit() {
116 let link = link();
117 assert_eq!(
118 archive_url("https://g1t.sh", &link, ArchiveOp::DownloadTarball),
119 "https://g1t.sh/acme/web/archive/4807077b296e6edbf410d55e72749d3e1170c291.tar.gz?token=abc_-123"
120 );
121 assert_eq!(
122 archive_url("http://localhost:8787", &link, ArchiveOp::DownloadZipball),
123 "http://localhost:8787/acme/web/archive/4807077b296e6edbf410d55e72749d3e1170c291.zip?token=abc_-123"
124 );
125 }
126
127 #[test]
128 fn each_operation_is_described_with_a_schema() {
129 for op in ArchiveOp::ALL {
130 assert!(crate::operations::Op::ALL.contains(&crate::operations::Op::Archives(op)), "{}", op.name());
131 assert!(!op.title().is_empty() && op.description().len() > 40, "{}", op.name());
132 assert_eq!(op.input()["required"], json!(["repo"]));
133 assert_eq!(g1t_contracts::scopes::scope_for(op.name()), Some(g1t_contracts::scopes::Scope::CodeRead), "{}", op.name());
134 }
135 }
136}

This file's history is long; its oldest lines are credited to the oldest commit read.