refactor(catalog): explicit org/repo terminology; npm targets coulomb Gitea registry

Gitea's "project/package/release" terms are overloaded, so the catalog now uses
the most explicit words:
- org  = coulomb (the Gitea organisation)
- repo = whynot-design (the Gitea repository/product) — not an org, not a scope
- npm scope @whynot and package @whynot/design are distinct from both

Changes:
- catalog schema: replace conflated `owner` with required `org` + `repo`; `owner`
  is now a derived `org/repo` slug property
- npm-config delivery is data-driven: registry + scope live in
  delivery_config.npm and are validated; engine no longer hardcodes a registry
- exec delivery writes `<scope>:registry=<url>` + scoped `:_authToken` for the
  configured Gitea registry (token still env-expanded, never written to disk)
- pilot lane points at https://gitea.coulomb.social/api/packages/coulomb/npm/,
  scope @whynot, KV path coulomb/whynot-design/npm/publish
- npm-publish-demo uses @whynot scope so dry-run resolves the Gitea registry
- docs: terminology table; routing owner shown as coulomb/whynot-design
- tests: org/repo required, npm-config validation, registry authkey mapping

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
tegwick 2026-06-28 12:44:55 +02:00
parent 147cf8acda
commit f87f4e5e4d
9 changed files with 153 additions and 17 deletions

View file

@ -49,16 +49,30 @@ def _fetch_value(client: OpenBaoClient, entry: CatalogEntry, field: str) -> str:
return data[field]
def _registry_authkey(registry: str) -> str:
"""Turn a registry URL into the npm `//host/path/:_authToken` config key."""
no_scheme = registry.split("://", 1)[-1]
if not no_scheme.endswith("/"):
no_scheme += "/"
return "//" + no_scheme
@contextmanager
def _npm_userconfig(token: str) -> Iterator[Path]:
"""Write a mode-0600 temp .npmrc, yield its path, delete it unconditionally."""
def _npm_userconfig(registry: str, scope: str) -> Iterator[Path]:
"""Write a mode-0600 temp .npmrc for the configured registry/scope.
The token itself is NOT written to the file npm expands ${SE_NPM_TOKEN}
from the child environment, so the value never touches disk.
"""
fd, name = tempfile.mkstemp(prefix="se-npmrc-", suffix=".ini")
path = Path(name)
try:
os.fchmod(fd, 0o600)
# Registry-scoped auth token; child npm reads this via NPM_CONFIG_USERCONFIG.
authkey = _registry_authkey(registry)
with os.fdopen(fd, "w") as fh:
fh.write("//registry.npmjs.org/:_authToken=${SE_NPM_TOKEN}\n")
# e.g. @whynot:registry=https://gitea.coulomb.social/api/packages/coulomb/npm/
fh.write(f"{scope}:registry={registry}\n")
fh.write(f"{authkey}:_authToken=${{SE_NPM_TOKEN}}\n")
yield path
finally:
try:
@ -110,7 +124,15 @@ def exec_with_secret(
child_env = dict(os.environ)
if mode == "npm-config":
with _npm_userconfig(value) as npmrc:
npm = entry.npm
registry = npm.get("registry", "")
scope = npm.get("scope", "")
if not registry or not scope:
raise DeliveryError(
f"lane '{entry.id}' npm-config delivery needs "
"delivery_config.npm.registry and .scope"
)
with _npm_userconfig(registry, scope) as npmrc:
child_env["NPM_CONFIG_USERCONFIG"] = str(npmrc)
child_env["SE_NPM_TOKEN"] = value
rc = _spawn(command, child_env, value)