29 Commits

Author SHA1 Message Date
cd16b7ea28 Merge pull request 'v1.2.2' (#16) from dev into main
All checks were successful
CI / test (push) Successful in 13s
Release / release (push) Successful in 34s
CI / build-dev (push) Has been skipped
Reviewed-on: #16
2026-03-28 14:01:33 -04:00
20d8a13375 Merge pull request 'chore: bump version to 1.2.2' (#15) from chore/bump-v1.2.2 into dev
All checks were successful
CI / test (push) Successful in 14s
CI / build-dev (push) Successful in 24s
CI / test (pull_request) Successful in 14s
CI / build-dev (pull_request) Has been skipped
Reviewed-on: #15
2026-03-28 13:59:53 -04:00
f72aaa52f8 chore: bump version to 1.2.2
All checks were successful
CI / test (pull_request) Successful in 12s
CI / build-dev (pull_request) Has been skipped
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-28 13:59:23 -04:00
dd47d5006e Merge pull request 'fix: collapse python3 one-liner to fix YAML indentation error' (#14) from fix/release-yaml-indent into dev
All checks were successful
CI / test (push) Successful in 12s
CI / build-dev (push) Successful in 21s
Reviewed-on: #14
2026-03-28 13:58:26 -04:00
10e25e1803 fix: collapse python3 one-liner to fix YAML indentation error
All checks were successful
CI / test (pull_request) Successful in 13s
CI / build-dev (pull_request) Has been skipped
Multi-line python3 -c "..." had unindented code outside the run: | block,
causing 'yaml: line 83: could not find expected :'. Collapsed to a single
indented line so the YAML parser sees it correctly.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-28 13:57:48 -04:00
afbdefa549 Merge pull request 'v1.2.1' (#13) from dev into main
All checks were successful
CI / test (push) Successful in 14s
CI / build-dev (push) Has been skipped
Reviewed-on: #13
2026-03-28 13:55:34 -04:00
1a62e2fdd9 Merge pull request 'chore: bump version to 1.2.1' (#12) from chore/bump-v1.2.1 into dev
All checks were successful
CI / test (push) Successful in 15s
CI / build-dev (push) Successful in 28s
CI / test (pull_request) Successful in 12s
CI / build-dev (pull_request) Has been skipped
Reviewed-on: #12
2026-03-28 13:54:12 -04:00
1271c061fd chore: bump version to 1.2.1
All checks were successful
CI / test (pull_request) Successful in 16s
CI / build-dev (pull_request) Has been skipped
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-28 13:52:40 -04:00
7b2a996c21 Merge pull request 'fix: remove npm cache and fix release notes shell injection' (#11) from fix/release-workflow into dev
All checks were successful
CI / test (push) Successful in 13s
CI / build-dev (push) Successful in 20s
Reviewed-on: #11
2026-03-28 13:51:49 -04:00
3233d65db0 fix: remove npm cache and fix release notes shell injection
All checks were successful
CI / test (pull_request) Successful in 14s
CI / build-dev (pull_request) Has been skipped
cache: npm caused ~4min ETIMEDOUT on every run (cache service unreachable).

Commit messages containing backticks were shell-expanded inside the
curl -d "..." string, causing 'sha: No such file or directory'. Fixed by
writing release notes to a temp file and using python3 to build the JSON
payload, then passing it to curl with --data @file.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-28 13:49:38 -04:00
f1e192c5d4 Merge pull request 'v1.2.0' (#10) from dev into main
Some checks failed
CI / test (push) Successful in 13s
Release / release (push) Failing after 5m14s
CI / build-dev (push) Has been skipped
Reviewed-on: #10
2026-03-28 13:24:34 -04:00
3037381084 Merge pull request 'chore: bump version to 1.2.0' (#9) from chore/bump-v1.2.0 into dev
All checks were successful
CI / test (push) Successful in 13s
CI / build-dev (push) Successful in 26s
CI / test (pull_request) Successful in 13s
CI / build-dev (pull_request) Has been skipped
Reviewed-on: #9
2026-03-28 13:22:15 -04:00
e54c1d4848 chore: bump version to 1.2.0
All checks were successful
CI / test (pull_request) Successful in 13s
CI / build-dev (pull_request) Has been skipped
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-28 13:21:18 -04:00
3ae3f98df5 Merge pull request 'fix: use git rev-parse for short SHA in build-dev' (#8) from fix/ci-short-sha into dev
All checks were successful
CI / test (push) Successful in 12s
CI / build-dev (push) Successful in 20s
Reviewed-on: #8
2026-03-28 13:19:23 -04:00
65d6514603 fix: use git rev-parse for short SHA in build-dev
All checks were successful
CI / test (pull_request) Successful in 12s
CI / build-dev (pull_request) Has been skipped
$GITEA_SHA is unset on Gitea runners — the nav showed "dev-" with an
empty SHA. git rev-parse --short HEAD works regardless of runner env vars.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-28 13:18:47 -04:00
bc44bcbde9 Merge pull request 'fix: remove npm cache from setup-node' (#7) from fix/ci-remove-npm-cache into dev
All checks were successful
CI / test (push) Successful in 12s
CI / build-dev (push) Successful in 13s
Reviewed-on: #7
2026-03-28 13:16:33 -04:00
cae0f2222a fix: remove npm cache from setup-node
All checks were successful
CI / test (pull_request) Successful in 13s
CI / build-dev (pull_request) Has been skipped
The Gitea runner's cache service is unreachable, causing a ~4 minute
ETIMEDOUT on every run before falling back to a cold install anyway.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-28 13:11:05 -04:00
28833a7ec6 Merge pull request 'feat: show dev-<sha> version string in nav for dev builds' (#6) from feat/dev-version-string into dev
All checks were successful
CI / test (push) Successful in 9m33s
CI / build-dev (push) Successful in 22s
Reviewed-on: #6
2026-03-28 13:03:23 -04:00
6ba02bf17d feat: show dev-<sha> version string in nav for dev builds
All checks were successful
CI / test (pull_request) Successful in 9m31s
CI / build-dev (pull_request) Has been skipped
Production images continue to display the semver (v1.x.x). Dev images
built by CI now receive BUILD_VERSION=dev-<7-char-sha> via a Docker ARG,
and app.js skips the v prefix for non-semver strings.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-28 12:52:15 -04:00
bfe71b2511 Merge pull request 'fix: centre badge text on instance cards' (#5) from fix/badge-alignment into dev
All checks were successful
CI / test (push) Successful in 9m32s
CI / build-dev (push) Successful in 20s
Reviewed-on: #5
2026-03-28 12:38:53 -04:00
0f2a37cb39 fix: centre badge text on instance cards
All checks were successful
CI / test (pull_request) Successful in 9m31s
CI / build-dev (pull_request) Has been skipped
.badge lacked text-align: center. Inside the card's flex-end right
column, badge text was left-justified within each pill, making state
labels (deployed / testing / degraded) appear skewed to the left.

TDD: CSS regression test added to tests/helpers.test.js — reads
css/app.css directly and asserts the rule is present, so this
cannot regress silently in future.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-28 12:28:44 -04:00
73f4eabbc7 Merge pull request 'fix: db volume ownership and explicit error handling for write failures' (#3) from fix/db-permissions-and-error-handling into dev
All checks were successful
CI / test (push) Successful in 9m33s
CI / build-dev (push) Successful in 21s
Reviewed-on: #3
2026-03-28 12:10:32 -04:00
515ff8ddb3 Merge branch 'dev' into fix/db-permissions-and-error-handling
All checks were successful
CI / test (pull_request) Successful in 9m28s
CI / build-dev (pull_request) Has been skipped
2026-03-28 11:48:36 -04:00
08c12c9394 fix: skip db boot init in test env to prevent parallel worker lock
All checks were successful
CI / test (pull_request) Successful in 9m33s
CI / build-dev (pull_request) Has been skipped
Vitest runs test files in parallel workers. Each worker imports server/db.js,
which triggered module-level init(DEFAULT_PATH) unconditionally. Two workers
racing to open the same SQLite file caused "database is locked", followed
by process.exit(1) killing the worker — surfacing as:

  Error: process.exit unexpectedly called with "1"

Fix: guard the boot init block behind NODE_ENV !== 'test'. Vitest sets
NODE_ENV=test automatically. Each worker's beforeEach(() => _resetForTest())
initialises its own :memory: database, so no file coordination is needed.

process.exit(1) is also guarded by the same condition — it must never
fire inside a test runner process.

TDD: two regression tests added to tests/db.test.js documenting the
expected boot behaviour and proving the module loads cleanly in parallel.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-28 11:48:07 -04:00
4ce7df4649 Merge pull request 'fix: skip db boot init in test env to prevent parallel worker lock' (#4) from fix/db-boot-test-isolation into dev
Some checks failed
CI / test (push) Successful in 9m29s
CI / build-dev (push) Has been cancelled
Reviewed-on: #4
2026-03-28 11:41:55 -04:00
6c04a30c3a fix: skip db boot init in test env to prevent parallel worker lock
All checks were successful
CI / test (pull_request) Successful in 9m29s
CI / build-dev (pull_request) Has been skipped
Vitest runs test files in parallel workers. Each worker imports server/db.js,
which triggered module-level init(DEFAULT_PATH) unconditionally. Two workers
racing to open the same SQLite file caused "database is locked", followed
by process.exit(1) killing the worker — surfacing as:

  Error: process.exit unexpectedly called with "1"

Fix: guard the boot init block behind NODE_ENV !== 'test'. Vitest sets
NODE_ENV=test automatically. Each worker's beforeEach(() => _resetForTest())
initialises its own :memory: database, so no file coordination is needed.

process.exit(1) is also guarded by the same condition — it must never
fire inside a test runner process.

TDD: two regression tests added to tests/db.test.js documenting the
expected boot behaviour and proving the module loads cleanly in parallel.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-28 11:31:55 -04:00
c6cd8098fd Merge branch 'dev' into fix/db-permissions-and-error-handling
Some checks failed
CI / test (pull_request) Failing after 4m52s
CI / build-dev (pull_request) Has been skipped
2026-03-28 11:16:31 -04:00
1412b2e0b7 Merge pull request 'feat: build :dev Docker image on push to dev' (#1) from chore/dev-staging-build into dev
All checks were successful
CI / test (push) Successful in 9m29s
CI / build-dev (push) Successful in 13s
Reviewed-on: #1
2026-03-28 10:38:48 -04:00
30b037ff9c feat: build :dev Docker image on push to dev
All checks were successful
CI / test (pull_request) Successful in 9m30s
CI / build-dev (pull_request) Has been skipped
Adds a build-dev job to ci.yml that fires after tests pass on direct
pushes to dev (not PRs). Pushes two tags to the registry:

  :dev          — mutable, always the latest integrated dev state
  :dev-<sha>    — immutable, for tracing exactly which commit is running

Staging servers can pull :dev to test before a release PR is opened.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-28 10:27:23 -04:00
9 changed files with 133 additions and 25 deletions

View File

@@ -6,6 +6,9 @@ on:
pull_request:
branches: [dev, main]
env:
IMAGE: ${{ vars.REGISTRY_HOST }}/${{ gitea.repository_owner }}/catalyst
jobs:
test:
runs-on: ubuntu-latest
@@ -16,8 +19,35 @@ jobs:
- uses: actions/setup-node@v4
with:
node-version: 'lts/*'
cache: npm
- run: npm ci
- run: npm test
build-dev:
runs-on: ubuntu-latest
needs: test
if: github.event_name == 'push' && github.ref == 'refs/heads/dev'
steps:
- uses: actions/checkout@v4
- name: Log in to registry
uses: docker/login-action@v3
with:
registry: ${{ vars.REGISTRY_HOST }}
username: ${{ gitea.actor }}
password: ${{ secrets.TOKEN }}
- name: Compute short SHA
run: echo "SHORT_SHA=$(git rev-parse --short HEAD)" >> $GITEA_ENV
- name: Build and push
uses: docker/build-push-action@v5
with:
context: .
push: true
build-args: BUILD_VERSION=dev-${{ env.SHORT_SHA }}
tags: |
${{ env.IMAGE }}:dev
${{ env.IMAGE }}:dev-${{ gitea.sha }}

View File

@@ -20,7 +20,6 @@ jobs:
- uses: actions/setup-node@v4
with:
node-version: 'lts/*'
cache: npm
- run: npm ci
- run: npm test
@@ -48,12 +47,10 @@ jobs:
run: |
LAST_TAG=$(git describe --tags --abbrev=0 HEAD^ 2>/dev/null || echo "")
if [ -n "$LAST_TAG" ]; then
NOTES=$(git log "${LAST_TAG}..HEAD" --pretty=format:"- %s" --no-merges)
git log "${LAST_TAG}..HEAD" --pretty=format:"- %s" --no-merges > /tmp/release_notes.txt
else
NOTES=$(git log --pretty=format:"- %s" --no-merges)
git log --pretty=format:"- %s" --no-merges > /tmp/release_notes.txt
fi
NOTES_JSON=$(printf '%s' "$NOTES" | python3 -c "import sys,json; print(json.dumps(sys.stdin.read()))")
echo "NOTES=${NOTES_JSON}" >> $GITEA_ENV
- name: Docker metadata
id: meta
@@ -82,14 +79,9 @@ jobs:
- name: Create Gitea release
run: |
python3 -c "import json,os; v=os.environ['VERSION']; img=os.environ['IMAGE']; notes=open('/tmp/release_notes.txt').read(); open('/tmp/release_body.json','w').write(json.dumps({'tag_name':'v'+v,'name':'Catalyst v'+v,'body':'### Changes\n\n'+notes+'\n\n### Image\n\n'+img+':'+v,'draft':False,'prerelease':False}))"
curl -sf -X POST \
-H "Authorization: token ${{ secrets.TOKEN }}" \
-H "Content-Type: application/json" \
"${{ gitea.server_url }}/api/v1/repos/${{ gitea.repository }}/releases" \
-d "{
\"tag_name\": \"v${{ env.VERSION }}\",
\"name\": \"Catalyst v${{ env.VERSION }}\",
\"body\": \"### Changes\n\n${{ env.NOTES }}\n\n### Image\n\n\`${{ env.IMAGE }}:${{ env.VERSION }}\`\",
\"draft\": false,
\"prerelease\": false
}"
--data @/tmp/release_body.json

View File

@@ -7,8 +7,13 @@ COPY package*.json ./
RUN npm ci --omit=dev
COPY . .
RUN awk -F'"' '/"version"/{printf "const VERSION = \"%s\";\n", $4; exit}' \
package.json > js/version.js
ARG BUILD_VERSION=""
RUN if [ -n "$BUILD_VERSION" ]; then \
printf 'const VERSION = "%s";\n' "$BUILD_VERSION" > js/version.js; \
else \
awk -F'"' '/"version"/{printf "const VERSION = \"%s\";\n", $4; exit}' \
package.json > js/version.js; \
fi
RUN mkdir -p /app/data && chown -R app:app /app
USER app

View File

@@ -289,6 +289,7 @@ select:focus { border-color: var(--accent); }
border-radius: 3px;
letter-spacing: 0.08em;
text-transform: uppercase;
text-align: center;
}
.badge.deployed { background: var(--accent2); color: var(--accent); }

View File

@@ -38,6 +38,9 @@ window.addEventListener('popstate', e => {
// ── Bootstrap ─────────────────────────────────────────────────────────────────
if (VERSION) document.getElementById('nav-version').textContent = `v${VERSION}`;
if (VERSION) {
const label = /^\d/.test(VERSION) ? `v${VERSION}` : VERSION;
document.getElementById('nav-version').textContent = label;
}
handleRoute();

View File

@@ -1,6 +1,6 @@
{
"name": "catalyst",
"version": "1.1.2",
"version": "1.2.2",
"type": "module",
"scripts": {
"start": "node server/server.js",

View File

@@ -133,13 +133,18 @@ export function _resetForTest() {
}
// ── Boot ──────────────────────────────────────────────────────────────────────
// Skipped in test environment — parallel Vitest workers would race to open
// the same file, causing "database is locked". _resetForTest() in beforeEach
// handles initialisation for every test worker using :memory: instead.
const DB_PATH = process.env.DB_PATH ?? DEFAULT_PATH;
try {
init(DB_PATH);
} catch (e) {
console.error('[catalyst] fatal: could not open database at', DB_PATH);
console.error('[catalyst] ensure the data directory exists and is writable by the server process.');
console.error(e);
process.exit(1);
if (process.env.NODE_ENV !== 'test') {
const DB_PATH = process.env.DB_PATH ?? DEFAULT_PATH;
try {
init(DB_PATH);
} catch (e) {
console.error('[catalyst] fatal: could not open database at', DB_PATH);
console.error('[catalyst] ensure the data directory exists and is writable by the server process.');
console.error(e);
process.exit(1);
}
}

View File

@@ -165,3 +165,23 @@ describe('deleteInstance', () => {
expect(getInstance(2)).not.toBeNull();
});
});
// ── Test environment boot isolation ───────────────────────────────────────────
describe('test environment boot isolation', () => {
it('vitest runs with NODE_ENV=test', () => {
// Vitest sets NODE_ENV=test automatically. This is the guard condition
// that prevents the boot init() from opening the real database file.
expect(process.env.NODE_ENV).toBe('test');
});
it('db module loads cleanly in parallel workers without locking the real db file', () => {
// Regression: the module-level init(DEFAULT_PATH) used to run unconditionally,
// causing "database is locked" when multiple test workers imported db.js at
// the same time. process.exit(1) then killed the worker mid-suite.
// Fix: boot init is skipped when NODE_ENV=test. _resetForTest() handles setup.
// Reaching this line proves the module loaded without calling process.exit.
expect(() => _resetForTest()).not.toThrow();
expect(getInstances()).toEqual([]);
});
});

View File

@@ -1,5 +1,7 @@
// @vitest-environment jsdom
import { describe, it, expect } from 'vitest'
import { readFileSync } from 'fs'
import { join } from 'path'
// ── esc() ─────────────────────────────────────────────────────────────────────
// Mirrors the implementation in ui.js exactly (DOM-based).
@@ -112,3 +114,53 @@ describe('fmtDateFull', () => {
expect(fmtDateFull('')).toBe('—')
})
})
// ── versionLabel() ───────────────────────────────────────────────────────────
// Mirrors the logic in app.js — semver strings get a v prefix, dev strings don't.
function versionLabel(v) {
return /^\d/.test(v) ? `v${v}` : v
}
describe('version label formatting', () => {
it('prepends v for semver strings', () => {
expect(versionLabel('1.1.2')).toBe('v1.1.2')
expect(versionLabel('2.0.0')).toBe('v2.0.0')
})
it('does not prepend v for dev build strings', () => {
expect(versionLabel('dev-abc1234')).toBe('dev-abc1234')
})
})
// ── CSS regressions ───────────────────────────────────────────────────────────
const css = readFileSync(join(__dirname, '../css/app.css'), 'utf8')
describe('CSS regressions', () => {
it('.badge has text-align: center so state labels are not left-skewed on cards', () => {
// Regression: badges rendered left-aligned inside the card's flex-end column.
// Without text-align: center, short labels (e.g. "deployed") appear
// left-justified inside their pill rather than centred.
expect(css).toMatch(/\.badge\s*\{[^}]*text-align\s*:\s*center/s)
})
})
// ── CI workflow regressions ───────────────────────────────────────────────────
const ciYml = readFileSync(join(__dirname, '../.gitea/workflows/ci.yml'), 'utf8')
describe('CI workflow regressions', () => {
it('build-dev job passes BUILD_VERSION build arg', () => {
// Regression: dev image showed semver instead of dev-<sha> because
// BUILD_VERSION was never passed to docker build.
expect(ciYml).toContain('BUILD_VERSION')
})
it('short SHA is computed with git rev-parse, not $GITEA_SHA (which is empty)', () => {
// Regression: ${GITEA_SHA::7} expands to "" on Gitea runners — nav showed "dev-".
// git rev-parse --short HEAD works regardless of which env vars the runner sets.
expect(ciYml).toContain('git rev-parse --short HEAD')
expect(ciYml).not.toContain('GITEA_SHA')
})
})