Add the Great Bear and Hops n Grains Mohali Sites
One image serves every Site, so a new Site is three small files: an inventory entry, a vars file naming its web address and data root, and a config that seeds its theme and first category list (baked into the image, which is why a new Site needs a new tag). Both reuse the directory their old static menus were served from, and the caddy role drops the snippet the retired nginx-menu playbook wrote for each hostname, so a deploy repoints each address at its own container with nothing else to do. The seeded category lists are a starting point, not a decision: categories, names and icons are all editable in each Site's own admin. A test now checks the inventory, config and vars agree for every Site and that no config is orphaned, because a half-added Site was the obvious way to get this wrong.
This commit is contained in:
1 parent
11f60c619f
commit
33a8d56e8b
6 files changed
+208
No files matched your search
@@ -10,6 +10,8 @@
|
|||||||
|
|
||||||
[menus]
|
[menus]
|
||||||
hopsngrains ansible_host=monoco.tanshu.com var_file=vars/hopsngrains.yml
|
hopsngrains ansible_host=monoco.tanshu.com var_file=vars/hopsngrains.yml
|
||||||
|
greatbear ansible_host=monoco.tanshu.com var_file=vars/greatbear.yml
|
||||||
|
hngmohali ansible_host=monoco.tanshu.com var_file=vars/hngmohali.yml
|
||||||
|
|
||||||
[all:vars]
|
[all:vars]
|
||||||
ansible_python_interpreter=/usr/bin/python3
|
ansible_python_interpreter=/usr/bin/python3
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
---
|
||||||
|
# The Great Bear. Its name, tagline, icon, categories and menus live on the server under
|
||||||
|
# app_root and are edited in the admin; config/greatbear.site.json only seeds the
|
||||||
|
# theme and the first category list.
|
||||||
|
title: greatbear.in
|
||||||
|
host: 'menu.{{ title }}'
|
||||||
|
|
||||||
|
docker_container: 'menu-app.{{ title }}'
|
||||||
|
docker_network: '{{ docker_container }}_net'
|
||||||
|
|
||||||
|
# Everything the app writes — data/ and the untracked site.env. Deliberately the
|
||||||
|
# directory the old static menus were served from.
|
||||||
|
app_root: '/var/lib/{{ host }}'
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
---
|
||||||
|
# Hops n Grains Mohali. Its name, tagline, icon, categories and menus live on the server under
|
||||||
|
# app_root and are edited in the admin; config/hngmohali.site.json only seeds the
|
||||||
|
# theme and the first category list.
|
||||||
|
title: hngmohali.com
|
||||||
|
host: 'menu.{{ title }}'
|
||||||
|
|
||||||
|
docker_container: 'menu-app.{{ title }}'
|
||||||
|
docker_network: '{{ docker_container }}_net'
|
||||||
|
|
||||||
|
# Everything the app writes — data/ and the untracked site.env. Deliberately the
|
||||||
|
# directory the old static menus were served from.
|
||||||
|
app_root: '/var/lib/{{ host }}'
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
{
|
||||||
|
"site": "menu.greatbear.in",
|
||||||
|
"brand": {
|
||||||
|
"name": "The Great Bear",
|
||||||
|
"tagline": "Craft Microbrewery",
|
||||||
|
"logo": "/assets/favicon.svg"
|
||||||
|
},
|
||||||
|
"theme": {
|
||||||
|
"steel": "#faf8f5",
|
||||||
|
"rail": "#f4f1ea",
|
||||||
|
"paper": "#ffffff",
|
||||||
|
"accent": "#c59b58",
|
||||||
|
"copper": "#b08544"
|
||||||
|
},
|
||||||
|
"sections": [
|
||||||
|
{
|
||||||
|
"slug": "food",
|
||||||
|
"label": "Food",
|
||||||
|
"shortLabel": "Food",
|
||||||
|
"order": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"slug": "bar",
|
||||||
|
"label": "Bar",
|
||||||
|
"shortLabel": "Bar",
|
||||||
|
"order": 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"slug": "wine",
|
||||||
|
"label": "Wine",
|
||||||
|
"shortLabel": "Wine",
|
||||||
|
"order": 3
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"slug": "straight-drinks",
|
||||||
|
"label": "Straight Drinks",
|
||||||
|
"shortLabel": "Drinks",
|
||||||
|
"order": 4
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"slug": "delivery",
|
||||||
|
"label": "Delivery",
|
||||||
|
"shortLabel": "Delivery",
|
||||||
|
"order": 5
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"slug": "whats-on",
|
||||||
|
"label": "What's On",
|
||||||
|
"shortLabel": "What's On",
|
||||||
|
"order": 6
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"limits": {
|
||||||
|
"maxUploadBytes": 31457280,
|
||||||
|
"maxPagesPerSet": 40
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
{
|
||||||
|
"site": "menu.hngmohali.com",
|
||||||
|
"brand": {
|
||||||
|
"name": "Hops n Grains Mohali",
|
||||||
|
"tagline": "Handcrafted Microbrewery",
|
||||||
|
"logo": "/assets/favicon.svg"
|
||||||
|
},
|
||||||
|
"theme": {
|
||||||
|
"steel": "#faf8f5",
|
||||||
|
"rail": "#f4f1ea",
|
||||||
|
"paper": "#ffffff",
|
||||||
|
"accent": "#c59b58",
|
||||||
|
"copper": "#b08544"
|
||||||
|
},
|
||||||
|
"sections": [
|
||||||
|
{
|
||||||
|
"slug": "food",
|
||||||
|
"label": "Food",
|
||||||
|
"shortLabel": "Food",
|
||||||
|
"order": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"slug": "bar",
|
||||||
|
"label": "Bar",
|
||||||
|
"shortLabel": "Bar",
|
||||||
|
"order": 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"slug": "wine",
|
||||||
|
"label": "Wine",
|
||||||
|
"shortLabel": "Wine",
|
||||||
|
"order": 3
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"slug": "straight-drinks",
|
||||||
|
"label": "Straight Drinks",
|
||||||
|
"shortLabel": "Drinks",
|
||||||
|
"order": 4
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"slug": "delivery",
|
||||||
|
"label": "Delivery",
|
||||||
|
"shortLabel": "Delivery",
|
||||||
|
"order": 5
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"slug": "whats-on",
|
||||||
|
"label": "What's On",
|
||||||
|
"shortLabel": "What's On",
|
||||||
|
"order": 6
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"limits": {
|
||||||
|
"maxUploadBytes": 31457280,
|
||||||
|
"maxPagesPerSet": 40
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
import { strict as assert } from 'node:assert';
|
||||||
|
import { readdir, readFile } from 'node:fs/promises';
|
||||||
|
import path from 'node:path';
|
||||||
|
import { describe, it } from 'node:test';
|
||||||
|
import { parseSiteConfig } from '../src/config.ts';
|
||||||
|
|
||||||
|
const root = path.join(import.meta.dirname, '..');
|
||||||
|
|
||||||
|
/** The inventory decides which Sites exist; the playbook names the vars file. */
|
||||||
|
async function inventory(): Promise<{ name: string; varFile: string }[]> {
|
||||||
|
const text = await readFile(path.join(root, 'ansible', 'hosts'), 'utf8');
|
||||||
|
const inMenus = text.split(/^\[/m).find((block) => block.startsWith('menus]')) ?? '';
|
||||||
|
return inMenus
|
||||||
|
.split('\n')
|
||||||
|
.slice(1)
|
||||||
|
.map((line) => line.trim())
|
||||||
|
.filter((line) => line !== '' && !line.startsWith('#'))
|
||||||
|
.map((line) => {
|
||||||
|
const [name, ...vars] = line.split(/\s+/);
|
||||||
|
const varFile = vars.find((entry) => entry.startsWith('var_file=')) ?? '';
|
||||||
|
return { name: name!, varFile: varFile.replace('var_file=', '') };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('every Site in the inventory is complete', () => {
|
||||||
|
it('has a config file, a vars file, and a config that parses', async () => {
|
||||||
|
const sites = await inventory();
|
||||||
|
assert.ok(sites.length >= 3, `expected the three Sites, found ${sites.length}`);
|
||||||
|
|
||||||
|
for (const { name, varFile } of sites) {
|
||||||
|
assert.notEqual(varFile, '', `${name} names no var_file`);
|
||||||
|
|
||||||
|
const configPath = path.join(root, 'config', `${name}.site.json`);
|
||||||
|
const raw = JSON.parse(await readFile(configPath, 'utf8'));
|
||||||
|
const config = parseSiteConfig(raw);
|
||||||
|
|
||||||
|
assert.equal(
|
||||||
|
config.site.startsWith('menu.'),
|
||||||
|
true,
|
||||||
|
`${name}: site should be a menu hostname`,
|
||||||
|
);
|
||||||
|
assert.ok(config.sections.length > 0, `${name} seeds no categories`);
|
||||||
|
assert.match(
|
||||||
|
config.theme.steel,
|
||||||
|
/^#[0-9a-f]{6}$/i,
|
||||||
|
`${name}: the theme should carry real colours`,
|
||||||
|
);
|
||||||
|
|
||||||
|
const varsRaw = await readFile(path.join(root, 'ansible', varFile), 'utf8');
|
||||||
|
// Each vars file only needs to say where the Site runs; the templating is
|
||||||
|
// quoted, which is what a first deploy got wrong.
|
||||||
|
assert.match(varsRaw, /^title: /m, `${name}: ${varFile} has no title`);
|
||||||
|
assert.match(varsRaw, /^host: 'menu\.\{\{ title \}\}'/m, `${name}: host is not quoted`);
|
||||||
|
assert.match(varsRaw, /^app_root: /m, `${name}: no app_root`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('leaves no Site config without an inventory entry', async () => {
|
||||||
|
const sites = new Set((await inventory()).map((site) => site.name));
|
||||||
|
const files = await readdir(path.join(root, 'config'));
|
||||||
|
for (const file of files.filter((entry) => entry.endsWith('.site.json'))) {
|
||||||
|
const name = file.replace('.site.json', '');
|
||||||
|
assert.ok(sites.has(name), `config/${file} belongs to no Site in the inventory`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in new issue
Block a user