create-svc 0.1.77 → 0.1.79

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-svc",
3
- "version": "0.1.77",
3
+ "version": "0.1.79",
4
4
  "description": "Local microservice bootstrap CLI for Cloud Run and Workers services with Neon-backed data.",
5
5
  "module": "index.ts",
6
6
  "type": "module",
@@ -1,6 +1,7 @@
1
1
  import { existsSync } from "node:fs";
2
2
  import { readFile, writeFile } from "node:fs/promises";
3
3
  import { dirname } from "node:path";
4
+ import { protectMainBranch } from "./github-protection";
4
5
 
5
6
  export type GitBootstrapConfig = {
6
7
  enabled: boolean;
@@ -50,6 +51,7 @@ export async function bootstrapGitHubRepository(targetDir: string, config: GitBo
50
51
  run(["gh", "repo", "create", repository, "--private", "--source", ".", "--remote", "origin", "--push"], targetDir, undefined, {
51
52
  quiet: true,
52
53
  });
54
+ protectMainBranch({ repo: repository, cwd: targetDir });
53
55
 
54
56
  return {
55
57
  status: "created",
@@ -0,0 +1,196 @@
1
+ type CommandOptions = {
2
+ cwd?: string;
3
+ input?: string;
4
+ };
5
+
6
+ type CommandResult = {
7
+ success: boolean;
8
+ stdout: string;
9
+ stderr: string;
10
+ exitCode: number;
11
+ };
12
+
13
+ export type CommandRunner = (command: string, args: string[], options?: CommandOptions) => CommandResult;
14
+
15
+ export type ProtectionOptions = {
16
+ repo?: string;
17
+ branch?: string;
18
+ requiredChecks?: string[];
19
+ cwd?: string;
20
+ runner?: CommandRunner;
21
+ };
22
+
23
+ export type BranchProtectionRequest = {
24
+ required_status_checks: {
25
+ strict: boolean;
26
+ contexts: string[];
27
+ };
28
+ enforce_admins: boolean;
29
+ required_pull_request_reviews: {
30
+ dismiss_stale_reviews: boolean;
31
+ required_approving_review_count: number;
32
+ };
33
+ restrictions: null;
34
+ required_linear_history: boolean;
35
+ allow_force_pushes: boolean;
36
+ allow_deletions: boolean;
37
+ block_creations: boolean;
38
+ required_conversation_resolution: boolean;
39
+ lock_branch: boolean;
40
+ allow_fork_syncing: boolean;
41
+ };
42
+
43
+ const DEFAULT_BRANCH = "main";
44
+ const DEFAULT_REQUIRED_CHECKS = ["test", "deploy"];
45
+ const decoder = new TextDecoder();
46
+ const encoder = new TextEncoder();
47
+
48
+ export function parseProtectMainArgs(argv: string[]) {
49
+ const parsed: Pick<ProtectionOptions, "repo" | "branch"> = {};
50
+
51
+ for (let index = 0; index < argv.length; index += 1) {
52
+ const token = argv[index];
53
+ if (!token) {
54
+ continue;
55
+ }
56
+
57
+ const next = argv[index + 1];
58
+ const readValue = () => {
59
+ if (!next || next.startsWith("-")) {
60
+ throw new Error(`Missing value for ${token}`);
61
+ }
62
+ index += 1;
63
+ return next;
64
+ };
65
+
66
+ if (token === "--repo") {
67
+ parsed.repo = readValue();
68
+ continue;
69
+ }
70
+
71
+ if (token.startsWith("--repo=")) {
72
+ parsed.repo = token.slice("--repo=".length);
73
+ continue;
74
+ }
75
+
76
+ if (token === "--branch") {
77
+ parsed.branch = readValue();
78
+ continue;
79
+ }
80
+
81
+ if (token.startsWith("--branch=")) {
82
+ parsed.branch = token.slice("--branch=".length);
83
+ continue;
84
+ }
85
+
86
+ throw new Error(`Unknown argument for protect-main: ${token}`);
87
+ }
88
+
89
+ return parsed;
90
+ }
91
+
92
+ export function protectMainBranch(options: ProtectionOptions = {}) {
93
+ const runner = options.runner ?? run;
94
+ const repo = normalizeRepo(options.repo ?? process.env.GITHUB_REPOSITORY ?? discoverRepo(runner, options.cwd));
95
+ const branch = options.branch ?? DEFAULT_BRANCH;
96
+ const requiredChecks = options.requiredChecks ?? DEFAULT_REQUIRED_CHECKS;
97
+ const request = buildBranchProtectionRequest(requiredChecks);
98
+ const endpoint = `/repos/${repo}/branches/${branch}/protection`;
99
+ const result = runner("gh", ["api", "--method", "PUT", endpoint, "--input", "-"], {
100
+ cwd: options.cwd,
101
+ input: `${JSON.stringify(request)}\n`,
102
+ });
103
+
104
+ if (!result.success) {
105
+ throw new Error(formatProtectionFailure(repo, branch, result));
106
+ }
107
+
108
+ return {
109
+ repo,
110
+ branch,
111
+ requiredChecks,
112
+ };
113
+ }
114
+
115
+ export function buildBranchProtectionRequest(requiredChecks = DEFAULT_REQUIRED_CHECKS): BranchProtectionRequest {
116
+ return {
117
+ required_status_checks: {
118
+ strict: true,
119
+ contexts: requiredChecks,
120
+ },
121
+ enforce_admins: true,
122
+ required_pull_request_reviews: {
123
+ dismiss_stale_reviews: true,
124
+ required_approving_review_count: 1,
125
+ },
126
+ restrictions: null,
127
+ required_linear_history: false,
128
+ allow_force_pushes: false,
129
+ allow_deletions: false,
130
+ block_creations: false,
131
+ required_conversation_resolution: true,
132
+ lock_branch: false,
133
+ allow_fork_syncing: true,
134
+ };
135
+ }
136
+
137
+ export function formatProtectionFailure(repo: string, branch: string, result: CommandResult) {
138
+ const details = [result.stderr, result.stdout].filter(Boolean).join("\n");
139
+ const permissionHint = isPermissionFailure(details)
140
+ ? [
141
+ "",
142
+ "The authenticated GitHub token must have repository administration permission for this generated service repo.",
143
+ "Grant repo admin access or a fine-grained token with Administration: write, then rerun `service protect-main`.",
144
+ ].join("\n")
145
+ : "";
146
+
147
+ return [`Failed to reconcile ${branch} branch protection for ${repo}.`, details, permissionHint].filter(Boolean).join("\n");
148
+ }
149
+
150
+ function discoverRepo(runner: CommandRunner, cwd?: string) {
151
+ const result = runner("git", ["config", "--get", "remote.origin.url"], { cwd });
152
+ if (!result.success || !result.stdout) {
153
+ throw new Error("Unable to determine GitHub repo. Pass --repo owner/name or set GITHUB_REPOSITORY.");
154
+ }
155
+ return result.stdout;
156
+ }
157
+
158
+ function normalizeRepo(value: string) {
159
+ const trimmed = value.trim();
160
+ const sshMatch = trimmed.match(/^git@github\.com:([^/]+\/[^/]+?)(?:\.git)?$/);
161
+ if (sshMatch?.[1]) {
162
+ return sshMatch[1];
163
+ }
164
+
165
+ const urlMatch = trimmed.match(/^https:\/\/github\.com\/([^/]+\/[^/]+?)(?:\.git)?$/);
166
+ if (urlMatch?.[1]) {
167
+ return urlMatch[1];
168
+ }
169
+
170
+ if (/^[^/\s]+\/[^/\s]+$/.test(trimmed)) {
171
+ return trimmed.replace(/\.git$/, "");
172
+ }
173
+
174
+ throw new Error(`Invalid GitHub repo: ${value}. Expected owner/name.`);
175
+ }
176
+
177
+ function isPermissionFailure(details: string) {
178
+ return /403|404|admin|administration|resource not accessible|not found|forbidden/i.test(details);
179
+ }
180
+
181
+ function run(command: string, args: string[], options: CommandOptions = {}): CommandResult {
182
+ const result = Bun.spawnSync([command, ...args], {
183
+ cwd: options.cwd ?? process.cwd(),
184
+ env: process.env,
185
+ stdin: options.input === undefined ? undefined : encoder.encode(options.input),
186
+ stdout: "pipe",
187
+ stderr: "pipe",
188
+ });
189
+
190
+ return {
191
+ success: result.success,
192
+ stdout: result.stdout ? decoder.decode(result.stdout).trim() : "",
193
+ stderr: result.stderr ? decoder.decode(result.stderr).trim() : "",
194
+ exitCode: result.exitCode,
195
+ };
196
+ }
@@ -195,6 +195,7 @@ test("scaffolds all runtime/framework variants with shared cloudrun config", asy
195
195
  expect(packageJson).toContain('"migrate": "service migrate"');
196
196
  expect(packageJson).toContain('"create": "service create"');
197
197
  expect(packageJson).toContain('"deploy": "service deploy"');
198
+ expect(packageJson).toContain('"protect-main": "service protect-main"');
198
199
  expect(packageJson).toContain('"destroy": "service destroy"');
199
200
 
200
201
  const mainGo = await Bun.file(join(generatedRoot, "cmd", "server", "main.go")).text();
@@ -224,6 +225,8 @@ test("scaffolds all runtime/framework variants with shared cloudrun config", asy
224
225
  expect(makefile).toContain("bun run ./scripts/ensure-local-db.ts");
225
226
  expect(makefile).toContain("bun run ./scripts/wait-for-db.ts");
226
227
  expect(makefile).toContain("bun run ./scripts/dev.ts go run ./cmd/server --worker go run ./cmd/worker");
228
+ expect(makefile).toContain("protect-main:");
229
+ expect(makefile).toContain("$(SERVICE) protect-main");
227
230
  expect(await Bun.file(join(generatedRoot, "atlas.hcl")).exists()).toBeTrue();
228
231
  const atlasConfig = await Bun.file(join(generatedRoot, "atlas.hcl")).text();
229
232
  expect(atlasConfig).toContain('revisions_schema = "public"');
@@ -241,6 +244,7 @@ test("scaffolds all runtime/framework variants with shared cloudrun config", asy
241
244
  expect(packageJson).toContain('"migrate": "service migrate"');
242
245
  expect(packageJson).toContain('"create": "service create"');
243
246
  expect(packageJson).toContain('"deploy": "service deploy"');
247
+ expect(packageJson).toContain('"protect-main": "service protect-main"');
244
248
  expect(packageJson).toContain('"dashboards": "service dashboards"');
245
249
  expect(packageJson).toContain('"observability-bootstrap": "service observability-bootstrap"');
246
250
  expect(packageJson).toContain('"auth": "service auth"');
@@ -260,6 +264,7 @@ test("scaffolds all runtime/framework variants with shared cloudrun config", asy
260
264
  expect(makefile).toContain("SERVICE := service");
261
265
  expect(makefile).toContain("dashboards:");
262
266
  expect(makefile).toContain("observability-bootstrap:");
267
+ expect(makefile).toContain("protect-main:");
263
268
  expect(makefile).toContain("auth:");
264
269
  expect(makefile).toContain("bun run dev");
265
270
  const devScript = await Bun.file(join(generatedRoot, "scripts", "dev.ts")).text();
@@ -295,6 +300,9 @@ test("scaffolds all runtime/framework variants with shared cloudrun config", asy
295
300
  expect(readme).toContain("verifies JWT bearer tokens");
296
301
  expect(readme).toContain("prod/apps/auth/authctl/cloudflare-access");
297
302
  expect(readme).toContain("service auth resource-server");
303
+ expect(readme).toContain("GitHub main branch protection");
304
+ expect(readme).toContain("service create");
305
+ expect(readme).toContain("service protect-main");
298
306
  }
299
307
 
300
308
  }
@@ -329,6 +337,8 @@ test("scaffolds a backend package cleanly into a nested monorepo-style directory
329
337
  expect(readme).toContain("waitlist/launch service");
330
338
  expect(readme).not.toContain("Neon main, preview, and personal branch provisioning");
331
339
  expect(readme).toContain("GitHub Actions deployment");
340
+ expect(readme).toContain("GitHub main branch protection");
341
+ expect(readme).toContain("service protect-main");
332
342
  expect(readme).toContain(".github/workflows/preview.yml");
333
343
  expect(readme).toContain(".github/workflows/deploy.yml");
334
344
  expect(readme).toContain("/deploy preview");
@@ -367,6 +377,7 @@ test("scaffolds the workers target with wrangler lifecycle commands", async () =
367
377
  expect(packageJson).toContain('"@anmho/authctl": "0.1.1"');
368
378
  expect(packageJson).toContain('"dev": "bun run ./scripts/dev.ts wrangler dev --ip 127.0.0.1 --port 8787 --show-interactive-dev-session=false"');
369
379
  expect(packageJson).toContain('"service": "service"');
380
+ expect(packageJson).toContain('"protect-main": "service protect-main"');
370
381
  expect(packageJson).toContain('"auth": "service auth"');
371
382
  expect(packageJson).toContain('"wrangler"');
372
383
  expect(packageJson).toContain('"pg"');
@@ -413,6 +424,7 @@ test("scaffolds the workers target with wrangler lifecycle commands", async () =
413
424
  const makefile = await Bun.file(join(generatedRoot, "Makefile")).text();
414
425
  expect(makefile).toContain('no generated code for workers');
415
426
  expect(makefile).toContain("auth:");
427
+ expect(makefile).toContain("protect-main:");
416
428
  expect(makefile).not.toContain("scripts/codegen.ts");
417
429
 
418
430
  expect(await Bun.file(join(generatedRoot, "scripts", "authctl.ts")).exists()).toBeFalse();
package/src/scaffold.ts CHANGED
@@ -247,6 +247,7 @@ function buildReplacements(config: ScaffoldConfig) {
247
247
  COMMAND_DEV_DOWN: "service dev down",
248
248
  COMMAND_BOOTSTRAP: "service create",
249
249
  COMMAND_DEPLOY: "service deploy",
250
+ COMMAND_PROTECT_MAIN: "service protect-main",
250
251
  COMMAND_OBSERVABILITY_BOOTSTRAP:
251
252
  config.runtime === "bun" ? "bun run observability-bootstrap" : "make observability-bootstrap",
252
253
  WORKFLOW_DEPLOY_MAIN_COMMAND: "service deploy --ci",
@@ -31,7 +31,7 @@ describe("local dev cleanup", () => {
31
31
  });
32
32
 
33
33
  test("stops service-owned listeners when the pid file is missing", async () => {
34
- if (!Bun.which("lsof")) {
34
+ if (!Bun.which("lsof") && !(process.platform === "linux" && Bun.which("fuser"))) {
35
35
  return;
36
36
  }
37
37
 
@@ -126,6 +126,10 @@ async function isReachable(port: number) {
126
126
  }
127
127
 
128
128
  function listenerHasPid(port: number, pid: number) {
129
+ if (!Bun.which("lsof")) {
130
+ return false;
131
+ }
132
+
129
133
  const result = Bun.spawnSync(["lsof", "-nP", `-iTCP:${port}`, "-sTCP:LISTEN", "-Fp"], {
130
134
  stdout: "pipe",
131
135
  stderr: "pipe",
@@ -144,10 +144,6 @@ function waitForExit(pid: number, timeoutMs: number) {
144
144
  }
145
145
 
146
146
  function findServicePortProcesses(root: string, ports: number[], pidFromFile?: number): PortProcess[] {
147
- if (!Bun.which("lsof")) {
148
- return [];
149
- }
150
-
151
147
  const resolvedRoot = realpath(root);
152
148
  const rootWithSlash = resolvedRoot.endsWith("/") ? resolvedRoot : `${resolvedRoot}/`;
153
149
  const seen = new Set<string>();
@@ -180,6 +176,21 @@ function realpath(path: string) {
180
176
  }
181
177
 
182
178
  function listeningPids(port: number) {
179
+ const pids = new Set<number>();
180
+ for (const pid of listeningPidsFromLsof(port)) {
181
+ pids.add(pid);
182
+ }
183
+ for (const pid of listeningPidsFromFuser(port)) {
184
+ pids.add(pid);
185
+ }
186
+ return [...pids];
187
+ }
188
+
189
+ function listeningPidsFromLsof(port: number) {
190
+ if (!Bun.which("lsof")) {
191
+ return [];
192
+ }
193
+
183
194
  const result = Bun.spawnSync(["lsof", "-nP", `-iTCP:${port}`, "-sTCP:LISTEN", "-Fp"], {
184
195
  stdout: "pipe",
185
196
  stderr: "pipe",
@@ -194,6 +205,26 @@ function listeningPids(port: number) {
194
205
  .filter((pid): pid is number => Boolean(pid && Number.isFinite(pid)));
195
206
  }
196
207
 
208
+ function listeningPidsFromFuser(port: number) {
209
+ if (process.platform !== "linux" || !Bun.which("fuser")) {
210
+ return [];
211
+ }
212
+
213
+ const result = Bun.spawnSync(["fuser", "-n", "tcp", String(port)], {
214
+ stdout: "pipe",
215
+ stderr: "pipe",
216
+ });
217
+ if (!result.success) {
218
+ return [];
219
+ }
220
+ return decoder
221
+ .decode(result.stdout)
222
+ .trim()
223
+ .split(/\s+/)
224
+ .map((value) => Number.parseInt(value, 10))
225
+ .filter((pid): pid is number => Boolean(pid && Number.isFinite(pid)));
226
+ }
227
+
197
228
  function processCwd(pid: number) {
198
229
  if (process.platform === "linux") {
199
230
  const procCwd = realpath(`/proc/${pid}/cwd`);
@@ -30,6 +30,7 @@ test("createSvcVersion reports the package version", async () => {
30
30
  test("formatOutsideServiceCommandError rejects repo-local commands outside generated services", () => {
31
31
  expect(formatOutsideServiceCommandError("destroy")).toContain("service destroy must be run inside a generated service repo");
32
32
  expect(formatOutsideServiceCommandError("deploy")).toContain("No service.jsonc was found");
33
+ expect(formatOutsideServiceCommandError("protect-main")).toContain("service protect-main must be run inside a generated service repo");
33
34
  });
34
35
 
35
36
  test("formatOutsideServiceCommandError does not treat positional names as scaffold commands", () => {
@@ -66,3 +67,9 @@ test("generatedServiceCommandHelp intercepts deploy help before side effects", (
66
67
  expect(generatedServiceCommandHelp(["deploy"])).toBeUndefined();
67
68
  expect(generatedServiceCommandHelp(["destroy", "--help"])).toBeUndefined();
68
69
  });
70
+
71
+ test("generatedServiceCommandHelp intercepts protect-main help before side effects", () => {
72
+ expect(generatedServiceCommandHelp(["protect-main", "--help"])).toContain("service protect-main");
73
+ expect(generatedServiceCommandHelp(["protect-main", "-h"])).toContain("--repo owner/name");
74
+ expect(generatedServiceCommandHelp(["protect-main"])).toBeUndefined();
75
+ });
package/src/service.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { existsSync, readFileSync } from "node:fs";
2
2
  import { dirname, join } from "node:path";
3
3
  import { formatScaffoldHelp, run as runScaffoldCli } from "./cli";
4
+ import { parseProtectMainArgs, protectMainBranch } from "./github-protection";
4
5
  import { parseJsonc } from "./jsonc";
5
6
 
6
7
  const SCAFFOLD_COMMANDS = new Set(["create", "new", "init"]);
@@ -14,6 +15,7 @@ const GENERATED_SERVICE_COMMANDS = new Set([
14
15
  "dns",
15
16
  "doctor",
16
17
  "migrate",
18
+ "protect-main",
17
19
  "sdk",
18
20
  "seed",
19
21
  ]);
@@ -109,7 +111,25 @@ async function delegateToGeneratedService(serviceRoot: string, argv: string[]) {
109
111
  process.chdir(serviceRoot);
110
112
  process.env.CREATE_SVC_SERVICE_ROOT = serviceRoot;
111
113
 
112
- const serviceConfig = parseJsonc(await Bun.file(join(serviceRoot, "service.jsonc")).text()) as { target?: string };
114
+ const serviceConfig = parseJsonc(await Bun.file(join(serviceRoot, "service.jsonc")).text()) as {
115
+ service_id?: string;
116
+ target?: string;
117
+ git?: {
118
+ owner?: string;
119
+ repository?: string;
120
+ };
121
+ };
122
+ if (argv[0] === "protect-main") {
123
+ const protectionArgs = parseProtectMainArgs(argv.slice(1));
124
+ const result = protectMainBranch({
125
+ repo: protectionArgs.repo ?? repoFromServiceConfig(serviceConfig),
126
+ branch: protectionArgs.branch,
127
+ cwd: serviceRoot,
128
+ });
129
+ console.log(`Protected ${result.repo} ${result.branch} with required checks: ${result.requiredChecks.join(", ")}`);
130
+ return;
131
+ }
132
+
113
133
  if (serviceConfig.target === "workers") {
114
134
  const { main } = await import("./service-runtime/workers/cli");
115
135
  await main(argv);
@@ -120,6 +140,15 @@ async function delegateToGeneratedService(serviceRoot: string, argv: string[]) {
120
140
  await main(argv);
121
141
  }
122
142
 
143
+ function repoFromServiceConfig(serviceConfig: { service_id?: string; git?: { owner?: string; repository?: string } }) {
144
+ const owner = serviceConfig.git?.owner || "anmho";
145
+ const repository = serviceConfig.git?.repository || serviceConfig.service_id;
146
+ if (!repository) {
147
+ throw new Error("service.jsonc is missing git.repository and service_id; pass --repo owner/name.");
148
+ }
149
+ return `${owner}/${repository}`;
150
+ }
151
+
123
152
  export function generatedDependenciesInstalled(serviceRoot: string) {
124
153
  return !existsSync(join(serviceRoot, "package.json")) || existsSync(join(serviceRoot, "node_modules"));
125
154
  }
@@ -146,6 +175,15 @@ function ensureGeneratedDependencies(serviceRoot: string) {
146
175
 
147
176
  export function generatedServiceCommandHelp(argv: string[]) {
148
177
  const [command, ...rest] = argv;
178
+ if (command === "protect-main" && hasHelpFlag(rest)) {
179
+ return [
180
+ "Usage:",
181
+ " service protect-main [--repo owner/name] [--branch main]",
182
+ "",
183
+ "Reconciles generated service branch protection with required test and deploy checks.",
184
+ ].join("\n");
185
+ }
186
+
149
187
  if (command !== "deploy" || !hasHelpFlag(rest)) {
150
188
  return undefined;
151
189
  }
@@ -31,6 +31,7 @@ console to create and deploy.
31
31
  {{COMMAND_TEST}}
32
32
  {{COMMAND_BOOTSTRAP}}
33
33
  {{COMMAND_DEPLOY}}
34
+ {{COMMAND_PROTECT_MAIN}}
34
35
  {{COMMAND_DEV_DOWN}}
35
36
  {{COMMAND_AUTH_RESOURCE}}
36
37
  {{COMMAND_AUTH_CLIENT}}
@@ -145,6 +146,16 @@ gcloud auth application-default login
145
146
 
146
147
  The generated backend scripts still use `gcloud` as the primary control plane even when ADC is present.
147
148
 
149
+ ## GitHub main branch protection
150
+
151
+ Generated repositories include GitHub Actions workflows for CI and production deploys. `service create` reconciles `main` branch protection after creating and pushing the GitHub repository. To rerun reconciliation for an existing generated repo:
152
+
153
+ ```bash
154
+ {{COMMAND_PROTECT_MAIN}}
155
+ ```
156
+
157
+ The protection requires the generated `test` and `deploy` status checks, pull requests, stale-review dismissal, conversation resolution, and admin enforcement. If GitHub permissions are missing, rerun the command with a token that has repo admin access or fine-grained `Administration: write`.
158
+
148
159
  Go variants use Atlas for migrations:
149
160
 
150
161
  ```bash
@@ -1,4 +1,4 @@
1
- .PHONY: dev migrate gen lint test create deploy dashboards auth destroy
1
+ .PHONY: dev migrate gen lint test create deploy protect-main dashboards auth destroy
2
2
 
3
3
  SERVICE := service
4
4
 
@@ -23,6 +23,9 @@ create:
23
23
  deploy:
24
24
  $(SERVICE) deploy $(ARGS)
25
25
 
26
+ protect-main:
27
+ $(SERVICE) protect-main $(ARGS)
28
+
26
29
  dashboards:
27
30
  $(SERVICE) dashboards
28
31
 
@@ -22,6 +22,7 @@ bun run trigger -- --help
22
22
  bun run trigger:dev
23
23
  service create
24
24
  service deploy
25
+ {{COMMAND_PROTECT_MAIN}}
25
26
  bun run dashboards
26
27
  bun run doctor
27
28
  bun run destroy
@@ -70,6 +71,16 @@ Worker deploy and fail clearly if these values are missing.
70
71
  GitHub Actions deploys require matching repository secrets:
71
72
  `TRIGGER_PROJECT_REF`, `TRIGGER_ACCESS_TOKEN`, and `TRIGGER_SECRET_KEY`.
72
73
 
74
+ ## GitHub main branch protection
75
+
76
+ Generated repositories include GitHub Actions workflows for CI and production deploys. `service create` reconciles `main` branch protection after creating and pushing the GitHub repository. To rerun reconciliation for an existing generated repo:
77
+
78
+ ```bash
79
+ {{COMMAND_PROTECT_MAIN}}
80
+ ```
81
+
82
+ The protection requires the generated `test` and `deploy` status checks, pull requests, stale-review dismissal, conversation resolution, and admin enforcement. If GitHub permissions are missing, rerun the command with a token that has repo admin access or fine-grained `Administration: write`.
83
+
73
84
  The Trigger.dev CLI is installed in this generated package as a dev dependency
74
85
  from the `trigger.dev` npm package.
75
86
  Use `bun run trigger -- <args>` for ad-hoc commands, `bun run trigger:dev` for
@@ -11,6 +11,7 @@
11
11
  "test": "bun test",
12
12
  "create": "service create",
13
13
  "deploy": "service deploy",
14
+ "protect-main": "service protect-main",
14
15
  "dashboards": "service dashboards",
15
16
  "auth": "service auth",
16
17
  "destroy": "service destroy",
@@ -1,4 +1,4 @@
1
- .PHONY: dev migrate gen lint test create deploy dashboards observability-bootstrap auth destroy
1
+ .PHONY: dev migrate gen lint test create deploy protect-main dashboards observability-bootstrap auth destroy
2
2
 
3
3
  SERVICE := service
4
4
 
@@ -23,6 +23,9 @@ create:
23
23
  deploy:
24
24
  $(SERVICE) deploy $(ARGS)
25
25
 
26
+ protect-main:
27
+ $(SERVICE) protect-main $(ARGS)
28
+
26
29
  dashboards:
27
30
  $(SERVICE) dashboards
28
31
 
@@ -11,6 +11,7 @@
11
11
  "test": "bun test",
12
12
  "create": "service create",
13
13
  "deploy": "service deploy",
14
+ "protect-main": "service protect-main",
14
15
  "dashboards": "service dashboards",
15
16
  "observability-bootstrap": "service observability-bootstrap",
16
17
  "auth": "service auth",
@@ -1,4 +1,4 @@
1
- .PHONY: dev migrate gen lint test create deploy dashboards observability-bootstrap auth destroy
1
+ .PHONY: dev migrate gen lint test create deploy protect-main dashboards observability-bootstrap auth destroy
2
2
 
3
3
  SERVICE := service
4
4
 
@@ -23,6 +23,9 @@ create:
23
23
  deploy:
24
24
  $(SERVICE) deploy $(ARGS)
25
25
 
26
+ protect-main:
27
+ $(SERVICE) protect-main $(ARGS)
28
+
26
29
  dashboards:
27
30
  $(SERVICE) dashboards
28
31
 
@@ -11,6 +11,7 @@
11
11
  "test": "bun test",
12
12
  "create": "service create",
13
13
  "deploy": "service deploy",
14
+ "protect-main": "service protect-main",
14
15
  "dashboards": "service dashboards",
15
16
  "observability-bootstrap": "service observability-bootstrap",
16
17
  "auth": "service auth",
@@ -1,4 +1,4 @@
1
- .PHONY: dev migrate migrate-lint gen lint test create deploy dashboards observability-bootstrap auth destroy
1
+ .PHONY: dev migrate migrate-lint gen lint test create deploy protect-main dashboards observability-bootstrap auth destroy
2
2
 
3
3
  SERVICE := service
4
4
  WITH_ENV := set -a; [ ! -f .env.local ] || . ./.env.local; set +a;
@@ -33,6 +33,9 @@ create:
33
33
  deploy:
34
34
  $(SERVICE) deploy $(ARGS)
35
35
 
36
+ protect-main:
37
+ $(SERVICE) protect-main $(ARGS)
38
+
36
39
  dashboards:
37
40
  $(SERVICE) dashboards
38
41
 
@@ -11,6 +11,7 @@
11
11
  "test": "make test",
12
12
  "create": "service create",
13
13
  "deploy": "service deploy",
14
+ "protect-main": "service protect-main",
14
15
  "auth": "service auth",
15
16
  "dashboards": "service dashboards",
16
17
  "destroy": "service destroy"
@@ -1,4 +1,4 @@
1
- .PHONY: dev migrate migrate-lint gen lint test create deploy dashboards observability-bootstrap auth destroy
1
+ .PHONY: dev migrate migrate-lint gen lint test create deploy protect-main dashboards observability-bootstrap auth destroy
2
2
 
3
3
  SERVICE := service
4
4
  WITH_ENV := set -a; [ ! -f .env.local ] || . ./.env.local; set +a;
@@ -34,6 +34,9 @@ create:
34
34
  deploy:
35
35
  $(SERVICE) deploy $(ARGS)
36
36
 
37
+ protect-main:
38
+ $(SERVICE) protect-main $(ARGS)
39
+
37
40
  dashboards:
38
41
  $(SERVICE) dashboards
39
42
 
@@ -11,6 +11,7 @@
11
11
  "test": "make test",
12
12
  "create": "service create",
13
13
  "deploy": "service deploy",
14
+ "protect-main": "service protect-main",
14
15
  "auth": "service auth",
15
16
  "dashboards": "service dashboards",
16
17
  "destroy": "service destroy"