flagon-io/g1t

public

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/deploy/self-host/workers/artifacts/index.js

148 lines4,410 bytesCodeBlame
1// The Artifacts binding, for self-hosted g1t.
2//
3// The repos service is written against Cloudflare Artifacts' Workers
4// binding (services/repos/src/store.rs). Self-hosted, its ARTIFACTS binding
5// is a service binding to this Worker instead, which offers the same
6// methods and keeps the repositories in the git store (gitstore/server.mjs):
7// plain bare repositories on disk. Hosted g1t never runs this.
8//
9// Only what g1t calls is implemented: create and get on the namespace;
10// info, createToken, log, readCommit, readTree, readBlob, readFile and fork
11// on a repository.
12
13import { RpcTarget, WorkerEntrypoint } from "cloudflare:workers";
14
15class ArtifactsError extends Error {
16 constructor(code, message) {
17 super(message);
18 this.name = "ArtifactsError";
19 this.code = code;
20 }
21}
22
23async function store(env, path, init = {}) {
24 const base = (env.GITSTORE_URL ?? "http://gitstore:8080").replace(/\/$/, "");
25 const response = await fetch(`${base}/api/repos${path}`, {
26 ...init,
27 headers: {
28 "x-gitstore-secret": env.GITSTORE_SECRET ?? "",
29 ...(init.body ? { "content-type": "application/json" } : {}),
30 },
31 });
32 return response;
33}
34
35async function json(response) {
36 if (response.ok) return response.json();
37 let code = "INTERNAL_ERROR";
38 let message = `git store answered ${response.status}`;
39 try {
40 const body = await response.json();
41 code = body.code ?? code;
42 message = body.message ?? message;
43 } catch {}
44 throw new ArtifactsError(code, message);
45}
46
47/** Bytes as something with `arrayBuffer()`, the way a Blob is read. */
48async function bytes(response) {
49 if (response.status === 404) return null;
50 if (!response.ok) await json(response);
51 return new Response(await response.arrayBuffer(), {
52 headers: { "content-type": response.headers.get("content-type") ?? "application/octet-stream" },
53 });
54}
55
56class Repo extends RpcTarget {
57 #env;
58 #name;
59
60 constructor(env, name) {
61 super();
62 this.#env = env;
63 this.#name = name;
64 }
65
66 #path(rest = "") {
67 return `/${encodeURIComponent(this.#name)}${rest}`;
68 }
69
70 async info() {
71 return json(await store(this.#env, this.#path()));
72 }
73
74 async createToken(scope = "write", ttl = 86400) {
75 return json(
76 await store(this.#env, this.#path("/tokens"), {
77 method: "POST",
78 body: JSON.stringify({ scope, ttl }),
79 }),
80 );
81 }
82
83 async log(opts = {}) {
84 const query = new URLSearchParams();
85 for (const [key, value] of Object.entries(opts ?? {})) {
86 if (value !== undefined && value !== null) query.set(key, String(value));
87 }
88 return json(await store(this.#env, this.#path(`/log?${query}`)));
89 }
90
91 async readCommit(hash) {
92 return json(await store(this.#env, this.#path(`/commits/${encodeURIComponent(hash)}`)));
93 }
94
95 async readTree(hash) {
96 return json(await store(this.#env, this.#path(`/trees/${encodeURIComponent(hash)}`)));
97 }
98
99 async readBlob(hash) {
100 return bytes(await store(this.#env, this.#path(`/blobs/${encodeURIComponent(hash)}`)));
101 }
102
103 async readFile({ ref, path }) {
104 const query = new URLSearchParams({ ref, path });
105 return bytes(await store(this.#env, this.#path(`/file?${query}`)));
106 }
107
108 async fork(name, opts = {}) {
109 const created = await json(
110 await store(this.#env, this.#path("/fork"), {
111 method: "POST",
112 body: JSON.stringify({ ...opts, name }),
113 }),
114 );
115 const token = await new Repo(this.#env, name).createToken("write");
116 return { ...created, token: token.plaintext };
117 }
118}
119
120export default class Artifacts extends WorkerEntrypoint {
121 async create(name, opts = {}) {
122 const created = await json(
123 await store(this.env, "", {
124 method: "POST",
125 body: JSON.stringify({
126 name,
127 description: opts?.description,
128 defaultBranch: opts?.setDefaultBranch,
129 readOnly: opts?.readOnly,
130 }),
131 }),
132 );
133 const token = await new Repo(this.env, name).createToken("write");
134 return { ...created, token: token.plaintext };
135 }
136
137 async get(name) {
138 // Artifacts answers NOT_FOUND here for a repository that does not exist.
139 await json(await store(this.env, `/${encodeURIComponent(name)}`));
140 return new Repo(this.env, name);
141 }
142
143 async fetch() {
144 return new Response("The Artifacts binding for self-hosted g1t. Bind to it; do not browse it.", {
145 status: 404,
146 });
147 }
148}