patch: swap to stdlib sqlite3 #21
@@ -7,10 +7,10 @@ jobs:
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: https://git.keligrubb.com/actions/checkout@v6
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v6
|
||||
uses: https://git.keligrubb.com/actions/setup-node@v6
|
||||
with:
|
||||
node-version: "24"
|
||||
cache: "npm"
|
||||
@@ -24,10 +24,10 @@ jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: https://git.keligrubb.com/actions/checkout@v6
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v6
|
||||
uses: https://git.keligrubb.com/actions/setup-node@v6
|
||||
with:
|
||||
node-version: "24"
|
||||
cache: "npm"
|
||||
@@ -43,10 +43,10 @@ jobs:
|
||||
container:
|
||||
image: mcr.microsoft.com/playwright:v1.58.2-noble
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: https://git.keligrubb.com/actions/checkout@v6
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v6
|
||||
uses: https://git.keligrubb.com/actions/setup-node@v6
|
||||
with:
|
||||
node-version: "24"
|
||||
cache: "npm"
|
||||
@@ -65,7 +65,7 @@ jobs:
|
||||
docker-build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: https://git.keligrubb.com/actions/checkout@v6
|
||||
|
||||
- name: Set Docker image tag
|
||||
id: image
|
||||
@@ -75,10 +75,10 @@ jobs:
|
||||
echo "tag=${REGISTRY}/${{ github.repository }}:latest" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v4
|
||||
uses: https://git.keligrubb.com/actions/docker-setup-buildx-action@v4
|
||||
|
||||
- name: Build (dry run)
|
||||
uses: docker/build-push-action@v7
|
||||
uses: https://git.keligrubb.com/actions/docker-build-push-action@v7
|
||||
with:
|
||||
context: .
|
||||
push: false
|
||||
|
||||
@@ -8,7 +8,7 @@ jobs:
|
||||
release-docker-helm:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: https://git.keligrubb.com/actions/checkout@v6
|
||||
with:
|
||||
token: ${{ secrets.KESTRELOS_REPO_TOKEN }}
|
||||
|
||||
@@ -24,17 +24,17 @@ jobs:
|
||||
./scripts/release.sh
|
||||
|
||||
- name: Log in to container registry
|
||||
uses: docker/login-action@v4
|
||||
uses: https://git.keligrubb.com/actions/docker-login-action@v4
|
||||
with:
|
||||
registry: git.keligrubb.com
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.KESTRELOS_REPO_TOKEN }}
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v4
|
||||
uses: https://git.keligrubb.com/actions/docker-setup-buildx-action@v4
|
||||
|
||||
- name: Build Docker image
|
||||
uses: docker/build-push-action@v7
|
||||
uses: https://git.keligrubb.com/actions/docker-build-push-action@v7
|
||||
with:
|
||||
context: .
|
||||
load: true
|
||||
@@ -50,7 +50,7 @@ jobs:
|
||||
done < .tags
|
||||
|
||||
- name: Set up Helm
|
||||
uses: azure/setup-helm@v4
|
||||
uses: https://git.keligrubb.com/actions/setup-helm@v4
|
||||
|
||||
- name: Package and push Helm chart
|
||||
env:
|
||||
|
||||
Generated
+2570
-3820
File diff suppressed because it is too large
Load Diff
@@ -31,7 +31,6 @@
|
||||
"nuxt": "^4.0.0",
|
||||
"openid-client": "^6.8.2",
|
||||
"qrcode": "^1.5.4",
|
||||
"sqlite3": "^5.1.7",
|
||||
"vue": "^3.4.0",
|
||||
"vue-router": "^5.0.0",
|
||||
"ws": "^8.18.0"
|
||||
|
||||
+43
-13
@@ -3,39 +3,69 @@ set -e
|
||||
|
||||
# version
|
||||
msg="${CI_COMMIT_MESSAGE:-}"
|
||||
# optional PR body (written by workflow from Gitea API when this commit is a merged PR)
|
||||
if [ -f .ci_pr_body ]; then
|
||||
CI_PR_DESCRIPTION=$(cat .ci_pr_body); rm -f .ci_pr_body
|
||||
else
|
||||
CI_PR_DESCRIPTION=""
|
||||
fi
|
||||
export CI_PR_DESCRIPTION
|
||||
bump=patch
|
||||
echo "$msg" | grep -qi minor: && bump=minor
|
||||
# Conventional commits: chore/fix => patch, feat => minor
|
||||
echo "$msg" | grep -Eqi '(^|[[:space:]])(fix|chore)(\([^)]*\))?:' && bump=patch
|
||||
echo "$msg" | grep -Eqi '(^|[[:space:]])feat(\([^)]*\))?:' && bump=minor
|
||||
# Conventional commits breaking change: type!:
|
||||
echo "$msg" | grep -Eqi '(^|[[:space:]])[a-zA-Z]+(\([^)]*\))?!:' && bump=major
|
||||
# Explicit bump prefixes still supported (but never downgrade a major bump)
|
||||
echo "$msg" | grep -qi minor: && [ "$bump" != "major" ] && bump=minor
|
||||
echo "$msg" | grep -qi major: && bump=major
|
||||
cur=$(awk '/"version"/ { match($0, /[0-9]+\.[0-9]+\.[0-9]+/); print substr($0, RSTART, RLENGTH); exit }' package.json)
|
||||
cur=$(cat VERSION | tr -d '\n')
|
||||
case "$cur" in
|
||||
[0-9]*.[0-9]*.[0-9]*) ;;
|
||||
*) echo "error: VERSION must be x.y.z (got: $cur)"; exit 1 ;;
|
||||
esac
|
||||
major=$(echo "$cur" | cut -d. -f1); minor=$(echo "$cur" | cut -d. -f2); patch=$(echo "$cur" | cut -d. -f3)
|
||||
case "$bump" in major) major=$((major+1)); minor=0; patch=0 ;; minor) minor=$((minor+1)); patch=0 ;; patch) patch=$((patch+1)) ;; esac
|
||||
newVersion="$major.$minor.$patch"
|
||||
[ -z "$cur" ] && { echo "error: could not read version from package.json"; exit 1; }
|
||||
[ -z "$cur" ] && { echo "error: could not read version from VERSION"; exit 1; }
|
||||
|
||||
# changelog entry (strip prefix from first line)
|
||||
changelogEntry=$(echo "$msg" | head -1 | awk '{sub(/^[mM]ajor:[ \t]*/,""); sub(/^[mM]inor:[ \t]*/,""); sub(/^[pP]atch:[ \t]*/,""); print}')
|
||||
# changelog entry (strip explicit bump prefixes & any conventional-commit type(scope):); optional PR description enriches it
|
||||
changelogEntry=$(
|
||||
echo "$msg" \
|
||||
| head -1 \
|
||||
| sed -E 's/^[[:space:]]*[mM]ajor:[[:space:]]*//; s/^[[:space:]]*[mM]inor:[[:space:]]*//; s/^[[:space:]]*[pP]atch:[[:space:]]*//' \
|
||||
| sed -E 's/^[[:space:]]*[a-zA-Z]+(\([^)]*\))?:[[:space:]]*//'
|
||||
)
|
||||
[ -z "$changelogEntry" ] && changelogEntry="Release v$newVersion"
|
||||
if [ -n "$CI_PR_DESCRIPTION" ]; then
|
||||
changelogFull="- $changelogEntry
|
||||
|
||||
$CI_PR_DESCRIPTION"
|
||||
else
|
||||
changelogFull="- $changelogEntry"
|
||||
fi
|
||||
|
||||
# bump files
|
||||
awk -v v="$newVersion" '/"version"/ { sub(/[0-9]+\.[0-9]+\.[0-9]+/, v) } { print }' package.json > package.json.tmp && mv package.json.tmp package.json
|
||||
awk -v v="$newVersion" '/^version:/ { $0 = "version: " v }; /^appVersion:/ { $0 = "appVersion: \"" v "\"" }; { print }' helm/kestrelos/Chart.yaml > helm/kestrelos/Chart.yaml.tmp && mv helm/kestrelos/Chart.yaml.tmp helm/kestrelos/Chart.yaml
|
||||
awk -v v="$newVersion" '/^ tag:/ { $0 = " tag: " v }; { print }' helm/kestrelos/values.yaml > helm/kestrelos/values.yaml.tmp && mv helm/kestrelos/values.yaml.tmp helm/kestrelos/values.yaml
|
||||
echo "$newVersion" > VERSION
|
||||
awk -v v="$newVersion" '/^version:/ { $0 = "version: " v }; /^appVersion:/ { $0 = "appVersion: \"" v "\"" }; { print }' helm/jasper/Chart.yaml > helm/jasper/Chart.yaml.tmp && mv helm/jasper/Chart.yaml.tmp helm/jasper/Chart.yaml
|
||||
awk -v v="$newVersion" '/^ tag:/ { $0 = " tag: " v }; { print }' helm/jasper/values.yaml > helm/jasper/values.yaml.tmp && mv helm/jasper/values.yaml.tmp helm/jasper/values.yaml
|
||||
|
||||
# changelog
|
||||
new="## [$newVersion] - $(date +%Y-%m-%d)
|
||||
### Changed
|
||||
- $changelogEntry
|
||||
$changelogFull
|
||||
|
||||
"
|
||||
{ [ ! -f CHANGELOG.md ] && printf '# Changelog\n\n'; printf '%s' "$new"; [ -f CHANGELOG.md ] && cat CHANGELOG.md; } > CHANGELOG.md.tmp && mv CHANGELOG.md.tmp CHANGELOG.md
|
||||
# Create CHANGELOG.md if missing (first release); otherwise prepend new entry to existing content.
|
||||
{ [ ! -f CHANGELOG.md ] && printf '# Changelog\n\n'; printf '%s' "$new"; [ -f CHANGELOG.md ] && cat CHANGELOG.md || true; } > CHANGELOG.md.tmp && mv CHANGELOG.md.tmp CHANGELOG.md
|
||||
|
||||
# git
|
||||
git config user.email "ci@kestrelos" && git config user.name "CI"
|
||||
git add package.json helm/kestrelos/Chart.yaml helm/kestrelos/values.yaml CHANGELOG.md
|
||||
git add VERSION helm/jasper/Chart.yaml helm/jasper/values.yaml CHANGELOG.md
|
||||
git commit -m "release v$newVersion [skip ci]"
|
||||
url="https://${CI_REPO_OWNER}:${GITEA_REPO_TOKEN}@${CI_FORGE_URL#https://}/${CI_REPO_OWNER}/${CI_REPO_NAME}.git"
|
||||
git tag "v$newVersion"
|
||||
# artifact for kaniko (tag list)
|
||||
# artifact for docker (tag list)
|
||||
printf '%s\n%s\n' "$newVersion" "latest" > .tags
|
||||
retry() { n=0; while ! "$@"; do n=$((n+1)); [ $n -ge 3 ] && return 1; sleep 2; done; }
|
||||
retry git push "$url" HEAD:main "v$newVersion"
|
||||
@@ -43,7 +73,7 @@ retry git push "$url" HEAD:main "v$newVersion"
|
||||
# gitea release
|
||||
body="## Changelog
|
||||
### Changed
|
||||
- $changelogEntry
|
||||
$changelogFull
|
||||
|
||||
## Installation
|
||||
- [Docker image](${CI_FORGE_URL}/${CI_REPO_OWNER}/-/packages/container/${CI_REPO_NAME})
|
||||
|
||||
+86
-24
@@ -1,14 +1,12 @@
|
||||
import { join, dirname } from 'node:path'
|
||||
import { mkdirSync, existsSync } from 'node:fs'
|
||||
import { randomBytes, randomUUID } from 'node:crypto'
|
||||
import { createRequire } from 'node:module'
|
||||
import { promisify } from 'node:util'
|
||||
import { randomBytes } from 'node:crypto'
|
||||
import { hashPassword } from './password.js'
|
||||
import { registerCleanup } from './shutdown.js'
|
||||
|
||||
// Resolve from project root so bundled server (e.g. .output) finds node_modules/sqlite3
|
||||
const requireFromRoot = createRequire(join(process.cwd(), 'package.json'))
|
||||
const sqlite3 = requireFromRoot('sqlite3')
|
||||
const { DatabaseSync } = requireFromRoot('node:sqlite')
|
||||
|
||||
const SCHEMA_VERSION = 4
|
||||
const DB_BUSY_TIMEOUT_MS = 5000
|
||||
@@ -144,7 +142,6 @@ const initDb = async (db, run, all, get) => {
|
||||
catch {
|
||||
// WAL not supported (e.g., network filesystem)
|
||||
}
|
||||
db.configure('busyTimeout', DB_BUSY_TIMEOUT_MS)
|
||||
|
||||
await run(SCHEMA.schema_version)
|
||||
await run(SCHEMA.users)
|
||||
@@ -168,7 +165,7 @@ const initDb = async (db, run, all, get) => {
|
||||
|
||||
await run(
|
||||
'INSERT INTO users (id, identifier, password_hash, role, created_at, auth_provider, oidc_issuer, oidc_sub) VALUES (?, ?, ?, ?, ?, ?, ?, ?)',
|
||||
[crypto.randomUUID(), identifier, hashPassword(plainPassword), 'admin', new Date().toISOString(), 'local', null, null],
|
||||
[randomUUID(), identifier, hashPassword(plainPassword), 'admin', new Date().toISOString(), 'local', null, null],
|
||||
)
|
||||
|
||||
if (!email || !password) {
|
||||
@@ -181,37 +178,104 @@ const initDb = async (db, run, all, get) => {
|
||||
export async function getDb() {
|
||||
if (dbInstance) return dbInstance
|
||||
|
||||
const db = new sqlite3.Database(getDbPath(), (err) => {
|
||||
if (err) {
|
||||
const sqliteDb = (() => {
|
||||
try {
|
||||
return new DatabaseSync(getDbPath(), { timeout: DB_BUSY_TIMEOUT_MS })
|
||||
}
|
||||
catch (err) {
|
||||
console.error('[db] Failed to open database:', err.message)
|
||||
throw err
|
||||
}
|
||||
})
|
||||
})()
|
||||
|
||||
const run = promisify(db.run.bind(db))
|
||||
const all = promisify(db.all.bind(db))
|
||||
const get = promisify(db.get.bind(db))
|
||||
const run = (sql, params = []) =>
|
||||
new Promise((resolve, reject) => {
|
||||
const stmt = sqliteDb.prepare(sql)
|
||||
try {
|
||||
stmt.run(...params)
|
||||
resolve()
|
||||
}
|
||||
catch (error) {
|
||||
reject(error)
|
||||
}
|
||||
finally {
|
||||
try {
|
||||
stmt.finalize()
|
||||
}
|
||||
catch {
|
||||
// ignore finalize errors
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const all = (sql, params = []) =>
|
||||
new Promise((resolve, reject) => {
|
||||
const stmt = sqliteDb.prepare(sql)
|
||||
try {
|
||||
const rows = stmt.all(...params)
|
||||
resolve(rows)
|
||||
}
|
||||
catch (error) {
|
||||
reject(error)
|
||||
}
|
||||
finally {
|
||||
try {
|
||||
stmt.finalize()
|
||||
}
|
||||
catch {
|
||||
// ignore finalize errors
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const get = (sql, params = []) =>
|
||||
new Promise((resolve, reject) => {
|
||||
const stmt = sqliteDb.prepare(sql)
|
||||
try {
|
||||
const row = stmt.get(...params)
|
||||
resolve(row)
|
||||
}
|
||||
catch (error) {
|
||||
reject(error)
|
||||
}
|
||||
finally {
|
||||
try {
|
||||
stmt.finalize()
|
||||
}
|
||||
catch {
|
||||
// ignore finalize errors
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const wrappedDb = {
|
||||
close(callback) {
|
||||
try {
|
||||
sqliteDb.close()
|
||||
if (typeof callback === 'function') callback(null)
|
||||
}
|
||||
catch (error) {
|
||||
if (typeof callback === 'function') callback(error)
|
||||
else throw error
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
try {
|
||||
await initDb(db, run, all, get)
|
||||
await initDb(wrappedDb, run, all, get)
|
||||
}
|
||||
catch (error) {
|
||||
db.close()
|
||||
wrappedDb.close()
|
||||
console.error('[db] Database initialization failed:', error.message)
|
||||
throw error
|
||||
}
|
||||
|
||||
dbInstance = { db, run, all, get }
|
||||
dbInstance = { db: wrappedDb, run, all, get }
|
||||
|
||||
registerCleanup(async () => {
|
||||
if (dbInstance) {
|
||||
try {
|
||||
await new Promise((resolve, reject) => {
|
||||
dbInstance.db.close((err) => {
|
||||
if (err) reject(err)
|
||||
else resolve()
|
||||
})
|
||||
})
|
||||
dbInstance.db.close()
|
||||
}
|
||||
catch (error) {
|
||||
console.error('[db] Error closing database during shutdown:', error?.message)
|
||||
@@ -290,9 +354,7 @@ export async function withTransaction(db, callback) {
|
||||
export function closeDb() {
|
||||
if (!dbInstance) return
|
||||
try {
|
||||
dbInstance.db.close((err) => {
|
||||
if (err) console.error('[db] Error closing database:', err.message)
|
||||
})
|
||||
dbInstance.db.close()
|
||||
}
|
||||
catch (error) {
|
||||
console.error('[db] Error closing database:', error.message)
|
||||
|
||||
Reference in New Issue
Block a user