Commit

The g1t CLI 0.1.0 is downloadable: g1t.sh/downloads/cli/latest/, five platforms with SHA256SUMS

- scripts/cli-release.mjs builds crates/g1t for Linux, macOS and Windows (x64 and arm64) in the cargo-zigbuild image, writes SHA256SUMS and the manifest, and publishes to the g1t-downloads bucket, latest.json last. Wrangler runs from apps/web, away from the repository's .env. - /downloads/<tool>/ serves the runner and the CLI from the same bucket. - The containers guide says where to get g1t for g1t push.

syntaqxcommitted Parente3455e6Browse files
4 files+138−130/4 viewed
+19−2
151151
152152 `g1t --version` prints its version, and `g1t help` its options.
153153
154−Release binaries of the g1t command line are coming. Until then, build it
155−from the g1t source with [Rust](https://www.rust-lang.org/tools/install):
154+### Getting g1t
155+
156+One file for each platform, from `https://g1t.sh/downloads/cli/latest/`:
157+
158+```sh
159+# Linux (x64; use g1t-linux-arm64 on ARM)
160+curl -fsSLo g1t https://g1t.sh/downloads/cli/latest/g1t-linux-x64 && chmod +x g1t
161+# macOS (Apple silicon; use g1t-macos-x64 on Intel)
162+curl -fsSLo g1t https://g1t.sh/downloads/cli/latest/g1t-macos-arm64 && chmod +x g1t
163+```
164+
165+```powershell
166+# Windows
167+Invoke-WebRequest https://g1t.sh/downloads/cli/latest/g1t-windows-x64.exe -OutFile g1t.exe
168+```
169+
170+Check a download against `https://g1t.sh/downloads/cli/latest/SHA256SUMS`.
171+To build it from source instead, with
172+[Rust](https://www.rust-lang.org/tools/install):
156173
157174 ```sh
158175 git clone https://g1t.sh/flagon-io/g1t
+2−2
5353 route("status", "routes/status.tsx"),
5454 route("status.json", "routes/status-json.ts"),
5555 route(".well-known/security.txt", "routes/security-txt.ts"),
56− // The self-hosted runner's releases, from R2.
57− route("downloads/runner/*", "routes/downloads-runner.ts"),
56+ // Releases of the self-hosted runner and the g1t CLI, from R2.
57+ route("downloads/:tool/*", "routes/downloads-runner.ts"),
5858 // A workspace's own pages sit under `-`, which no repository can be named.
5959 route(":owner", "routes/workspace/layout.tsx", [
6060 index("routes/workspace/overview.tsx"),
+14−9
33 import type { Route } from "./+types/downloads-runner";
44
55 /**
6− * The self-hosted runner's releases: `g1t.sh/downloads/runner/<version>/<file>`,
7− * served from the g1t-downloads R2 bucket that scripts/runner-release.mjs
8− * publishes to. `latest/<file>` is the newest release's, as its signed
9− * `latest.json` names it; `latest.json` and `latest.json.sig` themselves
10− * are what runners check before updating.
6+ * Releases: `g1t.sh/downloads/<tool>/<version>/<file>`, served from the
7+ * g1t-downloads R2 bucket. `runner` is the self-hosted runner
8+ * (scripts/runner-release.mjs), `cli` the g1t CLI (scripts/cli-release.mjs).
9+ * `latest/<file>` is the newest release's, as its `latest.json` names it;
10+ * the runner's `latest.json` and `latest.json.sig` are what runners check
11+ * before updating.
1112 */
13+const TOOLS = new Set(["runner", "cli"]);
1214 const TYPES: Record<string, string> = {
1315 json: "application/json",
1416 sig: "text/plain; charset=utf-8",
2123 }
2224
2325 export async function loader({ params }: Route.LoaderArgs) {
26+ const tool = params.tool ?? "";
2427 const path = (params["*"] ?? "").replace(/^\/+/, "");
25− if (!path || path.includes("..") || !/^[A-Za-z0-9._/-]+$/.test(path)) throw new Response("Not found\n", { status: 404 });
26− let key = `runner/${path}`;
28+ if (!TOOLS.has(tool) || !path || path.includes("..") || !/^[A-Za-z0-9._/-]+$/.test(path)) {
29+ throw new Response("Not found\n", { status: 404 });
30+ }
31+ let key = `${tool}/${path}`;
2732 // `latest/<file>`: the file of the newest release.
2833 if (path.startsWith("latest/")) {
29− const manifest = await object("runner/latest.json");
34+ const manifest = await object(`${tool}/latest.json`);
3035 if (!manifest) throw new Response("No release yet\n", { status: 404 });
3136 const { version } = (await manifest.json()) as { version: string };
32− key = `runner/${version}/${path.slice("latest/".length)}`;
37+ key = `${tool}/${version}/${path.slice("latest/".length)}`;
3338 }
3439 const found = await object(key);
3540 if (!found) throw new Response("Not found\n", { status: 404 });
+103−0
1+#!/usr/bin/env node
2+// Releases of the g1t CLI (crates/g1t): built for every platform,
3+// checksummed, and published to the g1t-downloads R2 bucket that g1t.sh
4+// serves at /downloads/cli/ (apps/web/app/routes/downloads-runner.ts).
5+//
6+// node scripts/cli-release.mjs build # every platform, in the cargo-zigbuild image (Docker)
7+// node scripts/cli-release.mjs publish [--dry-run]
8+//
9+// At cli/<version>/ in the bucket: g1t-linux-x64, -linux-arm64,
10+// -macos-x64, -macos-arm64, -windows-x64.exe, SHA256SUMS and
11+// manifest.json; at cli/: latest.json, the newest release's manifest.
12+// Unlike the runner, the CLI does not update itself, so nothing is signed:
13+// SHA256SUMS is what to check a download against.
14+
15+import { spawnSync } from "node:child_process";
16+import { createHash } from "node:crypto";
17+import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
18+import { dirname, join } from "node:path";
19+import { fileURLToPath } from "node:url";
20+
21+const ROOT = join(dirname(fileURLToPath(import.meta.url)), "..");
22+const BUCKET = "g1t-downloads";
23+const BUILDER = "ghcr.io/rust-cross/cargo-zigbuild:latest";
24+export const TARGETS = {
25+ "linux-x64": { triple: "x86_64-unknown-linux-musl", file: "g1t-linux-x64" },
26+ "linux-arm64": { triple: "aarch64-unknown-linux-musl", file: "g1t-linux-arm64" },
27+ "macos-x64": { triple: "x86_64-apple-darwin", file: "g1t-macos-x64" },
28+ "macos-arm64": { triple: "aarch64-apple-darwin", file: "g1t-macos-arm64" },
29+ "windows-x64": { triple: "x86_64-pc-windows-gnu", file: "g1t-windows-x64.exe", exe: true },
30+};
31+
32+export function version() {
33+ const found = /^version\s*=\s*"([^"]+)"/m.exec(readFileSync(join(ROOT, "crates/g1t/Cargo.toml"), "utf8"));
34+ if (!found) throw new Error("crates/g1t/Cargo.toml has no version");
35+ return found[1];
36+}
37+
38+const outDir = (v = version()) => join(ROOT, "target", "cli-release", v);
39+const sha256 = (bytes) => createHash("sha256").update(bytes).digest("hex");
40+
41+function run(command, args, options = {}) {
42+ const done = spawnSync(command, args, { stdio: "inherit", cwd: ROOT, ...options });
43+ if (done.status !== 0) throw new Error(`${command} ${args.join(" ")} failed`);
44+}
45+
46+function build() {
47+ const v = version();
48+ const dir = outDir(v);
49+ mkdirSync(dir, { recursive: true });
50+ const triples = Object.values(TARGETS).map((t) => t.triple).join(" ");
51+ // One container builds every platform; its target folder is kept apart
52+ // from the host's so the two never mix.
53+ run("docker", [
54+ "run", "--rm",
55+ "-e", "CARGO_TARGET_DIR=/src/target/zig",
56+ "-v", `${ROOT.replaceAll("\\", "/")}:/src`,
57+ "-v", "g1t-cargo-registry:/usr/local/cargo/registry",
58+ "-w", "/src", BUILDER, "sh", "-c",
59+ `set -e; for t in ${triples}; do rustup target add $t >/dev/null 2>&1; cargo zigbuild --release --locked --package g1t --target $t; done`,
60+ ], { env: { ...process.env, MSYS_NO_PATHCONV: "1" } });
61+ const files = {};
62+ for (const [platform, target] of Object.entries(TARGETS)) {
63+ const built = join(ROOT, "target", "zig", target.triple, "release", target.exe ? "g1t.exe" : "g1t");
64+ const bytes = readFileSync(built);
65+ writeFileSync(join(dir, target.file), bytes);
66+ files[platform] = { name: target.file, sha256: sha256(bytes) };
67+ }
68+ const manifest = { version: v, published_at: new Date().toISOString(), files };
69+ writeFileSync(join(dir, "manifest.json"), `${JSON.stringify(manifest, null, 2)}\n`);
70+ writeFileSync(join(dir, "latest.json"), `${JSON.stringify(manifest, null, 2)}\n`);
71+ writeFileSync(join(dir, "SHA256SUMS"), Object.values(files).map((f) => `${f.sha256} ${f.name}`).join("\n") + "\n");
72+ console.log(`built ${Object.keys(files).length} binaries of g1t ${v} into ${dir}`);
73+}
74+
75+function publish(dryRun) {
76+ const v = version();
77+ const dir = outDir(v);
78+ if (!existsSync(join(dir, "manifest.json"))) throw new Error(`nothing built for ${v}: node scripts/cli-release.mjs build`);
79+ const put = (key, file, type) => {
80+ if (dryRun) return console.log(`would put ${key}`);
81+ run("npx", ["wrangler", "r2", "object", "put", `${BUCKET}/${key}`, "--file", file, "--remote", "--content-type", type], {
82+ shell: process.platform === "win32",
83+ // Away from the repository's .env, which may hold a token meant for
84+ // something else (as scripts/deploy does).
85+ cwd: join(ROOT, "apps/web"),
86+ });
87+ };
88+ for (const target of Object.values(TARGETS)) put(`cli/${v}/${target.file}`, join(dir, target.file), "application/octet-stream");
89+ put(`cli/${v}/manifest.json`, join(dir, "manifest.json"), "application/json");
90+ put(`cli/${v}/SHA256SUMS`, join(dir, "SHA256SUMS"), "text/plain");
91+ // Last, so `latest` never names a version whose files are not up yet.
92+ put("cli/latest.json", join(dir, "latest.json"), "application/json");
93+}
94+
95+if (process.argv[1]?.replaceAll("\\", "/").endsWith("scripts/cli-release.mjs")) {
96+ const [command, ...rest] = process.argv.slice(2);
97+ if (command === "build") build();
98+ else if (command === "publish") publish(rest.includes("--dry-run"));
99+ else {
100+ console.error("usage: node scripts/cli-release.mjs build|publish [--dry-run]");
101+ process.exit(2);
102+ }
103+}