Compare commits
4 Commits
5a1204fbcb
...
322741e00c
| Author | SHA1 | Date | |
|---|---|---|---|
| 322741e00c | |||
| da1e46a921 | |||
| 08a9983984 | |||
| e1e5daa30a |
@@ -753,8 +753,13 @@ pipelines</strong>:</p>
|
||||
the disaster-recovery / replay path.</li>
|
||||
</ol>
|
||||
<p>Secrets: <code>NPM_REPO_URL</code>, <code>NPM_USERNAME</code>, <code>NPM_PASSWORD</code>. Auth is a generated
|
||||
<code>.npmrc</code> (<code>registry=</code> + <code>_auth=</code> base64 of <code>user:pass</code>) written from secrets at
|
||||
runtime — the committed <code>.npmrc</code> in a repo only points at the registry URL.</p>
|
||||
<code>.npmrc</code> written from secrets at runtime. It sets a <strong>scope-scoped</strong> registry,
|
||||
never a global one — <code>@aetoskia:registry=$NPM_REPO_URL</code> — plus a host-scoped
|
||||
auth line <code>//<host>/:_auth=<base64(user:pass)></code>. The scope and host are derived
|
||||
from <code>package.json</code> and <code>NPM_REPO_URL</code> respectively. A global <code>registry=</code> would
|
||||
push public transitive deps through Verdaccio (which has no uplink) and 404
|
||||
<code>npm install</code>; a bare <code>_auth=</code> is rejected by modern npm. See
|
||||
<a href="../93_library_registry/">93 – Library Registry</a>.</p>
|
||||
<ul>
|
||||
<li><strong>Ship as-is.</strong> Name and version come from <code>package.json</code>, so the same file
|
||||
is reused verbatim across every npm package.</li>
|
||||
|
||||
@@ -635,6 +635,39 @@
|
||||
</span>
|
||||
</a>
|
||||
|
||||
<nav class="md-nav" aria-label="Publishing & registry auth">
|
||||
<ul class="md-nav__list">
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="#endpoints" class="md-nav__link">
|
||||
<span class="md-ellipsis">
|
||||
Endpoints
|
||||
</span>
|
||||
</a>
|
||||
|
||||
</li>
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="#npm-scope-the-registry-never-set-it-globally" class="md-nav__link">
|
||||
<span class="md-ellipsis">
|
||||
npm: scope the registry, never set it globally
|
||||
</span>
|
||||
</a>
|
||||
|
||||
</li>
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="#verdaccio-storage-permissions" class="md-nav__link">
|
||||
<span class="md-ellipsis">
|
||||
Verdaccio storage permissions
|
||||
</span>
|
||||
</a>
|
||||
|
||||
</li>
|
||||
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
</li>
|
||||
|
||||
<li class="md-nav__item">
|
||||
@@ -839,6 +872,67 @@ so the CI credentials (<code>PIP_USERNAME</code> / <code>PIP_PASSWORD</code>, <c
|
||||
<code>NPM_PASSWORD</code>) are one and the same account — there is no separate npm user.
|
||||
Verdaccio requires <strong>bcrypt</strong> htpasswd entries; see
|
||||
<a href="../90_drone_pipelines/">90 – Drone Pipelines</a> for the CI wiring.</p>
|
||||
<h3 id="endpoints">Endpoints</h3>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Registry</th>
|
||||
<th>URL</th>
|
||||
<th>Backend</th>
|
||||
<th>CI secret</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>PyPI</td>
|
||||
<td><code>https://pip.aetoskia.com</code></td>
|
||||
<td>pypiserver (<code>server-pi:6007</code>)</td>
|
||||
<td><code>PIP_REPO_URL</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>npm</td>
|
||||
<td><code>https://npm.aetoskia.com</code></td>
|
||||
<td>Verdaccio (<code>server-pi:6008</code>)</td>
|
||||
<td><code>NPM_REPO_URL</code></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p>Both are exposed through Traefik on the <code>websecure</code> entrypoint as <strong>bare
|
||||
origins</strong> — no path, no trailing slash. Set the secret values exactly as above.</p>
|
||||
<h3 id="npm-scope-the-registry-never-set-it-globally">npm: scope the registry, never set it globally</h3>
|
||||
<p>Verdaccio here has <strong>no uplink / proxy</strong>, so it can only serve <code>@aetoskia/*</code>
|
||||
packages. A global <code>registry=https://npm.aetoskia.com</code> in <code>.npmrc</code> would route
|
||||
<em>every</em> dependency — including public transitive deps such as
|
||||
<code>zod-to-json-schema</code> (pulled in by <code>@modelcontextprotocol/sdk</code>) — through
|
||||
Verdaccio, which 404s on anything it does not host. Always scope the registry to
|
||||
the account:</p>
|
||||
<div class="language-text highlight"><table class="highlighttable"><tr><td class="linenos"><div class="linenodiv"><pre><span></span><span class="normal"><a href="#__codelineno-0-1">1</a></span>
|
||||
<span class="normal"><a href="#__codelineno-0-2">2</a></span></pre></div></td><td class="code"><div><pre><span></span><code><span id="__span-0-1"><a id="__codelineno-0-1" name="__codelineno-0-1"></a>@aetoskia:registry=https://npm.aetoskia.com
|
||||
</span><span id="__span-0-2"><a id="__codelineno-0-2" name="__codelineno-0-2"></a>//npm.aetoskia.com/:_auth=<base64(user:password)>
|
||||
</span></code></pre></div></td></tr></table></div>
|
||||
<p>Public deps then resolve from <code>registry.npmjs.org</code> and only <code>@aetoskia/*</code> hits
|
||||
the private registry. The auth line must be <strong>scoped to the host</strong>
|
||||
(<code>//host/:_auth=</code>), not a bare <code>_auth=</code> — modern npm rejects the bare form with
|
||||
<code>ERR_INVALID_AUTH</code>. The drone template derives both from the package scope and
|
||||
<code>NPM_REPO_URL</code> automatically; see
|
||||
<a href="../90_drone_pipelines/">90 – Drone Pipelines</a>.</p>
|
||||
<blockquote>
|
||||
<p>The Drone pipeline runs each <code>commands:</code> list item in a <strong>separate shell</strong>, so
|
||||
shell variables defined in one item do not survive into the next. Keep the
|
||||
variable definitions and the commands that use them in the same <code>|</code> block.</p>
|
||||
</blockquote>
|
||||
<h3 id="verdaccio-storage-permissions">Verdaccio storage permissions</h3>
|
||||
<p>Verdaccio runs as uid <strong>10001</strong> (<code>verdaccio</code>) inside <code>verdaccio/verdaccio</code>.
|
||||
The <code>storage</code> directory is bind-mounted from the server
|
||||
(<code>/mnt/omnissiah-vault/data/verdaccio/storage</code>); if it is owned by <code>root:root</code>
|
||||
(mode <code>755</code>) the process cannot write to it and every publish fails with a
|
||||
confusing <code>404 no such package available</code> (<code>NotFoundError</code> from
|
||||
<code>local-storage</code>). Ensure the directory — and its <code>plugins</code> subdirectory — are
|
||||
owned by <code>10001:101</code>:</p>
|
||||
<div class="language-sh highlight"><table class="highlighttable"><tr><td class="linenos"><div class="linenodiv"><pre><span></span><span class="normal"><a href="#__codelineno-1-1">1</a></span>
|
||||
<span class="normal"><a href="#__codelineno-1-2">2</a></span></pre></div></td><td class="code"><div><pre><span></span><code><span id="__span-1-1"><a id="__codelineno-1-1" name="__codelineno-1-1"></a>sudo<span class="w"> </span>chown<span class="w"> </span>-R<span class="w"> </span><span class="m">10001</span>:101<span class="w"> </span>/mnt/omnissiah-vault/data/verdaccio/storage
|
||||
</span><span id="__span-1-2"><a id="__codelineno-1-2" name="__codelineno-1-2"></a>docker<span class="w"> </span>restart<span class="w"> </span>npm-registry
|
||||
</span></code></pre></div></td></tr></table></div>
|
||||
<h2 id="notes">Notes</h2>
|
||||
<ul>
|
||||
<li><strong>Hexa</strong> is a full library with its own MCP server on <code>:8008</code>: read
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -22,17 +22,18 @@ steps:
|
||||
from_secret: NPM_PASSWORD
|
||||
commands:
|
||||
- apk add --no-cache curl
|
||||
- PACKAGE_NAME=$(node -p "require('./package.json').name")
|
||||
- VERSION=$(node -p "require('./package.json').version")
|
||||
- |
|
||||
PACKAGE_NAME=$(node -p "require('./package.json').name")
|
||||
VERSION=$(node -p "require('./package.json').version")
|
||||
if [ "$DRONE_TAG" != "$VERSION" ]; then
|
||||
echo "Tag '$DRONE_TAG' does not match package.json version '$VERSION'; refusing to publish."
|
||||
exit 1
|
||||
fi
|
||||
- echo "registry=$NPM_REPO_URL" > /tmp/.npmrc
|
||||
- echo "_auth=$(echo -n "$NPM_USERNAME:$NPM_PASSWORD" | base64 | tr -d '\n')" >> /tmp/.npmrc
|
||||
- echo "Checking if $PACKAGE_NAME@$VERSION exists on $NPM_REPO_URL ..."
|
||||
- |
|
||||
NPM_HOST=$(echo "$NPM_REPO_URL" | sed -E 's|^[a-z]+://||; s|/*$||')
|
||||
NPM_SCOPE="@$(node -p "require('./package.json').name.split('/')[0].replace(/^@/,'')")"
|
||||
echo "$NPM_SCOPE:registry=$NPM_REPO_URL" > /tmp/.npmrc
|
||||
echo "//$NPM_HOST/:_auth=$(echo -n "$NPM_USERNAME:$NPM_PASSWORD" | base64 | tr -d '\n')" >> /tmp/.npmrc
|
||||
echo "Checking if $PACKAGE_NAME@$VERSION exists on $NPM_REPO_URL ..."
|
||||
if npm view "$PACKAGE_NAME@$VERSION" version --userconfig /tmp/.npmrc >/dev/null 2>&1; then
|
||||
echo "$PACKAGE_NAME@$VERSION already exists; skipping build."
|
||||
exit 78
|
||||
@@ -55,8 +56,10 @@ steps:
|
||||
echo "NPM_REPO_URL / NPM_USERNAME / NPM_PASSWORD are not set" >&2
|
||||
exit 1
|
||||
fi
|
||||
- echo "registry=$NPM_REPO_URL" > .npmrc
|
||||
- echo "_auth=$(echo -n "$NPM_USERNAME:$NPM_PASSWORD" | base64 | tr -d '\n')" >> .npmrc
|
||||
NPM_HOST=$(echo "$NPM_REPO_URL" | sed -E 's|^[a-z]+://||; s|/*$||')
|
||||
NPM_SCOPE="@$(node -p "require('./package.json').name.split('/')[0].replace(/^@/,'')")"
|
||||
echo "$NPM_SCOPE:registry=$NPM_REPO_URL" > .npmrc
|
||||
echo "//$NPM_HOST/:_auth=$(echo -n "$NPM_USERNAME:$NPM_PASSWORD" | base64 | tr -d '\n')" >> .npmrc
|
||||
- npm install
|
||||
- echo "Running quality gate..."
|
||||
- npm run quality-gate
|
||||
@@ -79,8 +82,11 @@ steps:
|
||||
NPM_PASSWORD:
|
||||
from_secret: NPM_PASSWORD
|
||||
commands:
|
||||
- echo "registry=$NPM_REPO_URL" > .npmrc
|
||||
- echo "_auth=$(echo -n "$NPM_USERNAME:$NPM_PASSWORD" | base64 | tr -d '\n')" >> .npmrc
|
||||
- |
|
||||
NPM_HOST=$(echo "$NPM_REPO_URL" | sed -E 's|^[a-z]+://||; s|/*$||')
|
||||
NPM_SCOPE="@$(node -p "require('./package.json').name.split('/')[0].replace(/^@/,'')")"
|
||||
echo "$NPM_SCOPE:registry=$NPM_REPO_URL" > .npmrc
|
||||
echo "//$NPM_HOST/:_auth=$(echo -n "$NPM_USERNAME:$NPM_PASSWORD" | base64 | tr -d '\n')" >> .npmrc
|
||||
- echo "Publishing to private npm at $NPM_REPO_URL ..."
|
||||
- npm publish --access restricted
|
||||
|
||||
@@ -117,8 +123,11 @@ steps:
|
||||
from_secret: NPM_PASSWORD
|
||||
commands:
|
||||
- apk add --no-cache git
|
||||
- echo "registry=$NPM_REPO_URL" > /tmp/.npmrc
|
||||
- echo "_auth=$(echo -n "$NPM_USERNAME:$NPM_PASSWORD" | base64 | tr -d '\n')" >> /tmp/.npmrc
|
||||
- |
|
||||
NPM_HOST=$(echo "$NPM_REPO_URL" | sed -E 's|^[a-z]+://||; s|/*$||')
|
||||
NPM_SCOPE="@$(node -p "require('./package.json').name.split('/')[0].replace(/^@/,'')")"
|
||||
echo "$NPM_SCOPE:registry=$NPM_REPO_URL" > /tmp/.npmrc
|
||||
echo "//$NPM_HOST/:_auth=$(echo -n "$NPM_USERNAME:$NPM_PASSWORD" | base64 | tr -d '\n')" >> /tmp/.npmrc
|
||||
- |
|
||||
set -e
|
||||
|
||||
|
||||
@@ -72,8 +72,13 @@ pipelines**:
|
||||
the disaster-recovery / replay path.
|
||||
|
||||
Secrets: `NPM_REPO_URL`, `NPM_USERNAME`, `NPM_PASSWORD`. Auth is a generated
|
||||
`.npmrc` (`registry=` + `_auth=` base64 of `user:pass`) written from secrets at
|
||||
runtime — the committed `.npmrc` in a repo only points at the registry URL.
|
||||
`.npmrc` written from secrets at runtime. It sets a **scope-scoped** registry,
|
||||
never a global one — `@aetoskia:registry=$NPM_REPO_URL` — plus a host-scoped
|
||||
auth line `//<host>/:_auth=<base64(user:pass)>`. The scope and host are derived
|
||||
from `package.json` and `NPM_REPO_URL` respectively. A global `registry=` would
|
||||
push public transitive deps through Verdaccio (which has no uplink) and 404
|
||||
`npm install`; a bare `_auth=` is rejected by modern npm. See
|
||||
[93 – Library Registry](93_library_registry.md).
|
||||
|
||||
- **Ship as-is.** Name and version come from `package.json`, so the same file
|
||||
is reused verbatim across every npm package.
|
||||
|
||||
@@ -64,6 +64,56 @@ so the CI credentials (`PIP_USERNAME` / `PIP_PASSWORD`, `NPM_USERNAME` /
|
||||
Verdaccio requires **bcrypt** htpasswd entries; see
|
||||
[90 – Drone Pipelines](90_drone_pipelines.md) for the CI wiring.
|
||||
|
||||
### Endpoints
|
||||
|
||||
| Registry | URL | Backend | CI secret |
|
||||
|---|---|---|---|
|
||||
| PyPI | `https://pip.aetoskia.com` | pypiserver (`server-pi:6007`) | `PIP_REPO_URL` |
|
||||
| npm | `https://npm.aetoskia.com` | Verdaccio (`server-pi:6008`) | `NPM_REPO_URL` |
|
||||
|
||||
Both are exposed through Traefik on the `websecure` entrypoint as **bare
|
||||
origins** — no path, no trailing slash. Set the secret values exactly as above.
|
||||
|
||||
### npm: scope the registry, never set it globally
|
||||
|
||||
Verdaccio here has **no uplink / proxy**, so it can only serve `@aetoskia/*`
|
||||
packages. A global `registry=https://npm.aetoskia.com` in `.npmrc` would route
|
||||
*every* dependency — including public transitive deps such as
|
||||
`zod-to-json-schema` (pulled in by `@modelcontextprotocol/sdk`) — through
|
||||
Verdaccio, which 404s on anything it does not host. Always scope the registry to
|
||||
the account:
|
||||
|
||||
```
|
||||
@aetoskia:registry=https://npm.aetoskia.com
|
||||
//npm.aetoskia.com/:_auth=<base64(user:password)>
|
||||
```
|
||||
|
||||
Public deps then resolve from `registry.npmjs.org` and only `@aetoskia/*` hits
|
||||
the private registry. The auth line must be **scoped to the host**
|
||||
(`//host/:_auth=`), not a bare `_auth=` — modern npm rejects the bare form with
|
||||
`ERR_INVALID_AUTH`. The drone template derives both from the package scope and
|
||||
`NPM_REPO_URL` automatically; see
|
||||
[90 – Drone Pipelines](90_drone_pipelines.md).
|
||||
|
||||
> The Drone pipeline runs each `commands:` list item in a **separate shell**, so
|
||||
> shell variables defined in one item do not survive into the next. Keep the
|
||||
> variable definitions and the commands that use them in the same `|` block.
|
||||
|
||||
### Verdaccio storage permissions
|
||||
|
||||
Verdaccio runs as uid **10001** (`verdaccio`) inside `verdaccio/verdaccio`.
|
||||
The `storage` directory is bind-mounted from the server
|
||||
(`/mnt/omnissiah-vault/data/verdaccio/storage`); if it is owned by `root:root`
|
||||
(mode `755`) the process cannot write to it and every publish fails with a
|
||||
confusing `404 no such package available` (`NotFoundError` from
|
||||
`local-storage`). Ensure the directory — and its `plugins` subdirectory — are
|
||||
owned by `10001:101`:
|
||||
|
||||
```sh
|
||||
sudo chown -R 10001:101 /mnt/omnissiah-vault/data/verdaccio/storage
|
||||
docker restart npm-registry
|
||||
```
|
||||
|
||||
## Notes
|
||||
|
||||
- **Hexa** is a full library with its own MCP server on `:8008`: read
|
||||
|
||||
@@ -22,17 +22,18 @@ steps:
|
||||
from_secret: NPM_PASSWORD
|
||||
commands:
|
||||
- apk add --no-cache curl
|
||||
- PACKAGE_NAME=$(node -p "require('./package.json').name")
|
||||
- VERSION=$(node -p "require('./package.json').version")
|
||||
- |
|
||||
PACKAGE_NAME=$(node -p "require('./package.json').name")
|
||||
VERSION=$(node -p "require('./package.json').version")
|
||||
if [ "$DRONE_TAG" != "$VERSION" ]; then
|
||||
echo "Tag '$DRONE_TAG' does not match package.json version '$VERSION'; refusing to publish."
|
||||
exit 1
|
||||
fi
|
||||
- echo "registry=$NPM_REPO_URL" > /tmp/.npmrc
|
||||
- echo "_auth=$(echo -n "$NPM_USERNAME:$NPM_PASSWORD" | base64 | tr -d '\n')" >> /tmp/.npmrc
|
||||
- echo "Checking if $PACKAGE_NAME@$VERSION exists on $NPM_REPO_URL ..."
|
||||
- |
|
||||
NPM_HOST=$(echo "$NPM_REPO_URL" | sed -E 's|^[a-z]+://||; s|/*$||')
|
||||
NPM_SCOPE="@$(node -p "require('./package.json').name.split('/')[0].replace(/^@/,'')")"
|
||||
echo "$NPM_SCOPE:registry=$NPM_REPO_URL" > /tmp/.npmrc
|
||||
echo "//$NPM_HOST/:_auth=$(echo -n "$NPM_USERNAME:$NPM_PASSWORD" | base64 | tr -d '\n')" >> /tmp/.npmrc
|
||||
echo "Checking if $PACKAGE_NAME@$VERSION exists on $NPM_REPO_URL ..."
|
||||
if npm view "$PACKAGE_NAME@$VERSION" version --userconfig /tmp/.npmrc >/dev/null 2>&1; then
|
||||
echo "$PACKAGE_NAME@$VERSION already exists; skipping build."
|
||||
exit 78
|
||||
@@ -55,8 +56,10 @@ steps:
|
||||
echo "NPM_REPO_URL / NPM_USERNAME / NPM_PASSWORD are not set" >&2
|
||||
exit 1
|
||||
fi
|
||||
- echo "registry=$NPM_REPO_URL" > .npmrc
|
||||
- echo "_auth=$(echo -n "$NPM_USERNAME:$NPM_PASSWORD" | base64 | tr -d '\n')" >> .npmrc
|
||||
NPM_HOST=$(echo "$NPM_REPO_URL" | sed -E 's|^[a-z]+://||; s|/*$||')
|
||||
NPM_SCOPE="@$(node -p "require('./package.json').name.split('/')[0].replace(/^@/,'')")"
|
||||
echo "$NPM_SCOPE:registry=$NPM_REPO_URL" > .npmrc
|
||||
echo "//$NPM_HOST/:_auth=$(echo -n "$NPM_USERNAME:$NPM_PASSWORD" | base64 | tr -d '\n')" >> .npmrc
|
||||
- npm install
|
||||
- echo "Running quality gate..."
|
||||
- npm run quality-gate
|
||||
@@ -79,8 +82,11 @@ steps:
|
||||
NPM_PASSWORD:
|
||||
from_secret: NPM_PASSWORD
|
||||
commands:
|
||||
- echo "registry=$NPM_REPO_URL" > .npmrc
|
||||
- echo "_auth=$(echo -n "$NPM_USERNAME:$NPM_PASSWORD" | base64 | tr -d '\n')" >> .npmrc
|
||||
- |
|
||||
NPM_HOST=$(echo "$NPM_REPO_URL" | sed -E 's|^[a-z]+://||; s|/*$||')
|
||||
NPM_SCOPE="@$(node -p "require('./package.json').name.split('/')[0].replace(/^@/,'')")"
|
||||
echo "$NPM_SCOPE:registry=$NPM_REPO_URL" > .npmrc
|
||||
echo "//$NPM_HOST/:_auth=$(echo -n "$NPM_USERNAME:$NPM_PASSWORD" | base64 | tr -d '\n')" >> .npmrc
|
||||
- echo "Publishing to private npm at $NPM_REPO_URL ..."
|
||||
- npm publish --access restricted
|
||||
|
||||
@@ -117,8 +123,11 @@ steps:
|
||||
from_secret: NPM_PASSWORD
|
||||
commands:
|
||||
- apk add --no-cache git
|
||||
- echo "registry=$NPM_REPO_URL" > /tmp/.npmrc
|
||||
- echo "_auth=$(echo -n "$NPM_USERNAME:$NPM_PASSWORD" | base64 | tr -d '\n')" >> /tmp/.npmrc
|
||||
- |
|
||||
NPM_HOST=$(echo "$NPM_REPO_URL" | sed -E 's|^[a-z]+://||; s|/*$||')
|
||||
NPM_SCOPE="@$(node -p "require('./package.json').name.split('/')[0].replace(/^@/,'')")"
|
||||
echo "$NPM_SCOPE:registry=$NPM_REPO_URL" > /tmp/.npmrc
|
||||
echo "//$NPM_HOST/:_auth=$(echo -n "$NPM_USERNAME:$NPM_PASSWORD" | base64 | tr -d '\n')" >> /tmp/.npmrc
|
||||
- |
|
||||
set -e
|
||||
|
||||
|
||||
Reference in New Issue
Block a user