Pre-launch — Gluecron is in final validation. Public signups and git hosting for non-owner users open after launch review.
CodeIssuesPull RequestsActionsSecurityInsightsSettings
✨ AI
More
Blame · Line-by-line history

first-repo-scaffold.ts

Each line is annotated with the commit that last touched it. Click any SHA to jump to that commit and see the surrounding change.

first-repo-scaffold.tsBlame245 lines · 1 contributor
47b1dc7ccantynz-alt1/**
2 * Seed a brand-new repository so it arrives working rather than empty.
3 *
4 * `POST /new` already calls bootstrapRepository(), which creates the DB side —
5 * gate settings, branch protection, labels, a welcome issue. But it creates no
6 * git content, so a new user landed on an empty repository: no commit, no
7 * README, no workflow, and therefore no CI run to be green. Everything the
8 * product does on push had nothing to act on, and the first thing a new user
9 * saw was an empty-state page telling them to go and configure something.
10 *
11 * This writes an initial commit containing:
12 *
13 * README.md what the repo is, and what the platform
14 * already did to it
15 * .gluecron/workflows/ci.yml a workflow that runs on push and passes
16 *
17 * ...then calls the same syncAndEnqueuePushWorkflows() the git post-receive
18 * hook calls. That matters: these files are written through the git plumbing
19 * API rather than an actual push, so the post-receive hook never fires and
20 * the workflow would otherwise sit undiscovered on disk. Calling the shared
21 * seam means scaffolded repos take exactly the same path as pushed ones.
22 *
23 * Best-effort throughout. A repository that exists but has no README is a
24 * cosmetic disappointment; a failed repo creation is a broken product. So
25 * every step is caught and reported, never thrown.
26 */
27
28export interface ScaffoldResult {
29 readmeCommitted: boolean;
30 workflowCommitted: boolean;
31 /** Workflows discovered and upserted by the shared sync seam. */
32 workflowsSynced: number;
33 /** CI runs enqueued — this is what makes the repo show a green check. */
34 runsEnqueued: number;
5d13cf5ccantynz-alt35 /** Code files embedded, so semantic search works from the first minute. */
36 filesIndexed: number;
47b1dc7ccantynz-alt37 errors: string[];
38}
39
40/** The CI workflow every new repo starts with. Deliberately trivial and
41 * dependency-free so it passes on a bare runner: the point is to prove the
42 * pipeline works end to end, not to guess the user's stack. */
43function ciWorkflow(repoName: string): string {
44 return `name: CI
45
46on:
47 - push
48
49jobs:
50 verify:
51 runs-on: ubuntu-latest
52 steps:
53 - name: Say hello
54 run: echo "CI is running for ${repoName}"
55 - name: Check the workspace exists
56 run: ls -a
57`;
58}
59
60function readme(owner: string, repoName: string): string {
61 return `# ${repoName}
62
63Created on Gluecron. This repository was set up for you — nothing here needed
64configuring.
65
66## What is already switched on
67
68- **Branch protection** on the default branch
69- **Secret + security scanning** on every push
70- **AI review** on every pull request
71- **CI** — see \`.gluecron/workflows/ci.yml\`, which ran when this commit landed
72
73## Next
74
75Push some code:
76
77\`\`\`bash
78git clone https://gluecron.com/${owner}/${repoName}.git
79cd ${repoName}
80# ...
81git push
82\`\`\`
83
84Every push is scanned, reviewed and gated automatically.
85`;
86}
87
88/**
89 * Injectable dependencies.
90 *
91 * Deliberately mirrors the pattern in push-workflow-sync.ts. Both of these
92 * modules are imported by dozens of unrelated test files, so a
93 * `mock.module()` on either leaks a global mock across the entire test run —
94 * mocking them from this file's tests broke six tests in
95 * push-workflow-sync.test.ts, which is precisely what that file's header
96 * comment warns about.
97 */
98export interface ScaffoldDeps {
99 createOrUpdateFileOnBranch: (input: {
100 owner: string;
101 name: string;
102 branch: string;
103 filePath: string;
104 bytes: Uint8Array;
105 message: string;
106 authorName: string;
107 authorEmail: string;
108 }) => Promise<
109 { commitSha: string; blobSha: string; parentSha: string | null } | { error: string }
110 >;
111 syncAndEnqueuePushWorkflows: (opts: {
112 owner: string;
113 repo: string;
114 repositoryId: string;
115 branch: string;
116 commitSha: string;
117 triggeredBy?: string | null;
118 }) => Promise<{ synced: number; enqueued: number; errors: string[] }>;
119}
120
121async function realDeps(): Promise<ScaffoldDeps> {
122 const { createOrUpdateFileOnBranch } = await import("../git/repository");
123 const { syncAndEnqueuePushWorkflows } = await import("./push-workflow-sync");
124 return {
125 createOrUpdateFileOnBranch: createOrUpdateFileOnBranch as ScaffoldDeps["createOrUpdateFileOnBranch"],
126 syncAndEnqueuePushWorkflows,
127 };
128}
129
130export async function scaffoldFirstRepo(
131 opts: {
132 owner: string;
133 repoName: string;
134 repositoryId: string;
135 defaultBranch: string;
136 authorName: string;
137 authorEmail: string;
138 userId: string;
139 },
140 injected?: ScaffoldDeps
141): Promise<ScaffoldResult> {
142 const result: ScaffoldResult = {
143 readmeCommitted: false,
144 workflowCommitted: false,
145 workflowsSynced: 0,
146 runsEnqueued: 0,
5d13cf5ccantynz-alt147 filesIndexed: 0,
47b1dc7ccantynz-alt148 errors: [],
149 };
150
151 const deps = injected ?? (await realDeps());
152 const { createOrUpdateFileOnBranch } = deps;
153 const enc = new TextEncoder();
154 let headSha: string | null = null;
155
156 const write = async (filePath: string, body: string, message: string) => {
157 const res = await createOrUpdateFileOnBranch({
158 owner: opts.owner,
159 name: opts.repoName,
160 branch: opts.defaultBranch,
161 filePath,
162 bytes: enc.encode(body),
163 message,
164 authorName: opts.authorName,
165 authorEmail: opts.authorEmail,
166 });
167 if ("error" in res) throw new Error(`${filePath}: ${res.error}`);
168 return res.commitSha;
169 };
170
171 try {
172 headSha = await write(
173 "README.md",
174 readme(opts.owner, opts.repoName),
175 "Add README"
176 );
177 result.readmeCommitted = true;
178 } catch (err) {
179 result.errors.push(
180 `readme: ${err instanceof Error ? err.message : String(err)}`
181 );
182 }
183
184 try {
185 headSha = await write(
186 ".gluecron/workflows/ci.yml",
187 ciWorkflow(opts.repoName),
188 "Add CI workflow"
189 );
190 result.workflowCommitted = true;
191 } catch (err) {
192 result.errors.push(
193 `workflow: ${err instanceof Error ? err.message : String(err)}`
194 );
195 }
196
197 // Discover + enqueue via the SAME seam post-receive uses. Without this the
198 // workflow file exists on disk but no `workflows` row and no run are ever
199 // created, because writing through the plumbing API does not fire the git
200 // hook — the repo would look configured and do nothing.
201 if (result.workflowCommitted && headSha) {
202 try {
203 const sync = await deps.syncAndEnqueuePushWorkflows({
204 owner: opts.owner,
205 repo: opts.repoName,
206 repositoryId: opts.repositoryId,
207 branch: opts.defaultBranch,
208 commitSha: headSha,
209 triggeredBy: opts.userId,
210 });
211 result.workflowsSynced = sync.synced ?? 0;
212 result.runsEnqueued = sync.enqueued ?? 0;
213 } catch (err) {
214 result.errors.push(
215 `sync: ${err instanceof Error ? err.message : String(err)}`
216 );
217 }
218 }
219
5d13cf5ccantynz-alt220 // Semantic index. Same reason the workflow sync is here: indexChangedFiles()
221 // runs only from the git post-receive hook, and these files were written
222 // through the plumbing API. Without this the repo's semantic search, repo
223 // chat and "ask this repo" return nothing — and an empty index is
224 // indistinguishable from "no matches", so nobody notices.
225 if (headSha) {
226 try {
227 const { indexExistingRepo } = await import("./index-existing-repo");
228 const idx = await indexExistingRepo({
229 repositoryId: opts.repositoryId,
230 owner: opts.owner,
231 repoName: opts.repoName,
232 ref: opts.defaultBranch,
233 commitSha: headSha,
234 });
235 result.filesIndexed = idx.indexed;
236 if (idx.error) result.errors.push(`index: ${idx.error}`);
237 } catch (err) {
238 result.errors.push(
239 `index: ${err instanceof Error ? err.message : String(err)}`
240 );
241 }
242 }
243
47b1dc7ccantynz-alt244 return result;
245}