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.
- package/dist/adopt.d.ts.map +1 -1
- package/dist/adopt.js +147 -3
- package/dist/adopt.js.map +1 -1
- package/dist/index.js +13 -2
- package/dist/index.js.map +1 -1
- package/dist/provision/s3-buckets.d.ts +9 -1
- package/dist/provision/s3-buckets.d.ts.map +1 -1
- package/dist/provision/s3-buckets.js +50 -20
- package/dist/provision/s3-buckets.js.map +1 -1
- package/dist/scaffold/build-pipeline.d.ts +6 -0
- package/dist/scaffold/build-pipeline.d.ts.map +1 -1
- package/dist/scaffold/build-pipeline.js +11 -1
- package/dist/scaffold/build-pipeline.js.map +1 -1
- package/dist/templates/base/.dockerignore.hbs +5 -1
- package/dist/templates/build-pipeline/Dockerfile.client.hbs +33 -3
- package/dist/templates/build-pipeline/Dockerfile.server.hbs +31 -6
- package/dist/templates/build-pipeline/deploy.yml.hbs +21 -2
- package/dist/utils/dockerignore.d.ts +23 -0
- package/dist/utils/dockerignore.d.ts.map +1 -0
- package/dist/utils/dockerignore.js +58 -0
- package/dist/utils/dockerignore.js.map +1 -0
- package/package.json +1 -1
|
@@ -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
|
|
7
|
-
# (
|
|
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
|
-
|
|
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.
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
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
|
-
|
|
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
|
|
12
|
-
# COOLIFY_TOKEN
|
|
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