Add Docker build + GitHub Actions CI/CD (test -> build -> publish to GHCR)
This commit is contained in:
1 parent
3ba969dbf4
commit
20c3fb1c06
7 files changed
+208
-1
No files matched your search
@@ -0,0 +1,11 @@
|
||||
node_modules
|
||||
.git
|
||||
.github
|
||||
.env
|
||||
.env.example
|
||||
bot.log
|
||||
out.log
|
||||
Dockerfile
|
||||
.dockerignore
|
||||
README.md
|
||||
tests
|
||||
@@ -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
@@ -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"]
|
||||
@@ -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`)
|
||||
@@ -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
@@ -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",
|
||||
|
||||
@@ -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 + ' групп проверок пройдено');
|
||||
Reference in new issue
Block a user