Move the deployment into the app repo, and build it with podman
The playbook, its roles, its vars and a new inventory now live in ansible/, beside the app they deploy, the way the other apps on this machine are laid out. The inventory follows the same shape: one entry per Site, all on the same box, each naming its own vars file through var_file, so adding a Site is one line and a file rather than a command-line flag. ansible_host is written as the resolvable name rather than an ssh alias so the inventory works on any machine. The image is now built with podman as a multi-arch manifest and pushed to registry.tanshu.com/menu-admin, mirroring the Makefile used elsewhere on this machine: build-production, plus build-check and build-check-local for a build that does not push. deploy.sh runs the checks, builds, pushes, then runs the playbook for the Site named by SITE, with SKIP_DEPLOY for a push without a deploy. ESLint comes out again: this project's typescript@7 has no JavaScript compiler API, so typescript-eslint cannot install, and linting only the JavaScript was not worth keeping. Prettier stays, and the README records why ESLint is absent and what would bring it back.
This commit is contained in:
1 parent
e64f65642f
commit
75b6492493
17 files changed
+417
-1067
No files matched your search
Vendored
+1
-1
@@ -1,3 +1,3 @@
|
||||
{
|
||||
"recommendations": ["esbenp.prettier-vscode", "dbaeumer.vscode-eslint"]
|
||||
"recommendations": ["esbenp.prettier-vscode"]
|
||||
}
|
||||
Vendored
-6
@@ -13,12 +13,6 @@
|
||||
"script": "format",
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
"label": "lint",
|
||||
"type": "npm",
|
||||
"script": "lint",
|
||||
"problemMatcher": ["$eslint-stylish"]
|
||||
},
|
||||
{
|
||||
"label": "dev: seed placeholder menus",
|
||||
"type": "npm",
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
IMAGE ?= registry.tanshu.com/menu-admin
|
||||
PLATFORMS ?= linux/amd64,linux/arm64
|
||||
|
||||
.PHONY: build-production build-check build-check-local
|
||||
|
||||
# Mirrors the other apps on this machine: a podman manifest build, so one tag
|
||||
# carries both architectures, pushed to the private registry.
|
||||
build-production: ## Build and push the production image. Usage: make build-production TAG=2026-09-22
|
||||
@test -n "$(TAG)" || { echo "Usage: make build-production TAG=2026-09-22" >&2; exit 1; }
|
||||
@echo "Building image..."
|
||||
@podman manifest rm $(IMAGE):latest 2>/dev/null || true
|
||||
@podman build --platform $(PLATFORMS) --manifest $(IMAGE):latest --pull .
|
||||
@echo "Pushing manifests..."
|
||||
@podman manifest push --all $(IMAGE):latest docker://$(IMAGE):latest
|
||||
@podman manifest push --all $(IMAGE):latest docker://$(IMAGE):$(TAG)
|
||||
|
||||
build-check: ## Multi-arch build without pushing, to prove both platforms build
|
||||
@podman manifest rm menu-admin:check 2>/dev/null || true
|
||||
@podman build --platform $(PLATFORMS) --manifest menu-admin:check --pull .
|
||||
|
||||
build-check-local: ## Single-arch build without pushing: the quick local check
|
||||
@podman manifest rm menu-admin:check 2>/dev/null || true
|
||||
@podman build --platform linux/amd64 --manifest menu-admin:check .
|
||||
@@ -16,18 +16,15 @@ which needs a human present.
|
||||
## Deploying
|
||||
|
||||
```sh
|
||||
./deploy.sh 2026-09-22 # tests, build, push to registry.tanshu.com
|
||||
./deploy.sh 2026-09-22 # format, types, tests, podman build+push, deploy
|
||||
SKIP_DEPLOY=1 ./deploy.sh 2026-09-22 # build and push only
|
||||
```
|
||||
|
||||
then the playbook in `ansible-playbooks/menu-admin/` — its readme covers the
|
||||
parallel run, the `site.env` step that gives the admin a password, and cutover.
|
||||
|
||||
After deploying, check the running site from the outside (read-only, exits
|
||||
non-zero on failure, so it can gate a cutover):
|
||||
|
||||
```sh
|
||||
npm run check-site -- https://menu-new.hopsngrains.com
|
||||
```
|
||||
The image is built with **podman** as a multi-arch manifest and pushed to
|
||||
`registry.tanshu.com/menu-admin` (`Makefile`, mirrored from the other apps on
|
||||
this machine). The Ansible deployment lives in `ansible/` in this repo, with one
|
||||
inventory entry per Site; see `ansible/readme.md` for the first run, cutover and
|
||||
rollback.
|
||||
|
||||
## Layout
|
||||
|
||||
@@ -85,13 +82,11 @@ npm run verify # integrity of .dev/data
|
||||
npm run check-site -- http://127.0.0.1:3000 # checks a running site from outside
|
||||
```
|
||||
|
||||
### Linting and formatting
|
||||
### Formatting
|
||||
|
||||
```sh
|
||||
npm run lint # eslint
|
||||
npm run lint:fix
|
||||
npm run format # prettier --write
|
||||
npm run format:check # what CI and deploy.sh run
|
||||
npm run format:check # what deploy.sh runs
|
||||
```
|
||||
|
||||
Prettier owns formatting (`.prettierrc.json`); `docs/` is excluded because those
|
||||
@@ -99,17 +94,14 @@ are hand-wrapped notes rather than code. `embeddedLanguageFormatting` is off on
|
||||
purpose: the renderers build HTML inside `html` tagged template literals, and
|
||||
letting Prettier reflow those would change the markup customers receive.
|
||||
|
||||
**ESLint does not lint the TypeScript, and cannot as things stand.** This project
|
||||
uses the native TypeScript port (`typescript@7`), which ships no JavaScript
|
||||
compiler API — and every ESLint TypeScript parser needs one. `typescript-eslint`
|
||||
declares `typescript >=4.8.4 <6.1.0` as a peer and refuses to install; forcing it
|
||||
through would leave a parser that cannot read the compiler it was given. The
|
||||
TypeScript gate is therefore `npm run typecheck` (strict, with `noUnusedLocals`,
|
||||
`noUncheckedIndexedAccess`, `exactOptionalPropertyTypes` and
|
||||
`erasableSyntaxOnly`), and ESLint covers the JavaScript. To get ESLint over the
|
||||
`.ts` files as well, either pin `typescript@5.9` — which supports everything this
|
||||
project uses — and add `typescript-eslint`, or add `oxlint`, which parses
|
||||
TypeScript without the compiler API.
|
||||
**ESLint is deliberately absent.** This project's `typescript@7` is the native
|
||||
port and ships no JavaScript compiler API, which every ESLint TypeScript parser
|
||||
needs — `typescript-eslint` requires `typescript >=4.8.4 <6.1.0` and refuses to
|
||||
install, and forcing it through leaves a parser that cannot read the compiler.
|
||||
Linting only the JavaScript was not worth the config, so the TypeScript gate is
|
||||
`npm run typecheck` (strict, with `noUnusedLocals`, `noUncheckedIndexedAccess`,
|
||||
`exactOptionalPropertyTypes` and `erasableSyntaxOnly`) and ESLint can come back,
|
||||
with `typescript-eslint`, once it supports TypeScript 7.
|
||||
|
||||
Dragging Pages is the one interaction that only a browser can check, so there is
|
||||
a harness for it. It needs `geckodriver` on PATH and a running dev server:
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
# The Ansible inventory for the menus. Every Site runs on the same box, so each
|
||||
# entry names it and points at the vars file that describes it.
|
||||
#
|
||||
# cd ansible
|
||||
# ansible-playbook --inventory=hosts playbook.yml # every Site
|
||||
# ansible-playbook --inventory=hosts playbook.yml --limit hopsngrains
|
||||
#
|
||||
# ansible_host is written as the resolvable name rather than an ssh alias so the
|
||||
# inventory works on any machine, whatever its ~/.ssh/config says.
|
||||
|
||||
[menus]
|
||||
hopsngrains ansible_host=monoco.tanshu.com var_file=vars/hopsngrains.yml
|
||||
|
||||
[all:vars]
|
||||
ansible_python_interpreter=/usr/bin/python3
|
||||
@@ -0,0 +1,25 @@
|
||||
---
|
||||
# Deploys the menu app for one or more Sites. It lives here, beside the app it
|
||||
# deploys, and is run from this directory:
|
||||
#
|
||||
# make build-production TAG=<tag> (from the repo root)
|
||||
# ansible-playbook --inventory=hosts playbook.yml --limit hopsngrains
|
||||
#
|
||||
# The first run targets a temporary vhost (menu-new...) so the new site can be
|
||||
# checked against the live one. Cutover is the same playbook with the real vhost
|
||||
# and the two retire flags set; see readme.md.
|
||||
- hosts: menus
|
||||
become: true
|
||||
vars_files:
|
||||
- vars/default.yml
|
||||
|
||||
pre_tasks:
|
||||
- name: Load the vars file this Site names in the inventory
|
||||
ansible.builtin.include_vars:
|
||||
file: '{{ var_file }}'
|
||||
when: var_file is defined
|
||||
|
||||
roles:
|
||||
- network
|
||||
- menu-app
|
||||
- caddy
|
||||
@@ -0,0 +1,100 @@
|
||||
# The menu deployment
|
||||
|
||||
Lives here, beside the app it deploys, the way the other apps on this machine do.
|
||||
It owns the `menu-app` container and the Site's snippet in the shared Caddyfile.
|
||||
|
||||
## What lives where
|
||||
|
||||
| Thing | Where |
|
||||
| -------------------------------------- | --------------------------------------------------------------------------- |
|
||||
| Name, tagline, icon, categories, menus | on the server, under `/var/lib/menu-app.<site>/data` — the admin edits them |
|
||||
| Theme colours | `config/<site>.site.json` in the repo, baked into the image |
|
||||
| Admin password hash, session secret | `/var/lib/menu-app.<site>/site.env` (untracked, mode 600) |
|
||||
| Which image tag runs | `-e tag=<tag>` |
|
||||
| Which Site is being deployed | the inventory entry in `hosts` |
|
||||
|
||||
## Deploying
|
||||
|
||||
From the repo root:
|
||||
|
||||
```sh
|
||||
./deploy.sh 2026-09-22 # checks, podman build+push, then deploys
|
||||
SKIP_DEPLOY=1 ./deploy.sh 2026-09-22 # build and push only
|
||||
```
|
||||
|
||||
`deploy.sh` runs `make build-production TAG=<tag>`, which builds a multi-arch
|
||||
podman manifest (`linux/amd64,linux/arm64`) and pushes `latest` and the tag to
|
||||
`registry.tanshu.com/menu-admin`. Then it runs this playbook against the Site
|
||||
named by `SITE` (default `hopsngrains`).
|
||||
|
||||
By hand:
|
||||
|
||||
```sh
|
||||
make build-production TAG=2026-09-22
|
||||
cd ansible
|
||||
ansible-playbook --inventory=hosts playbook.yml --limit hopsngrains -e tag=2026-09-22
|
||||
```
|
||||
|
||||
Every Site runs on the same box, so `hosts` has one entry per Site pointing at
|
||||
the same `ansible_host` with its own `var_file`. `ansible_host` is written as the
|
||||
resolvable name rather than an ssh alias, so the inventory works whatever a
|
||||
machine's `~/.ssh/config` says. (`ssh` on some machines complains about
|
||||
`/etc/ssh/ssh_config.d/20-systemd-ssh-proxy.conf` being owned by `nobody`; that
|
||||
is a local file-permission bug with the fix `sudo chown -R root:root
|
||||
/etc/ssh/ssh_config.d`, not something this playbook works around.)
|
||||
|
||||
## The first run, and cutover
|
||||
|
||||
The first run targets a temporary vhost, so the live site keeps serving:
|
||||
|
||||
```sh
|
||||
ansible-playbook --inventory=hosts playbook.yml --limit hopsngrains \
|
||||
-e tag=2026-09-22 -e vhost=menu-new.hopsngrains.com
|
||||
```
|
||||
|
||||
`menu-new.hopsngrains.com` needs a DNS A record pointing at the same address as
|
||||
the live site before this runs: Caddy fetches a certificate for the name, which
|
||||
needs it to resolve publicly.
|
||||
|
||||
Then, from the outside (read-only, exits non-zero on failure):
|
||||
|
||||
```sh
|
||||
npm run check-site -- https://menu-new.hopsngrains.com
|
||||
```
|
||||
|
||||
And from a phone on restaurant wifi: the landing page, a Section page, the page
|
||||
counter, tap-to-zoom, and signing into `/admin` to publish something.
|
||||
|
||||
Cutover is the same playbook with the real vhost and the two retire flags:
|
||||
|
||||
```sh
|
||||
ansible-playbook --inventory=hosts playbook.yml --limit hopsngrains \
|
||||
-e tag=2026-09-22 -e vhost=menu.hopsngrains.com \
|
||||
-e retire_nginx_snippet=true -e retire_nginx_container=true
|
||||
```
|
||||
|
||||
That repoints the real vhost at the app, removes the snippet the old `nginx-menu`
|
||||
playbook wrote for that hostname, and stops the old container. The old container
|
||||
and `/var/lib/menu.hopsngrains.com` are kept, not deleted: deleting 319 MB of the
|
||||
only copy of the old menus is a human decision, made once the new site has been
|
||||
trusted.
|
||||
|
||||
## The admin password
|
||||
|
||||
The app starts with the admin disabled when `/var/lib/menu-app.<site>/site.env`
|
||||
is missing, and the playbook says so. Create it once per Site:
|
||||
|
||||
```sh
|
||||
scp scripts/provision-site-env.sh monoco:/tmp/
|
||||
ssh monoco bash /tmp/provision-site-env.sh /var/lib/menu-app.hopsngrains.com
|
||||
ssh monoco docker restart menu-app.hopsngrains.com
|
||||
```
|
||||
|
||||
## Rolling back
|
||||
|
||||
```sh
|
||||
ssh monoco docker start nginx-menu.hopsngrains.com
|
||||
```
|
||||
|
||||
then point Caddy back at it — the cutover flags above with
|
||||
`retire_nginx_snippet=false`.
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
- name: Reload Caddy
|
||||
# Caddy validates before it applies, so a mistake here leaves the running
|
||||
# config untouched and fails the play loudly.
|
||||
command: 'docker exec -w /etc/caddy {{ caddy_container }} caddy reload'
|
||||
listen: Reload Caddy
|
||||
@@ -0,0 +1,39 @@
|
||||
---
|
||||
- name: Check the Caddyfile exists
|
||||
stat:
|
||||
path: '{{ caddyfile_path }}'
|
||||
register: menu_caddyfile
|
||||
|
||||
- name: Fail when Caddy is not where we expect it
|
||||
fail:
|
||||
msg: >-
|
||||
{{ caddyfile_path }} is missing. Deploy the caddy playbook (which owns the
|
||||
Caddy container and its config) before this one.
|
||||
when: not menu_caddyfile.stat.exists
|
||||
|
||||
# blockinfile replaces whatever sits between its markers, so changing `vhost`
|
||||
# updates the snippet in place. The sibling nginx-menu role only appended a
|
||||
# block when its exact text was absent, which cannot change a proxy target.
|
||||
- name: Point Caddy at the app
|
||||
blockinfile:
|
||||
path: '{{ caddyfile_path }}'
|
||||
marker: '# {mark} ANSIBLE MANAGED: menu app ({{ site }})'
|
||||
block: '{{ caddy_snippet }}'
|
||||
create: no
|
||||
notify: Reload Caddy
|
||||
|
||||
- name: Remove the snippet the nginx-menu playbook wrote for this vhost
|
||||
# A different marker, written by a different role. Leaving it in place would
|
||||
# leave Caddy with two blocks for the same hostname.
|
||||
blockinfile:
|
||||
path: '{{ caddyfile_path }}'
|
||||
marker: '# {mark} Ansible managed Caddy snippet for {{ host }}'
|
||||
state: absent
|
||||
when: retire_nginx_snippet | bool
|
||||
notify: Reload Caddy
|
||||
|
||||
- name: Stop the old nginx container
|
||||
docker_container:
|
||||
name: '{{ legacy_container }}'
|
||||
state: stopped
|
||||
when: retire_nginx_container | bool
|
||||
@@ -0,0 +1,111 @@
|
||||
---
|
||||
- name: Pull the app image
|
||||
docker_image:
|
||||
name: '{{ image_name }}'
|
||||
tag: '{{ image_tag }}'
|
||||
source: pull
|
||||
force_source: yes
|
||||
|
||||
- name: Ensure the app root exists
|
||||
file:
|
||||
path: '{{ app_root }}'
|
||||
state: directory
|
||||
owner: '{{ app_uid }}'
|
||||
group: '{{ app_gid }}'
|
||||
mode: '0750'
|
||||
|
||||
- name: Ensure the data directory exists
|
||||
file:
|
||||
path: '{{ app_root }}/data'
|
||||
state: directory
|
||||
owner: '{{ app_uid }}'
|
||||
group: '{{ app_gid }}'
|
||||
mode: '0750'
|
||||
|
||||
- name: Look for the untracked site.env
|
||||
stat:
|
||||
path: '{{ app_root }}/site.env'
|
||||
register: menu_site_env
|
||||
|
||||
- name: Warn when the admin has no password yet
|
||||
debug:
|
||||
msg: >-
|
||||
{{ app_root }}/site.env is missing, so the admin is disabled: the public
|
||||
menu still serves. Run scripts/provision-site-env.sh {{ app_root }} on
|
||||
this host, then re-run this playbook.
|
||||
when: not menu_site_env.stat.exists
|
||||
|
||||
- name: Run the menu app
|
||||
docker_container:
|
||||
name: '{{ docker_container }}'
|
||||
image: '{{ image_name }}:{{ image_tag }}'
|
||||
state: started
|
||||
restart_policy: unless-stopped
|
||||
user: '{{ app_uid }}:{{ app_gid }}'
|
||||
env:
|
||||
MENU_ROOT: /data
|
||||
MENU_SITE_CONFIG: '/app/config/{{ site }}.site.json'
|
||||
SECURE_COOKIES: 'true'
|
||||
LOG_LEVEL: info
|
||||
PORT: '3000'
|
||||
volumes:
|
||||
- '{{ app_root }}:/data'
|
||||
networks:
|
||||
- name: '{{ docker_network }}'
|
||||
|
||||
- name: Keep site.env readable only by the app user
|
||||
file:
|
||||
path: '{{ app_root }}/site.env'
|
||||
owner: '{{ app_uid }}'
|
||||
group: '{{ app_gid }}'
|
||||
mode: '0600'
|
||||
when: menu_site_env.stat.exists
|
||||
|
||||
- name: Wait for the app to report healthy
|
||||
# The image's own HEALTHCHECK fetches /healthz inside the container. Polling
|
||||
# that is deliberate: a container name resolves only on the Docker network, so
|
||||
# the host cannot fetch http://{{ docker_container }}:3000/healthz.
|
||||
docker_container_info:
|
||||
name: '{{ docker_container }}'
|
||||
register: menu_health
|
||||
changed_when: false
|
||||
retries: 20
|
||||
delay: 5
|
||||
until: >-
|
||||
menu_health.container is defined and
|
||||
menu_health.container.State.Health is defined and
|
||||
menu_health.container.State.Health.Status == 'healthy'
|
||||
|
||||
- name: Fail when the app never becomes healthy
|
||||
fail:
|
||||
msg: >-
|
||||
{{ docker_container }} did not report healthy within 100s. Check
|
||||
`docker logs {{ docker_container }}`.
|
||||
when: menu_health.container.State.Health.Status | default('unknown') != 'healthy'
|
||||
|
||||
- name: Check the data directory for integrity problems
|
||||
# Reads the same directory the container writes, as the same user, so a
|
||||
# permissions mistake shows up here rather than at 8pm service.
|
||||
command:
|
||||
argv:
|
||||
- docker
|
||||
- run
|
||||
- --rm
|
||||
- --user
|
||||
- '{{ app_uid }}:{{ app_gid }}'
|
||||
- -e
|
||||
- MENU_ROOT=/data
|
||||
- -e
|
||||
- 'MENU_SITE_CONFIG=/app/config/{{ site }}.site.json'
|
||||
- --entrypoint
|
||||
- node
|
||||
- '{{ image_name }}:{{ image_tag }}'
|
||||
- src/cli/verify.ts
|
||||
register: menu_verify
|
||||
changed_when: false
|
||||
failed_when: false
|
||||
|
||||
- name: Report integrity problems
|
||||
debug:
|
||||
msg: '{{ menu_verify.stdout_lines | default([]) }}'
|
||||
when: menu_verify.rc != 0
|
||||
@@ -0,0 +1,8 @@
|
||||
---
|
||||
- name: 'Ensure {{ docker_network }} exists and Caddy is on it'
|
||||
docker_network:
|
||||
name: '{{ docker_network }}'
|
||||
state: present
|
||||
connected:
|
||||
- '{{ caddy_container }}'
|
||||
appends: yes
|
||||
@@ -0,0 +1,29 @@
|
||||
---
|
||||
# Defaults shared by every Site. Each Site's own file is named by the `var_file`
|
||||
# in ansible/hosts, and its slug is its inventory name.
|
||||
|
||||
# Which Site config inside the image to run, and therefore which vhost it is
|
||||
# for. The file lives in the app repo at config/<site>.site.json.
|
||||
site: '{{ inventory_hostname }}'
|
||||
|
||||
image_name: registry.tanshu.com/menu-admin
|
||||
image_tag: "{{ tag | default('latest') }}"
|
||||
|
||||
# The hostname Caddy serves this Site on. The parallel run uses a temporary
|
||||
# name; cutover points the real one at the app.
|
||||
vhost: '{{ host }}'
|
||||
|
||||
# Cutover switches. Both stay off until the parallel run has been checked.
|
||||
retire_nginx_snippet: false
|
||||
retire_nginx_container: false
|
||||
|
||||
app_uid: 1000
|
||||
app_gid: 1000
|
||||
|
||||
caddy_container: caddy
|
||||
caddyfile_path: /var/lib/caddy/conf/Caddyfile
|
||||
|
||||
caddy_snippet: |
|
||||
{{ vhost }} {
|
||||
reverse_proxy {{ docker_container }}:3000
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
---
|
||||
# Hops n Grains. The name, icon, categories and theme live in the app repo
|
||||
# (config/hopsngrains.site.json seeds the theme and `data/` holds the rest), so
|
||||
# this file only says where to run it.
|
||||
host: menu.hopsngrains.com
|
||||
|
||||
docker_container: menu-app.hopsngrains.com
|
||||
docker_network: '{{ docker_container }}_net'
|
||||
|
||||
# First deployment: a temporary hostname beside the existing site.
|
||||
vhost: menu-new.hopsngrains.com
|
||||
|
||||
# Everything the app writes — data/ and the untracked site.env.
|
||||
app_root: /var/lib/menu-app.hopsngrains.com
|
||||
|
||||
# The old nginx container serving /var/lib/menu.hopsngrains.com. Left alone
|
||||
# until cutover.
|
||||
legacy_container: nginx-menu.hopsngrains.com
|
||||
@@ -1,47 +1,45 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# Builds the image, runs the tests and pushes it to the private registry.
|
||||
# Builds the image with podman, pushes it to the private registry, then deploys
|
||||
# the Site with the playbook that lives in ansible/.
|
||||
#
|
||||
# ./deploy.sh 2026-09-22
|
||||
# ./deploy.sh 2026-09-22 deploy the default Site
|
||||
# SITE=greatbear ./deploy.sh 2026-09-22 deploy another Site
|
||||
# SKIP_DEPLOY=1 ./deploy.sh 2026-09-22 build and push only
|
||||
# SKIP_CHECKS=1 ./deploy.sh 2026-09-22 skip format, typecheck and tests
|
||||
#
|
||||
# Then deploy with the printed ansible-playbook command, or with the cutover
|
||||
# flags from deploy/ansible/readme.md.
|
||||
set -euo pipefail
|
||||
|
||||
TAG="${1:-}"
|
||||
if [[ -z "$TAG" ]]; then
|
||||
echo "usage: deploy.sh <tag> (e.g. $(date +%Y-%m-%d) or a git short sha)" >&2
|
||||
echo "usage: $0 <tag> (e.g. $(date +%Y-%m-%d) or a git short sha)" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
IMAGE="registry.tanshu.com/menu-admin:${TAG}"
|
||||
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
parent_path=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)
|
||||
cd "$parent_path"
|
||||
|
||||
cd "$ROOT"
|
||||
if ! command -v podman >/dev/null 2>&1; then
|
||||
echo "podman is required to build and push the image" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ -z "${SKIP_TESTS:-}" ]]; then
|
||||
echo "==> lint, format and tests"
|
||||
npm run lint
|
||||
if [[ -z "${SKIP_CHECKS:-}" ]]; then
|
||||
echo "==> format, types and tests"
|
||||
npm run format:check
|
||||
npm run typecheck
|
||||
npm test
|
||||
fi
|
||||
|
||||
echo "==> building ${IMAGE}"
|
||||
docker build -t "$IMAGE" .
|
||||
echo "==> building and pushing registry.tanshu.com/menu-admin:$TAG"
|
||||
make build-production TAG="$TAG"
|
||||
|
||||
echo "==> pushing ${IMAGE}"
|
||||
docker push "$IMAGE"
|
||||
if [[ -n "${SKIP_DEPLOY:-}" ]]; then
|
||||
echo "SKIP_DEPLOY is set, so the playbook was not run."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
cat <<EOF
|
||||
|
||||
pushed ${IMAGE}
|
||||
|
||||
parallel run:
|
||||
ansible-playbook -l monoco -e site=hopsngrains -e tag=${TAG} playbook.yml
|
||||
|
||||
cutover:
|
||||
ansible-playbook -l monoco -e site=hopsngrains -e tag=${TAG} \\
|
||||
-e vhost=menu.hopsngrains.com -e retire_nginx_snippet=true \\
|
||||
-e retire_nginx_container=true playbook.yml
|
||||
EOF
|
||||
SITE="${SITE:-hopsngrains}"
|
||||
echo "==> deploying $SITE"
|
||||
cd ansible
|
||||
ansible-playbook --inventory=hosts playbook.yml --limit "$SITE" -e "tag=$TAG"
|
||||
@@ -1,56 +0,0 @@
|
||||
// ESLint, flat config. Lints the JavaScript: the two browser scripts the menus
|
||||
// and the admin actually run, the build and check scripts, and this file.
|
||||
//
|
||||
// TypeScript is deliberately not linted here. The project's TypeScript is the
|
||||
// native port (typescript@7), which ships no JavaScript compiler API, and every
|
||||
// ESLint TypeScript parser needs one — typescript-eslint requires
|
||||
// `typescript >=4.8.4 <6.1.0` and refuses to install. `npm run typecheck`
|
||||
// (tsc --noEmit, strict, with noUnusedLocals, noUncheckedIndexedAccess,
|
||||
// exactOptionalPropertyTypes and erasableSyntaxOnly) is the TypeScript gate.
|
||||
// See README.md for the two ways to put ESLint on the .ts files as well.
|
||||
|
||||
import js from '@eslint/js';
|
||||
import globals from 'globals';
|
||||
|
||||
export default [
|
||||
{
|
||||
ignores: [
|
||||
'node_modules/**',
|
||||
'.dev/**',
|
||||
'.tmp/**',
|
||||
'.npm-cache/**',
|
||||
'public/fonts/**',
|
||||
'public/prototype/**',
|
||||
'docs/design/**',
|
||||
],
|
||||
},
|
||||
js.configs.recommended,
|
||||
{
|
||||
// The browser scripts: old-fashioned IIFEs loaded with a plain <script>.
|
||||
files: ['public/**/*.js'],
|
||||
languageOptions: {
|
||||
sourceType: 'script',
|
||||
globals: globals.browser,
|
||||
},
|
||||
rules: {
|
||||
// Deliberately plain ES5-shaped IIFEs loaded with a <script> tag, so `var`
|
||||
// stays allowed: rewriting it to let/const would be cosmetic, and a
|
||||
// hoisting difference here would only show up in a browser.
|
||||
'no-console': 'error',
|
||||
eqeqeq: ['error', 'smart'],
|
||||
'prefer-const': 'error',
|
||||
'no-implicit-globals': 'error',
|
||||
'no-shadow-restricted-names': 'error',
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['scripts/**/*.{js,mjs}', 'eslint.config.js'],
|
||||
languageOptions: {
|
||||
sourceType: 'module',
|
||||
globals: globals.node,
|
||||
},
|
||||
rules: {
|
||||
'no-console': 'off',
|
||||
},
|
||||
},
|
||||
];
|
||||
Generated
-947
File diff suppressed because it is too large.
Load diff
@@ -16,8 +16,6 @@
|
||||
"verify": "node src/cli/verify.ts",
|
||||
"repair": "node src/cli/repair.ts",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"lint": "eslint .",
|
||||
"lint:fix": "eslint . --fix",
|
||||
"format": "prettier --write .",
|
||||
"format:check": "prettier --check .",
|
||||
"test": "node --test test/*.test.ts",
|
||||
@@ -32,10 +30,7 @@
|
||||
"sharp": "^0.35.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^10.0.1",
|
||||
"@types/node": "^26.6.2",
|
||||
"eslint": "^10.11.0",
|
||||
"globals": "^17.12.0",
|
||||
"prettier": "^3.9.8",
|
||||
"typescript": "^7.0.2"
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user