feat: add frontend container delivery #16

Merged
ksv741 merged 1 commits from codex/containerized-deployment into develop 2026-09-16 10:23:22 +03:00
6 changed files with 181 additions and 0 deletions
+15
View File
@@ -0,0 +1,15 @@
**
!package.json
!package-lock.json
!.nvmrc
!index.html
!tsconfig*.json
!vite.config.ts
!.storybook/
!.storybook/**
!src/
!src/**
!public/
!public/**
!docker/
!docker/**
+61
View File
@@ -0,0 +1,61 @@
name: Frontend image
on:
push:
tags: ['v*']
concurrency:
group: frontend-image-publication
cancel-in-progress: false
jobs:
publish:
runs-on: ubuntu-latest
env:
IMAGE: git.personal.ksv741.ru/wealthledger/frontend
RELEASE_TAG: ${{ gitea.ref_name }}
REVISION: ${{ gitea.sha }}
DOCKER_CONFIG: ${{ runner.temp }}/wealthledger-frontend-docker
steps:
- uses: https://github.com/actions/checkout@v4
- uses: https://github.com/actions/setup-node@v4
with:
node-version-file: .nvmrc
- name: Validate release version
run: |
node --input-type=commonjs -e 'const p = require("./package.json"); const l = require("./package-lock.json"); if (!/^v\d+\.\d+\.\d+$/.test(process.env.RELEASE_TAG) || process.env.RELEASE_TAG !== `v${p.version}` || l.version !== p.version || l.packages[""].version !== p.version) process.exit(1)'
test "$(git rev-parse HEAD)" = "$REVISION"
docker version
docker buildx version
- run: npm ci
env:
HUSKY: '0'
- run: npm run verify
env:
KNIP_DISABLE_RAW_TRANSFER: '1'
- name: Login with dedicated package writer
env:
REGISTRY_USERNAME: ${{ secrets.REGISTRY_USERNAME }}
REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }}
run: |
test -n "$REGISTRY_USERNAME" && test -n "$REGISTRY_TOKEN"
install -d -m 700 "$DOCKER_CONFIG"
printf '%s' "$REGISTRY_TOKEN" | docker login git.personal.ksv741.ru --username "$REGISTRY_USERNAME" --password-stdin
- name: Publish immutable release tag
run: |
VERSION="${RELEASE_TAG#v}"
if docker manifest inspect "$IMAGE:$VERSION" >/dev/null 2>manifest-error.log; then
echo 'Refusing to overwrite an existing release tag'; exit 1
fi
if ! grep -Eq 'manifest unknown|no such manifest' manifest-error.log; then
echo 'Cannot prove tag absence; refusing publication'; exit 1
fi
docker buildx build --target production --build-arg VERSION="$VERSION" --build-arg REVISION="$REVISION" --tag "$IMAGE:$VERSION" --metadata-file image-metadata.json --push .
DIGEST="$(node --input-type=commonjs -p 'require("./image-metadata.json")["containerimage.digest"]')"
docker pull "$IMAGE@$DIGEST"
test "$(docker image inspect "$IMAGE@$DIGEST" --format '{{index .Config.Labels "org.opencontainers.image.revision"}}')" = "$REVISION"
docker run --rm --entrypoint sh "$IMAGE@$DIGEST" -c 'test -s /usr/share/nginx/html/index.html && nginx -t && nginx && wget -q -O /dev/null http://127.0.0.1:8080/'
printf 'Frontend image: %s@%s\n' "$IMAGE" "$DIGEST"
- name: Remove registry session
if: always()
run: rm -rf "$DOCKER_CONFIG"
+30
View File
@@ -0,0 +1,30 @@
FROM node:24.21.0-bookworm-slim AS dependencies
WORKDIR /app
ENV HUSKY=0
COPY package.json package-lock.json ./
RUN npm ci
FROM dependencies AS development
COPY . .
ENV VITE_API_BASE_URL="" API_PROXY_TARGET=http://backend:3000
EXPOSE 8080
CMD ["npm", "run", "dev", "--", "--host", "0.0.0.0", "--port", "8080", "--strictPort"]
FROM dependencies AS build
COPY . .
ENV VITE_API_BASE_URL=""
RUN npm run build
FROM nginx:1.28.0-alpine AS production
ARG VERSION
ARG REVISION
LABEL org.opencontainers.image.version=$VERSION \
org.opencontainers.image.revision=$REVISION
COPY docker/nginx.conf /etc/nginx/nginx.conf
COPY --from=build /app/dist /usr/share/nginx/html
USER nginx
EXPOSE 8080
ENTRYPOINT ["nginx"]
CMD ["-g", "daemon off;"]
HEALTHCHECK --interval=10s --timeout=3s --start-period=10s --retries=6 \
CMD wget -q -O /dev/null http://127.0.0.1:8080/ || exit 1
+25
View File
@@ -23,6 +23,31 @@ staged diff с полной редакцией секретов, затем за
Находка или отсутствие Gitleaks блокирует commit. Находка или отсутствие Gitleaks блокирует commit.
Проверка этого поведения: `node --test scripts/test-pre-commit.mjs`. Проверка этого поведения: `node --test scripts/test-pre-commit.mjs`.
## Контейнерный image
`Dockerfile` имеет targets `development` (Vite HMR и proxy `/api/` во внутренний backend)
и `production` (nginx на 8080, SPA fallback и same-origin API proxy). Node соответствует `.nvmrc`.
Build context ограничен `.dockerignore`; `.env`, local DB и host node_modules
не попадают в image.
```bash
docker build --target development -t wealthledger-frontend:development .
docker build --target production -t wealthledger-frontend:production .
```
Gitea `image.yml` запускается на защищённом release tag `vX.Y.Z`, проверяет
package/lock version и canonical gates. Runner `ubuntu-latest` должен иметь
Docker/buildx. Dedicated package writer задаётся Actions secrets
`REGISTRY_USERNAME`/`REGISTRY_TOKEN`; server использует отдельную read-only
identity. Публикуются semver tags, без `latest`; существующие
release tags не перезаписываются. Production выбирает опубликованный version tag.
Compose topology принадлежит workspace, эксплуатационная последовательность —
[документация](https://git.personal.ksv741.ru/WealthLedger/docs),
страница `architecture/deployment.md`.
Проверка публикации требует настроенного registry/runner; локальный build
не доказывает package permissions.
## Проверка ## Проверка
```bash ```bash
+43
View File
@@ -0,0 +1,43 @@
worker_processes auto;
pid /tmp/nginx.pid;
error_log /dev/stderr warn;
events { worker_connections 1024; }
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
# Do not log query strings, cookies, authorization or bodies.
log_format safe '$request_method $uri $status';
access_log /dev/stdout safe;
client_body_temp_path /tmp/client_temp;
proxy_temp_path /tmp/proxy_temp;
fastcgi_temp_path /tmp/fastcgi_temp;
uwsgi_temp_path /tmp/uwsgi_temp;
scgi_temp_path /tmp/scgi_temp;
map $http_x_forwarded_proto $forwarded_proto {
default $http_x_forwarded_proto;
'' $scheme;
}
server {
listen 8080;
server_name _;
root /usr/share/nginx/html;
resolver 127.0.0.11 valid=5s ipv6=off;
location /api/ {
set $backend http://backend:3000;
proxy_pass $backend$request_uri;
proxy_set_header Host $http_host;
proxy_set_header X-Forwarded-Host $http_host;
proxy_set_header X-Forwarded-Proto $forwarded_proto;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Request-ID $http_x_request_id;
}
location /assets/ {
try_files $uri =404;
add_header Cache-Control "public, max-age=31536000, immutable";
}
location / {
try_files $uri $uri/ /index.html;
add_header Cache-Control "no-cache" always;
}
}
}
+7
View File
@@ -7,4 +7,11 @@ import { defineConfig } from "vite";
export default defineConfig({ export default defineConfig({
plugins: [react(), babel({ presets: [reactCompilerPreset()] })], plugins: [react(), babel({ presets: [reactCompilerPreset()] })],
resolve: { alias: { "@": fileURLToPath(new URL("./src", import.meta.url)) } }, resolve: { alias: { "@": fileURLToPath(new URL("./src", import.meta.url)) } },
server: {
proxy: {
"/api/": {
target: process.env.API_PROXY_TARGET ?? "http://localhost:3000",
},
},
},
}); });