@nakedev/go-scaffold 0.1.3 → 0.3.0

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.
Files changed (124) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +143 -50
  3. package/dist/commands/auth.js +116 -11
  4. package/dist/commands/create.js +13 -1
  5. package/dist/commands/generate.js +23 -12
  6. package/dist/commands/method.js +94 -17
  7. package/dist/commands/observability.js +114 -0
  8. package/dist/commands/rbac.js +19 -2
  9. package/dist/commands/undo.js +331 -0
  10. package/dist/commands/worker.js +92 -32
  11. package/dist/index.js +368 -64
  12. package/dist/prompts/auth-wizard.js +29 -0
  13. package/dist/prompts/create-wizard.js +5 -2
  14. package/dist/prompts/generate-wizard.js +57 -0
  15. package/dist/prompts/worker-wizard.js +25 -0
  16. package/dist/templates/auth-manifest.js +20 -3
  17. package/dist/templates/create-manifest.js +13 -20
  18. package/dist/templates/module-manifest.js +2 -0
  19. package/dist/templates/observability-manifest.js +24 -0
  20. package/dist/templates/rbac-manifest.js +1 -0
  21. package/dist/templates/worker-manifest.js +23 -6
  22. package/dist/utils/auth-patcher.js +96 -21
  23. package/dist/utils/config.js +58 -10
  24. package/dist/utils/gocheck.js +57 -5
  25. package/dist/utils/golangci-patcher.js +73 -0
  26. package/dist/utils/gomod-patcher.js +53 -0
  27. package/dist/utils/main-patcher.js +58 -4
  28. package/dist/utils/marker-patch.js +125 -3
  29. package/dist/utils/method-patcher.js +97 -18
  30. package/dist/utils/module-location.js +58 -0
  31. package/dist/utils/naming.js +125 -14
  32. package/dist/utils/observability-patcher.js +107 -0
  33. package/dist/utils/openapi-patcher.js +19 -1
  34. package/dist/utils/platform-patcher.js +98 -12
  35. package/dist/utils/rbac-patcher.js +60 -10
  36. package/dist/utils/smoke-run.js +31 -0
  37. package/package.json +11 -4
  38. package/templates/add/auth/internal/app/user/errors.go.hbs +7 -0
  39. package/templates/add/auth/internal/app/user/handler.go.hbs +64 -23
  40. package/templates/add/auth/internal/app/user/jwt.go.hbs +31 -7
  41. package/templates/add/auth/internal/app/user/model/authtoken.go.hbs +39 -0
  42. package/templates/add/auth/internal/app/user/model/identity.go.hbs +3 -0
  43. package/templates/add/auth/internal/app/user/model/loginthrottle.go.hbs +26 -0
  44. package/templates/add/auth/internal/app/user/model/user.go.hbs +9 -1
  45. package/templates/add/auth/internal/app/user/repository.go.hbs +64 -11
  46. package/templates/add/auth/internal/app/user/repository_test.go.hbs +192 -0
  47. package/templates/add/auth/internal/app/user/service.go.hbs +105 -21
  48. package/templates/add/auth/internal/app/user/service_test.go.hbs +81 -2
  49. package/templates/add/auth/internal/app/user/tokenstore.go.hbs +9 -138
  50. package/templates/add/auth/internal/app/user/tokenstore_pg.go.hbs +144 -0
  51. package/templates/add/auth/internal/app/user/tokenstore_redis.go.hbs +147 -0
  52. package/templates/add/auth/internal/shared/middleware/ratelimit.go.hbs +21 -19
  53. package/templates/add/auth/internal/shared/middleware/ratelimit_memory.go.hbs +63 -0
  54. package/templates/add/auth/internal/shared/middleware/ratelimit_redis.go.hbs +32 -0
  55. package/templates/add/auth/migrations/create_auth_tokens.down.sql.hbs +1 -0
  56. package/templates/add/auth/migrations/create_auth_tokens.up.sql.hbs +16 -0
  57. package/templates/add/auth/migrations/create_identities.down.sql.hbs +1 -1
  58. package/templates/add/auth/migrations/create_identities.up.sql.hbs +9 -5
  59. package/templates/add/auth/migrations/create_login_throttle.down.sql.hbs +1 -0
  60. package/templates/add/auth/migrations/create_login_throttle.up.sql.hbs +10 -0
  61. package/templates/add/auth/migrations/create_users.down.sql.hbs +1 -1
  62. package/templates/add/auth/migrations/create_users.up.sql.hbs +13 -2
  63. package/templates/add/rbac/internal/app/role/dto.go.hbs +8 -3
  64. package/templates/add/rbac/internal/app/role/handler.go.hbs +4 -1
  65. package/templates/add/rbac/internal/app/role/model/permission.go.hbs +3 -0
  66. package/templates/add/rbac/internal/app/role/model/role.go.hbs +4 -0
  67. package/templates/add/rbac/internal/app/role/model/role_permission.go.hbs +3 -0
  68. package/templates/add/rbac/internal/app/role/repository.go.hbs +12 -11
  69. package/templates/add/rbac/internal/app/role/repository_test.go.hbs +176 -0
  70. package/templates/add/rbac/internal/app/role/service.go.hbs +7 -0
  71. package/templates/add/rbac/internal/shared/middleware/authz.go.hbs +19 -0
  72. package/templates/add/rbac/internal/shared/middleware/authz_test.go.hbs +1 -1
  73. package/templates/add/rbac/migrations/add_roles.down.sql.hbs +5 -5
  74. package/templates/add/rbac/migrations/add_roles.up.sql.hbs +17 -11
  75. package/templates/add/worker/cmd/worker/main.go.hbs +30 -24
  76. package/templates/add/worker/internal/platform/mail/mail.go.hbs +21 -0
  77. package/templates/add/worker/internal/platform/mail/task.go.hbs +31 -34
  78. package/templates/add/worker/internal/platform/queue/asynq.go.hbs +140 -0
  79. package/templates/add/worker/internal/platform/queue/queue.go.hbs +87 -0
  80. package/templates/add/worker/internal/platform/queue/river.go.hbs +148 -0
  81. package/templates/create/base/.claude/skills/go-scaffold/SKILL.md.hbs +59 -12
  82. package/templates/create/base/.dockerignore.hbs +13 -0
  83. package/templates/create/base/.env.example.hbs +18 -9
  84. package/templates/create/base/.github/dependabot.yml.hbs +20 -0
  85. package/templates/create/base/.github/workflows/ci.yml.hbs +29 -6
  86. package/templates/create/base/.golangci.yml.hbs +27 -0
  87. package/templates/create/base/AGENTS.md.hbs +14 -12
  88. package/templates/create/base/Dockerfile.hbs +42 -0
  89. package/templates/create/base/Makefile.hbs +52 -16
  90. package/templates/create/base/README.md.hbs +61 -12
  91. package/templates/create/base/cmd/api/main.go.hbs +17 -130
  92. package/templates/create/base/cmd/api/wiring.go.hbs +161 -0
  93. package/templates/create/base/go.mod.hbs +4 -4
  94. package/templates/create/base/internal/platform/database/database.go.hbs +30 -11
  95. package/templates/create/base/internal/shared/config/config.go.hbs +13 -7
  96. package/templates/create/base/internal/shared/pagination/pagination.go.hbs +11 -0
  97. package/templates/create/base/internal/shared/tx/tx.go.hbs +47 -0
  98. package/templates/create/base/redocly.yaml.hbs +21 -0
  99. package/templates/create/features/docs/architecture.md.hbs +32 -11
  100. package/templates/create/features/docs/openapi.yaml.hbs +4 -9
  101. package/templates/create/features/docs/patterns.md.hbs +91 -14
  102. package/templates/create/features/docs/techstack.md.hbs +8 -3
  103. package/templates/generate/module/docs/item.yaml.hbs +3 -3
  104. package/templates/generate/module/dto.go.hbs +8 -1
  105. package/templates/generate/module/errors.go.hbs +5 -0
  106. package/templates/generate/module/field-column.down.sql.hbs +2 -0
  107. package/templates/generate/module/field-column.up.sql.hbs +15 -0
  108. package/templates/generate/module/handler.go.hbs +18 -4
  109. package/templates/generate/module/handler_test.go.hbs +88 -62
  110. package/templates/generate/module/migration.down.sql.hbs +3 -1
  111. package/templates/generate/module/migration.up.sql.hbs +7 -2
  112. package/templates/generate/module/minimal/dto.go.hbs +3 -1
  113. package/templates/generate/module/minimal/handler.go.hbs +8 -2
  114. package/templates/generate/module/minimal/handler_test.go.hbs +5 -112
  115. package/templates/generate/module/minimal/service_test.go.hbs +39 -16
  116. package/templates/generate/module/model/model.go.hbs +16 -0
  117. package/templates/generate/module/permission.up.sql.hbs +3 -1
  118. package/templates/generate/module/repository.go.hbs +60 -6
  119. package/templates/generate/module/repository_test.go.hbs +109 -0
  120. package/templates/generate/module/service.go.hbs +15 -4
  121. package/templates/generate/module/service_test.go.hbs +113 -17
  122. package/dist/commands/remove.js +0 -89
  123. package/templates/add/worker/internal/platform/queue/client.go.hbs +0 -31
  124. package/templates/add/worker/internal/platform/queue/server.go.hbs +0 -68
@@ -1,4 +1,7 @@
1
1
  "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
2
5
  Object.defineProperty(exports, "__esModule", { value: true });
3
6
  exports.pluralize = pluralize;
4
7
  exports.toPascalCase = toPascalCase;
@@ -9,22 +12,21 @@ exports.toDbName = toDbName;
9
12
  exports.validateGoModulePath = validateGoModulePath;
10
13
  exports.assertValidGoModulePath = assertValidGoModulePath;
11
14
  exports.assertNotGoKeyword = assertNotGoKeyword;
15
+ exports.assertGoIdentifier = assertGoIdentifier;
12
16
  exports.validateModuleName = validateModuleName;
13
17
  exports.normalizeApiPrefix = normalizeApiPrefix;
14
18
  exports.validateApiPrefix = validateApiPrefix;
15
19
  exports.resolveModuleNaming = resolveModuleNaming;
20
+ exports.resolveExistingModuleNaming = resolveExistingModuleNaming;
21
+ exports.migrationSlug = migrationSlug;
22
+ exports.migrationSlugAliases = migrationSlugAliases;
16
23
  exports.resolveMethodNaming = resolveMethodNaming;
17
- // ponytail: heuristic pluralizer, not a dependency — covers common English
18
- // nouns (order/user/category/address); irregular plurals still need a manual
19
- // rename in the generated file, add a dictionary if that becomes frequent.
24
+ const pluralize_1 = __importDefault(require("pluralize"));
25
+ // Pluralization must be idempotent because module names may come from the
26
+ // interactive prompt (singular) or a documented/scripted command (often
27
+ // plural). Keep this wrapper exported for callers that only need inflection.
20
28
  function pluralize(word) {
21
- if (/[sxz]$/.test(word) || /[^aeiou](ch|sh)$/.test(word))
22
- return word + "es";
23
- if (/[^aeiou]y$/.test(word))
24
- return word.slice(0, -1) + "ies";
25
- if (word.endsWith("s"))
26
- return word;
27
- return word + "s";
29
+ return pluralize_1.default.plural(word);
28
30
  }
29
31
  function toPascalCase(value) {
30
32
  return value
@@ -86,6 +88,13 @@ const GO_PREDECLARED_TYPES = new Set([
86
88
  "float32", "float64", "int", "int8", "int16", "int32", "int64", "rune",
87
89
  "string", "uint", "uint8", "uint16", "uint32", "uint64", "uintptr",
88
90
  ]);
91
+ // Directory names the go tool reserves. A module named "vendors" singularises
92
+ // to "vendor", and internal/app/vendor is then treated as a vendor directory:
93
+ // the build fails with "use of vendored package not allowed" and "must be
94
+ // imported as model", for a project that was generated, not hand-written.
95
+ // Not caught by the drift check either — that compares before and after, and
96
+ // this breaks the whole module at once.
97
+ const GO_RESERVED_DIRS = new Set(["vendor", "testdata"]);
89
98
  // method/handler/param identifier: only keywords are hard-illegal (a param or
90
99
  // func named `string` is legal Go, just shadows the builtin locally).
91
100
  function assertNotGoKeyword(ident, role) {
@@ -93,12 +102,25 @@ function assertNotGoKeyword(ident, role) {
93
102
  throw new Error(`"${ident}" is a Go keyword — can't use it as a ${role} name; pick another`);
94
103
  }
95
104
  }
105
+ // Anything that ends up spliced into a Go declaration has to be a legal
106
+ // identifier first. Without this, `generate method thing 2fa` exits 0 over a
107
+ // `func (s *Service) 2fa(...)` spread across four files, and a --field like
108
+ // `x string) error { panic(0) } // ` is injected straight into the generated
109
+ // signatures. Neither is caught later: gofmt's failure is only advisory and
110
+ // go vet needs a project that compiled a moment ago.
111
+ function assertGoIdentifier(ident, role) {
112
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(ident)) {
113
+ throw new Error(`"${ident}" is not a valid Go identifier — can't use it as a ${role} name.\n` +
114
+ `Letters, digits and underscores only, and it can't start with a digit.`);
115
+ }
116
+ assertNotGoKeyword(ident, role);
117
+ }
96
118
  // module name becomes a Go package name; keywords and predeclared type names
97
119
  // both produce code that won't compile (`package func`, or a `string` package
98
120
  // shadowing the builtin in main.go). Returns true|message for inquirer, and
99
121
  // backs the assert in resolveModuleNaming — one source of truth for both.
100
122
  function validateModuleName(rawName) {
101
- const pkg = toPackageName(rawName);
123
+ const pkg = toPackageName(pluralize_1.default.singular(toKebabCase(rawName)));
102
124
  if (!pkg)
103
125
  return `invalid module name: "${rawName}" (must contain letters/numbers)`;
104
126
  if (/^[0-9]/.test(pkg)) {
@@ -107,6 +129,9 @@ function validateModuleName(rawName) {
107
129
  if (GO_KEYWORDS.has(pkg) || GO_PREDECLARED_TYPES.has(pkg)) {
108
130
  return `"${pkg}" is a reserved Go word — a package named it won't compile; pick another module name`;
109
131
  }
132
+ if (GO_RESERVED_DIRS.has(pkg)) {
133
+ return `"${pkg}" is a directory name the go tool reserves — internal/app/${pkg} would be treated as a ${pkg} directory and the project wouldn't build; pick another module name`;
134
+ }
110
135
  return true;
111
136
  }
112
137
  // strips whitespace and leading/trailing slashes so "/api/v1/" and "api/v1"
@@ -134,23 +159,109 @@ function resolveModuleNaming(rawName) {
134
159
  const check = validateModuleName(rawName);
135
160
  if (check !== true)
136
161
  throw new Error(check);
162
+ const singular = pluralize_1.default.singular(toKebabCase(rawName));
163
+ const pkg = toPackageName(singular);
164
+ const plural = pluralize_1.default.plural(singular);
165
+ return {
166
+ name: singular,
167
+ pkg,
168
+ pascalName: toPascalCase(singular),
169
+ plural,
170
+ tableName: toDbName(plural),
171
+ schemaName: `${pkg}_svc`,
172
+ errorPrefix: toDbName(singular).toUpperCase(),
173
+ };
174
+ }
175
+ function legacyPluralize(word) {
176
+ if (/[sxz]$/.test(word) || /[^aeiou](ch|sh)$/.test(word))
177
+ return word + "es";
178
+ if (/[^aeiou]y$/.test(word))
179
+ return word.slice(0, -1) + "ies";
180
+ if (word.endsWith("s"))
181
+ return word;
182
+ return word + "s";
183
+ }
184
+ function resolveLegacyModuleNaming(rawName) {
137
185
  const pkg = toPackageName(rawName);
186
+ const plural = legacyPluralize(pkg);
138
187
  return {
139
188
  name: pkg,
140
189
  pkg,
141
190
  pascalName: toPascalCase(pkg),
142
- plural: pluralize(pkg),
191
+ plural,
192
+ tableName: toDbName(plural),
193
+ // Not meaningful for a legacy match: its table already exists in
194
+ // whatever schema the project's own migrations put it in (usually
195
+ // "public", from before this field existed), and nothing re-renders its
196
+ // model/migration templates to move it. Every consumer of this result is
197
+ // generate method/undo module, neither of which reads schemaName.
198
+ schemaName: `${pkg}_svc`,
143
199
  errorPrefix: pkg.toUpperCase(),
144
200
  };
145
201
  }
202
+ // Projects generated before canonical inflection used the raw input as the Go
203
+ // package name. Prefer the canonical package when present, but keep locating
204
+ // legacy plural packages so upgrade does not make method/remove commands lose
205
+ // sight of existing code. Two matches are unsafe: silently choosing one can
206
+ // patch or delete the wrong module.
207
+ function resolveExistingModuleNaming(rawName, existingPackages) {
208
+ const requestedLegacy = resolveLegacyModuleNaming(rawName);
209
+ const existing = new Set(existingPackages);
210
+ let canonical;
211
+ try {
212
+ canonical = resolveModuleNaming(rawName);
213
+ }
214
+ catch (error) {
215
+ if (existing.has(requestedLegacy.pkg))
216
+ return requestedLegacy;
217
+ throw error;
218
+ }
219
+ const aliases = existingPackages.filter((pkg) => {
220
+ if (pkg === canonical.pkg)
221
+ return false;
222
+ try {
223
+ return resolveModuleNaming(pkg).pkg === canonical.pkg;
224
+ }
225
+ catch {
226
+ return false;
227
+ }
228
+ });
229
+ const matches = [
230
+ ...(existing.has(canonical.pkg) ? [canonical.pkg] : []),
231
+ ...aliases,
232
+ ];
233
+ if (matches.length > 1) {
234
+ throw new Error(`ambiguous module "${rawName}": ${matches.map((pkg) => `internal/app/${pkg}`).join(" and ")} exist`);
235
+ }
236
+ if (matches[0] === canonical.pkg)
237
+ return canonical;
238
+ if (matches[0])
239
+ return resolveLegacyModuleNaming(matches[0]);
240
+ return canonical;
241
+ }
242
+ // Migration filenames read like the thing they create, so they use the
243
+ // snake_case table name: create_order_items.up.sql for order_svc.order_items.
244
+ // Older projects were written with the kebab-case route slug instead
245
+ // (create_order-items.up.sql), so anything that *looks up* an existing pair
246
+ // has to accept both — upgrading the CLI must not strand a module's
247
+ // migrations where `undo` can no longer see them. Identical for the
248
+ // single-word names that are the common case.
249
+ function migrationSlug(naming) {
250
+ return naming.tableName;
251
+ }
252
+ function migrationSlugAliases(naming) {
253
+ return naming.tableName === naming.plural ? [naming.tableName] : [naming.tableName, naming.plural];
254
+ }
146
255
  function resolveMethodNaming(rawName) {
147
256
  const cleaned = rawName.trim();
148
257
  const pascalName = toPascalCase(cleaned);
149
258
  if (!pascalName) {
150
259
  throw new Error(`invalid method name: "${rawName}" (must contain letters/numbers)`);
151
260
  }
152
- // handlerName becomes a Go method name (`func (h *Handler) <name>`)
153
- assertNotGoKeyword(toCamelCase(cleaned), "method");
261
+ // both become Go identifiers: handlerName as a method name
262
+ // (`func (h *Handler) <name>`), pascalName as a DTO type (`<Name>Input`)
263
+ assertGoIdentifier(toCamelCase(cleaned), "method");
264
+ assertGoIdentifier(pascalName, "method");
154
265
  return {
155
266
  name: cleaned,
156
267
  pascalName,
@@ -0,0 +1,107 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.patchMainGoForObservability = patchMainGoForObservability;
7
+ exports.patchDatabaseGoForObservability = patchDatabaseGoForObservability;
8
+ exports.patchConfigForObservability = patchConfigForObservability;
9
+ exports.patchEnvExampleForObservability = patchEnvExampleForObservability;
10
+ exports.patchOpenapiIndexForObservability = patchOpenapiIndexForObservability;
11
+ const fs_extra_1 = __importDefault(require("fs-extra"));
12
+ const marker_patch_1 = require("./marker-patch");
13
+ const IMPORT_MARKER = "// go-scaffold:imports";
14
+ const PLATFORM_INIT_MARKER = "// go-scaffold:platform-init";
15
+ const EXTRA_ROUTES_MARKER = "// go-scaffold:extra-routes";
16
+ const CONFIG_FIELDS_MARKER = "// go-scaffold:config-fields";
17
+ const CONFIG_LOAD_MARKER = "// go-scaffold:config-load";
18
+ const OPENAPI_PATHS_MARKER = "# go-scaffold:paths";
19
+ // The exact line `create` renders — matched literally rather than through a
20
+ // marker because it's a single call in the middle of other middleware, not a
21
+ // standalone line a marker comment can sit next to.
22
+ const USE_LINE = "r.Use(gin.Recovery(), middleware.CORS(cfg.CORSAllowedOrigins), middleware.RequestID(), middleware.Logger(logger), middleware.Error(!cfg.IsProd()))";
23
+ // patchMainGoForObservability wires telemetry init, the tracing/metrics
24
+ // middleware, and the /metrics route into cmd/api/wiring.go — the same
25
+ // text-marker approach every other `add` command uses, since main.go is a
26
+ // real file a human may have already edited by the time this runs, not a
27
+ // template rendered fresh.
28
+ function patchMainGoForObservability(mainGoPath, goModule, projectName) {
29
+ let content = fs_extra_1.default.readFileSync(mainGoPath, "utf8");
30
+ const telemetryImport = `"${goModule}/internal/platform/telemetry"`;
31
+ content = (0, marker_patch_1.insertBeforeMarkerOnce)(content, IMPORT_MARKER, telemetryImport, telemetryImport);
32
+ const promhttpImport = `"github.com/prometheus/client_golang/prometheus/promhttp"`;
33
+ content = (0, marker_patch_1.insertBeforeMarkerOnce)(content, IMPORT_MARKER, promhttpImport, promhttpImport);
34
+ // telemetry.go's GORM plugin looks up otel.Tracer() fresh on every query
35
+ // rather than once at registration, so it doesn't matter that this runs
36
+ // after database.Open has already called db.Use(NewGormPlugin()) — no
37
+ // query happens between here and the server actually accepting traffic.
38
+ const initBlock = [
39
+ `shutdownTelemetry, err := telemetry.Init(context.Background(), "${projectName}", cfg.OTELExporterEndpoint)`,
40
+ "if err != nil {",
41
+ '\treturn fmt.Errorf("init telemetry: %w", err)',
42
+ "}",
43
+ "defer func() { _ = shutdownTelemetry(context.Background()) }()",
44
+ ].join("\n");
45
+ content = (0, marker_patch_1.insertBeforeMarkerOnce)(content, PLATFORM_INIT_MARKER, initBlock, "shutdownTelemetry, err := telemetry.Init(");
46
+ if (!content.includes(USE_LINE)) {
47
+ throw new Error("cmd/api/wiring.go's r.Use(...) call doesn't match the text this command expects — " +
48
+ "it looks like it's been hand-edited. Add middleware.Metrics() and middleware.Tracing(\"<project>\") to it yourself.");
49
+ }
50
+ const newUseLine = `${USE_LINE.slice(0, -1)}, middleware.Metrics(), middleware.Tracing("${projectName}"))`;
51
+ content = content.replace(USE_LINE, () => newUseLine);
52
+ // Not gated on APP_ENV the way /docs is: production is exactly where you
53
+ // want a scrape target, and Prometheus reaches it in-cluster. It is still
54
+ // unauthenticated and does disclose your route list and traffic shape, so
55
+ // block /metrics at the ingress rather than publishing it to the internet.
56
+ const metricsRoute = 'r.GET("/metrics", gin.WrapH(promhttp.Handler()))';
57
+ const metricsBlock = [
58
+ "// Unauthenticated on purpose (Prometheus scrapes it in-cluster) — block",
59
+ "// /metrics at your ingress so it isn't reachable from the internet.",
60
+ metricsRoute,
61
+ ].join("\n");
62
+ content = (0, marker_patch_1.insertBeforeMarkerOnce)(content, EXTRA_ROUTES_MARKER, metricsBlock, metricsRoute);
63
+ fs_extra_1.default.writeFileSync(mainGoPath, content);
64
+ }
65
+ // patchDatabaseGoForObservability wires the GORM OpenTelemetry plugin into
66
+ // database.Open, so every query gets a span alongside the HTTP request it
67
+ // came from.
68
+ function patchDatabaseGoForObservability(databaseGoPath, goModule) {
69
+ let content = fs_extra_1.default.readFileSync(databaseGoPath, "utf8");
70
+ const telemetryImport = `"${goModule}/internal/platform/telemetry"`;
71
+ content = (0, marker_patch_1.insertBeforeMarkerOnce)(content, IMPORT_MARKER, telemetryImport, telemetryImport);
72
+ const pluginBlock = ["if err := db.Use(telemetry.NewGormPlugin()); err != nil {", "\treturn nil, err", "}"].join("\n");
73
+ content = (0, marker_patch_1.insertBeforeMarkerOnce)(content, PLATFORM_INIT_MARKER, pluginBlock, "if err := db.Use(telemetry.NewGormPlugin())");
74
+ fs_extra_1.default.writeFileSync(databaseGoPath, content);
75
+ }
76
+ // patchConfigForObservability adds OTELExporterEndpoint to Config and its
77
+ // env() load — the same marker-based approach patchConfigForWorker uses.
78
+ function patchConfigForObservability(configGoPath) {
79
+ let content = fs_extra_1.default.readFileSync(configGoPath, "utf8");
80
+ content = (0, marker_patch_1.insertBeforeMarkerOnce)(content, CONFIG_FIELDS_MARKER, "OTELExporterEndpoint string", "OTELExporterEndpoint");
81
+ content = (0, marker_patch_1.insertBeforeMarkerOnce)(content, CONFIG_LOAD_MARKER, 'OTELExporterEndpoint: env("OTEL_EXPORTER_OTLP_ENDPOINT", ""),', 'env("OTEL_EXPORTER_OTLP_ENDPOINT"');
82
+ fs_extra_1.default.writeFileSync(configGoPath, content);
83
+ }
84
+ // patchEnvExampleForObservability appends OTEL_EXPORTER_OTLP_ENDPOINT —
85
+ // .env.example has no marker infrastructure of its own, so this follows the
86
+ // same append-once pattern as add worker/add auth's env patchers.
87
+ function patchEnvExampleForObservability(envExamplePath) {
88
+ if (!fs_extra_1.default.existsSync(envExamplePath))
89
+ return;
90
+ const content = fs_extra_1.default.readFileSync(envExamplePath, "utf8");
91
+ if (content.includes("OTEL_EXPORTER_OTLP_ENDPOINT"))
92
+ return;
93
+ fs_extra_1.default.writeFileSync(envExamplePath, content.replace(/\n?$/, "\n") +
94
+ "\n# OTLP/HTTP endpoint for trace export (e.g. localhost:4318) — empty disables\n" +
95
+ "# tracing entirely: no exporter is created, no network calls are made\n" +
96
+ "OTEL_EXPORTER_OTLP_ENDPOINT=\n");
97
+ }
98
+ // patchOpenapiIndexForObservability wires /metrics into docs/openapi.yaml.
99
+ // Not routed through patchOpenapiIndexRaw (used for auth/rbac's paths):
100
+ // /metrics is registered directly on the router like /livez and /readyz, not
101
+ // under the api prefix group, so it must never be prefixed.
102
+ function patchOpenapiIndexForObservability(openapiPath) {
103
+ let content = fs_extra_1.default.readFileSync(openapiPath, "utf8");
104
+ const block = "/metrics:\n $ref: './observability/metrics.yaml'";
105
+ content = (0, marker_patch_1.insertBeforeMarkerOnce)(content, OPENAPI_PATHS_MARKER, block, "/metrics:");
106
+ fs_extra_1.default.writeFileSync(openapiPath, content);
107
+ }
@@ -44,7 +44,25 @@ function patchOpenapiIndex(openapiPath, naming, apiPrefix) {
44
44
  function unpatchOpenapiIndex(openapiPath, naming, apiPrefix) {
45
45
  const content = fs_extra_1.default.readFileSync(openapiPath, "utf8");
46
46
  const { paths, schemas } = openapiLines(naming, apiPrefix);
47
- fs_extra_1.default.writeFileSync(openapiPath, (0, marker_patch_1.removeLines)(content, [...paths, ...schemas]));
47
+ const withoutKnownEntries = (0, marker_patch_1.removeLines)(content, [...paths, ...schemas]);
48
+ // `generate method` can add any number of module-specific path documents.
49
+ // Remove every two-line path/$ref block owned by this module, otherwise the
50
+ // index keeps dangling references after the docs folder is deleted.
51
+ const lines = withoutKnownEntries.split("\n");
52
+ const moduleRefPrefix = `./${naming.plural}/`;
53
+ const kept = [];
54
+ for (let i = 0; i < lines.length; i += 1) {
55
+ const current = lines[i];
56
+ const next = lines[i + 1];
57
+ const isPathKey = /^\s*\/[^:]+:\s*$/.test(current);
58
+ const refMatch = next?.match(/^\s*\$ref:\s*['"]([^'"]+)['"]\s*$/);
59
+ if (isPathKey && refMatch?.[1].startsWith(moduleRefPrefix)) {
60
+ i += 1;
61
+ continue;
62
+ }
63
+ kept.push(current);
64
+ }
65
+ fs_extra_1.default.writeFileSync(openapiPath, kept.join("\n"));
48
66
  }
49
67
  // patchOpenapiIndexRaw wires hand-written path docs into the index — used by
50
68
  // `add auth`/`add rbac`, whose endpoints aren't a single CRUD resource so
@@ -3,7 +3,11 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.patchComposeForRedis = patchComposeForRedis;
7
+ exports.patchCiForRedis = patchCiForRedis;
8
+ exports.patchConfigForRedis = patchConfigForRedis;
6
9
  exports.patchConfigForWorker = patchConfigForWorker;
10
+ exports.patchConfigForSMTP = patchConfigForSMTP;
7
11
  exports.patchMainGoForWorker = patchMainGoForWorker;
8
12
  const fs_extra_1 = __importDefault(require("fs-extra"));
9
13
  const marker_patch_1 = require("./marker-patch");
@@ -13,25 +17,107 @@ const CONFIG_LOAD_MARKER = "// go-scaffold:config-load";
13
17
  const PLATFORM_INIT_MARKER = "// go-scaffold:platform-init";
14
18
  const READYZ_MARKER = "// go-scaffold:readyz-checks";
15
19
  const SHUTDOWN_MARKER = "// go-scaffold:shutdown";
16
- // patchConfigForWorker adds RedisURL + SMTP_* fields to Config and their
17
- // env() loads to Load() via marker comments, the same text-insertion
18
- // approach as patchMainGo, since config.go.hbs is only rendered once (at
19
- // `create`) and everything after that is a real file a human may have
20
- // already edited.
21
- function patchConfigForWorker(configGoPath) {
20
+ // patchComposeForRedis adds a redis service to docker-compose.yml. Whatever
21
+ // pulls Redis in`add worker --queue redis`, or `add auth`'s refresh-token
22
+ // store on top of a Postgres queue makes cmd/api call cache.Open and adds a
23
+ // Redis ping to /readyz. Leaving compose Postgres-only meant the documented
24
+ // path (`make docker-up` then `make run`) produced a permanent 503, which is
25
+ // a rough first five minutes with a brand new project.
26
+ //
27
+ // No-op when the project was scaffolded with --no-docker.
28
+ function patchComposeForRedis(composePath) {
29
+ if (!fs_extra_1.default.existsSync(composePath))
30
+ return;
31
+ const content = fs_extra_1.default.readFileSync(composePath, "utf8");
32
+ if (/^\s{2}redis:/m.test(content))
33
+ return;
34
+ const service = [
35
+ " redis:",
36
+ " image: redis:7-alpine",
37
+ " ports:",
38
+ ' - "6379:6379"',
39
+ ].join("\n");
40
+ // before the top-level `volumes:` key, so redis stays inside `services:`
41
+ const out = content.includes("\nvolumes:")
42
+ ? content.replace("\nvolumes:", () => `\n${service}\n\nvolumes:`)
43
+ : content.replace(/\n?$/, "\n") + `\n${service}\n`;
44
+ fs_extra_1.default.writeFileSync(composePath, out);
45
+ }
46
+ // patchCiForRedis adds a redis service to .github/workflows/ci.yml, for the
47
+ // same reason patchComposeForRedis exists: once cmd/api calls cache.Open, a
48
+ // test that touches the token store has nothing to connect to on a runner
49
+ // whose only service is Postgres. The generated workflow was written at
50
+ // `create` time, before anything needed Redis, and nothing patched it after.
51
+ //
52
+ // No-op when the workflow was deleted or replaced by hand.
53
+ function patchCiForRedis(ciPath) {
54
+ if (!fs_extra_1.default.existsSync(ciPath))
55
+ return;
56
+ const content = fs_extra_1.default.readFileSync(ciPath, "utf8");
57
+ if (/^\s{6}redis:/m.test(content))
58
+ return;
59
+ // `steps:` sits one level under the job, so it's the first line that ends
60
+ // the `services:` block — insert the service just above it.
61
+ const stepsLine = content.split("\n").find((l) => l.trimEnd() === " steps:");
62
+ if (!stepsLine)
63
+ return;
64
+ const service = [
65
+ " redis:",
66
+ " image: redis:7-alpine",
67
+ " ports:",
68
+ " - 6379:6379",
69
+ " options: >-",
70
+ ' --health-cmd "redis-cli ping"',
71
+ " --health-interval 10s",
72
+ " --health-timeout 5s",
73
+ " --health-retries 5",
74
+ "",
75
+ ].join("\n");
76
+ fs_extra_1.default.writeFileSync(ciPath, content.replace(stepsLine, () => `${service}\n${stepsLine}`));
77
+ }
78
+ // patchConfigForRedis adds RedisURL to Config and its env() load to Load().
79
+ // Split out from the worker patch because Redis is no longer the worker's
80
+ // concern by default — `add auth` needs it for the refresh-token store even
81
+ // when the queue lives in Postgres.
82
+ function patchConfigForRedis(configGoPath) {
22
83
  let content = fs_extra_1.default.readFileSync(configGoPath, "utf8");
23
- const fieldsBlock = ["RedisURL string", "", "SMTPHost string", "SMTPPort string", "SMTPUsername string", "SMTPPassword string", "SMTPFrom string"].join("\n");
24
- content = (0, marker_patch_1.insertBeforeMarkerOnce)(content, CONFIG_FIELDS_MARKER, fieldsBlock, "RedisURL string");
84
+ content = (0, marker_patch_1.insertBeforeMarkerOnce)(content, CONFIG_FIELDS_MARKER, "RedisURL string", "RedisURL");
85
+ content = (0, marker_patch_1.insertBeforeMarkerOnce)(content, CONFIG_LOAD_MARKER, 'RedisURL: env("REDIS_URL", "redis://localhost:6379/0"),', 'env("REDIS_URL"');
86
+ fs_extra_1.default.writeFileSync(configGoPath, content);
87
+ }
88
+ // patchConfigForWorker adds the SMTP_* fields (and RedisURL, when Redis is
89
+ // the queue's backing store) to Config and their env() loads to Load() — via
90
+ // marker comments, the same text-insertion approach as patchMainGo, since
91
+ // config.go.hbs is only rendered once (at `create`) and everything after
92
+ // that is a real file a human may have already edited.
93
+ function patchConfigForWorker(configGoPath, opts) {
94
+ if (opts.redis)
95
+ patchConfigForRedis(configGoPath);
96
+ patchConfigForSMTP(configGoPath);
97
+ }
98
+ // Sentinels here match a bare identifier, never `Name string` or
99
+ // `Name: env(...)`: gofmt aligns struct fields and map values into columns, so
100
+ // the moment one of these blocks lands the literal spacing a sentinel was
101
+ // written with no longer exists in the file. A sentinel that misses means the
102
+ // block gets inserted a second time and the project stops compiling on a
103
+ // redeclared field — which is exactly what `add auth` then `add worker` did.
104
+ //
105
+ // patchConfigForSMTP is split out because `add auth` needs these fields even
106
+ // when there is no worker: without a queue it sends mail synchronously, and it
107
+ // still has to know where to send it. Idempotent, so whichever command gets
108
+ // here first wins and the second is a no-op.
109
+ function patchConfigForSMTP(configGoPath) {
110
+ let content = fs_extra_1.default.readFileSync(configGoPath, "utf8");
111
+ const fieldsBlock = ["SMTPHost string", "SMTPPort string", "SMTPUsername string", "SMTPPassword string", "SMTPFrom string"].join("\n");
112
+ content = (0, marker_patch_1.insertBeforeMarkerOnce)(content, CONFIG_FIELDS_MARKER, fieldsBlock, "SMTPHost");
25
113
  const loadBlock = [
26
- 'RedisURL: env("REDIS_URL", "redis://localhost:6379/0"),',
27
- "",
28
114
  'SMTPHost: env("SMTP_HOST", ""),',
29
115
  'SMTPPort: env("SMTP_PORT", "587"),',
30
116
  'SMTPUsername: env("SMTP_USERNAME", ""),',
31
117
  'SMTPPassword: env("SMTP_PASSWORD", ""),',
32
118
  'SMTPFrom: env("SMTP_FROM", "no-reply@example.local"),',
33
119
  ].join("\n");
34
- content = (0, marker_patch_1.insertBeforeMarkerOnce)(content, CONFIG_LOAD_MARKER, loadBlock, 'RedisURL: env("REDIS_URL"');
120
+ content = (0, marker_patch_1.insertBeforeMarkerOnce)(content, CONFIG_LOAD_MARKER, loadBlock, 'env("SMTP_HOST"');
35
121
  fs_extra_1.default.writeFileSync(configGoPath, content);
36
122
  }
37
123
  // patchMainGoForWorker wires Redis into cmd/api: opened alongside the DB, and
@@ -44,7 +130,7 @@ function patchMainGoForWorker(mainGoPath, goModule) {
44
130
  let content = fs_extra_1.default.readFileSync(mainGoPath, "utf8");
45
131
  const cacheImport = `"${goModule}/internal/platform/cache"`;
46
132
  content = (0, marker_patch_1.insertBeforeMarkerOnce)(content, IMPORT_MARKER, cacheImport, cacheImport);
47
- const initBlock = ["rdb, err := cache.Open(cfg)", "if err != nil {", '\tlogger.Error("open redis", "error", err)', "\tos.Exit(1)", "}"].join("\n");
133
+ const initBlock = ["rdb, err := cache.Open(cfg)", "if err != nil {", '\treturn fmt.Errorf("open redis: %w", err)', "}"].join("\n");
48
134
  content = (0, marker_patch_1.insertBeforeMarkerOnce)(content, PLATFORM_INIT_MARKER, initBlock, "rdb, err := cache.Open(cfg)");
49
135
  const readyzBlock = [
50
136
  "if err := rdb.Ping(c.Request.Context()).Err(); err != nil {",
@@ -13,12 +13,17 @@ exports.patchUserServiceTestForRbac = patchUserServiceTestForRbac;
13
13
  exports.patchUserDTOForRbac = patchUserDTOForRbac;
14
14
  exports.patchUserHandlerForRbac = patchUserHandlerForRbac;
15
15
  exports.patchUserErrorsForRbac = patchUserErrorsForRbac;
16
+ exports.userSvcLineFor = userSvcLineFor;
17
+ exports.assertRbacPatchable = assertRbacPatchable;
16
18
  exports.patchMainGoForRbac = patchMainGoForRbac;
17
19
  exports.patchCmdSeedForRbac = patchCmdSeedForRbac;
18
20
  const fs_extra_1 = __importDefault(require("fs-extra"));
21
+ const auth_patcher_1 = require("./auth-patcher");
19
22
  const marker_patch_1 = require("./marker-patch");
20
23
  const IMPORT_MARKER = "// go-scaffold:imports";
24
+ const SCHEMA_MARKER = "// go-scaffold:schemas";
21
25
  const MODEL_MARKER = "// go-scaffold:models";
26
+ const ROUTE_MARKER = "// go-scaffold:routes";
22
27
  const CONFIG_FIELDS_MARKER = "// go-scaffold:config-fields";
23
28
  const CONFIG_LOAD_MARKER = "// go-scaffold:config-load";
24
29
  // patchAuthDocsForRbac adds `role` to the hand-written MeResponse schema in
@@ -233,26 +238,71 @@ function patchUserErrorsForRbac(errorsGoPath) {
233
238
  // isn't enough — REPLACES the `add auth` PR's user.NewHandler(...) call with
234
239
  // a version that also builds roleSvc/authz and passes them through, plus
235
240
  // registers role's own routes right after it.
236
- function patchMainGoForRbac(mainGoPath, goModule) {
241
+ // userSvcLineFor rebuilds the exact line `add auth` wrote, from the same
242
+ // helper it used. Exported so the command can check for it *before* it starts
243
+ // patching: every other file rbac touches is edited first, and until this
244
+ // existed a mismatch here threw after user.NewService had already grown a
245
+ // roleChecker parameter — leaving a project that no longer compiled and an
246
+ // error telling you to restore a line that wouldn't have fixed it.
247
+ function userSvcLineFor(goModule, store, worker) {
248
+ const { tokenStore, mailer } = (0, auth_patcher_1.authWiringLines)({ goModule, queueBackend: "river", store, worker });
249
+ return `userSvc := user.NewService(user.NewRepository(db), ${tokenStore}, ${mailer}, cfg)`;
250
+ }
251
+ function assertRbacPatchable(mainGoPath, goModule, store, worker) {
252
+ const expected = userSvcLineFor(goModule, store, worker);
253
+ if (fs_extra_1.default.readFileSync(mainGoPath, "utf8").includes(expected))
254
+ return;
255
+ throw new Error("cmd/api/wiring.go's userSvc line doesn't match what `add auth` wrote, so `add rbac` can't extend it.\n" +
256
+ `Expected to find:\n ${expected}\n\n` +
257
+ "It was probably hand-edited. Restore that line and re-run — nothing has been changed yet.");
258
+ }
259
+ function patchMainGoForRbac(mainGoPath, goModule, store, worker) {
237
260
  let content = fs_extra_1.default.readFileSync(mainGoPath, "utf8");
238
261
  const importLine = `"${goModule}/internal/app/role"`;
239
262
  content = (0, marker_patch_1.insertBeforeMarkerOnce)(content, IMPORT_MARKER, importLine, importLine);
240
263
  const modelImportLine = `rolemodel "${goModule}/internal/app/role/model"`;
241
264
  content = (0, marker_patch_1.insertBeforeMarkerOnce)(content, IMPORT_MARKER, modelImportLine, modelImportLine);
265
+ const schemaBlock = [
266
+ 'if err := db.Exec("CREATE SCHEMA IF NOT EXISTS role_svc").Error; err != nil {',
267
+ '\treturn fmt.Errorf("create schema role_svc: %w", err)',
268
+ "}",
269
+ ].join("\n");
270
+ content = (0, marker_patch_1.insertBeforeMarkerOnce)(content, SCHEMA_MARKER, schemaBlock, "CREATE SCHEMA IF NOT EXISTS role_svc");
242
271
  const migrateLines = ["&rolemodel.Role{},", "&rolemodel.Permission{},", "&rolemodel.RolePermission{},"];
243
272
  for (const line of migrateLines) {
244
273
  content = (0, marker_patch_1.insertBeforeMarkerOnce)(content, MODEL_MARKER, line, line);
245
274
  }
246
- const oldRouteLine = "user.NewHandler(user.NewService(user.NewRepository(db), user.NewRedisTokenStore(rdb), mail.NewAsyncClient(q), cfg), cfg.JWTSecret, cfg.JWTRefreshTTL, cfg.CookieSecure, rdb).Register(api)";
247
- if (content.includes(oldRouteLine)) {
248
- const newRouteBlock = [
249
- "roleSvc := role.NewService(role.NewRepository(db))",
250
- "authz := middleware.NewAuthz(roleSvc.PermissionsOf, cfg.AuthzCacheTTL)",
251
- "user.NewHandler(user.NewService(user.NewRepository(db), user.NewRedisTokenStore(rdb), mail.NewAsyncClient(q), cfg, roleSvc), cfg.JWTSecret, cfg.JWTRefreshTTL, cfg.CookieSecure, rdb, authz).Register(api)",
252
- "role.NewHandler(roleSvc, cfg.JWTSecret, authz).Register(api)",
253
- ].join("\n");
254
- content = content.replace(oldRouteLine, newRouteBlock);
275
+ // roleSvc and authz have to be declared *above* userSvc, which now takes
276
+ // roleSvc — so this rewrites the service line in place rather than
277
+ // appending at the marker (which would land below it).
278
+ // Rebuilt from the same helper `add auth` used, so a project on either store
279
+ // gets its own line matched rather than a hardcoded guess at one of them.
280
+ const { limiter } = (0, auth_patcher_1.authWiringLines)({ goModule, queueBackend: "river", store, worker });
281
+ const userSvcLine = userSvcLineFor(goModule, store, worker);
282
+ // Throw rather than skip: the roleSvc/authz declarations this rewrite adds
283
+ // are what the unconditional patches below refer to. Skipping quietly still
284
+ // emits `roleSvc`/`authz` references with nothing declaring them, so the
285
+ // command reports success over a main.go that doesn't compile.
286
+ if (!content.includes(userSvcLine)) {
287
+ throw new Error(`cmd/api/wiring.go's userSvc line doesn't match what \`add auth\` wrote, so \`add rbac\` can't extend it.\n` +
288
+ `Expected to find:\n ${userSvcLine}\n\n` +
289
+ `It was probably hand-edited. Restore that line (add rbac will re-extend it), or apply the rbac wiring by hand:\n` +
290
+ ` roleSvc := role.NewService(role.NewRepository(db))\n` +
291
+ ` authz := middleware.NewAuthz(roleSvc.PermissionsOf, cfg.AuthzCacheTTL)\n` +
292
+ ` ...then pass roleSvc as user.NewService's last argument.`);
255
293
  }
294
+ content = content.replace(userSvcLine, [
295
+ "roleSvc := role.NewService(role.NewRepository(db))",
296
+ "authz := middleware.NewAuthz(roleSvc.PermissionsOf, cfg.AuthzCacheTTL)",
297
+ `${userSvcLine.slice(0, -1)}, roleSvc)`,
298
+ ].join("\n"));
299
+ const userRouteLine = `user.NewHandler(userSvc, cfg.JWTSecret, cfg.JWTRefreshTTL, cfg.CookieSecure, cfg.CookieSameSite, ${limiter}).Register(api)`;
300
+ // strip the trailing `).Register(api)` — not just `.Register(api)` — so authz
301
+ // lands inside NewHandler's argument list rather than after its closing paren
302
+ const tail = ").Register(api)";
303
+ content = content.replace(userRouteLine, `${userRouteLine.slice(0, -tail.length)}, authz${tail}`);
304
+ const roleRouteLine = "role.NewHandler(roleSvc, cfg.JWTSecret, authz).Register(api)";
305
+ content = (0, marker_patch_1.insertBeforeMarkerOnce)(content, ROUTE_MARKER, roleRouteLine, roleRouteLine);
256
306
  fs_extra_1.default.writeFileSync(mainGoPath, content);
257
307
  }
258
308
  // patchCmdSeedForRbac makes the seeded admin actually an admin: wires a
@@ -0,0 +1,31 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createSmokeRunConfig = createSmokeRunConfig;
4
+ const POSTGRES_HOST = "127.0.0.1";
5
+ function assertPort(port, label) {
6
+ if (!Number.isInteger(port) || port < 1 || port > 65535) {
7
+ throw new Error(`invalid smoke-test ${label}: ${port}`);
8
+ }
9
+ }
10
+ function createSmokeRunConfig(runID, port, dbPort = 5432) {
11
+ assertPort(port, "port");
12
+ assertPort(dbPort, "PostgreSQL port");
13
+ const normalizedRunID = runID.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
14
+ if (!normalizedRunID) {
15
+ throw new Error("smoke-test run ID must contain at least one letter or digit");
16
+ }
17
+ const dbName = `go_scaffold_smoke_${normalizedRunID}`;
18
+ return {
19
+ runID: normalizedRunID,
20
+ dbName,
21
+ dbHost: POSTGRES_HOST,
22
+ dbPort,
23
+ dbDsn: `postgres://postgres:postgres@${POSTGRES_HOST}:${dbPort}/${dbName}?sslmode=disable`,
24
+ port,
25
+ baseURL: `http://127.0.0.1:${port}`,
26
+ logPrefix: `go-scaffold-smoke-${normalizedRunID}`,
27
+ ownerToken: `go-scaffold-smoke-${normalizedRunID}`,
28
+ dockerLabel: `go-scaffold.smoke.owner=go-scaffold-smoke-${normalizedRunID}`,
29
+ containerNamePrefix: `go-scaffold-smoke-${normalizedRunID}`,
30
+ };
31
+ }