#!/usr/bin/env bash # ===================================================================== # sync-from-prod.sh — reflect production drift onto the golden template VM # WITHOUT re-cloning (the full re-clone is REFRESH-QUICKSTART.md). # # sync-from-prod.sh [--since YYYY-MM-DD] [--dry-run] [--prod IP] [--tpl IP] # # Runs on a workstation that can ssh as root to BOTH hosts. Transfers # prod -> template go directly (rsync over ssh with agent forwarding), so # run `ssh-add` first. Production is only ever READ. # # What it syncs (template keeps its own .env values, data/, DB data): # - apt packages where prod is newer (same repos) + packages only prod has # - Node version of prod's nvm default + the same pm2 version; pm2-root # unit + override from prod; erpfrontend restarted on the new Node # - /opt/erp-nuxt-frontend (build + node_modules) — .env and data/ kept; # env keys that only prod has are appended BLANK (review the list it # prints: non-secret ones like ANTHROPIC_MODEL need a value); # GITHASH/BUILDTIME taken from prod # - iDempiere: new tables (pg_dump -s), new physical columns (+ CHECK # constraints), Application-Dictionary rows created since --since # (ad_table/element/reference/ref_list/column/field + _trl), AD sequences # - Jasper report files, jetty.xml/jetty-template.xml, postfix main.cf + # sender_relay + tls_policy (never sasl_passwd; postfix stays disabled), # the data.logship.de hosts line, git pull of /opt/logship-scripts # Reported only (template data is intentionally different): config-row # updates in keep tables / price lists / shippers / order sources, new # AD windows/tabs/menus/processes/print formats, ad_sysconfig changes. # # --since defaults to the template's BUILDTIME (.env) minus one day. # Idempotent: a second run finds nothing to do. Every DB change runs in one # transaction; the previous frontend tree stays in /opt/erp-nuxt-frontend-rollback. # ===================================================================== set -euo pipefail PROD=192.168.12.72 TPL=192.168.12.67 SINCE="" DRY=0 while [ $# -gt 0 ]; do case "$1" in --since) SINCE="$2"; shift 2;; --dry-run) DRY=1; shift;; --prod) PROD="$2"; shift 2;; --tpl) TPL="$2"; shift 2;; *) echo "unknown arg $1" >&2; exit 2;; esac done SSH="ssh -o BatchMode=yes -o ConnectTimeout=20" P() { $SSH "root@$PROD" "$@"; } T() { $SSH "root@$TPL" "$@"; } WORK="$(mktemp -d /tmp/tpl-sync.XXXXXX)" STAMP="$(date +%Y%m%d-%H%M%S)" RDIR="/root/sync-$STAMP" say() { printf '\n== %s\n' "$*"; } # --- guards -------------------------------------------------------------- TPL_HOST="$(T hostname)" case "$TPL_HOST" in logship-production) echo "FATAL: target is production" >&2; exit 1;; esac P "hostname | grep -q '^logship-production$'" || { echo "FATAL: $PROD is not logship-production" >&2; exit 1; } if ! $SSH -A "root@$PROD" "ssh -o BatchMode=yes -o StrictHostKeyChecking=accept-new root@$TPL true" 2>/dev/null; then echo "FATAL: prod cannot reach the template over ssh — run 'ssh-add' (agent forwarding is used for rsync)" >&2; exit 1 fi if [ -z "$SINCE" ]; then bt="$(T "grep -oE '^BUILDTIME=[0-9.]+' /opt/erp-nuxt-frontend/.env | cut -d= -f2")" # dd.mm.yyyy SINCE="$(python3 -c "import datetime as d,sys; x=d.datetime.strptime('$bt','%d.%m.%Y')-d.timedelta(days=1); print(x.date())")" fi echo "prod $PROD -> template $TPL ($TPL_HOST), AD rows since $SINCE, dry-run=$DRY, work dir $WORK" # --- 1. collect ------------------------------------------------------------ say "1/7 collecting state" NODE_PROD="$(P 'cat /root/.nvm/alias/default')" NODE_TPL="$(T 'cat /root/.nvm/alias/default')" PM2_PROD="$(P "grep -o '\"version\": *\"[^\"]*\"' /root/.nvm/versions/node/$NODE_PROD/lib/node_modules/pm2/package.json | grep -oE '[0-9][0-9.]*'")" PM2_TPL="$(T "grep -o '\"version\": *\"[^\"]*\"' /root/.nvm/versions/node/$NODE_TPL/lib/node_modules/pm2/package.json 2>/dev/null | grep -oE '[0-9][0-9.]*' || echo none")" HASH_PROD="$(P "grep -oE '^GITHASH=.*' /opt/erp-nuxt-frontend/.env | cut -d= -f2")" HASH_TPL="$(T "grep -oE '^GITHASH=.*' /opt/erp-nuxt-frontend/.env | cut -d= -f2")" BUILD_PROD="$(P "grep -oE '^BUILDTIME=.*' /opt/erp-nuxt-frontend/.env | cut -d= -f2-")" echo "node prod $NODE_PROD / tpl $NODE_TPL" echo "pm2 prod $PM2_PROD / tpl $PM2_TPL" echo "frontend prod $HASH_PROD ($BUILD_PROD) / tpl $HASH_TPL" # packages: prod newer or prod-only P "dpkg-query -W -f='\${Package} \${Version}\n'" | sort > "$WORK/pkg-prod.txt" T "dpkg-query -W -f='\${Package} \${Version}\n'" | sort > "$WORK/pkg-tpl.txt" scp -q "$WORK/pkg-prod.txt" "root@$TPL:/tmp/pkg-prod.txt" T 'while read p v; do tv=$(dpkg-query -W -f=\${Version} "$p" 2>/dev/null || true); if [ -z "$tv" ]; then echo "$p (missing, prod $v)"; elif dpkg --compare-versions "$v" gt "$tv"; then echo "$p ($tv -> $v)"; fi; done < /tmp/pkg-prod.txt; rm -f /tmp/pkg-prod.txt' > "$WORK/pkg-diff.txt" || true PKGS="$(awk '{print $1}' "$WORK/pkg-diff.txt" | tr '\n' ' ')" echo "packages to upgrade/install: ${PKGS:-none}" [ -s "$WORK/pkg-diff.txt" ] && sed 's/^/ /' "$WORK/pkg-diff.txt" # iDempiere schema: template column + constraint + table lists -> prod generates DDL T "sudo -u postgres psql -d idempiere -Atc \"select c.relname||'.'||a.attname from pg_attribute a join pg_class c on c.oid=a.attrelid join pg_namespace n on n.oid=c.relnamespace where n.nspname='adempiere' and c.relkind='r' and a.attnum>0 and not a.attisdropped order by 1\" 2>/dev/null" > "$WORK/tpl-cols.txt" T "sudo -u postgres psql -d idempiere -Atc \"select conname from pg_constraint k join pg_class c on c.oid=k.conrelid join pg_namespace n on n.oid=c.relnamespace where n.nspname='adempiere'\" 2>/dev/null" > "$WORK/tpl-cons.txt" T "sudo -u postgres psql -d idempiere -Atc \"select relname from pg_class c join pg_namespace n on n.oid=c.relnamespace where n.nspname='adempiere' and relkind='r'\" 2>/dev/null" > "$WORK/tpl-tables.txt" P "mkdir -p /tmp/tpl-sync && chmod 777 /tmp/tpl-sync" scp -q "$WORK/tpl-cols.txt" "$WORK/tpl-cons.txt" "$WORK/tpl-tables.txt" "root@$PROD:/tmp/tpl-sync/" cat > "$WORK/gen-ddl.sql" <<'SQL' \set ON_ERROR_STOP on create temp table tpl_cols(tc text); create temp table tpl_cons(cn text); create temp table tpl_tables(tn text); \copy tpl_cols from '/tmp/tpl-sync/tpl-cols.txt' \copy tpl_cons from '/tmp/tpl-sync/tpl-cons.txt' \copy tpl_tables from '/tmp/tpl-sync/tpl-tables.txt' \o /tmp/tpl-sync/new-tables.txt select relname from pg_class c join pg_namespace n on n.oid=c.relnamespace where n.nspname='adempiere' and relkind='r' and relname not in (select tn from tpl_tables) order by 1; \o /tmp/tpl-sync/ddl-columns.sql select format('ALTER TABLE adempiere.%I ADD COLUMN %I %s%s%s;', c.relname, a.attname, format_type(a.atttypid,a.atttypmod), coalesce(' DEFAULT '||pg_get_expr(d.adbin,d.adrelid),''), case when a.attnotnull then ' NOT NULL' else '' end) from pg_attribute a join pg_class c on c.oid=a.attrelid join pg_namespace n on n.oid=c.relnamespace left join pg_attrdef d on d.adrelid=a.attrelid and d.adnum=a.attnum where n.nspname='adempiere' and c.relkind='r' and a.attnum>0 and not a.attisdropped and c.relname in (select tn from tpl_tables) and (c.relname||'.'||a.attname) not in (select tc from tpl_cols) order by c.relname, a.attnum; \o /tmp/tpl-sync/ddl-constraints.sql select format('ALTER TABLE adempiere.%I ADD CONSTRAINT %I %s;', c.relname, k.conname, pg_get_constraintdef(k.oid)) from pg_constraint k join pg_class c on c.oid=k.conrelid join pg_namespace n on n.oid=c.relnamespace where n.nspname='adempiere' and k.contype='c' and c.relname in (select tn from tpl_tables) and k.conname not in (select cn from tpl_cons) and exists (select 1 from unnest(k.conkey) ck join pg_attribute a on a.attrelid=k.conrelid and a.attnum=ck where (c.relname||'.'||a.attname) not in (select tc from tpl_cols)) order by 1; \o SQL scp -q "$WORK/gen-ddl.sql" "root@$PROD:/tmp/tpl-sync/gen-ddl.sql" P "cd /tmp/tpl-sync && sudo -u postgres psql -d idempiere -Atf gen-ddl.sql >/dev/null 2>gen.err || { grep -v perl gen.err; exit 1; }" # AD rows + sequences + report-only items, exported on prod P "cd /tmp/tpl-sync && export LANG=C.UTF-8 && Q='sudo -u postgres psql -d idempiere -At' && \ for t in ad_table ad_element ad_reference ad_ref_list ad_column ad_field; do \ \$Q -c \"COPY (select * from adempiere.\$t where created > '$SINCE') TO STDOUT\" > \$t.copy 2>/dev/null; \ \$Q -c \"COPY (select x.* from adempiere.\${t}_trl x join adempiere.\$t b on b.\${t}_id = x.\${t}_id where b.created > '$SINCE') TO STDOUT\" > \${t}_trl.copy 2>/dev/null; \ done; \ \$Q -c \"COPY (select name, currentnext from adempiere.ad_sequence where name in ('AD_Table','AD_Element','AD_Reference','AD_Ref_List','AD_Column','AD_Field')) TO STDOUT\" > sequences.copy 2>/dev/null; \ \$Q -F' ' -c \"select 'ad_window',count(*) from adempiere.ad_window where created > '$SINCE' union all select 'ad_tab',count(*) from adempiere.ad_tab where created > '$SINCE' union all select 'ad_menu',count(*) from adempiere.ad_menu where created > '$SINCE' union all select 'ad_process',count(*) from adempiere.ad_process where created > '$SINCE' union all select 'ad_printformat',count(*) from adempiere.ad_printformat where created > '$SINCE' union all select 'ad_sysconfig updated',count(*) from adempiere.ad_sysconfig where updated > '$SINCE' union all select 'ad_val_rule',count(*) from adempiere.ad_val_rule where created > '$SINCE'\" 2>/dev/null > manual.txt; \ \$Q -F' ' -c \"select 'm_pricelist',name,updated::date from adempiere.m_pricelist where updated > '$SINCE' union all select 'm_shipper',name,updated::date from adempiere.m_shipper where updated > '$SINCE' and ad_org_id in (0,1000000) union all select 'c_ordersource',name,updated::date from adempiere.c_ordersource where updated > '$SINCE' and ad_org_id in (0,1000000) union all select 'cust_printer',name,updated::date from adempiere.cust_printer where updated > '$SINCE' union all select 'cust_labeltemplate',name,updated::date from adempiere.cust_labeltemplate where updated > '$SINCE' union all select 'cust_commissiontable',name,updated::date from adempiere.cust_commissiontable where updated > '$SINCE' union all select 'c_doctype',name,updated::date from adempiere.c_doctype where updated > '$SINCE' union all select 'ad_role',name,updated::date from adempiere.ad_role where updated > '$SINCE'\" 2>/dev/null >> manual.txt; \ wc -l *.copy ddl-columns.sql ddl-constraints.sql new-tables.txt | grep -v total" scp -q "root@$PROD:/tmp/tpl-sync/*" "$WORK/" 2>/dev/null || true if [ -s "$WORK/new-tables.txt" ]; then P "cd /tmp/tpl-sync && sudo -u postgres pg_dump -s -n adempiere $(sed 's/^/-t adempiere./' "$WORK/new-tables.txt" | tr '\n' ' ') idempiere" > "$WORK/ddl-tables.sql" 2>/dev/null fi P "rm -rf /tmp/tpl-sync" echo "new tables: $(wc -l < "$WORK/new-tables.txt" | tr -d ' ') $(tr '\n' ' ' < "$WORK/new-tables.txt")" echo "new columns: $(wc -l < "$WORK/ddl-columns.sql" | tr -d ' ')"; sed 's/^/ /' "$WORK/ddl-columns.sql" echo "new constraints: $(wc -l < "$WORK/ddl-constraints.sql" | tr -d ' ')" for t in ad_table ad_element ad_reference ad_ref_list ad_column ad_field; do printf '%-16s %s rows (+%s trl)\n' "$t" "$(wc -l < "$WORK/$t.copy" | tr -d ' ')" "$(wc -l < "$WORK/${t}_trl.copy" | tr -d ' ')"; done echo "report-only (handle by hand if non-zero / listed):"; sed 's/^/ /' "$WORK/manual.txt" # files P "cd /opt/idempiere-server/data/reports && find . -type f -exec md5sum {} + | sort -k2" > "$WORK/jasper-prod.txt" T "cd /opt/idempiere-server/data/reports && find . -type f -exec md5sum {} + | sort -k2" > "$WORK/jasper-tpl.txt" JASPER_DIFF="$(diff "$WORK/jasper-prod.txt" "$WORK/jasper-tpl.txt" | grep -c '^[<>]' || true)" echo "jasper files differing: $JASPER_DIFF" for f in /opt/idempiere-server/jettyhome/etc/jetty.xml /opt/idempiere-server/jettyhome/etc/jetty-template.xml /etc/postfix/main.cf /etc/postfix/sender_relay /etc/postfix/tls_policy /etc/systemd/system/pm2-root.service /etc/systemd/system/pm2-root.service.d/override.conf; do a="$(P "md5sum $f 2>/dev/null | cut -c1-8")"; b="$(T "md5sum $f 2>/dev/null | cut -c1-8")" [ "$a" = "$b" ] || echo "config differs: $f (prod ${a:-none} / tpl ${b:-none})" done HOSTS_LINE="$(P "grep -E 'data\.logship\.de' /etc/hosts || true")" SCRIPTS_PROD="$(P 'cd /opt/logship-scripts && git rev-parse --short HEAD')"; SCRIPTS_TPL="$(T 'cd /opt/logship-scripts && git rev-parse --short HEAD')" echo "logship-scripts prod $SCRIPTS_PROD / tpl $SCRIPTS_TPL" # env keys only prod has P "grep -oE '^[A-Z_0-9]+=' /opt/erp-nuxt-frontend/.env | tr -d = | sort -u" > "$WORK/env-prod.txt" T "grep -oE '^[A-Z_0-9]+=' /opt/erp-nuxt-frontend/.env | tr -d = | sort -u" > "$WORK/env-tpl.txt" NEWKEYS="$(comm -23 "$WORK/env-prod.txt" "$WORK/env-tpl.txt" | tr '\n' ' ')" echo "env keys only on prod (appended blank): ${NEWKEYS:-none}" if [ "$DRY" = 1 ]; then echo; echo "dry run — nothing changed. work dir kept: $WORK"; exit 0; fi # --- 2. packages ------------------------------------------------------------- say "2/7 packages" if [ -n "$PKGS" ]; then T "export DEBIAN_FRONTEND=noninteractive; apt-get update -qq >/dev/null 2>&1 || true; apt-get install -y -q $PKGS 2>&1 | tail -3" else echo "nothing to do"; fi # --- 3. node + pm2 ----------------------------------------------------------- say "3/7 node $NODE_PROD + pm2 $PM2_PROD" if [ "$NODE_PROD" != "$NODE_TPL" ] || [ "$PM2_PROD" != "$PM2_TPL" ]; then T "export NVM_DIR=/root/.nvm; . \$NVM_DIR/nvm.sh >/dev/null; nvm install $NODE_PROD >/dev/null 2>&1; nvm alias default $NODE_PROD >/dev/null; nvm use $NODE_PROD >/dev/null; npm install -g pm2@$PM2_PROD >/dev/null 2>&1; node -v; pm2 -v" else echo "already equal"; fi # --- 4. frontend --------------------------------------------------------------- say "4/7 frontend $HASH_PROD" T "mkdir -p $RDIR" if [ "$HASH_PROD" != "$HASH_TPL" ] || ! T "test -d /opt/erp-nuxt-frontend/.output"; then $SSH -A "root@$PROD" "rsync -a --delete --exclude .env --exclude data/ --exclude 'logship__*.db' -e 'ssh -o BatchMode=yes' /opt/erp-nuxt-frontend/ root@$TPL:/opt/erp-nuxt-frontend-sync/" T "set -e; export NVM_DIR=/root/.nvm; . \$NVM_DIR/nvm.sh >/dev/null; export PATH=/root/.nvm/versions/node/$NODE_PROD/bin:\$PATH pm2 delete erpfrontend >/dev/null 2>&1 || true rm -rf /opt/erp-nuxt-frontend-rollback; mv /opt/erp-nuxt-frontend /opt/erp-nuxt-frontend-rollback; mv /opt/erp-nuxt-frontend-sync /opt/erp-nuxt-frontend cp -p /opt/erp-nuxt-frontend-rollback/.env /opt/erp-nuxt-frontend/.env; cp -p /opt/erp-nuxt-frontend-rollback/.env $RDIR/env.before mkdir -p /opt/erp-nuxt-frontend/data; printf '' > /opt/erp-nuxt-frontend/data/.gitkeep; chmod 755 /opt/erp-nuxt-frontend/data cd /opt/erp-nuxt-frontend/.output/server && node -e 'require(\"./node_modules/sqlite3\"); require(\"./node_modules/better-sqlite3\"); console.log(\"native modules load OK\")'" else echo "build already equal — skipping rsync"; fi # env: build id + new keys (blank) T "sed -i -E 's|^GITHASH=.*|GITHASH=$HASH_PROD|; s|^BUILDTIME=.*|BUILDTIME=$BUILD_PROD|' /opt/erp-nuxt-frontend/.env for k in $NEWKEYS; do grep -qE \"^\$k=\" /opt/erp-nuxt-frontend/.env || printf '%s=\n' \"\$k\" >> /opt/erp-nuxt-frontend/.env; done" # pm2 unit files from prod, restart under systemd P "cat /etc/systemd/system/pm2-root.service" > "$WORK/pm2-root.service" P "cat /etc/systemd/system/pm2-root.service.d/override.conf" > "$WORK/override.conf" scp -q "$WORK/pm2-root.service" "root@$TPL:/etc/systemd/system/pm2-root.service" scp -q "$WORK/override.conf" "root@$TPL:/etc/systemd/system/pm2-root.service.d/override.conf" T "set -e; export NVM_DIR=/root/.nvm; . \$NVM_DIR/nvm.sh >/dev/null; export PATH=/root/.nvm/versions/node/$NODE_PROD/bin:\$PATH systemctl daemon-reload cd /opt/erp-nuxt-frontend; pm2 delete erpfrontend >/dev/null 2>&1 || true pm2 start ecosystem.config.cjs --only erpfrontend --update-env --interpreter /root/.nvm/versions/node/$NODE_PROD/bin/node >/dev/null pm2 describe media-strapi-prod >/dev/null 2>&1 || (cd /opt/mediaserver && pm2 start ecosystem.config.js --update-env >/dev/null) pm2 save --force >/dev/null; systemctl restart pm2-root; sleep 10 # pm2 update replaces the daemon and may drop the apps: re-add + save if so pm2 describe erpfrontend >/dev/null 2>&1 || { pm2 start ecosystem.config.cjs --only erpfrontend --update-env --interpreter /root/.nvm/versions/node/$NODE_PROD/bin/node >/dev/null; pm2 save --force >/dev/null; } for i in \$(seq 1 30); do code=\$(curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:3001/signin); [ \"\$code\" = 200 ] && break; sleep 3; done; echo \"frontend /signin http \$code\" pm2 list | grep -E 'erpfrontend|media-strapi' | cut -c1-100" # --- 5. iDempiere ---------------------------------------------------------------- say "5/7 iDempiere schema + AD rows" cat > "$WORK/apply-db.sql" <> "$WORK/apply-db.sql" <> "$WORK/apply-db.sql" <<'SQL' CREATE TEMP TABLE tmp_seq (name varchar(60), currentnext numeric); \copy tmp_seq from 'sequences.copy' UPDATE adempiere.ad_sequence s SET currentnext = t.currentnext FROM tmp_seq t WHERE s.name = t.name AND s.currentnext < t.currentnext; SELECT 'columns' AS chk, count(*) FROM information_schema.columns WHERE table_schema = 'adempiere' UNION ALL SELECT 'ad_column', count(*) FROM adempiere.ad_column UNION ALL SELECT 'ad_element', count(*) FROM adempiere.ad_element; COMMIT; SQL touch "$WORK/ddl-tables.sql" T "mkdir -p $RDIR/db && chmod 755 $RDIR $RDIR/db" scp -q "$WORK"/*.copy "$WORK"/ddl-*.sql "$WORK/apply-db.sql" "root@$TPL:$RDIR/db/" T "chmod 644 $RDIR/db/*; cd $RDIR/db && export LANG=C.UTF-8; sudo -u postgres psql -d idempiere -v ON_ERROR_STOP=1 -f apply-db.sql > apply.log 2>&1 || { grep -v perl apply.log; echo DB_APPLY_FAILED; exit 1; }; grep -vE '^(perl|LANGUAGE|LC_|LANG|\s+are supported|CREATE TABLE|COPY|INSERT 0 0|BEGIN|COMMIT|SET|SELECT)' apply.log | grep -vE '^\s*$'" # --- 6. files ---------------------------------------------------------------------- say "6/7 report files, jetty, postfix, hosts, scripts" [ "$JASPER_DIFF" = 0 ] || $SSH -A "root@$PROD" "rsync -a -e 'ssh -o BatchMode=yes' /opt/idempiere-server/data/reports/ root@$TPL:/opt/idempiere-server/data/reports/" for f in /opt/idempiere-server/jettyhome/etc/jetty.xml /opt/idempiere-server/jettyhome/etc/jetty-template.xml /etc/postfix/main.cf /etc/postfix/sender_relay /etc/postfix/tls_policy; do P "cat $f" > "$WORK/$(basename "$f")" && scp -q "$WORK/$(basename "$f")" "root@$TPL:$f" done T "cd /etc/postfix && { [ -f sasl_passwd ] || { printf '# SASL credentials for the sender-dependent relay (see sender_relay); fill in per instance:\n# [host]:587 user@example.com:password\n' > sasl_passwd; chmod 600 sasl_passwd; }; }; postmap sender_relay tls_policy sasl_passwd 2>/dev/null || true; systemctl is-enabled postfix >/dev/null 2>&1 && echo 'WARNING: postfix enabled on the template' || true" [ -z "$HOSTS_LINE" ] || T "grep -qF '$HOSTS_LINE' /etc/hosts || echo '$HOSTS_LINE' >> /etc/hosts" [ "$SCRIPTS_PROD" = "$SCRIPTS_TPL" ] || T "cd /opt/logship-scripts && git pull --ff-only -q && git rev-parse --short HEAD" say "restarting idempiere" T "systemctl restart idempiere; for i in \$(seq 1 90); do curl -ks -o /dev/null --connect-timeout 2 https://127.0.0.1:8443/ && break; sleep 3; done; curl -ks -o /dev/null -w 'idempiere http %{http_code}\n' https://127.0.0.1:8443/" # --- 7. validate --------------------------------------------------------------------- say "7/7 validate" T "export LANG=C.UTF-8; bash /root/template-cleanup/validate-template.sh 2>/dev/null | grep -E 'active|signin|GITHASH|zero|c_order |ad_user_client|disk|/dev'" echo echo "done. Review above; new env keys appended blank: ${NEWKEYS:-none}. Reboot the template if the kernel changed (prod $(P uname -r) / tpl $(T uname -r))." echo "Then update TEMPLATE-README.md (build/date) and the memory note. Work dir: $WORK"