feat: add frontend container delivery
Frontend security / security (pull_request) Successful in 9s
Frontend verify / verify (pull_request) Successful in 3m55s
Conventional commits / commits (pull_request) Successful in 21s
Frontend security / security (push) Successful in 8s
Frontend verify / verify (push) Successful in 3m50s
Frontend security / security (pull_request) Successful in 9s
Frontend verify / verify (pull_request) Successful in 3m55s
Conventional commits / commits (pull_request) Successful in 21s
Frontend security / security (push) Successful in 8s
Frontend verify / verify (push) Successful in 3m50s
This commit was merged in pull request #16.
This commit is contained in:
@@ -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/**
|
||||
@@ -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
@@ -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
|
||||
@@ -23,6 +23,31 @@ staged diff с полной редакцией секретов, затем за
|
||||
Находка или отсутствие Gitleaks блокирует commit.
|
||||
Проверка этого поведения: `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
|
||||
|
||||
@@ -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,4 +7,11 @@ import { defineConfig } from "vite";
|
||||
export default defineConfig({
|
||||
plugins: [react(), babel({ presets: [reactCompilerPreset()] })],
|
||||
resolve: { alias: { "@": fileURLToPath(new URL("./src", import.meta.url)) } },
|
||||
server: {
|
||||
proxy: {
|
||||
"/api/": {
|
||||
target: process.env.API_PROXY_TARGET ?? "http://localhost:3000",
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user