2 Commits
6 changed files with 582 additions and 1 deletions
@@ -0,0 +1 @@
{"schemaVersion":"nodedc.mission-core-map-access.v1","state":"enabled"}
@@ -0,0 +1,48 @@
#!/usr/bin/env python3
"""Deterministic data-only declaration for the registered NAS map access domain."""
import argparse
import gzip
import hashlib
import io
import json
import re
import tarfile
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
def build(patch, state="enabled"):
if not re.fullmatch(r"[A-Za-z0-9._-]{1,96}", patch) or state not in ("enabled", "disabled"):
raise ValueError("Invalid map access release")
descriptor = json.loads((ROOT / "deployment/mission-core-map-access/access.json").read_text())
if descriptor != {"schemaVersion": "nodedc.mission-core-map-access.v1", "state": "enabled"}:
raise ValueError("Unexpected source contract")
descriptor["state"] = state
members = {
"manifest.env": f"id={patch}\ncomponent=mission-core-map-access\ntype=app-overlay\n".encode(),
"files.txt": b"access.json\n",
"payload/access.json": (json.dumps(descriptor, sort_keys=True, separators=(",", ":")) + "\n").encode(),
}
result = io.BytesIO()
with gzip.GzipFile(fileobj=result, mode="wb", filename="", mtime=0) as compressed:
with tarfile.open(fileobj=compressed, mode="w", format=tarfile.USTAR_FORMAT) as archive:
for name, content in members.items():
member = tarfile.TarInfo(name); member.mode = 0o644; member.size = len(content)
archive.addfile(member, io.BytesIO(content))
return result.getvalue()
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument("patch")
parser.add_argument("--state", choices=("enabled", "disabled"), default="enabled")
args = parser.parse_args()
raw = build(args.patch, args.state)
target = ROOT / "infra/deploy-artifacts" / ("nodedc-" + args.patch + ".tgz")
target.parent.mkdir(parents=True, exist_ok=True)
with target.open("xb") as output:
output.write(raw)
digest = hashlib.sha256(raw).hexdigest()
target.with_suffix(target.suffix + ".sha256").write_text(digest + " " + target.name + "\n")
print(json.dumps({"artifact": str(target), "sha256": digest, "bytes": len(raw)}))
@@ -0,0 +1,126 @@
#!/usr/bin/env node
import { createHash } from "node:crypto";
import { spawnSync } from "node:child_process";
import { cp, lstat, mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { dirname, join, resolve } from "node:path";
import { fileURLToPath } from "node:url";
const scriptDir = dirname(fileURLToPath(import.meta.url));
const platformRoot = resolve(scriptDir, "../..");
const taskerRoot = resolve(
process.env.NODEDC_TASKMANAGER_ROOT || resolve(platformRoot, "../../data/dc_taskmanager/NODEDC_TASKMANAGER"),
);
const artifactDir = resolve(process.env.NODEDC_DEPLOY_ARTIFACT_DIR || resolve(scriptDir, "../deploy-artifacts"));
const [release = "20260829-001", ...extra] = process.argv.slice(2);
if (extra.length || release !== "20260829-001") {
throw new Error("usage: build-tasker-attachment-formats-artifact.mjs [20260829-001]");
}
const descriptor = {
artifactBasename: `nodedc-tasker-attachment-formats-${release}.tgz`,
component: "tasker",
expectedCommit: "f9308539bfe71e4e2359c47ed5c230fa1fb386c9",
files: [
"plane-src/apps/api/plane/settings/common.py",
"plane-src/apps/web/core/components/issues/attachment/attachment-list-item.tsx",
"plane-src/apps/web/core/components/issues/peek-overview/view.tsx",
"plane-src/apps/web/styles/globals.css",
"plane-src/packages/services/src/file/helper.ts",
],
patchId: `tasker-attachment-formats-${release}`,
sourceRoot: taskerRoot,
};
const sourceCommit = gitOutput(descriptor.sourceRoot, ["rev-parse", "HEAD"]);
if (sourceCommit !== descriptor.expectedCommit) {
throw new Error(`source_commit_mismatch:${descriptor.component}:${sourceCommit}`);
}
const sourceStatus = gitOutput(descriptor.sourceRoot, ["status", "--porcelain"]);
if (sourceStatus) throw new Error(`source_worktree_not_clean:${descriptor.component}`);
await mkdir(artifactDir, { recursive: true });
const stage = await mkdtemp(join(tmpdir(), "nodedc-tasker-attachment-formats-"));
const payload = join(stage, "payload");
const artifact = join(artifactDir, descriptor.artifactBasename);
try {
await assertFresh(artifact);
await mkdir(payload, { recursive: true });
for (const relativePath of descriptor.files) {
const source = resolve(descriptor.sourceRoot, relativePath);
const sourceStat = await lstat(source);
if (!sourceStat.isFile() || sourceStat.isSymbolicLink()) {
throw new Error(`source_file_rejected:${descriptor.component}:${relativePath}`);
}
const destination = join(payload, relativePath);
await mkdir(dirname(destination), { recursive: true });
await cp(source, destination, { force: false, verbatimSymlinks: true });
}
await writeFile(
join(stage, "manifest.env"),
`id=${descriptor.patchId}\ncomponent=${descriptor.component}\ntype=app-overlay\n`,
"utf8",
);
await writeFile(join(stage, "files.txt"), `${descriptor.files.join("\n")}\n`, "utf8");
const tar = spawnSync("python3", ["-c", canonicalTarScript(), artifact, stage], {
encoding: "utf8",
maxBuffer: 128 * 1024 * 1024,
});
if (tar.status !== 0) throw new Error(`tar_failed:${descriptor.component}:${tar.stderr || tar.stdout}`);
const sha256 = createHash("sha256").update(await readFile(artifact)).digest("hex");
console.log(
JSON.stringify(
{
ok: true,
release,
artifact,
component: descriptor.component,
files: descriptor.files,
patchId: descriptor.patchId,
sha256,
sourceCommit,
},
null,
2,
),
);
} finally {
await rm(stage, { recursive: true, force: true });
}
function gitOutput(cwd, args) {
const result = spawnSync("git", args, { cwd, encoding: "utf8" });
if (result.status !== 0) throw new Error(`git_failed:${cwd}:${args.join("_")}:${result.stderr || result.stdout}`);
return result.stdout.trim();
}
async function assertFresh(path) {
try {
await lstat(path);
} catch (error) {
if (error?.code === "ENOENT") return;
throw error;
}
throw new Error(`output_already_exists:${path}`);
}
function canonicalTarScript() {
return [
"import gzip,io,pathlib,sys,tarfile",
"root=pathlib.Path(sys.argv[2])",
"with open(sys.argv[1],'wb') as out:",
" with gzip.GzipFile(filename='',mode='wb',fileobj=out,compresslevel=9,mtime=0) as gz:",
" with tarfile.open(fileobj=gz,mode='w',format=tarfile.PAX_FORMAT) as tar:",
" for top in ('manifest.env','files.txt','payload'):",
" p=root/top; paths=[p]+(sorted(p.rglob('*')) if p.is_dir() else [])",
" for x in paths:",
" info=tar.gettarinfo(str(x),arcname=x.relative_to(root).as_posix())",
" info.uid=info.gid=0; info.uname=info.gname='root'; info.mtime=0; info.mode=0o755 if info.isdir() else 0o644",
" with (open(x,'rb') if info.isfile() else io.BytesIO()) as src: tar.addfile(info,src if info.isfile() else None)",
].join("\n");
}
+203 -1
View File
@@ -14,6 +14,7 @@ import pwd
import re
import secrets
import shutil
import signal
import socket
import sqlite3
import stat
@@ -2919,6 +2920,12 @@ GITEA_SALVAGE_UNSUPPORTED_SCHEMA_TABLES = tuple(
)
COMPONENTS = {
"mission-core-map-access": {
"payload_root": Path("/volume1/docker/nodedc-platform/mission-core-map-access"),
"bootstrap_root": True,
"artifact_only": True,
"services": (),
},
"engine": {
"payload_root": Path("/volume2/nodedc-demo"),
"compose_root": Path("/volume2/nodedc-demo"),
@@ -4894,6 +4901,11 @@ def allowed_payload_path(component, rel):
if reason:
die(f"path rejected: {rel}: {reason}")
if component == "mission-core-map-access":
if rel == "access.json":
return True
die("Mission Core Map Access accepts only access.json")
if component == "engine":
if rel in ("nodedc-source/package.json", "nodedc-source/package-lock.json"):
return True
@@ -11490,6 +11502,8 @@ def load_artifact(artifact, work_dir):
die(f"files.txt entry missing in payload: {rel}")
validate_payload_tree(manifest["component"], payload_dir, entries)
if manifest["component"] == "mission-core-map-access":
read_map_access_descriptor(payload_dir, entries)
if is_gitea_fresh_install_slice(manifest["component"], entries):
validate_gitea_fresh_install_payload(payload_dir, entries)
elif is_gitea_incident_salvage_slice(manifest["component"], entries):
@@ -31181,6 +31195,172 @@ def accept_engine_n8n_runtime(descriptor):
}
MAP_ACCESS_CONFIG = Path("/etc/ssh/sshd_config")
MAP_ACCESS_SSHD = Path("/usr/bin/sshd")
MAP_ACCESS_PID = Path("/var/run/sshd.pid")
MAP_ACCESS_BASE_SHA = "adf3da038acb21e30a1b79089f53917e7e82b469183889567b7d22f7b83163ba"
MAP_ACCESS_APPEND = (
b"\n# BEGIN NODEDC MISSION CORE MAP ACCESS v1\n"
b"Match User dctouch\n"
b" AllowTcpForwarding local\n"
b" PermitOpen 127.0.0.1:18103\n"
b"Match all\n"
b"# END NODEDC MISSION CORE MAP ACCESS v1\n"
)
def read_map_access_descriptor(root, entries):
if tuple(entries) != ("access.json",):
die("Map access file selection mismatch")
value = read_strict_json(root / "access.json", "Map access descriptor", max_bytes=1024)
if (set(value) != {"schemaVersion", "state"}
or value["schemaVersion"] != "nodedc.mission-core-map-access.v1"
or value["state"] not in ("enabled", "disabled")):
die("Map access descriptor mismatch")
return value["state"]
def map_access_policy(raw, desired):
base = raw[:-len(MAP_ACCESS_APPEND)] if raw.endswith(MAP_ACCESS_APPEND) else raw
if hashlib.sha256(base).hexdigest() != MAP_ACCESS_BASE_SHA:
die("Map access SSH policy predecessor drift")
return base + MAP_ACCESS_APPEND if desired == "enabled" else base
def map_access_read_config():
metadata = MAP_ACCESS_CONFIG.lstat()
if (not stat.S_ISREG(metadata.st_mode) or metadata.st_uid != 0
or metadata.st_mode & 0o022 or metadata.st_size > 65536):
die("Map access SSH configuration metadata rejected")
return MAP_ACCESS_CONFIG.read_bytes()
def map_access_effective(config, user, address):
result = subprocess.run(
[str(MAP_ACCESS_SSHD), "-T", "-f", str(config), "-C",
f"user={user},host=localhost,addr={address}"],
capture_output=True, text=True, timeout=10,
)
if result.returncode != 0:
die("Map access SSH effective-policy validation failed")
return dict(line.split(" ", 1) for line in result.stdout.splitlines() if " " in line)
def map_access_validate_candidate(before, candidate):
check = subprocess.run([str(MAP_ACCESS_SSHD), "-t", "-f", str(candidate)],
capture_output=True, timeout=10)
if check.returncode != 0:
die("Map access SSH syntax validation failed")
enabled = candidate.read_bytes().endswith(MAP_ACCESS_APPEND)
with tempfile.NamedTemporaryFile(dir=TMP_DIR) as original:
original.write(before); original.flush()
for address in ("127.0.0.1", "100.114.248.4"):
for user in ("dctouch", "root", "admin", "anonymous"):
old = map_access_effective(original.name, user, address)
new = map_access_effective(candidate, user, address)
if user == "dctouch":
old["allowtcpforwarding"] = "local" if enabled else "no"
old["permitopen"] = "127.0.0.1:18103" if enabled else "any"
if old != new:
die("Map access would change unrelated SSH authority")
def preflight_map_access(root, entries):
desired = read_map_access_descriptor(root, entries)
before = map_access_read_config()
after = map_access_policy(before, desired)
with tempfile.NamedTemporaryFile(dir=TMP_DIR) as candidate:
candidate.write(after); candidate.flush()
map_access_validate_candidate(before, Path(candidate.name))
return before, after
def map_access_replace(raw):
# Same-filesystem atomic replace; host policy is never accepted from an artifact.
original = MAP_ACCESS_CONFIG.stat()
fd, name = tempfile.mkstemp(prefix=".nodedc-map-", dir=MAP_ACCESS_CONFIG.parent)
try:
with os.fdopen(fd, "wb") as stream:
os.fchown(stream.fileno(), 0, original.st_gid)
os.fchmod(stream.fileno(), stat.S_IMODE(original.st_mode))
stream.write(raw); stream.flush(); os.fsync(stream.fileno())
os.replace(name, MAP_ACCESS_CONFIG)
directory_fd = os.open(str(MAP_ACCESS_CONFIG.parent), os.O_RDONLY)
try:
os.fsync(directory_fd)
finally:
os.close(directory_fd)
finally:
if os.path.exists(name):
os.unlink(name)
def map_access_reload():
metadata = MAP_ACCESS_PID.lstat()
if not stat.S_ISREG(metadata.st_mode) or metadata.st_uid != 0 or metadata.st_mode & 0o022:
die("Map access SSH PID metadata rejected")
pid = int(MAP_ACCESS_PID.read_text().strip())
if pid <= 1 or Path(f"/proc/{pid}/exe").resolve() != MAP_ACCESS_SSHD.resolve():
die("Map access SSH master identity mismatch")
os.kill(pid, signal.SIGHUP)
deadline = time.monotonic() + 10
while time.monotonic() < deadline:
try:
with socket.create_connection(("127.0.0.1", 22), timeout=2) as connection:
connection.settimeout(2)
if connection.recv(255).startswith(b"SSH-2.0-"):
return
except OSError:
pass
time.sleep(0.25)
die("Map access SSH listener did not recover")
def backup_map_access(backup_dir):
raw = map_access_read_config()
map_access_policy(raw, "enabled")
path = backup_dir / "sshd-config-before"
with path.open("xb") as output:
os.fchmod(output.fileno(), 0o600)
output.write(raw); output.flush(); os.fsync(output.fileno())
def apply_map_access(root, entries):
before, after = preflight_map_access(root, entries)
if after != before:
map_access_replace(after)
map_access_reload()
def accept_map_access(root, entries):
before, after = preflight_map_access(root, entries)
if before != after:
die("Map access installed policy differs from the requested state")
healthcheck_url({
"url": "http://127.0.0.1:18103/healthz",
"headers": {"x-nodedc-user-id": "mission-core-loopback-operator"},
"expected_json": {"ok": True, "service": "nodedc-map-gateway"},
})
def rollback_map_access(root, backup_dir, entries, current_stamp):
original = backup_dir / "sshd-config-before"
metadata = original.lstat()
if not stat.S_ISREG(metadata.st_mode) or metadata.st_uid != 0 or metadata.st_mode & 0o077:
die("Map access backup metadata rejected")
raw = original.read_bytes()
map_access_policy(raw, "enabled")
current = map_access_read_config()
map_access_policy(current, "enabled")
map_access_validate_candidate(current, original)
if raw != current:
map_access_replace(raw)
map_access_reload()
restore_overlay_source(root, backup_dir, entries, current_stamp)
if map_access_read_config() != raw:
die("Map access rollback policy verification failed")
def plan_artifact(artifact):
validate_artifact_location(artifact)
ensure_layout()
@@ -31237,6 +31417,8 @@ def plan_artifact(artifact):
validate_gitea_compose_schema(payload_dir / GITEA_COMPOSE_REL)
if is_gitea_incident_salvage_slice(manifest["component"], entries):
validate_gitea_compose_schema(payload_dir / GITEA_COMPOSE_REL)
if manifest["component"] == "mission-core-map-access":
preflight_map_access(payload_dir, entries)
reject_failed_artifact_replay(manifest, sha)
reject_terminal_engine_l2_failed_artifact(manifest, sha)
reject_terminal_device_plane_foundation_artifact(manifest, sha)
@@ -31820,6 +32002,10 @@ def plan_artifact(artifact):
print(f"component={component}")
print(f"type={manifest['type']}")
print(f"payload_root={root}")
if component == "mission-core-map-access":
print("access=local-forward:dctouch:127.0.0.1:18103")
print("runtime=sshd-scoped-reload;docker=untouched;cache=preserved")
print("policy_predecessor=exact;rollback=automatic")
print(f"compose_root={compose_root}")
compose_project = component_compose_project(component)
if compose_project:
@@ -37346,6 +37532,9 @@ process.stdout.write('engine-l2-closed-loop:0.7.0:cas+safe-profile+external-plan
def run_healthchecks(component, entries=None, services=None):
if component == "mission-core-map-access":
accept_map_access(component_root(component), entries)
return
if is_gitea_fresh_install_slice(component, entries):
if tuple(services or ()) != (GITEA_SERVICE,):
die("Gitea fresh-install service set mismatch")
@@ -38486,6 +38675,8 @@ def apply_artifact(artifact):
compose_root = component_compose_root(component)
bootstrap_root = bool(COMPONENTS[component].get("bootstrap_root"))
services = component_services(component, entries)
if component == "mission-core-map-access":
preflight_map_access(payload_dir, entries)
artifact_only = component_artifact_only(component)
defer_bootstrap_root = is_gitea_bootstrap_slice(
component,
@@ -39011,6 +39202,8 @@ def apply_artifact(artifact):
include_nginx_html = component == "engine" and component_publish_dist(component, entries)
create_backup(root, backup_dir, entries, include_nginx_html)
if component == "mission-core-map-access":
backup_map_access(backup_dir)
if component == "device-plane":
if device_plane_runtime_before is None:
die("Device Plane pre-apply runtime inventory is missing")
@@ -39077,6 +39270,9 @@ def apply_artifact(artifact):
for rel in entries:
seal_n8n_private_extension_release(root, rel)
if component == "mission-core-map-access":
apply_map_access(root, entries)
if component_publish_dist(component, entries):
publish_engine_dist(root, current_stamp)
@@ -39194,7 +39390,13 @@ def apply_artifact(artifact):
and backup_dir is not None
and root is not None
):
if is_gitea_fresh_install_slice(component, entries):
if component == "mission-core-map-access":
try:
rollback_map_access(root, backup_dir, entries, current_stamp)
rollback_status = "ok:mission-core-map-access"
except Exception as rollback_exc:
rollback_status = f"failed:{type(rollback_exc).__name__}"
elif is_gitea_fresh_install_slice(component, entries):
try:
restored_action = rollback_gitea_fresh_install(
root,
@@ -0,0 +1,95 @@
import hashlib
import importlib.machinery
import importlib.util
import io
import json
import stat
from types import SimpleNamespace
import tarfile
import tempfile
import unittest
from pathlib import Path
from unittest.mock import patch
ROOT = Path(__file__).resolve().parent
def load(name, path):
loader = importlib.machinery.SourceFileLoader(name, str(path))
spec = importlib.util.spec_from_loader(name, loader)
module = importlib.util.module_from_spec(spec); loader.exec_module(module)
return module
R = load("map_access_runner", ROOT / "nodedc-deploy")
B = load("map_access_builder", ROOT / "build-mission-core-map-access-artifact.py")
class MapAccessTests(unittest.TestCase):
def test_scope_never_selects_docker_or_accepts_host_policy_payload(self):
name = "mission-core-map-access"
self.assertEqual(R.component_services(name, ["access.json"]), ())
self.assertEqual(R.component_builds(name, ["access.json"]), ())
self.assertTrue(R.component_artifact_only(name))
self.assertTrue(R.allowed_payload_path(name, "access.json"))
for path in ("sshd_config", "../sshd_config", "script.py", "docker-compose.yml", ".env", "secrets/key"):
with self.assertRaises(R.DeployError): R.allowed_payload_path(name, path)
def test_deterministic_data_bundle_and_fail_closed_descriptor(self):
raw = B.build("map-access-test-001")
self.assertEqual(raw, B.build("map-access-test-001"))
with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp)
artifact = root / "candidate.tgz"
artifact.write_bytes(raw)
with tarfile.open(fileobj=io.BytesIO(raw)) as archive:
self.assertEqual(archive.getnames(), ["manifest.env", "files.txt", "payload/access.json"])
unpacked = root / "unpacked"
unpacked.mkdir()
manifest, entries, payload = R.load_artifact(artifact, unpacked)
self.assertEqual(manifest["component"], "mission-core-map-access")
self.assertEqual(R.read_map_access_descriptor(payload, entries), "enabled")
for value in ({"schemaVersion":"nodedc.mission-core-map-access.v1","state":"enabled","user":"root"}, {"schemaVersion":"unknown","state":"enabled"}):
(payload/"access.json").write_text(json.dumps(value))
with self.assertRaises(R.DeployError): R.read_map_access_descriptor(payload, entries)
def test_exact_predecessor_idempotence_and_explicit_disable(self):
base = b"AllowTcpForwarding no\n"
with patch.object(R, "MAP_ACCESS_BASE_SHA", hashlib.sha256(base).hexdigest()):
enabled = R.map_access_policy(base, "enabled")
self.assertEqual(enabled, base + R.MAP_ACCESS_APPEND)
self.assertEqual(R.map_access_policy(enabled, "enabled"), enabled)
self.assertEqual(R.map_access_policy(enabled, "disabled"), base)
for drift in (base+b"# external drift\n", enabled+b"# external drift\n", enabled+R.MAP_ACCESS_APPEND):
with self.assertRaises(R.DeployError): R.map_access_policy(drift, "enabled")
def test_effective_policy_may_only_change_exact_user_and_destination(self):
with tempfile.TemporaryDirectory() as tmp, patch.object(R, "TMP_DIR", Path(tmp)), patch.object(R.subprocess,"run") as run:
run.return_value.returncode=0
candidate=Path(tmp)/"candidate";candidate.write_bytes(R.MAP_ACCESS_APPEND)
baseline={"allowtcpforwarding":"no", "permitopen":"any", "passwordauthentication":"yes"}
def effective(config,user,address):
value=dict(baseline)
if str(config)==str(candidate) and user=="dctouch":value.update(allowtcpforwarding="local",permitopen="127.0.0.1:18103")
return value
with patch.object(R,"map_access_effective",side_effect=effective):R.map_access_validate_candidate(b"baseline",candidate)
def broad(config,user,address):
value=effective(config,user,address)
if str(config)==str(candidate):value["permitopen"]="any"
return value
with patch.object(R,"map_access_effective",side_effect=broad), self.assertRaises(R.DeployError):R.map_access_validate_candidate(b"baseline",candidate)
def test_reload_failure_is_routed_to_domain_rollback(self):
with tempfile.TemporaryDirectory() as tmp, patch.object(R,"preflight_map_access",return_value=(b"old",b"new")), patch.object(R,"map_access_replace") as replace, patch.object(R,"map_access_reload",side_effect=R.DeployError("reload")):
with self.assertRaises(R.DeployError):R.apply_map_access(Path(tmp),["access.json"])
replace.assert_called_once_with(b"new")
def test_rollback_restores_exact_policy_and_descriptor(self):
base=b"AllowTcpForwarding no\n"
with tempfile.TemporaryDirectory() as tmp:
root=Path(tmp); backup=root/"backup"; backup.mkdir()
(backup/"sshd-config-before").write_bytes(base)
calls=[]
metadata=SimpleNamespace(st_mode=stat.S_IFREG|0o600,st_uid=0)
with patch.object(R,"MAP_ACCESS_BASE_SHA",hashlib.sha256(base).hexdigest()), patch.object(Path,"lstat",return_value=metadata), patch.object(R,"map_access_read_config",side_effect=[base+R.MAP_ACCESS_APPEND,base]), patch.object(R,"map_access_validate_candidate"), patch.object(R,"map_access_replace",side_effect=lambda value:calls.append(("replace",value))), patch.object(R,"map_access_reload",side_effect=lambda:calls.append(("reload",))), patch.object(R,"restore_overlay_source",side_effect=lambda *args:calls.append(("restore",))):
R.rollback_map_access(root,backup,["access.json"],"stamp")
self.assertEqual(calls,[("replace",base),("reload",),("restore",)])
if __name__ == "__main__": unittest.main()
@@ -0,0 +1,109 @@
#!/usr/bin/env python3
import hashlib
import importlib.machinery
import importlib.util
import json
import os
import subprocess
import tempfile
import unittest
from pathlib import Path
SCRIPT_DIR = Path(__file__).resolve().parent
PLATFORM_ROOT = SCRIPT_DIR.parent.parent
BUILDER_PATH = SCRIPT_DIR / "build-tasker-attachment-formats-artifact.mjs"
RUNNER_PATH = SCRIPT_DIR / "nodedc-deploy"
RELEASE = "20260829-001"
SOURCE_COMMIT = "f9308539bfe71e4e2359c47ed5c230fa1fb386c9"
EXPECTED_FILES = {
"plane-src/apps/api/plane/settings/common.py",
"plane-src/apps/web/core/components/issues/attachment/attachment-list-item.tsx",
"plane-src/apps/web/core/components/issues/peek-overview/view.tsx",
"plane-src/apps/web/styles/globals.css",
"plane-src/packages/services/src/file/helper.ts",
}
def load_runner():
loader = importlib.machinery.SourceFileLoader("nodedc_tasker_attachment_formats_runner", str(RUNNER_PATH))
spec = importlib.util.spec_from_loader(loader.name, loader)
module = importlib.util.module_from_spec(spec)
loader.exec_module(module)
return module
RUNNER = load_runner()
class TaskerAttachmentFormatsArtifactTest(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.temporary = tempfile.TemporaryDirectory(prefix="nodedc-tasker-attachment-formats-")
cls.root = Path(cls.temporary.name)
cls.builds = []
for index in range(2):
output = cls.root / f"build-{index}"
output.mkdir()
env = os.environ.copy()
env["NODEDC_DEPLOY_ARTIFACT_DIR"] = str(output)
result = subprocess.run(
["node", str(BUILDER_PATH), RELEASE],
cwd=PLATFORM_ROOT,
env=env,
check=True,
capture_output=True,
text=True,
)
cls.builds.append(json.loads(result.stdout))
@classmethod
def tearDownClass(cls):
cls.temporary.cleanup()
def test_artifact_is_reproducible_and_runner_accepted(self):
first = self.builds[0]
second = self.builds[1]
first_path = Path(first["artifact"])
second_path = Path(second["artifact"])
first_bytes = first_path.read_bytes()
self.assertEqual(first_bytes, second_path.read_bytes())
self.assertEqual(first_bytes[4:8], bytes(4))
self.assertEqual(first["sha256"], hashlib.sha256(first_bytes).hexdigest())
self.assertEqual(first["sourceCommit"], SOURCE_COMMIT)
self.assertEqual(first["patchId"], f"tasker-attachment-formats-{RELEASE}")
self.assertEqual(set(first["files"]), EXPECTED_FILES)
with tempfile.TemporaryDirectory() as directory:
manifest, entries, payload = RUNNER.load_artifact(first_path, Path(directory))
self.assertEqual(manifest, {
"id": first["patchId"],
"component": "tasker",
"type": "app-overlay",
})
self.assertEqual(entries, first["files"])
for relative_path in entries:
self.assertTrue((payload / relative_path).is_file())
def test_runner_selects_only_registered_tasker_scope(self):
entries = self.builds[0]["files"]
self.assertEqual(RUNNER.component_services("tasker", entries), ("api", "worker", "beat-worker", "web"))
self.assertEqual(len(RUNNER.component_builds("tasker", entries)), 2)
def test_builder_rejects_an_unexpected_release(self):
env = os.environ.copy()
env["NODEDC_DEPLOY_ARTIFACT_DIR"] = str(self.root / "unexpected-release")
result = subprocess.run(
["node", str(BUILDER_PATH), "20260829-002"],
cwd=PLATFORM_ROOT,
env=env,
capture_output=True,
text=True,
)
self.assertNotEqual(result.returncode, 0)
self.assertIn("usage: build-tasker-attachment-formats-artifact.mjs", result.stderr)
if __name__ == "__main__":
unittest.main(verbosity=2)