feat: update application versioning and enhance release management
Docker images / prepare-release (push) Successful in 38s
Docker images / backend-image (push) Has been skipped
Docker images / frontend-image (push) Has been skipped
Docker images / updater-image (push) Has been skipped
Docker images / publish-release (push) Has been skipped
Docker images / notify-webhook (push) Has been skipped
Docker images / prepare-release (push) Successful in 38s
Docker images / backend-image (push) Has been skipped
Docker images / frontend-image (push) Has been skipped
Docker images / updater-image (push) Has been skipped
Docker images / publish-release (push) Has been skipped
Docker images / notify-webhook (push) Has been skipped
- Bumped application version to 1.0.0 across all relevant package files, ensuring consistency in versioning. - Introduced new environment variables for application version and release URL in Dockerfiles, improving deployment transparency. - Enhanced the health check endpoint to return the current application version, providing better visibility for monitoring. - Updated CI/CD workflows to include steps for preparing and publishing releases, streamlining the release process. - Added a new "Releases" section in the application sidebar for easier access to version information.
This commit is contained in:
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"version": "1.0.0",
|
||||||
|
"tag": "v1.0.0",
|
||||||
|
"publishedAt": null,
|
||||||
|
"releaseUrl": "",
|
||||||
|
"commits": []
|
||||||
|
}
|
||||||
@@ -0,0 +1,238 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
import { execFileSync } from "node:child_process"
|
||||||
|
import { mkdirSync, writeFileSync } from "node:fs"
|
||||||
|
import { dirname, join } from "node:path"
|
||||||
|
import { fileURLToPath } from "node:url"
|
||||||
|
|
||||||
|
const __dirname = dirname(fileURLToPath(import.meta.url))
|
||||||
|
const repoRoot = join(__dirname, "..", "..")
|
||||||
|
const baseVersion = process.env.BASE_VERSION ?? "1.0.0"
|
||||||
|
const outputDir = process.env.RELEASE_OUTPUT_DIR ?? join(repoRoot, ".ci", "release")
|
||||||
|
const repository = process.env.GITEA_REPOSITORY ?? process.env.GITHUB_REPOSITORY ?? ""
|
||||||
|
const serverUrl = (process.env.GITEA_SERVER_URL ?? "https://git.shts.su").replace(/\/$/, "")
|
||||||
|
|
||||||
|
const CONVENTIONAL_RE =
|
||||||
|
/^(feat|fix|chore|docs|refactor|style|test|build|ci)(\([^)]+\))?!?:\s*(.+)$/i
|
||||||
|
const MINOR_TYPES = new Set(["feat"])
|
||||||
|
const PATCH_TYPES = new Set([
|
||||||
|
"fix",
|
||||||
|
"chore",
|
||||||
|
"docs",
|
||||||
|
"refactor",
|
||||||
|
"style",
|
||||||
|
"test",
|
||||||
|
"build",
|
||||||
|
"ci",
|
||||||
|
])
|
||||||
|
|
||||||
|
function git(...args) {
|
||||||
|
return execFileSync("git", args, {
|
||||||
|
cwd: repoRoot,
|
||||||
|
encoding: "utf8",
|
||||||
|
stdio: ["ignore", "pipe", "pipe"],
|
||||||
|
}).trim()
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseSemver(version) {
|
||||||
|
const match = /^(\d+)\.(\d+)\.(\d+)$/.exec(version)
|
||||||
|
if (!match) {
|
||||||
|
throw new Error(`Invalid semver: ${version}`)
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
major: Number(match[1]),
|
||||||
|
minor: Number(match[2]),
|
||||||
|
patch: Number(match[3]),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatSemver(parts) {
|
||||||
|
return `${parts.major}.${parts.minor}.${parts.patch}`
|
||||||
|
}
|
||||||
|
|
||||||
|
function bumpVersion(version, bump) {
|
||||||
|
const parts = parseSemver(version)
|
||||||
|
if (bump === "minor") {
|
||||||
|
parts.minor += 1
|
||||||
|
parts.patch = 0
|
||||||
|
return formatSemver(parts)
|
||||||
|
}
|
||||||
|
parts.patch += 1
|
||||||
|
return formatSemver(parts)
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseCommit(subject, body = "") {
|
||||||
|
const match = CONVENTIONAL_RE.exec(subject.trim())
|
||||||
|
const breaking =
|
||||||
|
/BREAKING CHANGE/i.test(body) ||
|
||||||
|
(match?.[1] && subject.includes(`${match[1]}!:`))
|
||||||
|
|
||||||
|
if (!match) {
|
||||||
|
return { type: "other", scope: null, description: subject.trim(), bump: "patch" }
|
||||||
|
}
|
||||||
|
|
||||||
|
const type = match[1].toLowerCase()
|
||||||
|
const scope = match[2]?.slice(1, -1) ?? null
|
||||||
|
const description = match[3].trim()
|
||||||
|
let bump = "patch"
|
||||||
|
|
||||||
|
if (breaking || MINOR_TYPES.has(type)) {
|
||||||
|
bump = "minor"
|
||||||
|
} else if (PATCH_TYPES.has(type)) {
|
||||||
|
bump = "patch"
|
||||||
|
}
|
||||||
|
|
||||||
|
return { type, scope, description, bump }
|
||||||
|
}
|
||||||
|
|
||||||
|
function getLatestTag() {
|
||||||
|
try {
|
||||||
|
const tags = git("tag", "--list", "v*.*.*", "--sort=-v:refname")
|
||||||
|
const first = tags.split("\n").map((line) => line.trim()).find(Boolean)
|
||||||
|
return first ?? null
|
||||||
|
} catch {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCommitsSince(tag) {
|
||||||
|
const range = tag ? `${tag}..HEAD` : "HEAD"
|
||||||
|
const raw = git(
|
||||||
|
"log",
|
||||||
|
range,
|
||||||
|
"--pretty=format:%H%x1f%h%x1f%an%x1f%s%x1f%b%x1e",
|
||||||
|
)
|
||||||
|
if (!raw) return []
|
||||||
|
|
||||||
|
return raw
|
||||||
|
.split("\x1e")
|
||||||
|
.map((entry) => entry.trim())
|
||||||
|
.filter(Boolean)
|
||||||
|
.map((entry) => {
|
||||||
|
const [sha, shortSha, author, subject, body] = entry.split("\x1f")
|
||||||
|
const parsed = parseCommit(subject, body)
|
||||||
|
return {
|
||||||
|
sha,
|
||||||
|
shortSha,
|
||||||
|
author,
|
||||||
|
subject,
|
||||||
|
type: parsed.type,
|
||||||
|
scope: parsed.scope,
|
||||||
|
bump: parsed.bump,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function groupCommits(commits) {
|
||||||
|
const groups = new Map()
|
||||||
|
for (const commit of commits) {
|
||||||
|
const key = commit.type
|
||||||
|
if (!groups.has(key)) groups.set(key, [])
|
||||||
|
groups.get(key).push(commit)
|
||||||
|
}
|
||||||
|
return groups
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderReleaseNotes({ tag, version, commits }) {
|
||||||
|
const lines = [`# ${tag}`, "", `Версия **${version}**.`, ""]
|
||||||
|
const groups = groupCommits(commits)
|
||||||
|
const order = ["feat", "fix", "chore", "docs", "refactor", "style", "test", "build", "ci", "other"]
|
||||||
|
|
||||||
|
for (const type of order) {
|
||||||
|
const items = groups.get(type)
|
||||||
|
if (!items?.length) continue
|
||||||
|
lines.push(`## ${type}`, "")
|
||||||
|
for (const item of items) {
|
||||||
|
lines.push(`- ${item.subject} (\`${item.shortSha}\`, ${item.author})`)
|
||||||
|
}
|
||||||
|
lines.push("")
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${lines.join("\n").trim()}\n`
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildReleaseUrl(tag) {
|
||||||
|
if (!repository) return ""
|
||||||
|
const [owner, repo] = repository.split("/")
|
||||||
|
if (!owner || !repo) return ""
|
||||||
|
return `${serverUrl}/${owner}/${repo}/releases/tag/${encodeURIComponent(tag)}`
|
||||||
|
}
|
||||||
|
|
||||||
|
function writeGithubOutput(values) {
|
||||||
|
const outputFile = process.env.GITHUB_OUTPUT
|
||||||
|
if (!outputFile) return
|
||||||
|
const lines = Object.entries(values)
|
||||||
|
.map(([key, value]) => `${key}=${String(value).replace(/\r?\n/g, "%0A")}`)
|
||||||
|
.join("\n")
|
||||||
|
writeFileSync(outputFile, `${lines}\n`, { encoding: "utf8", flag: "a" })
|
||||||
|
}
|
||||||
|
|
||||||
|
function main() {
|
||||||
|
const latestTag = getLatestTag()
|
||||||
|
const baseFromTag = latestTag?.replace(/^v/, "") ?? baseVersion
|
||||||
|
const commits = latestTag ? getCommitsSince(latestTag) : []
|
||||||
|
|
||||||
|
if (!latestTag || commits.length === 0) {
|
||||||
|
mkdirSync(outputDir, { recursive: true })
|
||||||
|
writeFileSync(join(outputDir, "release_notes.md"), "", "utf8")
|
||||||
|
writeFileSync(
|
||||||
|
join(outputDir, "release-manifest.json"),
|
||||||
|
`${JSON.stringify(
|
||||||
|
{
|
||||||
|
version: baseFromTag,
|
||||||
|
tag: latestTag ?? `v${baseFromTag}`,
|
||||||
|
publishedAt: null,
|
||||||
|
releaseUrl: latestTag ? buildReleaseUrl(latestTag) : "",
|
||||||
|
commits: [],
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
2,
|
||||||
|
)}\n`,
|
||||||
|
"utf8",
|
||||||
|
)
|
||||||
|
|
||||||
|
writeGithubOutput({
|
||||||
|
should_release: "false",
|
||||||
|
version: baseFromTag,
|
||||||
|
tag: latestTag ?? `v${baseFromTag}`,
|
||||||
|
bump: "none",
|
||||||
|
release_url: latestTag ? buildReleaseUrl(latestTag) : "",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const bump = commits.some((commit) => commit.bump === "minor") ? "minor" : "patch"
|
||||||
|
const version = bumpVersion(baseFromTag, bump)
|
||||||
|
const tag = `v${version}`
|
||||||
|
const publishedAt = new Date().toISOString()
|
||||||
|
const releaseUrl = buildReleaseUrl(tag)
|
||||||
|
const releaseNotes = renderReleaseNotes({ tag, version, commits })
|
||||||
|
const manifest = {
|
||||||
|
version,
|
||||||
|
tag,
|
||||||
|
publishedAt,
|
||||||
|
releaseUrl,
|
||||||
|
commits: commits.map((commit) => ({
|
||||||
|
sha: commit.sha,
|
||||||
|
shortSha: commit.shortSha,
|
||||||
|
subject: commit.subject,
|
||||||
|
author: commit.author,
|
||||||
|
type: commit.type,
|
||||||
|
})),
|
||||||
|
}
|
||||||
|
|
||||||
|
mkdirSync(outputDir, { recursive: true })
|
||||||
|
writeFileSync(join(outputDir, "release_notes.md"), releaseNotes, "utf8")
|
||||||
|
writeFileSync(join(outputDir, "release-manifest.json"), `${JSON.stringify(manifest, null, 2)}\n`, "utf8")
|
||||||
|
|
||||||
|
writeGithubOutput({
|
||||||
|
should_release: "true",
|
||||||
|
version,
|
||||||
|
tag,
|
||||||
|
bump,
|
||||||
|
release_url: releaseUrl,
|
||||||
|
})
|
||||||
|
|
||||||
|
process.stdout.write(`${tag}\n`)
|
||||||
|
}
|
||||||
|
|
||||||
|
main()
|
||||||
@@ -27,3 +27,4 @@ alwaysApply: false
|
|||||||
## Связь с проектом
|
## Связь с проектом
|
||||||
|
|
||||||
- Полные стандарты проекта (формат ответа, чеклисты) — в правиле **`next-shadcn-production.mdc`** (обычно уже подключено). Эти правила **дополняют** его ролью в пайплайне, не отменяют.
|
- Полные стандарты проекта (формат ответа, чеклисты) — в правиле **`next-shadcn-production.mdc`** (обычно уже подключено). Эти правила **дополняют** его ролью в пайплайне, не отменяют.
|
||||||
|
- Сообщения коммитов и ожидаемый semver bump — в **`release-versioning.mdc`**.
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ alwaysApply: false
|
|||||||
- Выравнивать UI/UX с **`/servers`** как с главным эталоном (layout, отступы, сетка, типографика, композиция).
|
- Выравнивать UI/UX с **`/servers`** как с главным эталоном (layout, отступы, сетка, типографика, композиция).
|
||||||
- Предпочитать **Server Components**; `'use client'` — только при необходимости интерактива (состояние, браузерные API, обработчики).
|
- Предпочитать **Server Components**; `'use client'` — только при необходимости интерактива (состояние, браузерные API, обработчики).
|
||||||
- UI: по возможности **shadcn/ui**; не вводить новый визуальный/UX-паттерн, если на `/servers` уже есть эквивалент.
|
- UI: по возможности **shadcn/ui**; не вводить новый визуальный/UX-паттерн, если на `/servers` уже есть эквивалент.
|
||||||
|
- При завершении задачи предлагать subject коммита по **`release-versioning.mdc`** (`fix:` для patch, `feat:` для minor).
|
||||||
|
|
||||||
## Формат вывода (строго)
|
## Формат вывода (строго)
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
---
|
||||||
|
description: "Conventional Commits и semver для patch/minor релизов RouterLists"
|
||||||
|
alwaysApply: true
|
||||||
|
---
|
||||||
|
|
||||||
|
# Версионирование релизов
|
||||||
|
|
||||||
|
- Версию **не** править вручную в UI, `package.json` и git-тегах; bump делает CI через [`.ci/scripts/compute-release.mjs`](.ci/scripts/compute-release.mjs).
|
||||||
|
- Базовая линия: `v1.0.0`. Формат отображения: `v1.2.3`.
|
||||||
|
|
||||||
|
## Patch (`1.0.x`)
|
||||||
|
|
||||||
|
`fix:`, `chore:`, `docs:`, `refactor:`, `style:`, `test:`, `build:`, `ci:`
|
||||||
|
|
||||||
|
- Один логический смысл на коммит; subject в повелительном наклонении, до ~72 символов.
|
||||||
|
- Scope по желанию: `fix(servers):`, `chore(ci):`.
|
||||||
|
|
||||||
|
## Minor (`1.x.0`)
|
||||||
|
|
||||||
|
`feat:`, `feat!`, footer `BREAKING CHANGE` — только для заметного core-функционала.
|
||||||
|
|
||||||
|
## Примеры
|
||||||
|
|
||||||
|
- `fix(servers): исправить опрос API после таймаута` — patch
|
||||||
|
- `feat(filters): добавить синхронизацию BGP in` — minor
|
||||||
|
- `chore: мелкие правки` — плохо; `chore(ci): добавить job prepare-release` — хорошо
|
||||||
|
|
||||||
|
## Антипаттерны
|
||||||
|
|
||||||
|
WIP, «update», смешение несвязанных изменений, маскировка фич под `fix`/`chore`, ручной bump в sidebar.
|
||||||
|
|
||||||
|
Подробности — раздел CI/CD в `README.md`.
|
||||||
+143
-8
@@ -8,9 +8,45 @@ on:
|
|||||||
|
|
||||||
env:
|
env:
|
||||||
REGISTRY: git.shts.su
|
REGISTRY: git.shts.su
|
||||||
|
GITEA_SERVER_URL: https://git.shts.su
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
prepare-release:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
outputs:
|
||||||
|
should_release: ${{ steps.release.outputs.should_release }}
|
||||||
|
version: ${{ steps.release.outputs.version }}
|
||||||
|
tag: ${{ steps.release.outputs.tag }}
|
||||||
|
bump: ${{ steps.release.outputs.bump }}
|
||||||
|
release_url: ${{ steps.release.outputs.release_url }}
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Compute release metadata
|
||||||
|
id: release
|
||||||
|
shell: bash
|
||||||
|
env:
|
||||||
|
GITEA_REPOSITORY: ${{ gitea.repository }}
|
||||||
|
GITEA_SERVER_URL: ${{ env.GITEA_SERVER_URL }}
|
||||||
|
BASE_VERSION: "1.0.0"
|
||||||
|
RELEASE_OUTPUT_DIR: .ci/release
|
||||||
|
run: node .ci/scripts/compute-release.mjs
|
||||||
|
|
||||||
|
- name: Upload release artifacts
|
||||||
|
if: steps.release.outputs.should_release == 'true'
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: release-artifacts
|
||||||
|
path: |
|
||||||
|
.ci/release/release_notes.md
|
||||||
|
.ci/release/release-manifest.json
|
||||||
|
|
||||||
backend-image:
|
backend-image:
|
||||||
|
needs: prepare-release
|
||||||
|
if: needs.prepare-release.outputs.should_release == 'true'
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
@@ -58,24 +94,35 @@ jobs:
|
|||||||
file: backend/Dockerfile
|
file: backend/Dockerfile
|
||||||
platforms: linux/amd64
|
platforms: linux/amd64
|
||||||
push: true
|
push: true
|
||||||
|
build-args: |
|
||||||
|
APP_VERSION=${{ needs.prepare-release.outputs.version }}
|
||||||
tags: |
|
tags: |
|
||||||
${{ env.IMAGE_NAME }}:latest
|
${{ env.IMAGE_NAME }}:latest
|
||||||
${{ env.IMAGE_NAME }}:${{ gitea.sha }}
|
${{ env.IMAGE_NAME }}:${{ gitea.sha }}
|
||||||
|
${{ env.IMAGE_NAME }}:${{ needs.prepare-release.outputs.version }}
|
||||||
cache-from: type=gha,scope=backend
|
cache-from: type=gha,scope=backend
|
||||||
cache-to: type=gha,mode=max,scope=backend
|
cache-to: type=gha,mode=max,scope=backend
|
||||||
labels: |
|
labels: |
|
||||||
org.opencontainers.image.title=mmapp-backend
|
org.opencontainers.image.title=mmapp-backend
|
||||||
org.opencontainers.image.description=MikroTik Manager backend (Fastify)
|
org.opencontainers.image.description=MikroTik Manager backend (Fastify)
|
||||||
org.opencontainers.image.version=latest
|
org.opencontainers.image.version=${{ needs.prepare-release.outputs.version }}
|
||||||
org.opencontainers.image.revision=${{ gitea.sha }}
|
org.opencontainers.image.revision=${{ gitea.sha }}
|
||||||
org.opencontainers.image.created=${{ gitea.event.head_commit.timestamp }}
|
org.opencontainers.image.created=${{ gitea.event.head_commit.timestamp }}
|
||||||
|
|
||||||
frontend-image:
|
frontend-image:
|
||||||
|
needs: prepare-release
|
||||||
|
if: needs.prepare-release.outputs.should_release == 'true'
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Download release artifacts
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: release-artifacts
|
||||||
|
path: .ci/release
|
||||||
|
|
||||||
- name: Configure frontend image name
|
- name: Configure frontend image name
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
@@ -91,7 +138,7 @@ jobs:
|
|||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
STAGING=".ci/docker/frontend"
|
STAGING=".ci/docker/frontend"
|
||||||
rm -rf "$STAGING"
|
rm -rf "$STAGING"
|
||||||
mkdir -p "$STAGING/packages/contracts"
|
mkdir -p "$STAGING/packages/contracts" "$STAGING/public"
|
||||||
cp package-lock.json next.config.ts tsconfig.json postcss.config.mjs eslint.config.mjs components.json "$STAGING/"
|
cp package-lock.json next.config.ts tsconfig.json postcss.config.mjs eslint.config.mjs components.json "$STAGING/"
|
||||||
node <<'NODE'
|
node <<'NODE'
|
||||||
const fs = require("node:fs")
|
const fs = require("node:fs")
|
||||||
@@ -106,7 +153,7 @@ jobs:
|
|||||||
cp -R "$dir" "$STAGING/"
|
cp -R "$dir" "$STAGING/"
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
mkdir -p "$STAGING/public"
|
cp .ci/release/release-manifest.json "$STAGING/public/release-manifest.json"
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v3
|
uses: docker/setup-buildx-action@v3
|
||||||
@@ -130,19 +177,24 @@ jobs:
|
|||||||
NEXT_PUBLIC_BACKEND_URL=same-origin
|
NEXT_PUBLIC_BACKEND_URL=same-origin
|
||||||
NEXT_PUBLIC_DEFAULT_DATA_SOURCE=live
|
NEXT_PUBLIC_DEFAULT_DATA_SOURCE=live
|
||||||
NEXT_PUBLIC_ALLOW_MOCK_DATA=false
|
NEXT_PUBLIC_ALLOW_MOCK_DATA=false
|
||||||
|
NEXT_PUBLIC_APP_VERSION=${{ needs.prepare-release.outputs.version }}
|
||||||
|
NEXT_PUBLIC_RELEASE_URL=${{ needs.prepare-release.outputs.release_url }}
|
||||||
tags: |
|
tags: |
|
||||||
${{ env.IMAGE_NAME }}:latest
|
${{ env.IMAGE_NAME }}:latest
|
||||||
${{ env.IMAGE_NAME }}:${{ gitea.sha }}
|
${{ env.IMAGE_NAME }}:${{ gitea.sha }}
|
||||||
|
${{ env.IMAGE_NAME }}:${{ needs.prepare-release.outputs.version }}
|
||||||
cache-from: type=gha,scope=frontend
|
cache-from: type=gha,scope=frontend
|
||||||
cache-to: type=gha,mode=max,scope=frontend
|
cache-to: type=gha,mode=max,scope=frontend
|
||||||
labels: |
|
labels: |
|
||||||
org.opencontainers.image.title=mmapp-frontend
|
org.opencontainers.image.title=mmapp-frontend
|
||||||
org.opencontainers.image.description=MikroTik Manager frontend (Next.js)
|
org.opencontainers.image.description=MikroTik Manager frontend (Next.js)
|
||||||
org.opencontainers.image.version=latest
|
org.opencontainers.image.version=${{ needs.prepare-release.outputs.version }}
|
||||||
org.opencontainers.image.revision=${{ gitea.sha }}
|
org.opencontainers.image.revision=${{ gitea.sha }}
|
||||||
org.opencontainers.image.created=${{ gitea.event.head_commit.timestamp }}
|
org.opencontainers.image.created=${{ gitea.event.head_commit.timestamp }}
|
||||||
|
|
||||||
updater-image:
|
updater-image:
|
||||||
|
needs: prepare-release
|
||||||
|
if: needs.prepare-release.outputs.should_release == 'true'
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
@@ -177,20 +229,103 @@ jobs:
|
|||||||
tags: |
|
tags: |
|
||||||
${{ env.IMAGE_NAME }}:latest
|
${{ env.IMAGE_NAME }}:latest
|
||||||
${{ env.IMAGE_NAME }}:${{ gitea.sha }}
|
${{ env.IMAGE_NAME }}:${{ gitea.sha }}
|
||||||
|
${{ env.IMAGE_NAME }}:${{ needs.prepare-release.outputs.version }}
|
||||||
cache-from: type=gha,scope=updater
|
cache-from: type=gha,scope=updater
|
||||||
cache-to: type=gha,mode=max,scope=updater
|
cache-to: type=gha,mode=max,scope=updater
|
||||||
labels: |
|
labels: |
|
||||||
org.opencontainers.image.title=mmapp-updater
|
org.opencontainers.image.title=mmapp-updater
|
||||||
org.opencontainers.image.description=MikroTik Manager autonomous Docker updater
|
org.opencontainers.image.description=MikroTik Manager autonomous Docker updater
|
||||||
org.opencontainers.image.version=latest
|
org.opencontainers.image.version=${{ needs.prepare-release.outputs.version }}
|
||||||
org.opencontainers.image.revision=${{ gitea.sha }}
|
org.opencontainers.image.revision=${{ gitea.sha }}
|
||||||
org.opencontainers.image.created=${{ gitea.event.head_commit.timestamp }}
|
org.opencontainers.image.created=${{ gitea.event.head_commit.timestamp }}
|
||||||
|
|
||||||
notify-webhook:
|
publish-release:
|
||||||
if: ${{ secrets.DEPLOY_WEBHOOK_URL != '' }}
|
|
||||||
needs:
|
needs:
|
||||||
|
- prepare-release
|
||||||
- backend-image
|
- backend-image
|
||||||
- frontend-image
|
- frontend-image
|
||||||
|
- updater-image
|
||||||
|
if: needs.prepare-release.outputs.should_release == 'true'
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Download release artifacts
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: release-artifacts
|
||||||
|
path: .ci/release
|
||||||
|
|
||||||
|
- name: Configure git author
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
git config user.name "gitea-actions[bot]"
|
||||||
|
git config user.email "gitea-actions[bot]@users.noreply.gitea"
|
||||||
|
|
||||||
|
- name: Create and push release tag
|
||||||
|
shell: bash
|
||||||
|
env:
|
||||||
|
TAG: ${{ needs.prepare-release.outputs.tag }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
if git rev-parse "$TAG" >/dev/null 2>&1; then
|
||||||
|
echo "Tag $TAG already exists, skipping tag push"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
git tag -a "$TAG" -m "Release $TAG"
|
||||||
|
git push origin "$TAG"
|
||||||
|
|
||||||
|
- name: Publish Gitea release
|
||||||
|
shell: bash
|
||||||
|
env:
|
||||||
|
TAG: ${{ needs.prepare-release.outputs.tag }}
|
||||||
|
VERSION: ${{ needs.prepare-release.outputs.version }}
|
||||||
|
REPOSITORY: ${{ gitea.repository }}
|
||||||
|
TOKEN: ${{ secrets.ACTIONS_PAT }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
owner="${REPOSITORY%%/*}"
|
||||||
|
repo="${REPOSITORY#*/}"
|
||||||
|
payload=$(node <<'NODE'
|
||||||
|
const fs = require("node:fs")
|
||||||
|
const body = fs.readFileSync(".ci/release/release_notes.md", "utf8")
|
||||||
|
const payload = {
|
||||||
|
tag_name: process.env.TAG,
|
||||||
|
target_commitish: "main",
|
||||||
|
name: process.env.VERSION,
|
||||||
|
body,
|
||||||
|
draft: false,
|
||||||
|
prerelease: false,
|
||||||
|
}
|
||||||
|
process.stdout.write(JSON.stringify(payload))
|
||||||
|
NODE
|
||||||
|
)
|
||||||
|
status=$(curl --silent --show-error --output /tmp/gitea-release.json --write-out "%{http_code}" \
|
||||||
|
--request POST \
|
||||||
|
--header "Authorization: token ${TOKEN}" \
|
||||||
|
--header "Content-Type: application/json" \
|
||||||
|
--data "$payload" \
|
||||||
|
"${{ env.GITEA_SERVER_URL }}/api/v1/repos/${owner}/${repo}/releases")
|
||||||
|
if [ "$status" = "409" ] || [ "$status" = "422" ]; then
|
||||||
|
echo "Release for ${TAG} already exists (${status}), skipping"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
if [ "$status" -lt 200 ] || [ "$status" -ge 300 ]; then
|
||||||
|
cat /tmp/gitea-release.json
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
notify-webhook:
|
||||||
|
if: ${{ secrets.DEPLOY_WEBHOOK_URL != '' && needs.prepare-release.outputs.should_release == 'true' }}
|
||||||
|
needs:
|
||||||
|
- prepare-release
|
||||||
|
- backend-image
|
||||||
|
- frontend-image
|
||||||
|
- publish-release
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Notify deploy webhook
|
- name: Notify deploy webhook
|
||||||
@@ -201,5 +336,5 @@ jobs:
|
|||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
curl --fail --silent --show-error --request POST \
|
curl --fail --silent --show-error --request POST \
|
||||||
--header "Content-Type: application/json" \
|
--header "Content-Type: application/json" \
|
||||||
--data "{\"repository\":\"${{ gitea.repository }}\",\"sha\":\"${{ gitea.sha }}\",\"ref\":\"${{ gitea.ref }}\"}" \
|
--data "{\"repository\":\"${{ gitea.repository }}\",\"sha\":\"${{ gitea.sha }}\",\"ref\":\"${{ gitea.ref }}\",\"version\":\"${{ needs.prepare-release.outputs.version }}\",\"tag\":\"${{ needs.prepare-release.outputs.tag }}\",\"releaseUrl\":\"${{ needs.prepare-release.outputs.release_url }}\"}" \
|
||||||
"$WEBHOOK_URL"
|
"$WEBHOOK_URL"
|
||||||
|
|||||||
@@ -12,10 +12,14 @@ ARG BACKEND_INTERNAL_URL=http://backend:8000
|
|||||||
ARG NEXT_PUBLIC_BACKEND_URL=same-origin
|
ARG NEXT_PUBLIC_BACKEND_URL=same-origin
|
||||||
ARG NEXT_PUBLIC_DEFAULT_DATA_SOURCE=live
|
ARG NEXT_PUBLIC_DEFAULT_DATA_SOURCE=live
|
||||||
ARG NEXT_PUBLIC_ALLOW_MOCK_DATA=false
|
ARG NEXT_PUBLIC_ALLOW_MOCK_DATA=false
|
||||||
|
ARG NEXT_PUBLIC_APP_VERSION=dev
|
||||||
|
ARG NEXT_PUBLIC_RELEASE_URL=
|
||||||
ENV BACKEND_INTERNAL_URL=$BACKEND_INTERNAL_URL
|
ENV BACKEND_INTERNAL_URL=$BACKEND_INTERNAL_URL
|
||||||
ENV NEXT_PUBLIC_BACKEND_URL=$NEXT_PUBLIC_BACKEND_URL
|
ENV NEXT_PUBLIC_BACKEND_URL=$NEXT_PUBLIC_BACKEND_URL
|
||||||
ENV NEXT_PUBLIC_DEFAULT_DATA_SOURCE=$NEXT_PUBLIC_DEFAULT_DATA_SOURCE
|
ENV NEXT_PUBLIC_DEFAULT_DATA_SOURCE=$NEXT_PUBLIC_DEFAULT_DATA_SOURCE
|
||||||
ENV NEXT_PUBLIC_ALLOW_MOCK_DATA=$NEXT_PUBLIC_ALLOW_MOCK_DATA
|
ENV NEXT_PUBLIC_ALLOW_MOCK_DATA=$NEXT_PUBLIC_ALLOW_MOCK_DATA
|
||||||
|
ENV NEXT_PUBLIC_APP_VERSION=$NEXT_PUBLIC_APP_VERSION
|
||||||
|
ENV NEXT_PUBLIC_RELEASE_URL=$NEXT_PUBLIC_RELEASE_URL
|
||||||
COPY packages/contracts packages/contracts
|
COPY packages/contracts packages/contracts
|
||||||
COPY next.config.ts tsconfig.json postcss.config.mjs components.json ./
|
COPY next.config.ts tsconfig.json postcss.config.mjs components.json ./
|
||||||
COPY app app
|
COPY app app
|
||||||
|
|||||||
@@ -203,7 +203,18 @@ npm --prefix backend run db:studio
|
|||||||
|
|
||||||
Кэш Buildx: `type=gha`, отдельные scope `backend`, `frontend`, `updater`.
|
Кэш Buildx: `type=gha`, отдельные scope `backend`, `frontend`, `updater`.
|
||||||
|
|
||||||
Опциональный job **`notify-webhook`**: POST JSON `{"repository","sha","ref"}` на URL из секрета **`DEPLOY_WEBHOOK_URL`** после успешной сборки backend и frontend (updater в `needs` не входит).
|
Опциональный job **`notify-webhook`**: POST JSON `{"repository","sha","ref","version","tag","releaseUrl"}` на URL из секрета **`DEPLOY_WEBHOOK_URL`** после успешной сборки backend и frontend и публикации релиза (updater в `needs` не входит).
|
||||||
|
|
||||||
|
### Версионирование и релизы
|
||||||
|
|
||||||
|
- Базовая версия: **`v1.0.0`**. Линия semver: `1.x.y` (major `2.x` вне scope).
|
||||||
|
- Job **`prepare-release`** запускает [`.ci/scripts/compute-release.mjs`](.ci/scripts/compute-release.mjs): коммиты с последнего тега `v*.*.*`, bump по **Conventional Commits**.
|
||||||
|
- **`feat` / `feat!` / `BREAKING CHANGE`** → minor (`1.x.0`); **`fix`**, `chore`, `docs`, `refactor`, `style`, `test`, `build`, `ci` → patch (`1.0.x`).
|
||||||
|
- Если после последнего тега нет новых коммитов, релиз и сборка образов **пропускаются**.
|
||||||
|
- Job **`publish-release`**: annotated tag `v1.2.3`, Gitea Release на `https://git.shts.su` (markdown notes), образы с тегами `:latest`, `:<sha>`, `:<semver>`.
|
||||||
|
- UI: версия в sidebar и страница **`/releases`**; manifest `public/release-manifest.json` (в CI подставляется из артефакта).
|
||||||
|
- Bootstrap: один раз выставить `1.0.0` в workspace `package.json` и создать тег **`v1.0.0`** на `main` перед первым автоматическим bump.
|
||||||
|
- Сообщения коммитов: см. [`.cursor/rules/release-versioning.mdc`](.cursor/rules/release-versioning.mdc).
|
||||||
|
|
||||||
## Прод-развёртывание Docker
|
## Прод-развёртывание Docker
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,93 @@
|
|||||||
|
import Link from "next/link"
|
||||||
|
import { PageHeader } from "@/components/page-header"
|
||||||
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
|
||||||
|
import { buttonVariants } from "@/components/ui/button"
|
||||||
|
import { formatAppVersionLabel, getAppVersion, getReleaseUrl } from "@/lib/app-version"
|
||||||
|
import { readReleaseManifest } from "@/lib/release-manifest"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
import { ExternalLinkIcon } from "lucide-react"
|
||||||
|
|
||||||
|
function formatPublishedAt(value: string | null): string {
|
||||||
|
if (!value) return "Локальная сборка"
|
||||||
|
return new Intl.DateTimeFormat("ru-RU", {
|
||||||
|
dateStyle: "long",
|
||||||
|
timeStyle: "short",
|
||||||
|
}).format(new Date(value))
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ReleasesPage() {
|
||||||
|
const manifest = readReleaseManifest()
|
||||||
|
const version = getAppVersion()
|
||||||
|
const releaseUrl = getReleaseUrl() || manifest.releaseUrl || null
|
||||||
|
const commits = manifest.commits
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col h-full">
|
||||||
|
<PageHeader
|
||||||
|
crumbs={[{ label: "Система" }, { label: "Релизы" }]}
|
||||||
|
actions={
|
||||||
|
releaseUrl ? (
|
||||||
|
<Link
|
||||||
|
href={releaseUrl}
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
className={cn(buttonVariants({ variant: "outline", size: "sm" }), "h-8 inline-flex items-center gap-2")}
|
||||||
|
>
|
||||||
|
<ExternalLinkIcon className="size-4" />
|
||||||
|
Открыть в Gitea
|
||||||
|
</Link>
|
||||||
|
) : null
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="flex-1 overflow-y-auto p-6">
|
||||||
|
<div className="max-w-[960px] mx-auto space-y-6">
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Текущая версия</CardTitle>
|
||||||
|
<CardDescription>Сборка RouterLists, опубликованная через CI/CD.</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-3">
|
||||||
|
<div className="flex flex-wrap items-center gap-3">
|
||||||
|
<span className="text-2xl font-semibold tracking-tight">{formatAppVersionLabel(version)}</span>
|
||||||
|
<span className="text-sm text-muted-foreground">{formatPublishedAt(manifest.publishedAt)}</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
Версия формируется автоматически от базы <span className="font-mono">v1.0.0</span> по Conventional Commits.
|
||||||
|
</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Сводка коммитов</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
{commits.length > 0
|
||||||
|
? `Изменения, вошедшие в ${formatAppVersionLabel(manifest.version)}.`
|
||||||
|
: "Для локальной разработки список коммитов пуст. В прод-сборке он заполняется CI."}
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
{commits.length === 0 ? (
|
||||||
|
<p className="text-sm text-muted-foreground">Коммитов для отображения пока нет.</p>
|
||||||
|
) : (
|
||||||
|
<ul className="space-y-3">
|
||||||
|
{commits.map((commit) => (
|
||||||
|
<li key={commit.sha} className="rounded-md border px-3 py-2">
|
||||||
|
<div className="flex flex-wrap items-center gap-2 text-xs text-muted-foreground">
|
||||||
|
<span className="font-mono uppercase">{commit.type}</span>
|
||||||
|
<span className="font-mono">{commit.shortSha}</span>
|
||||||
|
<span>{commit.author}</span>
|
||||||
|
</div>
|
||||||
|
<p className="mt-1 text-sm">{commit.subject}</p>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -12,6 +12,8 @@ RUN npm ci --workspace=@mmapp/contracts --workspace=mikrotik-manager-backend --i
|
|||||||
&& npm rebuild better-sqlite3
|
&& npm rebuild better-sqlite3
|
||||||
|
|
||||||
FROM deps AS build
|
FROM deps AS build
|
||||||
|
ARG APP_VERSION=dev
|
||||||
|
ENV APP_VERSION=$APP_VERSION
|
||||||
COPY packages/contracts packages/contracts
|
COPY packages/contracts packages/contracts
|
||||||
COPY backend backend
|
COPY backend backend
|
||||||
RUN npm run build -w @mmapp/contracts \
|
RUN npm run build -w @mmapp/contracts \
|
||||||
@@ -20,9 +22,11 @@ RUN npm run build -w @mmapp/contracts \
|
|||||||
|
|
||||||
FROM node:22-bookworm-slim AS runner
|
FROM node:22-bookworm-slim AS runner
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
ARG APP_VERSION=dev
|
||||||
ENV NODE_ENV=production
|
ENV NODE_ENV=production
|
||||||
ENV PORT=8000
|
ENV PORT=8000
|
||||||
ENV DATABASE_PATH=/app/data/mikrotik.db
|
ENV DATABASE_PATH=/app/data/mikrotik.db
|
||||||
|
ENV APP_VERSION=$APP_VERSION
|
||||||
COPY --from=build /app/package.json /app/package-lock.json ./
|
COPY --from=build /app/package.json /app/package-lock.json ./
|
||||||
COPY --from=build /app/node_modules ./node_modules
|
COPY --from=build /app/node_modules ./node_modules
|
||||||
COPY --from=build /app/packages/contracts ./packages/contracts
|
COPY --from=build /app/packages/contracts ./packages/contracts
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "mikrotik-manager-backend",
|
"name": "mikrotik-manager-backend",
|
||||||
"version": "0.1.0",
|
"version": "1.0.0",
|
||||||
"description": "MikroTik Manager backend — Fastify + Drizzle + SQLite",
|
"description": "MikroTik Manager backend — Fastify + Drizzle + SQLite",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@@ -13,7 +13,7 @@
|
|||||||
"db:studio": "drizzle-kit studio"
|
"db:studio": "drizzle-kit studio"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@mmapp/contracts": "0.1.0",
|
"@mmapp/contracts": "1.0.0",
|
||||||
"@fastify/cors": "^11.2.0",
|
"@fastify/cors": "^11.2.0",
|
||||||
"@fastify/type-provider-zod": "^1.0.0",
|
"@fastify/type-provider-zod": "^1.0.0",
|
||||||
"better-sqlite3": "^12.9.0",
|
"better-sqlite3": "^12.9.0",
|
||||||
|
|||||||
@@ -47,7 +47,11 @@ await app.register(cors, {
|
|||||||
|
|
||||||
// ── routes ─────────────────────────────────────────────────────────────────────
|
// ── routes ─────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
app.get("/health", async () => ({ status: "ok", timestamp: new Date().toISOString() }))
|
app.get("/health", async () => ({
|
||||||
|
status: "ok",
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
version: process.env.APP_VERSION ?? "dev",
|
||||||
|
}))
|
||||||
|
|
||||||
await app.register(serversRoutes, { prefix: "/api/servers" })
|
await app.register(serversRoutes, { prefix: "/api/servers" })
|
||||||
await app.register(bgpRoutes, { prefix: "/api" })
|
await app.register(bgpRoutes, { prefix: "/api" })
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ import {
|
|||||||
mockSidebarBadgesByUrl,
|
mockSidebarBadgesByUrl,
|
||||||
type SidebarCountsDto,
|
type SidebarCountsDto,
|
||||||
} from "@/lib/sidebar-badges"
|
} from "@/lib/sidebar-badges"
|
||||||
|
import { formatAppVersionLabel, getAppVersion } from "@/lib/app-version"
|
||||||
|
|
||||||
// ─── Command palette trigger button ──────────────────────────────────────────
|
// ─── Command palette trigger button ──────────────────────────────────────────
|
||||||
|
|
||||||
@@ -117,6 +118,7 @@ const navStructure: { label: string; items: NavItemBase[] }[] = [
|
|||||||
items: [
|
items: [
|
||||||
{ title: "Оповещения", url: "/alerts", icon: <BellIcon /> },
|
{ title: "Оповещения", url: "/alerts", icon: <BellIcon /> },
|
||||||
{ title: "Сбор данных", url: "/data-collection", icon: <DatabaseIcon /> },
|
{ title: "Сбор данных", url: "/data-collection", icon: <DatabaseIcon /> },
|
||||||
|
{ title: "Релизы", url: "/releases", icon: <BadgeCheckIcon /> },
|
||||||
{ title: "Настройки", url: "/settings", icon: <SettingsIcon /> },
|
{ title: "Настройки", url: "/settings", icon: <SettingsIcon /> },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@@ -215,6 +217,7 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
|
|||||||
}, [mounted, mode, backendStatus, liveCounts, mockBadges, evo.enabled, evo.snapshot])
|
}, [mounted, mode, backendStatus, liveCounts, mockBadges, evo.enabled, evo.snapshot])
|
||||||
|
|
||||||
const isDark = (resolvedTheme ?? "dark") === "dark"
|
const isDark = (resolvedTheme ?? "dark") === "dark"
|
||||||
|
const appVersionLabel = formatAppVersionLabel(getAppVersion())
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Sidebar collapsible="icon" {...props}>
|
<Sidebar collapsible="icon" {...props}>
|
||||||
@@ -225,7 +228,7 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col leading-tight group-data-[collapsible=icon]:hidden">
|
<div className="flex flex-col leading-tight group-data-[collapsible=icon]:hidden">
|
||||||
<span className="font-semibold text-sidebar-foreground text-sm tracking-tight">RouterLists</span>
|
<span className="font-semibold text-sidebar-foreground text-sm tracking-tight">RouterLists</span>
|
||||||
<span className="text-[10px] font-mono text-sidebar-foreground/40">v3.4.1</span>
|
<span className="text-[10px] font-mono text-sidebar-foreground/40">{appVersionLabel}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<CommandPaletteButton />
|
<CommandPaletteButton />
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ const ALL_ITEMS: CommandItem[] = [
|
|||||||
// Система
|
// Система
|
||||||
{ id: "alerts", title: "Оповещения", group: "Система", url: "/alerts", icon: <BellIcon />, keywords: ["alert","notification","уведомление"] },
|
{ id: "alerts", title: "Оповещения", group: "Система", url: "/alerts", icon: <BellIcon />, keywords: ["alert","notification","уведомление"] },
|
||||||
{ id: "data-collection", title: "Сбор данных", group: "Система", url: "/data-collection", icon: <DatabaseIcon />, keywords: ["scheduler","планировщик","коллектор","uptime","трафик","журнал","прогон"] },
|
{ id: "data-collection", title: "Сбор данных", group: "Система", url: "/data-collection", icon: <DatabaseIcon />, keywords: ["scheduler","планировщик","коллектор","uptime","трафик","журнал","прогон"] },
|
||||||
|
{ id: "releases", title: "Релизы", group: "Система", url: "/releases", icon: <BadgeCheckIcon />, keywords: ["release","version","версия","changelog","релиз"] },
|
||||||
{ id: "settings", title: "Настройки", group: "Система", url: "/settings", icon: <SettingsIcon />, keywords: ["settings","config","конфигурация"] },
|
{ id: "settings", title: "Настройки", group: "Система", url: "/settings", icon: <SettingsIcon />, keywords: ["settings","config","конфигурация"] },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import packageJson from "@/package.json"
|
||||||
|
|
||||||
|
const packageVersion = packageJson.version
|
||||||
|
|
||||||
|
export function getAppVersion(): string {
|
||||||
|
return process.env.NEXT_PUBLIC_APP_VERSION?.trim() || `${packageVersion}-dev`
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatAppVersionLabel(version = getAppVersion()): string {
|
||||||
|
return `v${version}`
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getReleaseUrl(): string | null {
|
||||||
|
const value = process.env.NEXT_PUBLIC_RELEASE_URL?.trim()
|
||||||
|
return value || null
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import { readFileSync } from "node:fs"
|
||||||
|
import { join } from "node:path"
|
||||||
|
|
||||||
|
export interface ReleaseCommit {
|
||||||
|
sha: string
|
||||||
|
shortSha: string
|
||||||
|
subject: string
|
||||||
|
author: string
|
||||||
|
type: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ReleaseManifest {
|
||||||
|
version: string
|
||||||
|
tag: string
|
||||||
|
publishedAt: string | null
|
||||||
|
releaseUrl: string
|
||||||
|
commits: ReleaseCommit[]
|
||||||
|
}
|
||||||
|
|
||||||
|
const fallbackManifest: ReleaseManifest = {
|
||||||
|
version: "1.0.0",
|
||||||
|
tag: "v1.0.0",
|
||||||
|
publishedAt: null,
|
||||||
|
releaseUrl: "",
|
||||||
|
commits: [],
|
||||||
|
}
|
||||||
|
|
||||||
|
export function readReleaseManifest(): ReleaseManifest {
|
||||||
|
try {
|
||||||
|
const filePath = join(process.cwd(), "public", "release-manifest.json")
|
||||||
|
const raw = readFileSync(filePath, "utf8")
|
||||||
|
return JSON.parse(raw) as ReleaseManifest
|
||||||
|
} catch {
|
||||||
|
return fallbackManifest
|
||||||
|
}
|
||||||
|
}
|
||||||
Generated
+21
-6
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "mmapp",
|
"name": "mmapp",
|
||||||
"version": "0.1.0",
|
"version": "1.0.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "mmapp",
|
"name": "mmapp",
|
||||||
"version": "0.1.0",
|
"version": "1.0.0",
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
"packages/*",
|
"packages/*",
|
||||||
@@ -14,7 +14,7 @@
|
|||||||
],
|
],
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@base-ui/react": "^1.4.1",
|
"@base-ui/react": "^1.4.1",
|
||||||
"@mmapp/contracts": "0.1.0",
|
"@mmapp/contracts": "1.0.0",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"lucide-react": "^1.11.0",
|
"lucide-react": "^1.11.0",
|
||||||
@@ -39,11 +39,11 @@
|
|||||||
},
|
},
|
||||||
"backend": {
|
"backend": {
|
||||||
"name": "mikrotik-manager-backend",
|
"name": "mikrotik-manager-backend",
|
||||||
"version": "0.1.0",
|
"version": "1.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fastify/cors": "^11.2.0",
|
"@fastify/cors": "^11.2.0",
|
||||||
"@fastify/type-provider-zod": "^1.0.0",
|
"@fastify/type-provider-zod": "^1.0.0",
|
||||||
"@mmapp/contracts": "0.1.0",
|
"@mmapp/contracts": "1.0.0",
|
||||||
"better-sqlite3": "^12.9.0",
|
"better-sqlite3": "^12.9.0",
|
||||||
"dotenv": "^16.4.7",
|
"dotenv": "^16.4.7",
|
||||||
"drizzle-orm": "^0.45.2",
|
"drizzle-orm": "^0.45.2",
|
||||||
@@ -12670,10 +12670,25 @@
|
|||||||
},
|
},
|
||||||
"packages/contracts": {
|
"packages/contracts": {
|
||||||
"name": "@mmapp/contracts",
|
"name": "@mmapp/contracts",
|
||||||
"version": "0.1.0",
|
"version": "1.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"zod": "^4.4.1"
|
"zod": "^4.4.1"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@next/swc-win32-x64-msvc": {
|
||||||
|
"version": "16.2.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.2.4.tgz",
|
||||||
|
"integrity": "sha512-kMVGgsqhO5YTYODD9IPGGhA6iprWidQckK3LmPeW08PIFENRmgfb4MjXHO+p//d+ts2rpjvK5gXWzXSMrPl9cw==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-2
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "mmapp",
|
"name": "mmapp",
|
||||||
"version": "0.1.0",
|
"version": "1.0.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
"packages/*",
|
"packages/*",
|
||||||
@@ -14,7 +14,7 @@
|
|||||||
"lint": "eslint"
|
"lint": "eslint"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@mmapp/contracts": "0.1.0",
|
"@mmapp/contracts": "1.0.0",
|
||||||
"@base-ui/react": "^1.4.1",
|
"@base-ui/react": "^1.4.1",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@mmapp/contracts",
|
"name": "@mmapp/contracts",
|
||||||
"version": "0.1.0",
|
"version": "1.0.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"files": [
|
"files": [
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"version": "1.0.0",
|
||||||
|
"tag": "v1.0.0",
|
||||||
|
"publishedAt": null,
|
||||||
|
"releaseUrl": "",
|
||||||
|
"commits": []
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user