Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 13 additions & 5 deletions packages/gittensory-miner/DEPLOYMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ Two form factors for running `@jsonbored/gittensory-miner`: **laptop mode** (sin
| **Best for** | One contributor machine, local experimentation | Many parallel miner attempts on a host or small cluster |
| **Dependencies** | Node.js `>=22.13.0` only | Docker (or compatible runtime) + Node image or custom image |
| **State** | SQLite files under `~/.config/gittensory-miner/` (override with `GITTENSORY_MINER_CONFIG_DIR`) | Same SQLite layout on a mounted `/data` (or `GITTENSORY_MINER_CONFIG_DIR`) volume |
| **Setup** | `npm install -g @jsonbored/gittensory-miner` or workspace build | `docker run` with env + volume (see below) |
| **Setup** | `npm install -g @jsonbored/gittensory-miner` or workspace build | `docker build` + `docker run` with env + volume (see below) |
| **Footprint** | One Node process, local disk for ledgers/queues | One container per worker; scale horizontally by adding containers |

## Laptop mode walkthrough
Expand Down Expand Up @@ -44,18 +44,26 @@ Two form factors for running `@jsonbored/gittensory-miner`: **laptop mode** (sin

## Fleet mode walkthrough

There is no separate published miner fleet image yet. Run the same CLI inside a standard Node container, mount persistent state, and inject secrets at runtime (never bake them into the image):
Build the fleet image from the **monorepo root** (the Dockerfile needs the full workspace on disk before `npm ci` — see comments in [`Dockerfile`](Dockerfile)):

```sh
docker build -f packages/gittensory-miner/Dockerfile -t gittensory-miner:latest .
```

Run a disposable worker with persistent SQLite state on a mounted volume. Inject secrets at runtime (never bake them into the image):

```sh
docker run --rm -it \
-e GITTENSORY_MINER_CONFIG_DIR=/data/miner \
-e GITHUB_TOKEN \
-v miner-data:/data/miner \
node:24-slim \
bash -lc 'npm install -g @jsonbored/gittensory-miner@latest && gittensory-miner doctor && gittensory-miner status'
gittensory-miner:latest \
doctor
```

- **`/data` volume** — holds all SQLite state so containers are disposable.
The image entrypoint is `gittensory-miner`; pass subcommands after the image name (`status`, `doctor`, `claim`, …).

- **`/data/miner` volume** — holds all SQLite state (`claim-ledger.sqlite3`, `plan-store.sqlite3`, etc.) so containers are disposable. Defaults to `GITTENSORY_MINER_CONFIG_DIR=/data/miner` in the image.
- **`GITHUB_TOKEN`** — supplied by the operator at run time; the image contains no credentials.
- **Scale** — launch additional containers with the same volume (or partitioned config dirs) for parallel attempts.

Expand Down
38 changes: 38 additions & 0 deletions packages/gittensory-miner/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# Fleet-mode image for @jsonbored/gittensory-miner (#4295). Build context = monorepo root:
# docker build -f packages/gittensory-miner/Dockerfile -t gittensory-miner:latest .
# SECRETS ARE NEVER BAKED: supply operator credentials at `docker run` time only.
# Persistent SQLite ledgers live on a mounted volume (default GITTENSORY_MINER_CONFIG_DIR=/data/miner).

ARG GITTENSORY_VERSION=

# --- build: workspace install + engine compile + miner syntax check ----------------------------
FROM public.ecr.aws/docker/library/node:24-slim AS build
WORKDIR /app
# Full source BEFORE `npm ci`: npm workspaces only symlinks packages that already exist on disk.
# Same ordering fix as the root gittensory-api Dockerfile — @jsonbored/gittensory-engine must be
# present when `npm ci` runs or gittensory-miner's workspace dependency cannot resolve.
COPY . .
RUN npm ci --ignore-scripts
RUN npm --workspace @jsonbored/gittensory-engine run build
RUN npm --workspace @jsonbored/gittensory-miner run build
RUN npm prune --omit=dev --ignore-scripts

# --- runtime: non-root CLI image with a mounted config volume -----------------------------------
FROM public.ecr.aws/docker/library/node:24-slim AS runtime
WORKDIR /app
ARG GITTENSORY_VERSION=

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: npm prune runs lifecycle scripts of removed packages without --ignore-scripts

npm prune without --ignore-scripts can execute uninstall scripts of removed devDependencies during the build.

Add --ignore-scripts to npm prune to prevent lifecycle script execution during pruning.

AI prompt
Check if this security scanner issue is valid. If so, understand the root cause and fix it. If appropriate, update or add tests. Keep the change focused and preserve intended behavior.

<file name="packages/gittensory-miner/Dockerfile">
<violation number="1" location="packages/gittensory-miner/Dockerfile:23">
<priority>P2</priority>
<title>npm prune runs lifecycle scripts of removed packages without --ignore-scripts</title>
<evidence>RUN npm prune --omit=dev runs in the build stage and can execute preuninstall/uninstall/postuninstall scripts of packages being removed. A compromised devDependency could use these lifecycle hooks to execute malicious code or tamper with production files before they are copied to the runtime stage.</evidence>
<recommendation>Add --ignore-scripts to the npm prune --omit=dev command: RUN npm prune --omit=dev --ignore-scripts</recommendation>
</violation>
</file>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: npm prune runs lifecycle scripts of removed packages without --ignore-scripts

npm prune without --ignore-scripts can execute uninstall scripts of removed devDependencies during the build.

Add --ignore-scripts to npm prune to prevent lifecycle script execution during pruning.

AI prompt
Check if this security scanner issue is valid. If so, understand the root cause and fix it. If appropriate, update or add tests. Keep the change focused and preserve intended behavior.

<file name="packages/gittensory-miner/Dockerfile">
<violation number="1" location="packages/gittensory-miner/Dockerfile:23">
<priority>P2</priority>
<title>npm prune runs lifecycle scripts of removed packages without --ignore-scripts</title>
<evidence>RUN npm prune --omit=dev runs in the build stage and can execute preuninstall/uninstall/postuninstall scripts of packages being removed. A compromised devDependency could use these lifecycle hooks to execute malicious code or tamper with production files before they are copied to the runtime stage.</evidence>
<recommendation>Add --ignore-scripts to the npm prune --omit=dev command: RUN npm prune --omit=dev --ignore-scripts</recommendation>
</violation>
</file>

ENV NODE_ENV=production \
GITTENSORY_MINER_CONFIG_DIR=/data/miner \
GITTENSORY_MINER_VERSION=${GITTENSORY_VERSION} \
PATH=/app/node_modules/.bin:$PATH
COPY --from=build --chown=node:node /app/node_modules ./node_modules
COPY --from=build --chown=node:node /app/packages/gittensory-miner ./packages/gittensory-miner
COPY --from=build --chown=node:node /app/packages/gittensory-engine ./packages/gittensory-engine
RUN mkdir -p /data/miner && chown -R node:node /data
USER node
VOLUME ["/data/miner"]
# No HEALTHCHECK: the miner is a batch/CLI workload (`docker run … gittensory-miner <cmd>`), not a
# long-running HTTP service — there is no steady-state endpoint to probe unless an operator wraps
# the container in their own supervising loop.
ENTRYPOINT ["gittensory-miner"]
CMD ["doctor"]
15 changes: 15 additions & 0 deletions test/unit/miner-deployment-doc.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,25 @@ describe("miner deployment guide (#2330)", () => {
expect(doc).toContain("GITTENSORY_MINER_CONFIG_DIR");
expect(doc).toContain("100% client-side");
expect(doc).toContain("credentials");
expect(doc).toContain("docker build");
expect(doc).toContain("docker run");
expect(doc).toContain("packages/gittensory-miner/Dockerfile");
expect(doc).toContain("gittensory-miner:latest");
expect(doc).toContain("docker-compose.yml");
});

it("ships a fleet Dockerfile with non-root runtime and no baked secrets (#4295)", () => {
const dockerfile = readFileSync(join(process.cwd(), "packages/gittensory-miner/Dockerfile"), "utf8");
expect(dockerfile).toContain("COPY . .");
expect(dockerfile).toContain("npm prune --omit=dev --ignore-scripts");
expect(dockerfile).toContain("@jsonbored/gittensory-engine");
expect(dockerfile).toContain("USER node");
expect(dockerfile).toContain("GITTENSORY_MINER_CONFIG_DIR=/data/miner");
expect(dockerfile).toContain("VOLUME");
expect(dockerfile).toMatch(/No HEALTHCHECK/i);
expect(dockerfile).not.toMatch(/GITHUB_TOKEN|ghp_|github_pat_/i);
});

it("is linked from the miner package README", () => {
const readme = readFileSync(README_PATH, "utf8");
expect(readme).toContain("DEPLOYMENT.md");
Expand Down