Add Docker build + GitHub Actions CI/CD (test -> build -> publish to GHCR)

This commit is contained in:
kromskii2 committed 2026-09-10 15:02:53 +07:00
1 parent 3ba969dbf4
commit 20c3fb1c06
7 files changed
+208 -1

No files matched your search

+11
View File
@@ -0,0 +1,11 @@
node_modules
.git
.github
.env
.env.example
bot.log
out.log
Dockerfile
.dockerignore
README.md
tests
+90
View File
@@ -0,0 +1,90 @@
name: CI/CD — Docker build & push
on:
push:
branches: [main]
tags: ['v*']
pull_request:
branches: [main]
workflow_dispatch:
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
jobs:
test:
name: Syntax & generator checks
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
- name: Install deps
run: npm ci --omit=dev
- name: Syntax check
run: node --check index.js && node --check lib/generator.js
- name: Generator smoke test
run: node tests/smoke.mjs
build:
name: Build Docker image
runs-on: ubuntu-latest
needs: test
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v4
- uses: docker/setup-buildx-action@v3
- name: Log in to GHCR (only on push to main/tags)
if: github.event_name != 'pull_request'
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Docker meta (tags & labels)
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=raw,value=latest,enable={{is_default_branch}}
type=ref,event=branch
type=ref,event=pr
type=sha
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
- name: Build (and push on main/tags)
uses: docker/build-push-action@v6
with:
context: .
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
deploy:
name: Report ready image
runs-on: ubuntu-latest
needs: build
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
steps:
- name: Summary
run: |
echo "### Image published" >> "$GITHUB_STEP_SUMMARY"
echo "\`${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest\` — готов к запуску:" >> "$GITHUB_STEP_SUMMARY"
echo '```' >> "$GITHUB_STEP_SUMMARY"
echo "docker run -d --env-file .env ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest" >> "$GITHUB_STEP_SUMMARY"
echo '```' >> "$GITHUB_STEP_SUMMARY"
+19
View File
@@ -0,0 +1,19 @@
FROM node:22-alpine
ENV NODE_ENV=production
WORKDIR /app
# Сначала зависимости — слой кешируется
COPY package.json package-lock.json ./
RUN npm ci --omit=dev && npm cache clean --force
# Затем код
COPY index.js ./
COPY lib ./lib
# Нерут-пользователь
RUN addgroup -S bot && adduser -S -D -G bot bot
USER bot
CMD ["node", "index.js"]
+23
View File
@@ -35,3 +35,26 @@ OWNER_ID=ваш_user_id
index.js # бот: меню, хэндлеры кнопок
lib/generator.js # генератор + оценка энтропии
```
## Docker / CI-CD
✅ GitHub Actions: `.github/workflows/docker.yml` — при пуше в `main`, PR и теге `v*`:
1. **test** — `node --check` + смоук-тесты генератора (`npm test`)
2. **build** — сборка образа и публикация в GHCR: `ghcr.io/kromskii2/tg_bot_gen` (теги `latest`, `main`, `sha-xxx`, semver для тегов)
Локально:
```bash
npm test # проверки
docker build -t tg_bot_gen . # или npm run docker
docker run --env-file .env tg_bot_gen
# или:
docker compose up -d --build
```
Запуск собранного в CI образа:
```bash
docker run -d --name tg_bot_gen --env-file .env ghcr.io/kromskii2/tg_bot_gen:latest
```
(для private-репозитория: `docker login ghcr.io` с PAT со scope `read:packages`)
+7
View File
@@ -0,0 +1,7 @@
services:
bot:
build: .
image: ghcr.io/kromskii2/tg_bot_gen:latest
env_file: .env
restart: unless-stopped
# long polling — порты наружу не нужны
+3 -1
View File
@@ -6,7 +6,9 @@
"type": "module",
"scripts": {
"start": "node index.js",
"dev": "node --watch index.js"
"dev": "node --watch index.js",
"test": "node tests/smoke.mjs",
"docker": "docker build -t tg_bot_gen ."
},
"keywords": [
"telegram",
+55
View File
@@ -0,0 +1,55 @@
// Смоук-тест генератора: запускается в CI (node tests/smoke.mjs)
import assert from 'node:assert/strict';
import { generatePassword, strength } from '../lib/generator.js';
const cases = [];
// 1. Обычный пароль: все наборы, длина соблюдена
for (let i = 0; i < 50; i++) {
const p = generatePassword({ length: 16, upper: true, lower: true, digits: true, symbols: true });
assert.equal(p.length, 16, 'length 16');
assert.match(p, /[A-Z]/);
assert.match(p, /[a-z]/);
assert.match(p, /[0-9]/);
assert.match(p, /[^a-zA-Z0-9]/);
cases.push('random16');
}
// 2. Без похожих символов: ni один запрещённый символ
for (let i = 0; i < 50; i++) {
const p = generatePassword({ length: 24, upper: true, lower: true, digits: true, symbols: true, excludeAmbiguous: true });
assert.equal(p.length, 24);
assert.doesNotMatch(p, /[l1oOI0]/, 'no ambiguous chars');
cases.push('noAmbiguous24');
}
// 3. Произносимый режим
const pr = generatePassword({ length: 12, pronounceable: true });
assert.equal(pr.length, 12);
assert.match(pr, /[aeiou]/, 'has vowels');
cases.push('pronounceable12');
// 4. Границы длины
assert.equal(generatePassword({ length: 1, upper: true }).length, 4, 'min length clamps to 4');
assert.equal(generatePassword({ length: 999 }).length, 128, 'max length clamps to 128');
cases.push('bounds');
// 5. Пустые наборы -> не падает
const p5 = generatePassword({ length: 8 });
assert.equal(p5.length, 8);
cases.push('noSetsFallback');
// 6. Стойкость
const st = strength('aZ1!');
assert.ok(st.bits > 0 && typeof st.level === 'string');
const stStrong = strength(generatePassword({ length: 64 }));
assert.ok(stStrong.bits >= 200, 'long password has high entropy');
cases.push('strength');
// 7. Уникальность (1000 паролей не повторяются)
const set = new Set();
for (let i = 0; i < 1000; i++) set.add(generatePassword({ length: 16 }));
assert.equal(set.size, 1000, 'all unique');
cases.push('uniqueness');
console.log('SMOKE OK — ' + cases.length + ' групп проверок пройдено');