fix(hacs): keep exactly one *manifest.json in the tree
Validate / golden (push) Failing after 6m50s
Validate / hacs (push) Failing after 11s
Validate / hassfest (push) Failing after 14s
Validate / frontend (push) Successful in 8m18s
Validate / backend (push) Failing after 10m34s
Validate / smoke (push) Failing after 24m45s
Validate / performance_smoke (push) Failing after 9m42s
Full Performance / performance (push) Failing after 54m31s

The HACS submission check does not read hacs.json to find the integration: it
globs `*manifest.json` over the whole clone of the default branch and exits 1
unless there is exactly one (hacs/default, scripts/helpers/integration_path.py).
Three files matched — the two stand-only integrations added on 2026-07-31 and
the golden baseline index added on 2026-08-11 — so the Hassfest job of PR #9004
went red five weeks into the review queue, with a log that named no file.

The stand manifests ship as manifest.template.json and demo/stand/install.sh
renames them at install time; the golden index becomes baselines-index.json
(the exported constant keeps its name, so no consumer changes).
test/repo-hygiene.test.mjs fails if a second manifest ever appears, and the
existing golden-policy assertion — which compared against 'manifest.json' and
happily passed on 'baseline-manifest.json' — now checks the suffix.
This commit is contained in:
Matysh
2026-08-11 14:03:38 +03:00
parent 02373bb31f
commit 9419842333
8 changed files with 83 additions and 9 deletions
+4 -1
View File
@@ -1,4 +1,7 @@
export const GOLDEN_BASELINE_MANIFEST = 'baseline-manifest.json';
// The name must NOT end with `manifest.json`: the HACS submission check globs
// `*manifest.json` over the whole clone of the default branch and refuses a
// repository with more than one match (test/repo-hygiene.test.mjs).
export const GOLDEN_BASELINE_MANIFEST = 'baselines-index.json';
export const assertGoldenInvocation = (mode, scenarioFilter = '') => {
if (!['capture', 'verify'].includes(mode)) throw new Error(`unknown golden mode: ${mode}`);
+19 -6
View File
@@ -4,10 +4,10 @@ Things that live on the public stand (demo.houseplan.tech) but are not part
of the shipped integration.
- `demo_robot/` — the scripted robot vacuum (docs/VACUUM.md, "demo stand gets
a scripted synthetic robot"). Deployed by copying the folder to
`custom_components/demo_robot` in both stand seeds and adding `demo_robot:`
to configuration.yaml; the dev stand picks it up automatically from this
path (`/opt/hp/bin/hp-update-dev.sh`). The rest of the stand-only config
a scripted synthetic robot"). Deployed with `./install.sh <ha-config-dir>`
into both stand seeds, plus `demo_robot:` in configuration.yaml; the dev
stand picks it up automatically from this path
(`/opt/hp/bin/hp-update-dev.sh`, which calls the same script). The rest of the stand-only config
(template LQI sensors, alarm helpers, the smoke automation) lives in the
seeds on the stand host — see `docs/TESTING-DEMO.md` and the memory note
`houseplan-demo-stand`.
@@ -16,8 +16,8 @@ of the shipped integration.
administrator (the card editor is gated on `is_admin`) and kept restarting
HA from the UI, which looked like the stand crashing between hourly resets.
The component re-registers `homeassistant.restart`/`homeassistant.stop` as
no-ops after startup. Deployed to `custom_components/demo_guard` in
seed-demo only (the dev stand sits behind basic auth) + `demo_guard:` in
no-ops after startup. Deployed by the same `install.sh` into seed-demo only
(the dev stand sits behind basic auth) + `demo_guard:` in
configuration.yaml.
- `www/stand-reset-timer.js` — console-only countdown to the next hourly
@@ -27,3 +27,16 @@ of the shipped integration.
switching to `console.warn` for the last 5 minutes. `www/stand-dev-info.js`
is the dev-stand counterpart: a single `console.info` saying the dev stand
only resets on deploy.
## Why the manifests are templates
Both components ship their manifest as `manifest.template.json`, and
`install.sh` renames it to `manifest.json` on the stand.
The reason is the HACS submission check: `hacs/default` validates a repository
by globbing `*manifest.json` over the whole clone of the DEFAULT branch and
refuses anything that does not have exactly one
(`scripts/helpers/integration_path.py` — "No manifest", exit 1). Two stand-only
manifests turned the Hassfest job of PR #9004 red on 2026-08-11, five weeks into
the review queue. `test/repo-hygiene.test.mjs` now fails if a second one
appears, so this cannot be rediscovered by a reviewer again.
+23
View File
@@ -0,0 +1,23 @@
#!/usr/bin/env sh
# Install the stand-only integrations into a Home Assistant config directory.
#
# ./install.sh /mnt/data/supervisor/homeassistant
#
# The manifests are stored as `manifest.template.json` on purpose: the HACS
# submission check globs `*manifest.json` over the whole clone of the default
# branch and refuses a repository with more than one of them
# (hacs/default, scripts/helpers/integration_path.py). See README.md.
set -eu
target="${1:?usage: install.sh <path to the Home Assistant config directory>}"
here="$(cd "$(dirname "$0")" && pwd)"
for comp in demo_robot demo_guard; do
src="$here/$comp"
[ -d "$src" ] || { echo "missing $src" >&2; exit 1; }
dst="$target/custom_components/$comp"
mkdir -p "$dst"
cp -a "$src/." "$dst/"
mv -f "$dst/manifest.template.json" "$dst/manifest.json"
echo "installed $comp -> $dst"
done
+6 -2
View File
@@ -7,8 +7,12 @@ import {
} from '../demo/golden/policy.mjs';
test('golden metadata cannot be mistaken for a Home Assistant integration manifest', () => {
assert.equal(GOLDEN_BASELINE_MANIFEST, 'baseline-manifest.json');
assert.notEqual(GOLDEN_BASELINE_MANIFEST, 'manifest.json');
assert.equal(GOLDEN_BASELINE_MANIFEST, 'baselines-index.json');
// The old check compared against 'manifest.json' and passed while the file
// was called 'baseline-manifest.json' — which is exactly what the HACS glob
// `*manifest.json` matches, and what turned Hassfest red on PR #9004
// (2026-08-11). Ending with the word is enough to break the submission.
assert.equal(GOLDEN_BASELINE_MANIFEST.endsWith('manifest.json'), false);
});
test('golden capture fails on runtime errors but permits missing baselines', () => {
+31
View File
@@ -0,0 +1,31 @@
import assert from 'node:assert/strict';
import { readdirSync, statSync } from 'node:fs';
import { join, relative, sep } from 'node:path';
import { fileURLToPath } from 'node:url';
import test from 'node:test';
const ROOT = fileURLToPath(new URL('..', import.meta.url));
const SKIP = new Set(['.git', 'node_modules', 'dist', 'coverage', '.venv', '__pycache__', '.pytest_cache']);
const walk = (dir, out = []) => {
for (const name of readdirSync(dir)) {
if (SKIP.has(name)) continue;
const full = join(dir, name);
if (statSync(full).isDirectory()) walk(full, out);
else out.push(relative(ROOT, full).split(sep).join('/'));
}
return out;
};
// The HACS submission check does not read hacs.json to find the integration: it
// globs `*manifest.json` over the whole clone of the DEFAULT branch and exits 1
// unless there is exactly one (hacs/default, scripts/helpers/integration_path.py
// -> "No manifest"). Two stand-only manifests under demo/stand turned the
// Hassfest job of PR #9004 red on 2026-08-11, five weeks into the review queue,
// and the failure said nothing about which file was to blame. Anything that
// needs a second manifest ships it as `manifest.template.json` and renames it at
// install time — see demo/stand/install.sh.
test('the tree carries exactly one *manifest.json: HACS rejects a repository with two', () => {
const found = walk(ROOT).filter((path) => path.split('/').pop().endsWith('manifest.json'));
assert.deepEqual(found, ['custom_components/houseplan/manifest.json']);
});