Synchronize public module directory publication
This commit is contained in:
@@ -186,6 +186,15 @@ the current workspace. Each entry binds its Python wheel and optional WebUI
|
|||||||
tarball to the registry URL, filename, size, SHA-256, package identity, source
|
tarball to the registry URL, filename, size, SHA-256, package identity, source
|
||||||
tag, and source commit before the complete catalog is signed.
|
tag, and source commit before the complete catalog is signed.
|
||||||
|
|
||||||
|
The same publication transaction regenerates and prunes the browsable static
|
||||||
|
directory under `public/catalogs/v1/modules/`. It writes a global
|
||||||
|
`modules/index.json`, one `<module>/index.json`, and one
|
||||||
|
`<module>/<version>/manifest.json` for every entry in the signed channel.
|
||||||
|
These files are derived from that exact signed payload and keyring; stale JSON
|
||||||
|
from an older partial catalog is removed while unrelated static assets are left
|
||||||
|
untouched. The signed channel remains the trust anchor, while the module
|
||||||
|
directory provides stable discovery URLs for browsers and external tooling.
|
||||||
|
|
||||||
Official GovOPlaN modules are open-source directory entries and do not require
|
Official GovOPlaN modules are open-source directory entries and do not require
|
||||||
license entitlements. The generic `license_features` contract remains available
|
license entitlements. The generic `license_features` contract remains available
|
||||||
for third-party package directories, support/configuration packages, or future
|
for third-party package directories, support/configuration packages, or future
|
||||||
@@ -201,6 +210,13 @@ cache. A saved plan is rejected if any package ref, artifact identity, catalog
|
|||||||
channel, sequence, or signing-key identity differs from the currently validated
|
channel, sequence, or signing-key identity differs from the currently validated
|
||||||
catalog.
|
catalog.
|
||||||
|
|
||||||
|
The Admin directory can be searched by module, package, repository, or tag and
|
||||||
|
filtered by available, installed, update, and blocked/withdrawn states. It
|
||||||
|
shows the source revision, artifact digest, release notes, and configuration
|
||||||
|
requirements. Missing dependency/interface providers and unsupported update
|
||||||
|
windows are surfaced before an operator adds the entry to a plan; installer
|
||||||
|
preflight remains authoritative.
|
||||||
|
|
||||||
Package lifecycle and availability are intentionally separate:
|
Package lifecycle and availability are intentionally separate:
|
||||||
|
|
||||||
- install, update, and uninstall change the instance-wide package composition;
|
- install, update, and uninstall change the instance-wide package composition;
|
||||||
|
|||||||
@@ -172,8 +172,10 @@ Implementation status as of the current source tree:
|
|||||||
after durable administrator access is established.
|
after durable administrator access is established.
|
||||||
3. **Read-only online module directory (implemented foundation).** Admin falls
|
3. **Read-only online module directory (implemented foundation).** Admin falls
|
||||||
back to the signed public stable directory, presents installed/update state,
|
back to the signed public stable directory, presents installed/update state,
|
||||||
compatibility and provenance, and retains operator-configured catalogs as an
|
searchable availability/blocker filters, immutable source/artifact
|
||||||
explicit override.
|
provenance, configuration requirements, release notes, and technical
|
||||||
|
compatibility. Withdrawn releases remain visible but cannot be planned.
|
||||||
|
Operator-configured catalogs remain an explicit override.
|
||||||
4. **Durable module plan and install (implemented local boundary).** Catalog
|
4. **Durable module plan and install (implemented local boundary).** Catalog
|
||||||
selection creates a reviewed plan; the installer queue, lock, preflight,
|
selection creates a reviewed plan; the installer queue, lock, preflight,
|
||||||
maintenance gate, digest-verified artifact cache, rollback drill, and run
|
maintenance gate, digest-verified artifact cache, rollback drill, and run
|
||||||
|
|||||||
@@ -103,6 +103,13 @@ class ReleaseEntrypointGateTests(unittest.TestCase):
|
|||||||
|
|
||||||
self.assertLess(gate, write)
|
self.assertLess(gate, write)
|
||||||
|
|
||||||
|
def test_full_catalog_publication_synchronizes_browsable_module_directory(self) -> None:
|
||||||
|
publisher = (META_ROOT / "tools" / "release" / "publish-release-catalog.sh").read_text()
|
||||||
|
|
||||||
|
self.assertIn('--module-directory-output "$WEB_ROOT/public/catalogs/v1"', publisher)
|
||||||
|
self.assertIn('git -C "$WEB_ROOT" add -A', publisher)
|
||||||
|
self.assertIn('"$MODULE_DIRECTORY_PATH"', publisher)
|
||||||
|
|
||||||
def test_candidate_publication_uses_existing_keyring_as_trust_anchor(self) -> None:
|
def test_candidate_publication_uses_existing_keyring_as_trust_anchor(self) -> None:
|
||||||
publisher = (META_ROOT / "tools" / "release" / "govoplan_release" / "publisher.py").read_text()
|
publisher = (META_ROOT / "tools" / "release" / "govoplan_release" / "publisher.py").read_text()
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import sys
|
import sys
|
||||||
|
import tempfile
|
||||||
import unittest
|
import unittest
|
||||||
from unittest import mock
|
from unittest import mock
|
||||||
|
|
||||||
@@ -14,6 +16,7 @@ if str(RELEASE_TOOLS_ROOT) not in sys.path:
|
|||||||
from govoplan_release.module_directory import ( # noqa: E402
|
from govoplan_release.module_directory import ( # noqa: E402
|
||||||
module_directory_payloads,
|
module_directory_payloads,
|
||||||
safe_path_part,
|
safe_path_part,
|
||||||
|
write_module_directory,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -77,6 +80,78 @@ class ReleaseModuleDirectoryTests(unittest.TestCase):
|
|||||||
catalog_payload={}, keyring_payload={}, channel="../stable"
|
catalog_payload={}, keyring_payload={}, channel="../stable"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def test_prune_removes_stale_json_but_keeps_unrelated_assets(self) -> None:
|
||||||
|
catalog = {
|
||||||
|
"generated_at": "2026-08-06T12:00:00Z",
|
||||||
|
"sequence": 8,
|
||||||
|
"modules": [
|
||||||
|
{
|
||||||
|
"module_id": "files",
|
||||||
|
"name": "Files",
|
||||||
|
"version": "1.2.3",
|
||||||
|
"python_package": "govoplan-files",
|
||||||
|
"source": {
|
||||||
|
"repository": "govoplan-files",
|
||||||
|
"tag": "v1.2.3",
|
||||||
|
"commit": "a" * 40,
|
||||||
|
},
|
||||||
|
"artifact_integrity": {
|
||||||
|
"python": {"sha256": "b" * 64},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}
|
||||||
|
with tempfile.TemporaryDirectory() as value:
|
||||||
|
output_root = Path(value)
|
||||||
|
stale = output_root / "modules" / "legacy" / "0.1.0" / "manifest.json"
|
||||||
|
stale.parent.mkdir(parents=True)
|
||||||
|
stale.write_text("{}\n", encoding="utf-8")
|
||||||
|
unrelated = output_root / "modules" / "README.txt"
|
||||||
|
unrelated.write_text("keep\n", encoding="utf-8")
|
||||||
|
|
||||||
|
written = write_module_directory(
|
||||||
|
catalog_payload=catalog,
|
||||||
|
keyring_payload={},
|
||||||
|
output_root=output_root,
|
||||||
|
channel="stable",
|
||||||
|
prune=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertFalse(stale.exists())
|
||||||
|
self.assertTrue(unrelated.exists())
|
||||||
|
self.assertEqual(3, len(written))
|
||||||
|
manifest = json.loads(
|
||||||
|
(output_root / "modules" / "files" / "1.2.3" / "manifest.json").read_text()
|
||||||
|
)
|
||||||
|
self.assertEqual("govoplan-files", manifest["module"]["repo"])
|
||||||
|
self.assertEqual("v1.2.3", manifest["module"]["python_tag"])
|
||||||
|
self.assertEqual("b" * 64, manifest["module"]["artifact_integrity"]["python"]["sha256"])
|
||||||
|
|
||||||
|
def test_writer_refuses_nested_symlink_targets(self) -> None:
|
||||||
|
catalog = {
|
||||||
|
"modules": [{"module_id": "files", "version": "1.2.3"}],
|
||||||
|
}
|
||||||
|
with tempfile.TemporaryDirectory() as value:
|
||||||
|
root = Path(value)
|
||||||
|
output_root = root / "public"
|
||||||
|
external = root / "external"
|
||||||
|
(output_root / "modules").mkdir(parents=True)
|
||||||
|
external.mkdir()
|
||||||
|
(output_root / "modules" / "files").symlink_to(
|
||||||
|
external,
|
||||||
|
target_is_directory=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
with self.assertRaisesRegex(ValueError, "symlinks"):
|
||||||
|
write_module_directory(
|
||||||
|
catalog_payload=catalog,
|
||||||
|
keyring_payload={},
|
||||||
|
output_root=output_root,
|
||||||
|
channel="stable",
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual([], list(external.iterdir()))
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ from govoplan_release.catalog_entry_synthesis import ( # noqa: E402
|
|||||||
synthesize_repository_catalog_entries,
|
synthesize_repository_catalog_entries,
|
||||||
validate_initial_entry_closure,
|
validate_initial_entry_closure,
|
||||||
)
|
)
|
||||||
|
from govoplan_release.module_directory import write_module_directory # noqa: E402
|
||||||
|
|
||||||
|
|
||||||
SHA256 = re.compile(r"^[0-9a-f]{64}$")
|
SHA256 = re.compile(r"^[0-9a-f]{64}$")
|
||||||
@@ -44,6 +45,11 @@ def main() -> int:
|
|||||||
parser.add_argument("--expires-days", type=int, default=90)
|
parser.add_argument("--expires-days", type=int, default=90)
|
||||||
parser.add_argument("--catalog-output", type=Path, required=True)
|
parser.add_argument("--catalog-output", type=Path, required=True)
|
||||||
parser.add_argument("--keyring-output", type=Path)
|
parser.add_argument("--keyring-output", type=Path)
|
||||||
|
parser.add_argument(
|
||||||
|
"--module-directory-output",
|
||||||
|
type=Path,
|
||||||
|
help="Catalog v1 root under which the browsable modules directory is synchronized.",
|
||||||
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--catalog-signing-key",
|
"--catalog-signing-key",
|
||||||
action="append",
|
action="append",
|
||||||
@@ -84,17 +90,32 @@ def main() -> int:
|
|||||||
output.parent.mkdir(parents=True, exist_ok=True)
|
output.parent.mkdir(parents=True, exist_ok=True)
|
||||||
output.write_text(json.dumps(catalog, indent=2, sort_keys=True) + "\n", encoding="utf-8")
|
output.write_text(json.dumps(catalog, indent=2, sort_keys=True) + "\n", encoding="utf-8")
|
||||||
|
|
||||||
|
keyring = _keyring(signing_keys=signing_keys, generated_at=generated_at)
|
||||||
if args.keyring_output is not None:
|
if args.keyring_output is not None:
|
||||||
keyring_output = args.keyring_output.expanduser()
|
keyring_output = args.keyring_output.expanduser()
|
||||||
keyring_output.parent.mkdir(parents=True, exist_ok=True)
|
keyring_output.parent.mkdir(parents=True, exist_ok=True)
|
||||||
keyring_output.write_text(
|
keyring_output.write_text(
|
||||||
json.dumps(_keyring(signing_keys=signing_keys, generated_at=generated_at), indent=2, sort_keys=True) + "\n",
|
json.dumps(keyring, indent=2, sort_keys=True) + "\n",
|
||||||
encoding="utf-8",
|
encoding="utf-8",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
module_directory_files: tuple[Path, ...] = ()
|
||||||
|
if args.module_directory_output is not None:
|
||||||
|
module_directory_files = write_module_directory(
|
||||||
|
catalog_payload=catalog,
|
||||||
|
keyring_payload=keyring,
|
||||||
|
output_root=args.module_directory_output.expanduser(),
|
||||||
|
channel=args.channel,
|
||||||
|
public_base_url=args.public_base_url,
|
||||||
|
prune=True,
|
||||||
|
)
|
||||||
|
|
||||||
print(f"catalog={output}")
|
print(f"catalog={output}")
|
||||||
if args.keyring_output is not None:
|
if args.keyring_output is not None:
|
||||||
print(f"keyring={args.keyring_output.expanduser()}")
|
print(f"keyring={args.keyring_output.expanduser()}")
|
||||||
|
if args.module_directory_output is not None:
|
||||||
|
print(f"module_directory={args.module_directory_output.expanduser()}")
|
||||||
|
print(f"module_directory_files={len(module_directory_files)}")
|
||||||
print(f"channel={args.channel}")
|
print(f"channel={args.channel}")
|
||||||
print(f"sequence={sequence}")
|
print(f"sequence={sequence}")
|
||||||
print(f"version={version}")
|
print(f"version={version}")
|
||||||
@@ -164,11 +185,17 @@ def _catalog_payload(
|
|||||||
for entry in entries:
|
for entry in entries:
|
||||||
python_ref = _python_ref(name, python_artifact)
|
python_ref = _python_ref(name, python_artifact)
|
||||||
entry["python_ref"] = python_ref
|
entry["python_ref"] = python_ref
|
||||||
|
repository_url = f"https://git.add-ideas.de/GovOPlaN/{repository}"
|
||||||
|
source_commit = str(package["commit"])
|
||||||
entry["source"] = {
|
entry["source"] = {
|
||||||
"repository": repository,
|
"repository": repository,
|
||||||
"tag": package["tag"],
|
"tag": package["tag"],
|
||||||
"commit": package["commit"],
|
"commit": source_commit,
|
||||||
|
"repository_url": repository_url,
|
||||||
|
"revision_url": f"{repository_url}/commit/{source_commit}",
|
||||||
}
|
}
|
||||||
|
entry["availability"] = "available"
|
||||||
|
entry["release_notes_url"] = f"{repository_url}/releases/tag/{package['tag']}"
|
||||||
integrity: dict[str, object] = {
|
integrity: dict[str, object] = {
|
||||||
"python": _artifact_integrity(python_artifact, ref=python_ref),
|
"python": _artifact_integrity(python_artifact, ref=python_ref),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ def write_module_directory(
|
|||||||
output_root: Path,
|
output_root: Path,
|
||||||
channel: str,
|
channel: str,
|
||||||
public_base_url: str = DEFAULT_PUBLIC_BASE_URL,
|
public_base_url: str = DEFAULT_PUBLIC_BASE_URL,
|
||||||
|
prune: bool = False,
|
||||||
) -> tuple[Path, ...]:
|
) -> tuple[Path, ...]:
|
||||||
payloads = module_directory_payloads(
|
payloads = module_directory_payloads(
|
||||||
catalog_payload=catalog_payload,
|
catalog_payload=catalog_payload,
|
||||||
@@ -29,13 +30,62 @@ def write_module_directory(
|
|||||||
)
|
)
|
||||||
written: list[Path] = []
|
written: list[Path] = []
|
||||||
for relative_path, payload in payloads:
|
for relative_path, payload in payloads:
|
||||||
path = output_root / relative_path
|
path = _prepare_output_path(output_root=output_root, relative_path=relative_path)
|
||||||
path.parent.mkdir(parents=True, exist_ok=True)
|
|
||||||
path.write_text(json.dumps(payload, indent=2, sort_keys=True) + "\n", encoding="utf-8")
|
path.write_text(json.dumps(payload, indent=2, sort_keys=True) + "\n", encoding="utf-8")
|
||||||
written.append(path)
|
written.append(path)
|
||||||
|
if prune:
|
||||||
|
_prune_stale_module_directory_files(
|
||||||
|
output_root=output_root,
|
||||||
|
expected={relative_path for relative_path, _payload in payloads},
|
||||||
|
)
|
||||||
return tuple(written)
|
return tuple(written)
|
||||||
|
|
||||||
|
|
||||||
|
def _prepare_output_path(*, output_root: Path, relative_path: Path) -> Path:
|
||||||
|
if output_root.is_symlink():
|
||||||
|
raise ValueError("module-directory output root must not be a symlink")
|
||||||
|
output_root.mkdir(parents=True, exist_ok=True)
|
||||||
|
current = output_root
|
||||||
|
for part in relative_path.parent.parts:
|
||||||
|
current = current / part
|
||||||
|
if current.is_symlink():
|
||||||
|
raise ValueError("module-directory path must not contain symlinks")
|
||||||
|
if current.exists():
|
||||||
|
if not current.is_dir():
|
||||||
|
raise ValueError("module-directory parent path must be a directory")
|
||||||
|
continue
|
||||||
|
current.mkdir()
|
||||||
|
path = output_root / relative_path
|
||||||
|
if path.is_symlink() or (path.exists() and not path.is_file()):
|
||||||
|
raise ValueError("module-directory output path must be a regular file")
|
||||||
|
return path
|
||||||
|
|
||||||
|
|
||||||
|
def _prune_stale_module_directory_files(
|
||||||
|
*,
|
||||||
|
output_root: Path,
|
||||||
|
expected: set[Path],
|
||||||
|
) -> None:
|
||||||
|
module_root = output_root / "modules"
|
||||||
|
if module_root.is_symlink():
|
||||||
|
raise ValueError("module-directory root must not be a symlink")
|
||||||
|
if not module_root.exists():
|
||||||
|
return
|
||||||
|
for path in module_root.rglob("*.json"):
|
||||||
|
relative_path = path.relative_to(output_root)
|
||||||
|
if relative_path not in expected:
|
||||||
|
path.unlink()
|
||||||
|
for path in sorted(
|
||||||
|
(item for item in module_root.rglob("*") if item.is_dir()),
|
||||||
|
key=lambda item: len(item.parts),
|
||||||
|
reverse=True,
|
||||||
|
):
|
||||||
|
try:
|
||||||
|
path.rmdir()
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
def module_directory_payloads(
|
def module_directory_payloads(
|
||||||
*,
|
*,
|
||||||
catalog_payload: dict[str, Any],
|
catalog_payload: dict[str, Any],
|
||||||
|
|||||||
@@ -185,17 +185,24 @@ def module_rows(payload: object) -> list[dict[str, object]]:
|
|||||||
for item in raw_modules:
|
for item in raw_modules:
|
||||||
if not isinstance(item, dict):
|
if not isinstance(item, dict):
|
||||||
continue
|
continue
|
||||||
repo = module_repo(item)
|
source = item.get("source") if isinstance(item.get("source"), dict) else {}
|
||||||
|
repo = string(source.get("repository")) or module_repo(item)
|
||||||
|
source_tag = string(source.get("tag"))
|
||||||
modules.append(
|
modules.append(
|
||||||
{
|
{
|
||||||
"module_id": string(item.get("module_id")),
|
"module_id": string(item.get("module_id")),
|
||||||
"name": string(item.get("name")),
|
"name": string(item.get("name")),
|
||||||
|
"description": string(item.get("description")),
|
||||||
"version": string(item.get("version")),
|
"version": string(item.get("version")),
|
||||||
"repo": repo,
|
"repo": repo,
|
||||||
|
"source": dict(source),
|
||||||
"python_ref": string(item.get("python_ref")),
|
"python_ref": string(item.get("python_ref")),
|
||||||
"python_tag": ref_tag(string(item.get("python_ref"))),
|
"python_tag": source_tag or ref_tag(string(item.get("python_ref"))),
|
||||||
"webui_ref": string(item.get("webui_ref")),
|
"webui_ref": string(item.get("webui_ref")),
|
||||||
"webui_tag": ref_tag(string(item.get("webui_ref"))),
|
"webui_tag": source_tag or ref_tag(string(item.get("webui_ref"))),
|
||||||
|
"artifact_integrity": dict(item.get("artifact_integrity")) if isinstance(item.get("artifact_integrity"), dict) else {},
|
||||||
|
"dependencies": tuple(str(value) for value in item.get("dependencies", ()) if isinstance(value, str)) if isinstance(item.get("dependencies"), list) else (),
|
||||||
|
"optional_dependencies": tuple(str(value) for value in item.get("optional_dependencies", ()) if isinstance(value, str)) if isinstance(item.get("optional_dependencies"), list) else (),
|
||||||
"provides_interfaces": interface_list(item.get("provides_interfaces")),
|
"provides_interfaces": interface_list(item.get("provides_interfaces")),
|
||||||
"requires_interfaces": requirement_list(item.get("requires_interfaces")),
|
"requires_interfaces": requirement_list(item.get("requires_interfaces")),
|
||||||
"migration_safety": string(item.get("migration_safety")),
|
"migration_safety": string(item.get("migration_safety")),
|
||||||
|
|||||||
@@ -187,6 +187,7 @@ fi
|
|||||||
|
|
||||||
CATALOG_PATH="$WEB_ROOT/public/catalogs/v1/channels/$CHANNEL.json"
|
CATALOG_PATH="$WEB_ROOT/public/catalogs/v1/channels/$CHANNEL.json"
|
||||||
KEYRING_PATH="$WEB_ROOT/public/catalogs/v1/keyring.json"
|
KEYRING_PATH="$WEB_ROOT/public/catalogs/v1/keyring.json"
|
||||||
|
MODULE_DIRECTORY_PATH="$WEB_ROOT/public/catalogs/v1/modules"
|
||||||
TAG_NAME="catalog-v$VERSION"
|
TAG_NAME="catalog-v$VERSION"
|
||||||
TEMP_ROOT="$(mktemp -d "${TMPDIR:-/tmp}/govoplan-release-catalog.XXXXXXXX")"
|
TEMP_ROOT="$(mktemp -d "${TMPDIR:-/tmp}/govoplan-release-catalog.XXXXXXXX")"
|
||||||
PACKAGE_SET="$TEMP_ROOT/release-packages.json"
|
PACKAGE_SET="$TEMP_ROOT/release-packages.json"
|
||||||
@@ -213,6 +214,7 @@ GEN_ARGS=(
|
|||||||
--expires-days "$EXPIRES_DAYS"
|
--expires-days "$EXPIRES_DAYS"
|
||||||
--catalog-output "$CATALOG_PATH"
|
--catalog-output "$CATALOG_PATH"
|
||||||
--keyring-output "$KEYRING_PATH"
|
--keyring-output "$KEYRING_PATH"
|
||||||
|
--module-directory-output "$WEB_ROOT/public/catalogs/v1"
|
||||||
--public-base-url "$PUBLIC_BASE_URL"
|
--public-base-url "$PUBLIC_BASE_URL"
|
||||||
)
|
)
|
||||||
if [[ -n "$SEQUENCE" ]]; then
|
if [[ -n "$SEQUENCE" ]]; then
|
||||||
@@ -252,7 +254,7 @@ if [[ "$BUILD_WEB" -eq 1 ]]; then
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ "$COMMIT" -eq 1 ]]; then
|
if [[ "$COMMIT" -eq 1 ]]; then
|
||||||
run git -C "$WEB_ROOT" add "$CATALOG_PATH" "$KEYRING_PATH"
|
run git -C "$WEB_ROOT" add -A "$CATALOG_PATH" "$KEYRING_PATH" "$MODULE_DIRECTORY_PATH"
|
||||||
if [[ "$DRY_RUN" -eq 0 ]]; then
|
if [[ "$DRY_RUN" -eq 0 ]]; then
|
||||||
if git -C "$WEB_ROOT" diff --cached --quiet; then
|
if git -C "$WEB_ROOT" diff --cached --quiet; then
|
||||||
echo "No addideas-govoplan-website catalog changes to commit."
|
echo "No addideas-govoplan-website catalog changes to commit."
|
||||||
@@ -282,4 +284,5 @@ fi
|
|||||||
echo "Catalog ready:"
|
echo "Catalog ready:"
|
||||||
echo " $CATALOG_PATH"
|
echo " $CATALOG_PATH"
|
||||||
echo " $KEYRING_PATH"
|
echo " $KEYRING_PATH"
|
||||||
|
echo " $MODULE_DIRECTORY_PATH"
|
||||||
echo " URL: $PUBLIC_BASE_URL/catalogs/v1/channels/$CHANNEL.json"
|
echo " URL: $PUBLIC_BASE_URL/catalogs/v1/channels/$CHANNEL.json"
|
||||||
|
|||||||
Reference in New Issue
Block a user