Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Shell scripts run on Linux runners and on Git bash alike; a CRLF checkout on
# Windows (core.autocrlf=true, the Git for Windows default) would put a `\r` at
# the end of every line, which bash reports as a command not found.
*.sh text eol=lf
77 changes: 76 additions & 1 deletion .github/workflows/build-image.yml
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,32 @@ on:
type: string
required: false
default: ghcr.io
runs-on:
description: >-
Where to run the build: a single runner label, or a JSON array or
object for anything more (`'["self-hosted", "linux"]'`,
`'{"group": "internal"}'`). Defaults to ubuntu-latest.
type: string
required: false
default: ubuntu-latest
npm-registry:
description: >-
npm registry to install the build's dependencies from, for runners
that cannot reach registry.npmjs.org — an Artifactory npm remote, say.
Empty means whatever npm resolves on the runner. The lockfile needs
no change: npm rewrites its registry.npmjs.org hosts to this registry.
type: string
required: false
default: ''
node-mirror:
description: >-
Mirror of https://nodejs.org to download Node from, for runners that
can reach neither nodejs.org nor the github.com release assets
`setup-node` tries first. Not needed when the runner image already
carries Node in its tool cache.
type: string
required: false
default: ''
secrets:
docs-token:
description: >-
Expand All @@ -74,6 +100,14 @@ on:
required: false
registry-password:
required: false
npm-token:
description: >-
Bearer token for `npm-registry`. Leave unset for a registry that
allows anonymous reads.
required: false
node-mirror-token:
description: Authorization header value for `node-mirror`.
required: false
outputs:
image:
description: 'The first pushed image reference, when one was pushed.'
Expand All @@ -85,7 +119,10 @@ permissions:
jobs:
build:
name: Build
runs-on: ubuntu-latest
# A label is used as it is; anything starting with `[` or `{` is parsed, so
# a caller can name several labels or a runner group. `&&`/`||` short-circuit,
# so fromJSON never sees a bare label.
runs-on: ${{ (startsWith(inputs.runs-on, '[') || startsWith(inputs.runs-on, '{')) && fromJSON(inputs.runs-on) || inputs.runs-on }}
outputs:
image: ${{ steps.push.outputs.image }}
steps:
Expand All @@ -108,9 +145,44 @@ jobs:
node-version: '24'
cache: npm
cache-dependency-path: knowledge-base/package-lock.json
mirror: ${{ inputs.node-mirror }}
mirror-token: ${{ secrets.node-mirror-token }}

# Same mechanism as the publishing actions (actions/lib/npm-registry.sh):
# project-level npm config next to the lockfile, applied to this install
# and nothing else. Inlined rather than run from the checkout so that a
# caller pinning an older kb-ref still gets the registry it asked for.
- name: Point npm at a private registry
if: ${{ inputs.npm-registry != '' }}
working-directory: knowledge-base
env:
KB_NPM_REGISTRY: ${{ inputs.npm-registry }}
KB_NPM_TOKEN: ${{ secrets.npm-token }}
run: |
set -euo pipefail
case "$KB_NPM_REGISTRY" in
*[[:space:]]*)
echo "::error::npm-registry must be a single URL with no whitespace, got '$KB_NPM_REGISTRY'"
exit 1 ;;
http://*|https://*) ;;
*)
echo "::error::npm-registry must be an http(s) URL, got '$KB_NPM_REGISTRY'"
exit 1 ;;
esac
registry="${KB_NPM_REGISTRY%/}/"
{
echo "registry=${registry}"
# A reference npm expands at run time, so the token never lands on disk.
if [ -n "${KB_NPM_TOKEN:-}" ]; then
echo "${registry#*:}:_authToken=\${KB_NPM_TOKEN}"
fi
} > .npmrc
echo "npm installs from ${registry}${KB_NPM_TOKEN:+ (authenticated)}"

- name: Install
working-directory: knowledge-base
env:
KB_NPM_TOKEN: ${{ secrets.npm-token }}
run: npm ci

- name: Resolve the registry
Expand Down Expand Up @@ -141,6 +213,9 @@ jobs:
# self-hosted runner need not have. github.token covers public
# docs repos; private ones need docs-token.
GITHUB_TOKEN: ${{ secrets.docs-token || github.token }}
# The docs repos and the registry are the only things a build should
# talk to. Matters on a runner whose egress is a short allowlist.
ASTRO_TELEMETRY_DISABLED: '1'
run: node scripts/build-vite.js ${{ inputs.headless && '--headless' || '' }}

# Without an image name this is a dry run: prove the registry builds, keep
Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,9 @@ jobs:
kb-ref: ${{ github.sha }}
registry: apps.json
strict: false
# The JSON form, so the branch of the runs-on expression that parses it
# runs here; the bare-label default is what every other caller exercises.
runs-on: '["ubuntu-latest"]'
# ── 5. Container image ─────────────────────────────────────────────────────
#
# Builds the runtime image from the dist/ the build job produced, then scans
Expand Down
11 changes: 11 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,17 @@ anything there, rebuild, then run it.
repository must carry `"optional": true` so a fresh clone still builds. Never
add a registry entry that requires network or a sibling checkout without it.

### Lockfiles resolve to registry.npmjs.org

Every `resolved` in `package-lock.json` and `actions/package-lock.json` must
point at `https://registry.npmjs.org/`, and `tests/private-registry.spec.js`
fails the build otherwise. Consumers on private networks install from an
internal mirror by *configuring* a registry; npm rewrites the default host to
it at fetch time, but only the default host. A lockfile regenerated behind a
corporate `~/.npmrc` bakes that registry's URLs in and installs in one network
only. If your machine has such an `.npmrc`, run
`npm install --registry=https://registry.npmjs.org/` when touching either lockfile.

### Two build modes, one document

Every page renders through `src/layouts/Base.astro`. Headless (web-fragment) and
Expand Down
4 changes: 4 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,10 @@ commands listed in `AGENTS.md`:
- `artifact-safety.spec.js` — tarball extraction guards (traversal, absolute paths, symlinks).
- `nginx-config.spec.js` — static assertions on `nginx.conf`/`nginx.headers.conf`, including
that the CSP the Express mirror serves is byte-identical to nginx's.
- `private-registry.spec.js` — both lockfiles resolve to `registry.npmjs.org` (npm rewrites
only that host to a configured mirror), the `npm-registry`/`npm-token`/`node-mirror` inputs
exist on both actions and `build-image.yml`, and `actions/lib/npm-registry.sh` writes the
project `.npmrc` without ever putting the token on disk.
- `standalone.spec.js` — the `:3000` fragment server directly (`playwright.config.ci.js`).
- `container.spec.js` — the real nginx image (`playwright.config.docker.js`, needs Docker).
- `support/fragment.js` — shadow-DOM traversal + reframed-body wait/query helpers.
Expand Down
4 changes: 4 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ The build and tests are fully hermetic — they use the committed
`tests/fixtures/docs-example.dist.tar.gz` fixture (registered via `apps.json`),
so no `GITHUB_TOKEN`, network, or sibling repository is required.

If your `~/.npmrc` points at a corporate registry, install with
`--registry=https://registry.npmjs.org/` before committing a lockfile change:
both lockfiles must keep resolving to the public registry (see `AGENTS.md`).

See [`CLAUDE.md`](CLAUDE.md) for an architecture overview and the full command
list, and [`README.md`](README.md) for usage.

Expand Down
69 changes: 69 additions & 0 deletions actions/lib/npm-registry.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
#!/usr/bin/env bash
# Points the `npm ci` that follows at a private npm registry.
#
# Consuming repositories may run on runners inside a private network where
# registry.npmjs.org is unreachable and every package has to come from an
# internal mirror (a JFrog Artifactory npm remote, typically). The lockfiles in
# this repository resolve every package to registry.npmjs.org, and that is fine:
# npm's `replace-registry-host` (default `npmjs`) rewrites that host to the
# configured registry at fetch time, and the lockfile's integrity hashes still
# verify because the mirror serves the same tarballs. All that is missing is a
# way to say which registry — this script.
#
# Usage: npm-registry.sh [DIR]
#
# KB_NPM_REGISTRY the registry URL. Empty: write nothing, so npm resolves
# the registry the ordinary way — registry.npmjs.org, or
# whatever the runner's own npm configuration says.
# KB_NPM_TOKEN optional bearer token for that registry.
#
# Writes DIR/.npmrc (default: the current directory), which npm reads as
# project-level config for anything installed from DIR. Project config layers
# on top of the runner's user and global config rather than replacing it, and
# it reaches no other step of the calling workflow.
#
# The token is written as a `${KB_NPM_TOKEN}` reference, which npm expands from
# the environment when it runs, so the secret never lands on disk. The step
# that runs `npm ci` must therefore carry KB_NPM_TOKEN in its environment.
set -euo pipefail

dir="${1:-.}"
registry="${KB_NPM_REGISTRY:-}"
token="${KB_NPM_TOKEN:-}"

if [ -z "$registry" ]; then
exit 0
fi

# An .npmrc is line-oriented; anything that is not a single http(s) URL is
# either a typo or an attempt to smuggle a second setting in, and both should
# fail before npm sees them.
case "$registry" in
*[[:space:]]*)
echo "::error::npm-registry must be a single URL with no whitespace, got '$registry'"
exit 1
;;
http://*|https://*) ;;
*)
echo "::error::npm-registry must be an http(s) URL, got '$registry'"
exit 1
;;
esac

# npm keys credentials by the registry URL without its scheme, trailing slash
# included, so normalise to exactly one.
registry="${registry%/}/"
auth_key="${registry#*:}"

{
echo "registry=${registry}"
if [ -n "$token" ]; then
echo "${auth_key}:_authToken=\${KB_NPM_TOKEN}"
fi
} > "${dir}/.npmrc"

if [ -n "$token" ]; then
echo "npm installs from ${registry} (authenticated)"
else
echo "npm installs from ${registry}"
fi
36 changes: 36 additions & 0 deletions actions/publish-docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@ markdown for you and needs no manifest at all.
| `github-token` | ☐ | `${{ github.token }}` | Needs `contents: write` |
| `notify-repo` | ☐ | — | `owner/name` of a deployment repo to notify on publish |
| `notify-token` | ☐ | — | Token with `contents: write` on `notify-repo` only |
| `npm-registry` | ☐ | — | npm registry to install the action's dependencies from, for runners that cannot reach `registry.npmjs.org` |
| `npm-token` | ☐ | — | Bearer token for `npm-registry` |
| `node-mirror` | ☐ | — | Mirror of `https://nodejs.org` for `setup-node`, when the runner reaches neither nodejs.org nor github.com release assets |
| `node-mirror-token` | ☐ | — | `Authorization` header value for `node-mirror` |

`dist` is the app's own directory when the manifest declares **one** app — a repo
publishing a single site should not have to invent a subdirectory named after its
Expand Down Expand Up @@ -92,6 +96,38 @@ GitHub-hosted and self-hosted runners:
The runner must be new enough for Node 24 actions (`actions/runner` ≥ 2.327.1),
which `actions/setup-node@v7` already requires.

### Runners in a private network

If your runners cannot reach `registry.npmjs.org` and every package has to come
from an internal mirror — an Artifactory npm remote, typically — point the
action at it:

```yaml
- uses: AbsaOSS/knowledge-base/actions/publish-docs@v1
with:
manifest: kb-docs.json
dist: dist
npm-registry: https://artifactory.example.com/artifactory/api/npm/npm-remote/
npm-token: ${{ secrets.ARTIFACTORY_TOKEN }} # omit for anonymous reads
```

The action's lockfile resolves every package to `registry.npmjs.org` and stays
that way: npm rewrites that host to the configured registry when it fetches
(`replace-registry-host`), and the lockfile's integrity hashes still verify
because the mirror serves the same tarballs. The registry is applied as
project-level npm config for this install only — the runner's own npm
configuration and the rest of your workflow are untouched, and the token is
read from the environment rather than written to disk.

A runner whose own `~/.npmrc` already names the mirror needs none of this: leave
`npm-registry` empty and npm uses what the machine says.

`setup-node` fetches Node from the `actions/node-versions` releases on
github.com, falling back to nodejs.org. If both are blocked, `node-mirror`
names a mirror of `https://nodejs.org` (an Artifactory generic remote works),
with `node-mirror-token` as its `Authorization` header. A runner image that
already carries Node 20 in its tool cache downloads nothing.

## Notifying a deployment

Set `notify-repo` and `notify-token` to fire a `kb-docs-published`
Expand Down
46 changes: 46 additions & 0 deletions actions/publish-docs/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,32 @@ inputs:
`github.token` cannot dispatch to another repository.
required: false
default: ''
npm-registry:
description: >-
npm registry to install the action's own dependencies from, for runners
that cannot reach registry.npmjs.org — an Artifactory npm remote, say.
Empty means whatever npm resolves on the runner, which is the public
registry unless the machine's npm configuration says otherwise.
required: false
default: ''
npm-token:
description: >-
Bearer token for `npm-registry`. Leave empty for a registry that allows
anonymous reads.
required: false
default: ''
node-mirror:
description: >-
Mirror of https://nodejs.org to download Node from, for runners that can
reach neither nodejs.org nor the github.com release assets `setup-node`
tries first. Not needed when the runner image already carries Node in its
tool cache.
required: false
default: ''
node-mirror-token:
description: Authorization header value for `node-mirror`.
required: false
default: ''

outputs:
slugs:
Expand All @@ -72,10 +98,30 @@ runs:
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: '20'
mirror: ${{ inputs.node-mirror }}
mirror-token: ${{ inputs.node-mirror-token }}

# A private registry is applied as project-level npm config next to the
# lockfile, so it reaches nothing outside this install: not the runner's own
# npm configuration, not the calling workflow's later steps. The lockfile
# itself needs no change — npm rewrites its registry.npmjs.org hosts to the
# configured registry (`replace-registry-host`). Skipped when unset.
- name: Point npm at a private registry
if: ${{ inputs.npm-registry != '' }}
shell: bash
working-directory: ${{ github.action_path }}/..
env:
KB_NPM_REGISTRY: ${{ inputs.npm-registry }}
KB_NPM_TOKEN: ${{ inputs.npm-token }}
run: bash "$GITHUB_ACTION_PATH/../lib/npm-registry.sh"

- name: Install publisher dependencies
shell: bash
working-directory: ${{ github.action_path }}/..
env:
# Expanded by npm from the .npmrc the previous step wrote; the token is
# never written to disk.
KB_NPM_TOKEN: ${{ inputs.npm-token }}
run: npm ci --omit=dev --no-audit --no-fund

- name: Verify and pack the artifact
Expand Down
Loading