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 # бот: меню, хэндлеры кнопок
|
index.js # бот: меню, хэндлеры кнопок
|
||||||
lib/generator.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",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "node index.js",
|
"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": [
|
"keywords": [
|
||||||
"telegram",
|
"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