hatchkit 0.1.28 → 0.1.30

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.
@@ -3,8 +3,26 @@
3
3
  # Static-site image for {{name}}.
4
4
  # Built by .github/workflows/deploy.yml, pushed to GHCR, pulled by
5
5
  # Coolify via docker-compose.yml. nginx serves the built bundle —
6
- # no runtime Node, so dotenvx encryption isn't relevant here
7
- # (anything sensitive should never reach the browser bundle anyway).
6
+ # no runtime Node, so any sensitive values must be build-time-only
7
+ # (baked into the static bundle) or non-sensitive.
8
+ #
9
+ # .env.production is committed to the repo dotenvx-encrypted. The
10
+ # build stage decrypts it via the dotenvx_private_key BuildKit secret
11
+ # (passed by .github/workflows/deploy.yml from the GH Actions secret
12
+ # DOTENV_PRIVATE_KEY_PRODUCTION) so `pnpm build` sees the plain values
13
+ # when it bakes them into the static output.
14
+ #
15
+ # Why BuildKit secrets and not ARG: ARG values land in `docker history`
16
+ # and the image manifest. BuildKit secrets are mounted as tmpfs at
17
+ # build time and never persist — `docker history` won't show them, the
18
+ # final image won't either. Requires BuildKit (default in modern
19
+ # Docker; the GH Actions workflow uses docker/setup-buildx-action
20
+ # which enables it).
21
+ #
22
+ # What ends up in the bundle: NEXT_PUBLIC_* / VITE_* / similar values
23
+ # that the framework explicitly inlines into client JS. What does NOT:
24
+ # any non-public values, since this static site has no runtime to
25
+ # read them.
8
26
  ARG NODE_VERSION={{nodeMajor}}
9
27
 
10
28
  FROM node:${NODE_VERSION}-alpine AS build
@@ -12,7 +30,19 @@ WORKDIR /app
12
30
  COPY package.json pnpm-lock.yaml* ./
13
31
  RUN corepack enable && pnpm install --frozen-lockfile
14
32
  COPY . .
15
- RUN pnpm build
33
+ # `--mount=type=secret,id=dotenvx_private_key,env=DOTENV_PRIVATE_KEY_PRODUCTION`
34
+ # exposes the secret value as the env var the wrapped command reads,
35
+ # only for the duration of this RUN. dotenvx picks it up, decrypts
36
+ # .env.production in memory, and re-exports each KEY=VALUE for
37
+ # `pnpm build`. If the secret isn't supplied, dotenvx silently skips
38
+ # decryption and the build would bake `encrypted:...` strings into
39
+ # asset URLs — fail fast instead.
40
+ RUN --mount=type=secret,id=dotenvx_private_key,env=DOTENV_PRIVATE_KEY_PRODUCTION \
41
+ if [ -z "$DOTENV_PRIVATE_KEY_PRODUCTION" ]; then \
42
+ echo "ERROR: dotenvx_private_key build secret not supplied. The workflow at .github/workflows/deploy.yml should pass it via 'secrets:' from the GH Actions secret DOTENV_PRIVATE_KEY_PRODUCTION." >&2; \
43
+ exit 1; \
44
+ fi && \
45
+ pnpm dlx @dotenvx/dotenvx run -- pnpm build
16
46
 
17
47
  FROM nginx:alpine AS runner
18
48
  COPY --from=build /app/dist /usr/share/nginx/html
@@ -2,10 +2,23 @@
2
2
  #
3
3
  # Server / fullstack image for {{name}}.
4
4
  # Built by .github/workflows/deploy.yml, pushed to GHCR, pulled by
5
- # Coolify via docker-compose.yml. Uses dotenvx at runtime so the
6
- # committed encrypted .env.production decrypts only inside the
7
- # container (the private key comes from DOTENV_PRIVATE_KEY_PRODUCTION,
8
- # set on the Coolify app's env).
5
+ # Coolify via docker-compose.yml. dotenvx decrypts the committed
6
+ # encrypted .env.production at TWO points:
7
+ #
8
+ # 1. Build stage — uses the dotenvx_private_key BuildKit secret
9
+ # (passed by the workflow from the GH Actions secret
10
+ # DOTENV_PRIVATE_KEY_PRODUCTION) to decrypt in-memory before
11
+ # running `pnpm build`. Required for fullstack builds that bake
12
+ # NEXT_PUBLIC_* / VITE_* values into a static client bundle —
13
+ # those values are inlined at build time and unreachable from
14
+ # the runtime CMD. The secret is mounted as tmpfs and never
15
+ # lands in `docker history` or any image layer.
16
+ #
17
+ # 2. Runtime CMD — `dotenvx run` reads the same .env.production and
18
+ # decrypts again, this time using DOTENV_PRIVATE_KEY_PRODUCTION
19
+ # from the container env (forwarded by docker-compose.yml from
20
+ # Coolify's app env). This covers server-side values the runtime
21
+ # reads with `process.env.X`.
9
22
  ARG NODE_VERSION={{nodeMajor}}
10
23
 
11
24
  FROM node:${NODE_VERSION}-alpine AS deps
@@ -17,7 +30,18 @@ FROM node:${NODE_VERSION}-alpine AS build
17
30
  WORKDIR /app
18
31
  COPY --from=deps /app/node_modules ./node_modules
19
32
  COPY . .
20
- RUN corepack enable && pnpm build
33
+ # Build-time decryption see the header comment for the rationale.
34
+ # `if [ -z "$DOTENV_PRIVATE_KEY_PRODUCTION" ]` fails fast when the
35
+ # workflow forgot to pass the secret; without it dotenvx would
36
+ # silently export zero env vars and the build would bake broken
37
+ # `encrypted:...` strings into the static bundle.
38
+ RUN --mount=type=secret,id=dotenvx_private_key,env=DOTENV_PRIVATE_KEY_PRODUCTION \
39
+ if [ -z "$DOTENV_PRIVATE_KEY_PRODUCTION" ]; then \
40
+ echo "ERROR: dotenvx_private_key build secret not supplied. The workflow at .github/workflows/deploy.yml should pass it via 'secrets:' from the GH Actions secret DOTENV_PRIVATE_KEY_PRODUCTION." >&2; \
41
+ exit 1; \
42
+ fi && \
43
+ corepack enable && \
44
+ pnpm dlx @dotenvx/dotenvx run -- pnpm build
21
45
 
22
46
  FROM node:${NODE_VERSION}-alpine AS runner
23
47
  WORKDIR /app
@@ -27,5 +51,6 @@ COPY --from=build /app /app
27
51
  EXPOSE {{port}}
28
52
  # `dotenvx run` reads .env.production (encrypted, committed) and
29
53
  # decrypts in-process using DOTENV_PRIVATE_KEY_PRODUCTION before
30
- # spawning the actual server.
54
+ # spawning the actual server. corepack isn't enabled in this stage,
55
+ # so we fetch dotenvx via npx instead of pnpm dlx.
31
56
  CMD ["sh", "-c", "npx --yes @dotenvx/dotenvx run -- node {{entrypoint}}"]
@@ -8,8 +8,19 @@
8
8
  # Required repo secrets (hatchkit sets these via `gh secret set`
9
9
  # during adopt / create; rotate via the GitHub UI / `gh` whenever
10
10
  # needed):
11
- # COOLIFY_WEBHOOK_URL — full URL incl. uuid query param
12
- # COOLIFY_TOKEN — bearer token for the deploy endpoint
11
+ # COOLIFY_WEBHOOK_URL — full URL incl. uuid query param
12
+ # COOLIFY_TOKEN — bearer token for the deploy endpoint
13
+ # DOTENV_PRIVATE_KEY_PRODUCTION — dotenvx private key; passed to the
14
+ # docker build as a BuildKit secret
15
+ # (mounted as tmpfs, never written
16
+ # to a layer or `docker history`).
17
+ # The Dockerfile uses it to decrypt
18
+ # .env.production in memory and bake
19
+ # build-time values (NEXT_PUBLIC_*
20
+ # etc.) into the output. Stored in
21
+ # your OS keychain under
22
+ # `dotenvx:<project>:production-private-key`
23
+ # — `hatchkit keys show <project>` to retrieve.
13
24
  #
14
25
  # GHCR auth uses the workflow's built-in GITHUB_TOKEN. For Coolify
15
26
  # to pull a private package, configure GHCR credentials in the
@@ -69,6 +80,14 @@ jobs:
69
80
  cache-from: type=gha
70
81
  cache-to: type=gha,mode=max
71
82
  platforms: linux/amd64
83
+ # BuildKit secret for decrypting .env.production at build
84
+ # time. Mounted as tmpfs inside the Dockerfile's RUN step,
85
+ # never written to a layer or `docker history`. The Dockerfile
86
+ # exposes it as the DOTENV_PRIVATE_KEY_PRODUCTION env var for
87
+ # `dotenvx run -- pnpm build`, which decrypts in memory and
88
+ # bakes NEXT_PUBLIC_* / VITE_* values into the bundle.
89
+ secrets: |
90
+ dotenvx_private_key=${{ secrets.DOTENV_PRIVATE_KEY_PRODUCTION }}
72
91
 
73
92
  - name: Trigger Coolify deploy
74
93
  env:
@@ -0,0 +1,23 @@
1
+ export interface EnsureDockerignoreResult {
2
+ /** Absolute path we inspected (whether or not it existed). */
3
+ path: string;
4
+ /** True iff there was no `.dockerignore` before this call. We never
5
+ * create one — see the module docstring for why. */
6
+ fileExisted: boolean;
7
+ /** True iff we appended a hatchkit-managed line. False when the
8
+ * negation was already present, or when there was no file. */
9
+ modified: boolean;
10
+ }
11
+ /** Idempotently ensure `<repoRoot>/.dockerignore` allows `.env.production`
12
+ * through. Appends `!.env.production` (with a `# hatchkit:` bread crumb)
13
+ * when the negation is missing AND a `.dockerignore` exists.
14
+ *
15
+ * The negation is a safe append — if nothing actually excluded
16
+ * `.env.production`, the line is a redundant no-op. We don't try to
17
+ * parse the user's patterns to decide whether the line is needed; the
18
+ * parsing surface area (globs, `**`, anchored vs. unanchored, repeated
19
+ * rule overrides) isn't worth getting subtly wrong when the redundant
20
+ * case is harmless.
21
+ */
22
+ export declare function ensureDockerignoreAllowsEnvProduction(repoRoot: string): EnsureDockerignoreResult;
23
+ //# sourceMappingURL=dockerignore.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dockerignore.d.ts","sourceRoot":"","sources":["../../src/utils/dockerignore.ts"],"names":[],"mappings":"AA4BA,MAAM,WAAW,wBAAwB;IACvC,8DAA8D;IAC9D,IAAI,EAAE,MAAM,CAAC;IACb;yDACqD;IACrD,WAAW,EAAE,OAAO,CAAC;IACrB;mEAC+D;IAC/D,QAAQ,EAAE,OAAO,CAAC;CACnB;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,qCAAqC,CAAC,QAAQ,EAAE,MAAM,GAAG,wBAAwB,CAsBhG"}
@@ -0,0 +1,58 @@
1
+ /*
2
+ * Tiny helper for keeping `.env.production` *in* the Docker build context.
3
+ *
4
+ * hatchkit-managed projects commit a dotenvx-encrypted `.env.production`
5
+ * to git (encryption is the whole point — the file's content is
6
+ * `KEY="encrypted:..."`, safe to ship). Many existing repos already
7
+ * carry a defensive `.dockerignore` that wildcards out the entire
8
+ * `.env*` family, which inadvertently strips the encrypted file from
9
+ * the build context too. dotenvx then can't find the file at build
10
+ * (or runtime) and silently exports zero env vars — `next build` /
11
+ * `pnpm build` runs with empty NEXT_PUBLIC_* values, baking broken
12
+ * URLs into the image.
13
+ *
14
+ * The fix is one line: append `!.env.production` so the encrypted file
15
+ * survives the wildcard. We do NOT remove the user's existing exclude
16
+ * rules — those still keep plaintext `.env`, `.env.local`, and (most
17
+ * importantly) `.env.keys` (the private key) out of the image.
18
+ *
19
+ * No-op when `.dockerignore` doesn't exist: Docker copies everything by
20
+ * default, so the encrypted file is already in the context.
21
+ */
22
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
23
+ import { join } from "node:path";
24
+ const SECTION_HEADER = "# hatchkit: dotenvx-encrypted .env.production must remain in the build context (decrypted in-memory by `dotenvx run`)";
25
+ /** Idempotently ensure `<repoRoot>/.dockerignore` allows `.env.production`
26
+ * through. Appends `!.env.production` (with a `# hatchkit:` bread crumb)
27
+ * when the negation is missing AND a `.dockerignore` exists.
28
+ *
29
+ * The negation is a safe append — if nothing actually excluded
30
+ * `.env.production`, the line is a redundant no-op. We don't try to
31
+ * parse the user's patterns to decide whether the line is needed; the
32
+ * parsing surface area (globs, `**`, anchored vs. unanchored, repeated
33
+ * rule overrides) isn't worth getting subtly wrong when the redundant
34
+ * case is harmless.
35
+ */
36
+ export function ensureDockerignoreAllowsEnvProduction(repoRoot) {
37
+ const path = join(repoRoot, ".dockerignore");
38
+ if (!existsSync(path)) {
39
+ return { path, fileExisted: false, modified: false };
40
+ }
41
+ const existing = readFileSync(path, "utf-8");
42
+ // Match `!.env.production` exactly — trim whitespace, ignore inline
43
+ // comments. We don't accept `!**/.env.production` or other variants
44
+ // here; they're rare enough that erring on the side of duplicate
45
+ // negations is fine.
46
+ const hasNegate = existing.split(/\r?\n/).some((l) => l.trim() === "!.env.production");
47
+ if (hasNegate) {
48
+ return { path, fileExisted: true, modified: false };
49
+ }
50
+ // Append with a leading blank line + comment so the appended block
51
+ // visually separates from whatever the user already had. End with a
52
+ // trailing newline so a future append doesn't glue onto our line.
53
+ const needsLeadingNewline = existing.length > 0 && !existing.endsWith("\n");
54
+ const block = `${needsLeadingNewline ? "\n" : ""}\n${SECTION_HEADER}\n!.env.production\n`;
55
+ writeFileSync(path, existing + block);
56
+ return { path, fileExisted: true, modified: true };
57
+ }
58
+ //# sourceMappingURL=dockerignore.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dockerignore.js","sourceRoot":"","sources":["../../src/utils/dockerignore.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAClE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAEjC,MAAM,cAAc,GAClB,uHAAuH,CAAC;AAa1H;;;;;;;;;;GAUG;AACH,MAAM,UAAU,qCAAqC,CAAC,QAAgB;IACpE,MAAM,IAAI,GAAG,IAAI,CAAC,QAAQ,EAAE,eAAe,CAAC,CAAC;IAC7C,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,EAAE,CAAC;QACtB,OAAO,EAAE,IAAI,EAAE,WAAW,EAAE,KAAK,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAC;IACvD,CAAC;IACD,MAAM,QAAQ,GAAG,YAAY,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;IAC7C,oEAAoE;IACpE,oEAAoE;IACpE,iEAAiE;IACjE,qBAAqB;IACrB,MAAM,SAAS,GAAG,QAAQ,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,KAAK,kBAAkB,CAAC,CAAC;IACvF,IAAI,SAAS,EAAE,CAAC;QACd,OAAO,EAAE,IAAI,EAAE,WAAW,EAAE,IAAI,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAC;IACtD,CAAC;IAED,mEAAmE;IACnE,oEAAoE;IACpE,kEAAkE;IAClE,MAAM,mBAAmB,GAAG,QAAQ,CAAC,MAAM,GAAG,CAAC,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;IAC5E,MAAM,KAAK,GAAG,GAAG,mBAAmB,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,KAAK,cAAc,sBAAsB,CAAC;IAC1F,aAAa,CAAC,IAAI,EAAE,QAAQ,GAAG,KAAK,CAAC,CAAC;IACtC,OAAO,EAAE,IAAI,EAAE,WAAW,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;AACrD,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hatchkit",
3
- "version": "0.1.28",
3
+ "version": "0.1.30",
4
4
  "packageManager": "pnpm@10.33.2",
5
5
  "description": "Interactive CLI for scaffolding full-stack projects and provisioning observability/email clients",
6
6
  "type": "module",