Compare commits
10
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7726835b3c
|
||
|
|
4141008a1b
|
||
|
|
0757348c5a
|
||
|
|
c3aacfb711
|
||
|
|
12fc9beb23
|
||
|
|
55aad6e251
|
||
|
|
9b59f7f6cd
|
||
|
|
05e65f904c
|
||
|
|
2e99a3b0a9 | ||
|
|
4f9fcaef0a |
No files matched your search
@@ -0,0 +1,14 @@
|
||||
name: build
|
||||
on:
|
||||
push:
|
||||
jobs:
|
||||
container:
|
||||
uses: self:KofTwentyTwo/ci-workflows/.gitea/workflows/container.yaml@v1
|
||||
secrets: inherit
|
||||
with:
|
||||
images: nuncio-website|Dockerfile
|
||||
# The Dockerfile runs the install and build; nothing to run before it.
|
||||
prebuild: 'true'
|
||||
publish-branch: main
|
||||
# Deploys: Argo CD Image Updater in cluster-gitops follows the newest image pushed from
|
||||
# this branch, so CI needs no GitOps write token.
|
||||
@@ -25,6 +25,10 @@ jobs:
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Audit dependencies (report-only)
|
||||
run: npm audit --audit-level=high
|
||||
continue-on-error: true
|
||||
|
||||
- name: Lint
|
||||
run: npm run lint
|
||||
|
||||
|
||||
+2
-1
@@ -26,7 +26,8 @@ ENV NEXT_TELEMETRY_DISABLED=1
|
||||
ENV PORT=3000
|
||||
ENV HOSTNAME="0.0.0.0"
|
||||
|
||||
RUN addgroup --system --gid 1001 nodejs && \
|
||||
RUN apk upgrade --no-cache && \
|
||||
addgroup --system --gid 1001 nodejs && \
|
||||
adduser --system --uid 1001 nextjs
|
||||
|
||||
# Strip build-time package-manager tooling inherited from the base image.
|
||||
|
||||
Generated
+236
-343
File diff suppressed because it is too large.
Load diff
+10
-6
@@ -10,7 +10,8 @@
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "eslint src"
|
||||
"lint": "eslint src",
|
||||
"test": "node --test tests/http-smoke.test.mjs"
|
||||
},
|
||||
"dependencies": {
|
||||
"@headlessui/react": "^2.2.6",
|
||||
@@ -18,9 +19,9 @@
|
||||
"@tailwindcss/typography": "^0.5.16",
|
||||
"clsx": "^2.1.1",
|
||||
"lucide-react": "^1.26.0",
|
||||
"next": "^16.2.11",
|
||||
"next": "16.4.3",
|
||||
"next-themes": "^0.4.6",
|
||||
"postcss": "^8.5.22",
|
||||
"postcss": "^8.5.28",
|
||||
"react": "19.2.8",
|
||||
"react-dom": "19.2.4",
|
||||
"tailwindcss": "^4.2.0",
|
||||
@@ -31,10 +32,13 @@
|
||||
"@types/react": "19.2.17",
|
||||
"@types/react-dom": "19.2.3",
|
||||
"eslint": "^9.39.5",
|
||||
"eslint-config-next": "16.1.6"
|
||||
"eslint-config-next": "16.4.3"
|
||||
},
|
||||
"overrides": {
|
||||
"sharp": "^0.35.0",
|
||||
"postcss": "$postcss"
|
||||
"sharp": "^0.35.4",
|
||||
"postcss": "$postcss",
|
||||
"minimatch": "^10.0.3",
|
||||
"js-yaml": "^4.3.2",
|
||||
"nanoid": "^3.3.18"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { spawn } from 'node:child_process';
|
||||
import { once } from 'node:events';
|
||||
import { after, before, test } from 'node:test';
|
||||
|
||||
const base = process.env.SMOKE_BASE_URL || 'http://127.0.0.1:3210';
|
||||
let server;
|
||||
let logs = '';
|
||||
|
||||
before(async () => {
|
||||
if (process.env.SMOKE_BASE_URL) return;
|
||||
server = spawn(process.execPath, [
|
||||
'node_modules/next/dist/bin/next', 'dev', '--webpack',
|
||||
'--hostname', '127.0.0.1', '--port', '3210',
|
||||
], { env: { ...process.env, NEXT_TELEMETRY_DISABLED: '1' }, stdio: ['ignore', 'pipe', 'pipe'] });
|
||||
for (const stream of [server.stdout, server.stderr]) {
|
||||
stream.on('data', chunk => { logs = (logs + chunk).slice(-8000); });
|
||||
}
|
||||
const deadline = Date.now() + 120_000;
|
||||
while (Date.now() < deadline) {
|
||||
if (server.exitCode !== null) throw new Error(`Next exited: ${logs}`);
|
||||
try {
|
||||
const response = await fetch(`${base}/icon.svg`, { signal: AbortSignal.timeout(2000) });
|
||||
if (response.ok) return;
|
||||
} catch {}
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
}
|
||||
throw new Error(`Next did not become ready: ${logs}`);
|
||||
}, { timeout: 125_000 });
|
||||
|
||||
after(async () => {
|
||||
if (!server || server.exitCode !== null) return;
|
||||
const exited = once(server, 'exit');
|
||||
server.kill('SIGTERM');
|
||||
const force = setTimeout(() => server.kill('SIGKILL'), 5000);
|
||||
await exited;
|
||||
clearTimeout(force);
|
||||
});
|
||||
|
||||
for (const path of ['/', '/features', '/security', '/docs/account-setup']) {
|
||||
test(`renders ${path} as a real application page`, { timeout: 120_000 }, async () => {
|
||||
const response = await fetch(base + path, { signal: AbortSignal.timeout(110_000) });
|
||||
assert.equal(response.status, 200, `${path}: ${logs}`);
|
||||
assert.match(response.headers.get('content-type'), /text\/html/);
|
||||
const html = await response.text();
|
||||
assert.match(html, /<title>[^<]*Nuncio/);
|
||||
assert.match(html, /<h1(?:\s|>)/);
|
||||
assert.doesNotMatch(html, /Application error: a server-side exception/);
|
||||
});
|
||||
}
|
||||
|
||||
test('serves the brand icon as SVG', async () => {
|
||||
const response = await fetch(base + '/icon.svg');
|
||||
assert.equal(response.status, 200);
|
||||
assert.match(response.headers.get('content-type'), /image\/svg\+xml/);
|
||||
assert.match(await response.text(), /<svg/);
|
||||
});
|
||||
|
||||
test('unknown routes return 404 rather than a misleading successful page', { timeout: 120_000 }, async () => {
|
||||
const response = await fetch(base + '/qualification-route-that-does-not-exist', {
|
||||
signal: AbortSignal.timeout(110_000),
|
||||
});
|
||||
assert.equal(response.status, 404);
|
||||
});
|
||||
Reference in new issue
Block a user