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.
|
Находка или отсутствие 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
|
||||||
|
|||||||
@@ -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({
|
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",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user