Add npm backfill pipeline, shared htpasswd registry auth docs
All checks were successful
continuous-integration/drone/push Build is passing
All checks were successful
continuous-integration/drone/push Build is passing
- drone npm template gains backfill-npm-from-tags (mirrors PyPI) - document the shared /auth/htpasswd credential model (npm + PyPI + docker) and Verdaccio's bcrypt requirement - point doc-weaver source/path at ../react/doc-weaver
This commit is contained in:
@@ -42,7 +42,7 @@ repos:
|
|||||||
mcp: { bundle: docs/mcp, server: docforge, port: 8001 }
|
mcp: { bundle: docs/mcp, server: docforge, port: 8001 }
|
||||||
|
|
||||||
- name: doc-weaver
|
- name: doc-weaver
|
||||||
source: ../doc-weaver
|
source: ../react/doc-weaver
|
||||||
title: Doc Weaver
|
title: Doc Weaver
|
||||||
description: TypeScript documentation compiler using TypeDoc + JSDoc (RDFC), generating a TypeDoc site and a doc-forge-shaped MCP bundle for React packages.
|
description: TypeScript documentation compiler using TypeDoc + JSDoc (RDFC), generating a TypeDoc site and a doc-forge-shaped MCP bundle for React packages.
|
||||||
section: libraries
|
section: libraries
|
||||||
|
|||||||
@@ -733,18 +733,25 @@ with <code>from_secret</code> — never commit credentials.</p>
|
|||||||
</ul>
|
</ul>
|
||||||
<hr />
|
<hr />
|
||||||
<h2 id="type-b-npm-package-npm-libyaml">Type B — npm package (<code>npm-lib.yaml</code>)</h2>
|
<h2 id="type-b-npm-package-npm-libyaml">Type B — npm package (<code>npm-lib.yaml</code>)</h2>
|
||||||
<p>For internal npm libraries (the React packages). One pipeline,
|
<p>For internal npm libraries (the React packages). The file contains <strong>two
|
||||||
<strong><code>build-and-publish-npm</code></strong>, triggered on <code>tag</code>:</p>
|
pipelines</strong>:</p>
|
||||||
<ul>
|
<ol>
|
||||||
|
<li><strong><code>build-and-publish-npm</code></strong> — triggered on <code>tag</code>:</li>
|
||||||
<li><code>check-version</code> — <strong>guard</strong>: fail unless <code>$DRONE_TAG</code> equals the version in
|
<li><code>check-version</code> — <strong>guard</strong>: fail unless <code>$DRONE_TAG</code> equals the version in
|
||||||
<code>package.json</code>; query <code>$NPM_REPO_URL</code> for <code>name@version</code>; if it already
|
<code>package.json</code>; query <code>$NPM_REPO_URL</code> for <code>name@version</code>; if it already
|
||||||
exists, exit <code>78</code> and skip the whole build.</li>
|
exists, exit <code>78</code> and skip the whole build.</li>
|
||||||
<li><code>quality-gate</code> — write <code>.npmrc</code> from the secrets, <code>npm install</code>, then
|
<li><code>quality-gate</code> — write <code>.npmrc</code> from the secrets, <code>npm install</code>, then
|
||||||
<code>npm run quality-gate</code>.</li>
|
<code>npm run quality-gate</code>.</li>
|
||||||
<li><code>build-package</code> — <code>npm install && npm run build</code> into <code>dist/</code>.</li>
|
<li><code>build-package</code> — <code>npm install && npm run build</code> into <code>dist/</code>.</li>
|
||||||
<li><code>publish-to-private-npm</code> — <code>npm publish --access restricted</code> to
|
<li><code>publish-to-private-npm</code> — <code>npm publish --access restricted</code> to
|
||||||
<code>$NPM_REPO_URL</code>.</li>
|
<code>$NPM_REPO_URL</code>.</li>
|
||||||
</ul>
|
<li><strong><code>backfill-npm-from-tags</code></strong> — triggered on <code>custom</code>:</li>
|
||||||
|
<li><code>fetch-tags</code> — <code>git fetch --tags --force</code>.</li>
|
||||||
|
<li><code>build-and-publish-missing</code> — walk every tag in version order, skip the
|
||||||
|
ones already on the registry, <code>git checkout</code> the rest, verify
|
||||||
|
<code>tag == package.json version</code> at that commit, build, and publish. This is
|
||||||
|
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
|
<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
|
<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>
|
runtime — the committed <code>.npmrc</code> in a repo only points at the registry URL.</p>
|
||||||
@@ -752,6 +759,16 @@ runtime — the committed <code>.npmrc</code> in a repo only points at the regis
|
|||||||
<li><strong>Ship as-is.</strong> Name and version come from <code>package.json</code>, so the same file
|
<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>
|
is reused verbatim across every npm package.</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
<blockquote>
|
||||||
|
<p><strong>Registry credentials are shared.</strong> The private npm registry (Verdaccio at
|
||||||
|
<code>npm.aetoskia.com</code>) authenticates against the <strong>same <code>htpasswd</code> file</strong> as the
|
||||||
|
private PyPI index and the Docker registry (<code>/auth/htpasswd</code>), with
|
||||||
|
registration disabled (<code>max_users: -1</code>). <code>NPM_USERNAME</code> / <code>NPM_PASSWORD</code> are
|
||||||
|
therefore the <strong>same credentials as <code>PIP_USERNAME</code> / <code>PIP_PASSWORD</code></strong> — no
|
||||||
|
separate npm account exists. Verdaccio's htpasswd plugin accepts <strong>bcrypt</strong>
|
||||||
|
hashes (<code>$2y$…</code>); non-bcrypt entries that pypiserver tolerates will not
|
||||||
|
authenticate against npm. See <a href="../93_library_registry/">93 – Library Registry</a>.</p>
|
||||||
|
</blockquote>
|
||||||
<hr />
|
<hr />
|
||||||
<h2 id="type-c-docker-deployment-service-dockeryaml">Type C — Docker deployment (<code>service-docker.yaml</code>)</h2>
|
<h2 id="type-c-docker-deployment-service-dockeryaml">Type C — Docker deployment (<code>service-docker.yaml</code>)</h2>
|
||||||
<p>For a service that ships a container image — a long-running API/worker, e.g.
|
<p>For a service that ships a container image — a long-running API/worker, e.g.
|
||||||
@@ -821,8 +838,8 @@ triggered on <code>tag</code>:</p>
|
|||||||
<code>package.json</code> version; a service needs a root <code>VERSION</code> file. See
|
<code>package.json</code> version; a service needs a root <code>VERSION</code> file. See
|
||||||
<a href="../91_versioning_and_changelog/">91 – Versioning & Changelog</a>.</li>
|
<a href="../91_versioning_and_changelog/">91 – Versioning & Changelog</a>.</li>
|
||||||
<li>Do not rename the pipelines; tooling and runbooks key off
|
<li>Do not rename the pipelines; tooling and runbooks key off
|
||||||
<code>build-and-publish-pypi</code>, <code>build-and-publish-npm</code>, and
|
<code>build-and-publish-pypi</code>, <code>build-and-publish-npm</code>,
|
||||||
<code>build-and-deploy-service</code>.</li>
|
<code>build-and-deploy-service</code>, and their <code>backfill-*-from-tags</code> companions.</li>
|
||||||
</ul>
|
</ul>
|
||||||
<hr />
|
<hr />
|
||||||
<h2 id="related">Related</h2>
|
<h2 id="related">Related</h2>
|
||||||
|
|||||||
@@ -626,6 +626,15 @@
|
|||||||
</span>
|
</span>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="md-nav__item">
|
||||||
|
<a href="#publishing-registry-auth" class="md-nav__link">
|
||||||
|
<span class="md-ellipsis">
|
||||||
|
Publishing & registry auth
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
<li class="md-nav__item">
|
<li class="md-nav__item">
|
||||||
@@ -821,6 +830,15 @@ matching library, its import name, its "use instead of" guidance, and its MCP
|
|||||||
endpoint (or, for the auth-server service, a spec pointer). It also serves the
|
endpoint (or, for the auth-server service, a spec pointer). It also serves the
|
||||||
pinned auth-server OpenAPI contract at <code>docs://spec</code>. Query it before writing
|
pinned auth-server OpenAPI contract at <code>docs://spec</code>. Query it before writing
|
||||||
any infrastructure.</p>
|
any infrastructure.</p>
|
||||||
|
<h2 id="publishing-registry-auth">Publishing & registry auth</h2>
|
||||||
|
<p>Python packages publish to the private index (<code>pip.aetoskia.com</code>, <code>pypiserver</code>)
|
||||||
|
and npm packages to the private registry (<code>npm.aetoskia.com</code>, Verdaccio). Both
|
||||||
|
authenticate against the <strong>same <code>htpasswd</code> file</strong> (<code>/auth/htpasswd</code>) used by the
|
||||||
|
Docker registry and the Traefik basic-auth middleware; registration is disabled,
|
||||||
|
so the CI credentials (<code>PIP_USERNAME</code> / <code>PIP_PASSWORD</code>, <code>NPM_USERNAME</code> /
|
||||||
|
<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>
|
||||||
<h2 id="notes">Notes</h2>
|
<h2 id="notes">Notes</h2>
|
||||||
<ul>
|
<ul>
|
||||||
<li><strong>Hexa</strong> is a full library with its own MCP server on <code>:8008</code>: read
|
<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
@@ -2,7 +2,7 @@
|
|||||||
"module": "wiki_development_releasing",
|
"module": "wiki_development_releasing",
|
||||||
"content": {
|
"content": {
|
||||||
"path": "wiki_development_releasing",
|
"path": "wiki_development_releasing",
|
||||||
"docstring": "# Releasing & Versioning\n\nHow to cut a release of any Aetos repository. The rule lives in the reference\npage [Versioning & Changelog](../lib/91_versioning_and_changelog/); this is the\nrunbook you follow.\n\nEvery repository is one of two Drone shapes — a **PyPI package** or a **Docker\ndeployment** ([Drone Pipelines](../lib/90_drone_pipelines/)). Both release the\nsame way: the version bump and changelog promotion go **in the merge request**,\nand the tag is created on the merged commit.\n\n---\n\n## 🧾 Before you start\n\n- You have an open **merge request** holding the change, and `main` is green\n (the Drone quality gate passed on the last run).\n- You know whether this is a **breaking**, **feature**, or **fix** release.\n- The version file exists:\n - package → `version` in `pyproject.toml`\n - service → `VERSION` at the repo root\n\n---\n\n## 🚦 Pick the next version\n\nThe version file and the tag must be identical. For `0.y.z`, `0.y` is the major\nline (see the reference page); once at `1.0.0`, normal SemVer applies.\n\n| Change | `0.y.z` → | `1.0.0+` → |\n|--------|-----------|------------|\n| Breaking | `0.(y+1).0` | `(x+1).0.0` |\n| Feature | `0.y.(z+1)` | `x.(y+1).0` |\n| Fix | `0.y.(z+1)` | `x.y.(z+1)` |\n\n---\n\n## 🌿 Release in the MR\n\nThe version is known while the change is still a merge request, so the bump\nrides along with it. Do all edits **in the MR**:\n\n- **Reconcile the changelog first.** `[Unreleased]` is a **draft, not the source\n of truth**: diff the change against the last tag\n (`git log <last-tag>..HEAD`, `git diff --stat <last-tag>..HEAD`) and add an\n entry for every user-facing change that is missing. **Always show the proposed\n additions to the user and wait for approval** before promoting — ask rather\n than guess when an entry is ambiguous.\n\n- **Promote the changelog** in `CHANGELOG.md`: move the (now reconciled)\n `[Unreleased]` content under a new `## [x.y.z] - YYYY-MM-DD` heading, leave a\n fresh empty `[Unreleased]` on top, and add the compare links — naming the\n **version/tag**, never a commit id:\n\n ```markdown\n [Unreleased]: https://git.aetoskia.com/aetos/<repo>/compare/x.y.z...HEAD\n [x.y.z]: https://git.aetoskia.com/aetos/<repo>/compare/x.y.(z-1)...x.y.z\n ```\n\n- **Bump the version file** to the intended release version: `version = \"x.y.z\"`\n in `pyproject.toml` (packages) or the `VERSION` file (services).\n\n---\n\n## 🏷️ Tag the merged commit\n\n1. **Merge or squash** the MR into `main`.\n2. **Create the tag** on the resulting commit: `git tag x.y.z` (lightweight).\n3. **Push the tag** (`git push origin x.y.z`) — Drone builds the release.\n\n### 🤖 Agent-assisted tagging\n\nThe agent does not author or merge the MR and never tags before the merge. After\nyou confirm the MR is merged/squashed and tell it to proceed, the agent:\n\n1. Waits for that explicit go-ahead.\n2. Finds the commit that introduced the bump:\n - package:\n `git log -S 'version = \"x.y.z\"' -n 1 --format='%H %s' -- pyproject.toml`\n - service:\n `git log -S 'x.y.z' -n 1 --format='%H %s' -- VERSION`\n3. Verifies that commit's version file equals `x.y.z`, then tags **only** that\n commit: `git tag x.y.z <sha>`.\n4. Does **not** push — it asks you to run `git push origin x.y.z`.\n\n---\n\n## 🤖 What Drone does\n\n- **Type A (package):** asserts `tag == pyproject version`, then skips if that\n version already exists on the index, runs the quality gate, builds, and\n uploads the wheel + sdist.\n- **Type B (service):** asserts `tag == VERSION`, resolves the tag, skips if the\n image already exists, runs the quality gate, builds, pushes, and restarts the\n container.\n\nIf the guard fails, nothing is published — fix the version file, amend the\ncommit, and move the tag **only** if it was never pushed.\n\n---\n\n## 🛟 Backfill / recovery\n\nIf an older tag never published (a past version-file mismatch, an index\noutage):\n\n- **Package:** run the `backfill-pypi-from-tags` pipeline (custom event). It\n checks out every tag, verifies `tag == pyproject version` at that commit,\n and uploads the ones missing from the index. Already-fixed history is never\n rewritten.\n- **Service:** rebuild the image for the missing tag with a fresh commit and a\n new patch version; do not move a published tag.\n\n---\n\n## 🔁 If a release is wrong\n\n- Never reuse or force-move a pushed tag.\n- Fix forward: bump to the next patch, promote the changelog, tag again.\n\n---\n\n## Related\n\n- [Testing & Quality Gates](01_testing.md) · [Versioning & Changelog](../lib/91_versioning_and_changelog/) · [Drone Pipelines](../lib/90_drone_pipelines/)\n",
|
"docstring": "# Releasing & Versioning\n\nHow to cut a release of any Aetos repository. The rule lives in the reference\npage [Versioning & Changelog](../lib/91_versioning_and_changelog/); this is the\nrunbook you follow.\n\nEvery repository is one of two Drone shapes — a **PyPI package** or a **Docker\ndeployment** ([Drone Pipelines](../lib/90_drone_pipelines/)). Both release the\nsame way: the version bump and changelog promotion go **in the merge request**,\nand the tag is created on the merged commit.\n\n---\n\n## 🧾 Before you start\n\n- You have an open **merge request** holding the change, and `main` is green\n (the Drone quality gate passed on the last run).\n- You know whether this is a **breaking**, **feature**, or **fix** release.\n- The version file exists:\n - package → `version` in `pyproject.toml`\n - service → `VERSION` at the repo root\n\n---\n\n## 🚦 Pick the next version\n\nThe version file and the tag must be identical. For `0.y.z`, `0.y` is the major\nline (see the reference page); once at `1.0.0`, normal SemVer applies.\n\n| Change | `0.y.z` → | `1.0.0+` → |\n|--------|-----------|------------|\n| Breaking | `0.(y+1).0` | `(x+1).0.0` |\n| Feature | `0.y.(z+1)` | `x.(y+1).0` |\n| Fix | `0.y.(z+1)` | `x.y.(z+1)` |\n\n---\n\n## 🌿 Release in the MR\n\nThe version is known while the change is still a merge request, so the bump\nrides along with it. Do all edits **in the MR**:\n\n- **Reconcile the changelog first.** `[Unreleased]` is a **draft, not the source\n of truth**: diff the change against the last tag\n (`git log <last-tag>..HEAD`, `git diff --stat <last-tag>..HEAD`) and add an\n entry for every user-facing change that is missing. **Always show the proposed\n additions to the user and wait for approval** before promoting — ask rather\n than guess when an entry is ambiguous.\n\n- **Promote the changelog** in `CHANGELOG.md`: move the (now reconciled)\n `[Unreleased]` content under a new `## [x.y.z] - YYYY-MM-DD` heading, leave a\n fresh empty `[Unreleased]` on top, and add the compare links — naming the\n **version/tag**, never a commit id:\n\n ```markdown\n [Unreleased]: https://git.aetoskia.com/aetos/<repo>/compare/x.y.z...HEAD\n [x.y.z]: https://git.aetoskia.com/aetos/<repo>/compare/x.y.(z-1)...x.y.z\n ```\n\n- **Bump the version file** to the intended release version: `version = \"x.y.z\"`\n in `pyproject.toml` (packages) or the `VERSION` file (services).\n\n---\n\n## 🏷️ Tag the merged commit\n\n1. **Merge or squash** the MR into `main`.\n2. **Create the tag** on the resulting commit: `git tag x.y.z` (lightweight).\n3. **Push the tag** (`git push origin x.y.z`) — Drone builds the release.\n\n### 🤖 Agent-assisted tagging\n\nThe agent does not author or merge the MR and never tags before the merge. After\nyou confirm the MR is merged/squashed and tell it to proceed, the agent:\n\n1. Waits for that explicit go-ahead.\n2. Finds the commit that introduced the bump:\n - package:\n `git log -S 'version = \"x.y.z\"' -n 1 --format='%H %s' -- pyproject.toml`\n - service:\n `git log -S 'x.y.z' -n 1 --format='%H %s' -- VERSION`\n3. Verifies that commit's version file equals `x.y.z`, then tags **only** that\n commit: `git tag x.y.z <sha>`.\n4. Does **not** push — it asks you to run `git push origin x.y.z`.\n\n---\n\n## 🤖 What Drone does\n\n- **Type A (package):** asserts `tag == pyproject version`, then skips if that\n version already exists on the index, runs the quality gate, builds, and\n uploads the wheel + sdist.\n- **Type B (service):** asserts `tag == VERSION`, resolves the tag, skips if the\n image already exists, runs the quality gate, builds, pushes, and restarts the\n container.\n\nIf the guard fails, nothing is published — fix the version file, amend the\ncommit, and move the tag **only** if it was never pushed.\n\n---\n\n## 🛟 Backfill / recovery\n\nIf an older tag never published (a past version-file mismatch, an index\noutage):\n\n- **Package:** run the matching `backfill-*-from-tags` pipeline (custom event) —\n `backfill-pypi-from-tags` for Python, `backfill-npm-from-tags` for npm. It\n checks out every tag, verifies `tag ==` the version file at that commit, and\n uploads the ones missing from the index. Already-fixed history is never\n rewritten.\n- **Service:** rebuild the image for the missing tag with a fresh commit and a\n new patch version; do not move a published tag.\n\n---\n\n## 🔁 If a release is wrong\n\n- Never reuse or force-move a pushed tag.\n- Fix forward: bump to the next patch, promote the changelog, tag again.\n\n---\n\n## Related\n\n- [Testing & Quality Gates](01_testing.md) · [Versioning & Changelog](../lib/91_versioning_and_changelog/) · [Drone Pipelines](../lib/90_drone_pipelines/)\n",
|
||||||
"objects": {}
|
"objects": {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -38,7 +38,7 @@
|
|||||||
{
|
{
|
||||||
"name": "doc-weaver",
|
"name": "doc-weaver",
|
||||||
"type": "package",
|
"type": "package",
|
||||||
"path": "doc-weaver",
|
"path": "react/doc-weaver",
|
||||||
"version_source": "package.json",
|
"version_source": "package.json",
|
||||||
"drone": "npm-lib.yaml",
|
"drone": "npm-lib.yaml",
|
||||||
"trigger": "tag",
|
"trigger": "tag",
|
||||||
|
|||||||
@@ -87,3 +87,70 @@ steps:
|
|||||||
trigger:
|
trigger:
|
||||||
event:
|
event:
|
||||||
- tag
|
- tag
|
||||||
|
|
||||||
|
---
|
||||||
|
kind: pipeline
|
||||||
|
type: docker
|
||||||
|
name: backfill-npm-from-tags
|
||||||
|
|
||||||
|
platform:
|
||||||
|
os: linux
|
||||||
|
arch: arm64
|
||||||
|
|
||||||
|
workspace:
|
||||||
|
path: /drone/src
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: fetch-tags
|
||||||
|
image: alpine/git
|
||||||
|
commands:
|
||||||
|
- git fetch --tags --force
|
||||||
|
|
||||||
|
- name: build-and-publish-missing
|
||||||
|
image: node:22-alpine
|
||||||
|
environment:
|
||||||
|
NPM_REPO_URL:
|
||||||
|
from_secret: NPM_REPO_URL
|
||||||
|
NPM_USERNAME:
|
||||||
|
from_secret: NPM_USERNAME
|
||||||
|
NPM_PASSWORD:
|
||||||
|
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
|
||||||
|
- |
|
||||||
|
set -e
|
||||||
|
|
||||||
|
PACKAGE_NAME=$(node -p "require('./package.json').name")
|
||||||
|
echo "Package: $PACKAGE_NAME"
|
||||||
|
|
||||||
|
for TAG in $(git tag --sort=version:refname); do
|
||||||
|
VERSION="$TAG"
|
||||||
|
echo "Version: $VERSION"
|
||||||
|
|
||||||
|
if npm view "$PACKAGE_NAME@$VERSION" version --userconfig /tmp/.npmrc >/dev/null 2>&1; then
|
||||||
|
echo "Exists, skipping"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
git checkout --force "$TAG"
|
||||||
|
|
||||||
|
TAG_VERSION=$(node -p "require('./package.json').version")
|
||||||
|
if [ "$TAG" != "$TAG_VERSION" ]; then
|
||||||
|
echo "Tag $TAG != package.json version $TAG_VERSION at that commit; skipping"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Building $VERSION"
|
||||||
|
rm -rf dist
|
||||||
|
npm install
|
||||||
|
npm run build
|
||||||
|
|
||||||
|
echo "Publishing $VERSION"
|
||||||
|
npm publish --access restricted --userconfig /tmp/.npmrc
|
||||||
|
done
|
||||||
|
|
||||||
|
trigger:
|
||||||
|
event:
|
||||||
|
- custom
|
||||||
|
|||||||
@@ -52,17 +52,24 @@ Secrets: `PIP_REPO_URL`, `PIP_USERNAME`, `PIP_PASSWORD`.
|
|||||||
|
|
||||||
## Type B — npm package (`npm-lib.yaml`)
|
## Type B — npm package (`npm-lib.yaml`)
|
||||||
|
|
||||||
For internal npm libraries (the React packages). One pipeline,
|
For internal npm libraries (the React packages). The file contains **two
|
||||||
**`build-and-publish-npm`**, triggered on `tag`:
|
pipelines**:
|
||||||
|
|
||||||
- `check-version` — **guard**: fail unless `$DRONE_TAG` equals the version in
|
1. **`build-and-publish-npm`** — triggered on `tag`:
|
||||||
`package.json`; query `$NPM_REPO_URL` for `name@version`; if it already
|
- `check-version` — **guard**: fail unless `$DRONE_TAG` equals the version in
|
||||||
exists, exit `78` and skip the whole build.
|
`package.json`; query `$NPM_REPO_URL` for `name@version`; if it already
|
||||||
- `quality-gate` — write `.npmrc` from the secrets, `npm install`, then
|
exists, exit `78` and skip the whole build.
|
||||||
`npm run quality-gate`.
|
- `quality-gate` — write `.npmrc` from the secrets, `npm install`, then
|
||||||
- `build-package` — `npm install && npm run build` into `dist/`.
|
`npm run quality-gate`.
|
||||||
- `publish-to-private-npm` — `npm publish --access restricted` to
|
- `build-package` — `npm install && npm run build` into `dist/`.
|
||||||
`$NPM_REPO_URL`.
|
- `publish-to-private-npm` — `npm publish --access restricted` to
|
||||||
|
`$NPM_REPO_URL`.
|
||||||
|
2. **`backfill-npm-from-tags`** — triggered on `custom`:
|
||||||
|
- `fetch-tags` — `git fetch --tags --force`.
|
||||||
|
- `build-and-publish-missing` — walk every tag in version order, skip the
|
||||||
|
ones already on the registry, `git checkout` the rest, verify
|
||||||
|
`tag == package.json version` at that commit, build, and publish. This is
|
||||||
|
the disaster-recovery / replay path.
|
||||||
|
|
||||||
Secrets: `NPM_REPO_URL`, `NPM_USERNAME`, `NPM_PASSWORD`. Auth is a generated
|
Secrets: `NPM_REPO_URL`, `NPM_USERNAME`, `NPM_PASSWORD`. Auth is a generated
|
||||||
`.npmrc` (`registry=` + `_auth=` base64 of `user:pass`) written from secrets at
|
`.npmrc` (`registry=` + `_auth=` base64 of `user:pass`) written from secrets at
|
||||||
@@ -71,6 +78,15 @@ runtime — the committed `.npmrc` in a repo only points at the registry URL.
|
|||||||
- **Ship as-is.** Name and version come from `package.json`, so the same file
|
- **Ship as-is.** Name and version come from `package.json`, so the same file
|
||||||
is reused verbatim across every npm package.
|
is reused verbatim across every npm package.
|
||||||
|
|
||||||
|
> **Registry credentials are shared.** The private npm registry (Verdaccio at
|
||||||
|
> `npm.aetoskia.com`) authenticates against the **same `htpasswd` file** as the
|
||||||
|
> private PyPI index and the Docker registry (`/auth/htpasswd`), with
|
||||||
|
> registration disabled (`max_users: -1`). `NPM_USERNAME` / `NPM_PASSWORD` are
|
||||||
|
> therefore the **same credentials as `PIP_USERNAME` / `PIP_PASSWORD`** — no
|
||||||
|
> separate npm account exists. Verdaccio's htpasswd plugin accepts **bcrypt**
|
||||||
|
> hashes (`$2y$…`); non-bcrypt entries that pypiserver tolerates will not
|
||||||
|
> authenticate against npm. See [93 – Library Registry](93_library_registry.md).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Type C — Docker deployment (`service-docker.yaml`)
|
## Type C — Docker deployment (`service-docker.yaml`)
|
||||||
@@ -121,8 +137,8 @@ Placeholders to fill in per repo:
|
|||||||
`package.json` version; a service needs a root `VERSION` file. See
|
`package.json` version; a service needs a root `VERSION` file. See
|
||||||
[91 – Versioning & Changelog](91_versioning_and_changelog.md).
|
[91 – Versioning & Changelog](91_versioning_and_changelog.md).
|
||||||
- Do not rename the pipelines; tooling and runbooks key off
|
- Do not rename the pipelines; tooling and runbooks key off
|
||||||
`build-and-publish-pypi`, `build-and-publish-npm`, and
|
`build-and-publish-pypi`, `build-and-publish-npm`,
|
||||||
`build-and-deploy-service`.
|
`build-and-deploy-service`, and their `backfill-*-from-tags` companions.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -53,6 +53,17 @@ endpoint (or, for the auth-server service, a spec pointer). It also serves the
|
|||||||
pinned auth-server OpenAPI contract at `docs://spec`. Query it before writing
|
pinned auth-server OpenAPI contract at `docs://spec`. Query it before writing
|
||||||
any infrastructure.
|
any infrastructure.
|
||||||
|
|
||||||
|
## Publishing & registry auth
|
||||||
|
|
||||||
|
Python packages publish to the private index (`pip.aetoskia.com`, `pypiserver`)
|
||||||
|
and npm packages to the private registry (`npm.aetoskia.com`, Verdaccio). Both
|
||||||
|
authenticate against the **same `htpasswd` file** (`/auth/htpasswd`) used by the
|
||||||
|
Docker registry and the Traefik basic-auth middleware; registration is disabled,
|
||||||
|
so the CI credentials (`PIP_USERNAME` / `PIP_PASSWORD`, `NPM_USERNAME` /
|
||||||
|
`NPM_PASSWORD`) are one and the same account — there is no separate npm user.
|
||||||
|
Verdaccio requires **bcrypt** htpasswd entries; see
|
||||||
|
[90 – Drone Pipelines](90_drone_pipelines.md) for the CI wiring.
|
||||||
|
|
||||||
## Notes
|
## Notes
|
||||||
|
|
||||||
- **Hexa** is a full library with its own MCP server on `:8008`: read
|
- **Hexa** is a full library with its own MCP server on `:8008`: read
|
||||||
|
|||||||
@@ -26,7 +26,7 @@
|
|||||||
{
|
{
|
||||||
"name": "doc-weaver",
|
"name": "doc-weaver",
|
||||||
"type": "package",
|
"type": "package",
|
||||||
"path": "doc-weaver",
|
"path": "react/doc-weaver",
|
||||||
"version_source": "package.json",
|
"version_source": "package.json",
|
||||||
"drone": "npm-lib.yaml",
|
"drone": "npm-lib.yaml",
|
||||||
"trigger": "tag",
|
"trigger": "tag",
|
||||||
|
|||||||
@@ -87,3 +87,70 @@ steps:
|
|||||||
trigger:
|
trigger:
|
||||||
event:
|
event:
|
||||||
- tag
|
- tag
|
||||||
|
|
||||||
|
---
|
||||||
|
kind: pipeline
|
||||||
|
type: docker
|
||||||
|
name: backfill-npm-from-tags
|
||||||
|
|
||||||
|
platform:
|
||||||
|
os: linux
|
||||||
|
arch: arm64
|
||||||
|
|
||||||
|
workspace:
|
||||||
|
path: /drone/src
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: fetch-tags
|
||||||
|
image: alpine/git
|
||||||
|
commands:
|
||||||
|
- git fetch --tags --force
|
||||||
|
|
||||||
|
- name: build-and-publish-missing
|
||||||
|
image: node:22-alpine
|
||||||
|
environment:
|
||||||
|
NPM_REPO_URL:
|
||||||
|
from_secret: NPM_REPO_URL
|
||||||
|
NPM_USERNAME:
|
||||||
|
from_secret: NPM_USERNAME
|
||||||
|
NPM_PASSWORD:
|
||||||
|
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
|
||||||
|
- |
|
||||||
|
set -e
|
||||||
|
|
||||||
|
PACKAGE_NAME=$(node -p "require('./package.json').name")
|
||||||
|
echo "Package: $PACKAGE_NAME"
|
||||||
|
|
||||||
|
for TAG in $(git tag --sort=version:refname); do
|
||||||
|
VERSION="$TAG"
|
||||||
|
echo "Version: $VERSION"
|
||||||
|
|
||||||
|
if npm view "$PACKAGE_NAME@$VERSION" version --userconfig /tmp/.npmrc >/dev/null 2>&1; then
|
||||||
|
echo "Exists, skipping"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
git checkout --force "$TAG"
|
||||||
|
|
||||||
|
TAG_VERSION=$(node -p "require('./package.json').version")
|
||||||
|
if [ "$TAG" != "$TAG_VERSION" ]; then
|
||||||
|
echo "Tag $TAG != package.json version $TAG_VERSION at that commit; skipping"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Building $VERSION"
|
||||||
|
rm -rf dist
|
||||||
|
npm install
|
||||||
|
npm run build
|
||||||
|
|
||||||
|
echo "Publishing $VERSION"
|
||||||
|
npm publish --access restricted --userconfig /tmp/.npmrc
|
||||||
|
done
|
||||||
|
|
||||||
|
trigger:
|
||||||
|
event:
|
||||||
|
- custom
|
||||||
|
|||||||
@@ -104,9 +104,10 @@ commit, and move the tag **only** if it was never pushed.
|
|||||||
If an older tag never published (a past version-file mismatch, an index
|
If an older tag never published (a past version-file mismatch, an index
|
||||||
outage):
|
outage):
|
||||||
|
|
||||||
- **Package:** run the `backfill-pypi-from-tags` pipeline (custom event). It
|
- **Package:** run the matching `backfill-*-from-tags` pipeline (custom event) —
|
||||||
checks out every tag, verifies `tag == pyproject version` at that commit,
|
`backfill-pypi-from-tags` for Python, `backfill-npm-from-tags` for npm. It
|
||||||
and uploads the ones missing from the index. Already-fixed history is never
|
checks out every tag, verifies `tag ==` the version file at that commit, and
|
||||||
|
uploads the ones missing from the index. Already-fixed history is never
|
||||||
rewritten.
|
rewritten.
|
||||||
- **Service:** rebuild the image for the missing tag with a fresh commit and a
|
- **Service:** rebuild the image for the missing tag with a fresh commit and a
|
||||||
new patch version; do not move a published tag.
|
new patch version; do not move a published tag.
|
||||||
|
|||||||
@@ -1011,9 +1011,10 @@ commit, and move the tag <strong>only</strong> if it was never pushed.</p>
|
|||||||
<p>If an older tag never published (a past version-file mismatch, an index
|
<p>If an older tag never published (a past version-file mismatch, an index
|
||||||
outage):</p>
|
outage):</p>
|
||||||
<ul>
|
<ul>
|
||||||
<li><strong>Package:</strong> run the <code>backfill-pypi-from-tags</code> pipeline (custom event). It
|
<li><strong>Package:</strong> run the matching <code>backfill-*-from-tags</code> pipeline (custom event) —
|
||||||
checks out every tag, verifies <code>tag == pyproject version</code> at that commit,
|
<code>backfill-pypi-from-tags</code> for Python, <code>backfill-npm-from-tags</code> for npm. It
|
||||||
and uploads the ones missing from the index. Already-fixed history is never
|
checks out every tag, verifies <code>tag ==</code> the version file at that commit, and
|
||||||
|
uploads the ones missing from the index. Already-fixed history is never
|
||||||
rewritten.</li>
|
rewritten.</li>
|
||||||
<li><strong>Service:</strong> rebuild the image for the missing tag with a fresh commit and a
|
<li><strong>Service:</strong> rebuild the image for the missing tag with a fresh commit and a
|
||||||
new patch version; do not move a published tag.</li>
|
new patch version; do not move a published tag.</li>
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user