Skip to content

Commit a247d89

Browse files
Merge pull request #29 from QuentinHourdeaux/feat/0.1-hardening
Feat/0.1 hardening
2 parents 03b8449 + 3a3ce86 commit a247d89

12 files changed

Lines changed: 577 additions & 110 deletions

File tree

.github/workflows/ci.yml

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,24 @@ jobs:
2121
with:
2222
deno-version: v2.9.1
2323

24-
- name: Install locked dependencies
25-
run: deno install --frozen
24+
- name: Set up repository
25+
run: |
26+
require_setup_report() {
27+
expected_database_status="$1"
28+
setup_output="$(./scripts/setup.sh)"
29+
printf '%s\n' "$setup_output"
30+
31+
grep -Eq '^ OK[[:space:]]+Development workflow' <<< "$setup_output"
32+
grep -Eq '^ OK[[:space:]]+Docker workflow' <<< "$setup_output"
33+
grep -Eq "^ ${expected_database_status}[[:space:]]+Development database" <<< "$setup_output"
34+
grep -Eq "^ ${expected_database_status}[[:space:]]+Production database" <<< "$setup_output"
35+
}
36+
37+
require_setup_report INITIALIZED
38+
require_setup_report CURRENT
2639
2740
- name: Verify application
2841
run: deno task ci
2942

30-
- name: Build container
31-
run: docker build .
43+
- name: Verify container runtime
44+
run: ./scripts/verify-docker-runtime.sh

Dockerfile

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,14 @@ COPY --from=build /app/node_modules ./node_modules
2525
COPY deno.json deno.lock package.json ./
2626
COPY api ./api
2727
COPY migrations ./migrations
28+
COPY scripts/docker-entrypoint.sh /usr/local/bin/stackdraft-entrypoint
2829
COPY --from=build /app/dist ./dist
2930

30-
RUN mkdir -p /data && chown -R deno:deno /app /data
31+
RUN mkdir -p /data \
32+
&& chown deno:deno /data \
33+
&& chmod +x /usr/local/bin/stackdraft-entrypoint
3134

32-
USER deno
35+
ENTRYPOINT ["stackdraft-entrypoint"]
3336

3437
EXPOSE 8000
3538
VOLUME ["/data"]

README.md

Lines changed: 64 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -38,9 +38,33 @@ persistent SQLite database. Remaining work is described by the planned PRs.
3838
- React 19 and Vite
3939
- One production container with a mounted data directory
4040

41+
## Set up a clone
42+
43+
After cloning the repository, run the post-clone setup as your normal user:
44+
45+
```sh
46+
./scripts/setup.sh
47+
```
48+
49+
The command prepares the development and production data directories, enables
50+
the tracked Git hooks, and reports Docker and local-development readiness
51+
separately. For every available workflow, it also installs or builds the locked
52+
application dependencies and creates or migrates that workflow's SQLite
53+
database.
54+
55+
Setup does not install Docker or Deno, create `.env`, or start Stackdraft. It
56+
prints the missing prerequisite and the next command for each workflow that is
57+
ready. The command is idempotent and can be rerun after pulling migrations or
58+
dependency changes. If the Compose service is running, paused, or restarting,
59+
setup leaves it alone and asks you to stop it before migrating the production
60+
database. Do not run setup with `sudo`.
61+
4162
## Run with Docker
4263

4364
Docker is the only host dependency for the production-style workflow.
65+
`./scripts/setup.sh` verifies Docker and Compose, builds the production image,
66+
and initializes or migrates `./data/prod/stackdraft.sqlite` in a one-shot
67+
container without publishing a port.
4468

4569
```sh
4670
docker compose up --build
@@ -59,16 +83,16 @@ The database remains in `./data/prod`.
5983

6084
## Develop locally
6185

62-
Install Deno 2.9.1, then install the locked dependencies:
86+
Install Deno 2.9.1, then run the same post-clone setup command:
6387

6488
```sh
65-
deno install --frozen
66-
deno task setup:git-hooks
89+
./scripts/setup.sh
6790
```
6891

69-
`setup:git-hooks` points this clone at `.githooks/` so the `prepare-commit-msg`
70-
hook can strip `Co-authored-by: Cursor` lines from agent commits. Run it once
71-
after checkout.
92+
When Deno 2.9.1 is available, setup runs `deno install --frozen`, applies the
93+
development migrations to `./data/dev/stackdraft.sqlite`, and points this clone
94+
at `.githooks/`. The `prepare-commit-msg` hook strips `Co-authored-by: Cursor`
95+
lines from agent commits.
7296

7397
Start the API and Vite development server together:
7498

@@ -97,10 +121,12 @@ installing the extension so it replaces the default TypeScript language server.
97121

98122
### Setup
99123

100-
| Command | Purpose |
101-
| --------------------------- | ---------------------------------------- |
102-
| `deno install --frozen` | Install locked Deno and npm dependencies |
103-
| `deno task setup:git-hooks` | Enable tracked Git hooks for this clone |
124+
| Command | Purpose |
125+
| --------------------------- | --------------------------------------------------- |
126+
| `./scripts/setup.sh` | Prepare the clone and report workflow readiness |
127+
| `deno task setup` | Run post-clone setup when Deno is already available |
128+
| `deno install --frozen` | Install locked Deno and npm dependencies |
129+
| `deno task setup:git-hooks` | Enable tracked Git hooks for this clone |
104130

105131
### Development
106132

@@ -111,15 +137,17 @@ installing the extension so it replaces the default TypeScript language server.
111137
| `deno task dev:web` | Run the Vite frontend dev server. Proxies `/api` to the Deno API. |
112138
| `deno task start` | Run the API as a single local process without file watching. Also defaults to the development database. |
113139

114-
### Database (development only)
140+
### Database
115141

116142
| Command | Purpose |
117143
| -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
118144
| `deno task db:migrate:dev` | Apply pending SQL migrations to `./data/dev/stackdraft.sqlite` without starting the HTTP server. |
119145
| `deno task db:reset:dev` | Delete development SQLite files under `./data/dev` and recreate a fresh database. Refuses `./data/prod` and any path outside `./data/dev`. |
120146

121-
There are no production-style migration or reset tasks yet. Docker Compose still
122-
applies migrations automatically when the container starts.
147+
The direct database tasks target development only. For production-style data,
148+
`./scripts/setup.sh` applies migrations through a one-shot container without
149+
starting the application. There is deliberately no production reset command.
150+
Docker Compose also applies pending migrations when the application starts.
123151

124152
### Quality and build
125153

@@ -178,6 +206,10 @@ Runtime SQLite data lives in two host directories:
178206
- `./data/dev` for local Deno development
179207
- `./data/prod` for Docker Compose
180208

209+
`./scripts/setup.sh` creates or migrates the database for each workflow whose
210+
runtime is available. Application startup also keeps migrations idempotently
211+
current.
212+
181213
For a safe v0.1 backup of the production-style database:
182214

183215
```sh
@@ -186,14 +218,17 @@ cp data/prod/stackdraft.sqlite stackdraft-backup.sqlite
186218
docker compose start
187219
```
188220

189-
To restore or transfer Stackdraft:
221+
To restore a stopped backup into a clean checkout or deployment:
190222

191-
1. Stop Stackdraft.
192-
2. Copy the repository and the relevant `data/prod` directory to the destination
193-
machine.
194-
3. Install Docker.
223+
1. Stop Stackdraft if the destination is already running.
224+
2. Create `data/prod` in the destination checkout if it does not exist.
225+
3. Copy the backup to `data/prod/stackdraft.sqlite`.
195226
4. Run `docker compose up -d --build`.
196227

228+
To transfer the complete deployment, copy the repository and its stopped
229+
`data/prod` directory to the destination machine, install Docker, and run the
230+
same Compose command.
231+
197232
Do not copy the SQLite file while Stackdraft is running. An online backup
198233
command can be added later.
199234

@@ -231,19 +266,19 @@ core behavior and typed failures. SQLite owns persistent state.
231266

232267
## Current scope
233268

234-
The skeleton intentionally implements only:
269+
Stackdraft v0.1 implements the Draft, Stack, and State workflows described in
270+
[`docs/v0.1-spec.md`](docs/v0.1-spec.md):
235271

236-
- Application shell
237-
- `GET /api/health`
238-
- State catalog, create, update, move, and default-selection APIs
239-
- Effect health and state services
240-
- SQLite connection and migration runner
241-
- Development and production build paths
242-
- Docker persistence
243-
- Merge-blocking assembled API QA harness
272+
- Global standalone and stacked Draft capture, editing, assignment, and State
273+
filtering
274+
- Stack creation, editing, State filtering, and Stack-specific Draft capture
275+
- Stack and Draft State creation, editing, ordering, default selection, and
276+
guarded deletion
277+
- Persistent SQLite storage, migrations, health reporting, Docker deployment,
278+
and stopped-database backup and restoration
244279

245-
It does not yet implement Stacks, Drafts, State deletion, or authentication. See
246-
[`docs/v0.1-spec.md`](docs/v0.1-spec.md) for the product scope.
280+
Stack and Draft deletion, authentication, and the specification's explicit
281+
non-goals remain outside v0.1.
247282

248283
## License
249284

deno.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
"lint": "deno lint",
2727
"qa:api:full": "deno run --allow-env --allow-net --allow-read --allow-write --allow-run qa/api-suite.ts --mode full",
2828
"qa:api:smoke": "deno run --allow-net qa/api-suite.ts --mode smoke",
29+
"setup": "bash scripts/setup.sh",
2930
"setup:git-hooks": "bash scripts/setup-git-hooks.sh",
3031
"start": "deno run --allow-env --allow-net --allow-read --allow-write api/main.ts",
3132
"test": "deno task test:api && deno task test:web",

docs/planned-prs/18-v0.1-hardening.md

Lines changed: 0 additions & 74 deletions
This file was deleted.

frontend/src/features/draft/draft-quick-create-form.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,9 @@ export function DraftQuickCreateForm({
4141

4242
const trimmedTitle = title.trim();
4343
if (trimmedTitle.length === 0) {
44+
setTitleError("Title is required.");
45+
setFormError(null);
46+
titleInputRef.current?.focus();
4447
return;
4548
}
4649

frontend/src/features/state/state-settings-screen.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ export function StateSettingsScreen() {
2020
<StateScopeSection
2121
scope="draft"
2222
title="Draft states"
23-
description="Used when tracking Draft work inside a Stack."
23+
description="Used when tracking standalone Drafts or Drafts in a Stack."
2424
/>
2525
</div>
2626
</section>

frontend/tests/draft-list.test.tsx

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -472,6 +472,40 @@ describe("draft list screen", () => {
472472
expect(createBody).toEqual({ title: "Quick capture" });
473473
});
474474

475+
it("shows a field error for a whitespace-only quick-create title", async () => {
476+
let createCount = 0;
477+
478+
mockFetch((input, init) => {
479+
const url = new URL(String(input), "http://stackdraft.local");
480+
const method = init?.method ?? "GET";
481+
482+
if (url.pathname === "/api/drafts" && method === "POST") {
483+
createCount += 1;
484+
}
485+
486+
return Promise.resolve(defaultDraftHandler()(input, init));
487+
});
488+
489+
const user = userEvent.setup();
490+
renderApp("/");
491+
492+
const createForm = await screen.findByRole("form", {
493+
name: "Capture your first Draft",
494+
});
495+
const titleInput = within(createForm).getByLabelText("Title");
496+
497+
await user.type(titleInput, " ");
498+
await user.click(
499+
within(createForm).getByRole("button", { name: "Add Draft" }),
500+
);
501+
502+
expect(within(createForm).getByText("Title is required."))
503+
.toBeInTheDocument();
504+
expect(titleInput).toHaveAttribute("aria-invalid", "true");
505+
expect(titleInput).toHaveFocus();
506+
expect(createCount).toBe(0);
507+
});
508+
475509
it("submits on Enter and prevents duplicate submissions", async () => {
476510
let createCount = 0;
477511

frontend/tests/state-settings.test.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,11 @@ describe("state settings screen", () => {
163163

164164
expect(within(stackSection).getAllByText("Default")).toHaveLength(1);
165165
expect(within(draftSection).getAllByText("Default")).toHaveLength(1);
166+
expect(
167+
within(draftSection).getByText(
168+
"Used when tracking standalone Drafts or Drafts in a Stack.",
169+
),
170+
).toBeInTheDocument();
166171
expect(
167172
within(stackSection).getByLabelText("Planned color"),
168173
).toHaveStyle({ backgroundColor: "rgb(141, 152, 165)" });

scripts/docker-entrypoint.sh

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
#!/bin/sh
2+
set -eu
3+
4+
if [ "$(id -u)" -eq 0 ]; then
5+
data_owner=$(stat -c '%u:%g' /data)
6+
data_uid=${data_owner%:*}
7+
data_gid=${data_owner#*:}
8+
9+
# Docker Desktop presents bind mounts as root-owned while still allowing the
10+
# image's deno user to write. Native Linux preserves the host directory's
11+
# owner, so run with that identity to keep the bind mount writable without
12+
# changing ownership of the user's data directory.
13+
if [ "$data_uid" -eq 0 ]; then
14+
data_uid=$(id -u deno)
15+
data_gid=$(id -g deno)
16+
fi
17+
18+
exec setpriv \
19+
--reuid="$data_uid" \
20+
--regid="$data_gid" \
21+
--clear-groups \
22+
"$@"
23+
fi
24+
25+
exec "$@"

0 commit comments

Comments
 (0)