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.tsBlame218 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;
35 errors: string[];
36}
37
38/** The CI workflow every new repo starts with. Deliberately trivial and
39 * dependency-free so it passes on a bare runner: the point is to prove the
40 * pipeline works end to end, not to guess the user's stack. */
41function ciWorkflow(repoName: string): string {
42 return `name: CI
43
44on:
45 - push
46
47jobs:
48 verify:
49 runs-on: ubuntu-latest
50 steps:
51 - name: Say hello
52 run: echo "CI is running for ${repoName}"
53 - name: Check the workspace exists
54 run: ls -a
55`;
56}
57
58function readme(owner: string, repoName: string): string {
59 return `# ${repoName}
60
61Created on Gluecron. This repository was set up for you — nothing here needed
62configuring.
63
64## What is already switched on
65
66- **Branch protection** on the default branch
67- **Secret + security scanning** on every push
68- **AI review** on every pull request
69- **CI** — see \`.gluecron/workflows/ci.yml\`, which ran when this commit landed
70
71## Next
72
73Push some code:
74
75\`\`\`bash
76git clone https://gluecron.com/${owner}/${repoName}.git
77cd ${repoName}
78# ...
79git push
80\`\`\`
81
82Every push is scanned, reviewed and gated automatically.
83`;
84}
85
86/**
87 * Injectable dependencies.
88 *
89 * Deliberately mirrors the pattern in push-workflow-sync.ts. Both of these
90 * modules are imported by dozens of unrelated test files, so a
91 * `mock.module()` on either leaks a global mock across the entire test run —
92 * mocking them from this file's tests broke six tests in
93 * push-workflow-sync.test.ts, which is precisely what that file's header
94 * comment warns about.
95 */
96export interface ScaffoldDeps {
97 createOrUpdateFileOnBranch: (input: {
98 owner: string;
99 name: string;
100 branch: string;
101 filePath: string;
102 bytes: Uint8Array;
103 message: string;
104 authorName: string;
105 authorEmail: string;
106 }) => Promise<
107 { commitSha: string; blobSha: string; parentSha: string | null } | { error: string }
108 >;
109 syncAndEnqueuePushWorkflows: (opts: {
110 owner: string;
111 repo: string;
112 repositoryId: string;
113 branch: string;
114 commitSha: string;
115 triggeredBy?: string | null;
116 }) => Promise<{ synced: number; enqueued: number; errors: string[] }>;
117}
118
119async function realDeps(): Promise<ScaffoldDeps> {
120 const { createOrUpdateFileOnBranch } = await import("../git/repository");
121 const { syncAndEnqueuePushWorkflows } = await import("./push-workflow-sync");
122 return {
123 createOrUpdateFileOnBranch: createOrUpdateFileOnBranch as ScaffoldDeps["createOrUpdateFileOnBranch"],
124 syncAndEnqueuePushWorkflows,
125 };
126}
127
128export async function scaffoldFirstRepo(
129 opts: {
130 owner: string;
131 repoName: string;
132 repositoryId: string;
133 defaultBranch: string;
134 authorName: string;
135 authorEmail: string;
136 userId: string;
137 },
138 injected?: ScaffoldDeps
139): Promise<ScaffoldResult> {
140 const result: ScaffoldResult = {
141 readmeCommitted: false,
142 workflowCommitted: false,
143 workflowsSynced: 0,
144 runsEnqueued: 0,
145 errors: [],
146 };
147
148 const deps = injected ?? (await realDeps());
149 const { createOrUpdateFileOnBranch } = deps;
150 const enc = new TextEncoder();
151 let headSha: string | null = null;
152
153 const write = async (filePath: string, body: string, message: string) => {
154 const res = await createOrUpdateFileOnBranch({
155 owner: opts.owner,
156 name: opts.repoName,
157 branch: opts.defaultBranch,
158 filePath,
159 bytes: enc.encode(body),
160 message,
161 authorName: opts.authorName,
162 authorEmail: opts.authorEmail,
163 });
164 if ("error" in res) throw new Error(`${filePath}: ${res.error}`);
165 return res.commitSha;
166 };
167
168 try {
169 headSha = await write(
170 "README.md",
171 readme(opts.owner, opts.repoName),
172 "Add README"
173 );
174 result.readmeCommitted = true;
175 } catch (err) {
176 result.errors.push(
177 `readme: ${err instanceof Error ? err.message : String(err)}`
178 );
179 }
180
181 try {
182 headSha = await write(
183 ".gluecron/workflows/ci.yml",
184 ciWorkflow(opts.repoName),
185 "Add CI workflow"
186 );
187 result.workflowCommitted = true;
188 } catch (err) {
189 result.errors.push(
190 `workflow: ${err instanceof Error ? err.message : String(err)}`
191 );
192 }
193
194 // Discover + enqueue via the SAME seam post-receive uses. Without this the
195 // workflow file exists on disk but no `workflows` row and no run are ever
196 // created, because writing through the plumbing API does not fire the git
197 // hook — the repo would look configured and do nothing.
198 if (result.workflowCommitted && headSha) {
199 try {
200 const sync = await deps.syncAndEnqueuePushWorkflows({
201 owner: opts.owner,
202 repo: opts.repoName,
203 repositoryId: opts.repositoryId,
204 branch: opts.defaultBranch,
205 commitSha: headSha,
206 triggeredBy: opts.userId,
207 });
208 result.workflowsSynced = sync.synced ?? 0;
209 result.runsEnqueued = sync.enqueued ?? 0;
210 } catch (err) {
211 result.errors.push(
212 `sync: ${err instanceof Error ? err.message : String(err)}`
213 );
214 }
215 }
216
217 return result;
218}