103 lines
6.6 KiB
Markdown
103 lines
6.6 KiB
Markdown
# Phase 0 Research: Continuous Integration Pipeline
|
|||
|
|
|
||
|
|
No `NEEDS CLARIFICATION` markers remained in the Technical Context after `/speckit-plan`'s
|
||
|
|
Technical Context pass — this document records the decisions behind that context rather than
|
||
|
|
resolving open unknowns.
|
||
|
|
|
||
|
|
## Decision: CI system — Jenkins declarative pipeline
|
||
|
|
|
||
|
|
- **Decision**: Use a single `Jenkinsfile` (declarative syntax) at the repo root.
|
||
|
|
- **Rationale**: The constitution's Technology & Platform Constraints section and
|
||
|
|
`docs/09-testing-observability-cicd.md` §3 both name Jenkins explicitly, with a defined stage
|
||
|
|
order. This isn't a free choice — using anything else would need a constitution amendment.
|
||
|
|
- **Alternatives considered**: GitHub Actions / GitLab CI — rejected only because the governing
|
||
|
|
docs already commit to Jenkins; otherwise equally viable for this repo's needs.
|
||
|
|
|
||
|
|
## Decision: Environment/secrets handling in the pipeline
|
||
|
|
|
||
|
|
- **Decision**: The pipeline injects `POSTGRES_PASSWORD`, `REDIS_PASSWORD`, `JWT_SECRET`, and AWS
|
||
|
|
credentials from Jenkins' credentials store as environment variables / a generated `.env.*`
|
||
|
|
file written into the workspace at runtime — never read from a file committed to the
|
||
|
|
repository.
|
||
|
|
- **Rationale**: `.env.development`, `.env.test`, and `.env.prod` were found committed to git
|
||
|
|
with real dev credentials in plain text (fixed separately: untracked, `.env.example` added,
|
||
|
|
see repo commit `2093898`). `docker-compose.test.yml`'s `app` service still declares
|
||
|
|
`env_file: .env.test`, so the pipeline's test stage must materialize a `.env.test` in the
|
||
|
|
workspace from Jenkins credentials immediately before `docker compose up`, then discard it when
|
||
|
|
the stage ends — the checked-in `.env.test` template must only ever contain non-secret
|
||
|
|
placeholder values from here on, matching `.env.prod`'s existing `CHANGE_ME` pattern.
|
||
|
|
Production deploy correspondingly generates `.env.prod` the same way, from Jenkins prod
|
||
|
|
credentials, never from the repo copy.
|
||
|
|
- **Alternatives considered**: Docker secrets / mounted files instead of generated `.env` files —
|
||
|
|
viable but a larger change to `docker-compose.*.yml`; deferred as out of scope since it doesn't
|
||
|
|
change the pipeline's external behavior (FR-008 is satisfied either way).
|
||
|
|
|
||
|
|
## Decision: Environment validation stage
|
||
|
|
|
||
|
|
- **Decision**: The environment-validation stage runs the existing `env.ts` Zod schema
|
||
|
|
(`src/config/env.ts`) against the materialized environment before any test stage starts, by
|
||
|
|
invoking a lightweight script (e.g. `node --env-file=.env.<target> -e "require('./dist/src/config/env.js')"`
|
||
|
|
post-build, or a dedicated `tsx` invocation pre-build) so a missing/malformed variable fails
|
||
|
|
immediately with the schema's existing descriptive Zod error, satisfying FR-002.
|
||
|
|
- **Rationale**: `src/config/env.ts` already throws a specific, actionable error
|
||
|
|
(`❌ Invalid environment variables: ...`) on `safeParse` failure — no new validation logic is
|
||
|
|
needed, just an early pipeline invocation of the existing one.
|
||
|
|
- **Alternatives considered**: A separate shell script re-implementing required-var checks —
|
||
|
|
rejected as duplicate logic that could drift from the real Zod schema.
|
||
|
|
|
||
|
|
## Decision: Quality stage contents
|
||
|
|
|
||
|
|
- **Decision**: Stage-to-script mapping is direct:
|
||
|
|
- `install` → `npm ci`
|
||
|
|
- `typecheck` → `npm run typecheck`
|
||
|
|
- `lint` → `npm run lint` (consider folding `scripts/check-architecture.ts`'s module-boundary
|
||
|
|
check into this stage, since it enforces constitution Principle III and already runs in
|
||
|
|
`.husky/pre-commit` — confirmed as in-scope, see Assumptions below)
|
||
|
|
- `format check` → `npm run format:check`
|
||
|
|
- `unit test` → `npm run test:unit`
|
||
|
|
- `integration test` → `npm run test:integration` (requires `docker-compose.test.yml`'s
|
||
|
|
`postgres`/`redis` services running first)
|
||
|
|
- `e2e test` → `npm run test:e2e` (same dependency)
|
||
|
|
- `build` → `npm run build:prod` (or `build:test`/`build:development` depending on target,
|
||
|
|
matching the `BUILD_COMMAND` pattern already used by each `docker-compose.*.yml`)
|
||
|
|
- `docker build` → `docker build` using the existing root `Dockerfile`
|
||
|
|
- **Rationale**: Every stage maps to a script that already exists and is already exercised
|
||
|
|
locally/in the pre-commit hook — the pipeline's job is orchestration and environment isolation,
|
||
|
|
not defining new checks (matches plan.md's Summary).
|
||
|
|
- **Alternatives considered**: None — this mapping is essentially forced by "don't introduce new
|
||
|
|
checks" (spec.md Assumptions).
|
||
|
|
|
||
|
|
## Decision: Publish/deploy mechanism
|
||
|
|
|
||
|
|
- **Decision**: `publish` pushes the built image to a container registry (registry choice left to
|
||
|
|
implementation/tasks phase — no registry is currently configured in the repo); `deploy` runs
|
||
|
|
`docker compose --env-file <generated .env> -f docker-compose.<target>.yml up -d` on the target
|
||
|
|
host/agent, reusing the `docker:up:*` npm scripts' underlying compose invocation.
|
||
|
|
- **Rationale**: The repo already models per-environment deployment as
|
||
|
|
`docker compose -f docker-compose.<env>.yml up -d` (see `docker:up:dev`, `docker:up:test`,
|
||
|
|
`docker:up:prod` in `package.json`) — the pipeline should drive the same mechanism an engineer
|
||
|
|
would run by hand today, not invent a new one.
|
||
|
|
- **Alternatives considered**: Kubernetes/Helm deploy — no k8s manifests exist in the repo today;
|
||
|
|
out of scope unless a future feature introduces them.
|
||
|
|
|
||
|
|
## Decision: Concurrent-run isolation (FR-009)
|
||
|
|
|
||
|
|
- **Decision**: Rely on Jenkins' per-build workspace isolation (each pipeline run gets its own
|
||
|
|
workspace directory and, for the Docker-dependent stages, project-scoped Compose project names
|
||
|
|
e.g. `-p support-test-${BUILD_NUMBER}`) rather than building custom isolation logic.
|
||
|
|
- **Rationale**: This is a built-in Jenkins guarantee once each build uses its own workspace and
|
||
|
|
Compose project name; no additional application code is needed.
|
||
|
|
- **Alternatives considered**: None needed — default Jenkins behavior already satisfies this when
|
||
|
|
Compose project names are parameterized by build number.
|
||
|
|
|
||
|
|
## Assumptions carried over from spec.md, confirmed against the codebase
|
||
|
|
|
||
|
|
- `package.json` scripts (`typecheck`, `lint`, `format:check`, `test:unit`, `test:integration`,
|
||
|
|
`test:e2e`, `build*`, `docker:*`) are confirmed present and are the source of truth for stage
|
||
|
|
behavior.
|
||
|
|
- `docker-compose.development.yml` / `.test.yml` / `.prod.yml` are confirmed present and already
|
||
|
|
encode per-environment deploy shape.
|
||
|
|
- `.husky/pre-commit` already runs `lint-staged` and `scripts/check-architecture.ts` locally —
|
||
|
|
the CI lint stage should run the same architecture check server-side so a bypassed/missing
|
||
|
|
local hook can't let a boundary violation merge.
|