# Build this UI kit and publish it to a Nexus npm-hosted repository. # # This is the npm counterpart of the NuGet pipeline the same Nexus hosts: # dotnet restore/build/pack → artifacts/*.nupkg → dotnet nuget push # npm install / build → artifacts/*.tgz → npm publish # # ── What you must set before it can publish ────────────────────────────────── # Settings → CI/CD → Variables. Every one of these is *masked* and *protected* # unless noted: # # NEXUS_NPM_TOKEN A Nexus npm bearer token ("NpmToken.xxxxxxxx…"), from # Nexus → your user → NPM Bearer Token realm. Preferred. # — or — # NEXUS_USERNAME Falls back to basic auth if no token is set. The npm # NEXUS_PASSWORD client sends these base64-encoded, so both must exist. # # Nothing else is required: the registry URL is a plain variable below, not a # secret. If the `publish` job runs on a protected branch only, mark the # credential variables *protected* as well or the job cannot read them. # # ── Why the package is built here rather than by `npm publish` ─────────────── # `prepublishOnly` (build + verify) runs when you publish a *directory*. This # pipeline publishes the *tarball* produced by the earlier stage — which is the # artifact CI actually inspected — and npm runs no lifecycle scripts for that. # So `build` and `verify` are explicit steps in `package`, exactly the way the # NuGet job runs `dotnet build` before `dotnet pack --no-build`. image: node:20 stages: - check - package - publish variables: NEXUS_URL: "https://nexus.cbar.az" NEXUS_NPM_HOSTED_REPO: "npm-hosted" # The trailing slash matters: npm derives the auth key from the registry URL, # and `…/npm-hosted` and `…/npm-hosted/` are two different keys to it. NPM_REGISTRY_URL: "${NEXUS_URL}/repository/${NEXUS_NPM_HOSTED_REPO}/" # The package is scoped (@cbar/uikit), so the CI .npmrc binds the scope to # Nexus as well as setting the default registry. The scope line is what a # consuming app needs; the default line is what `npm publish` reads. Keep # this in step with `package.json#name` — it is the one thing here that has # to agree with the manifest. NPM_SCOPE: "@cbar" # Credentials are written to this file at publish time instead of the repo's # own `.npmrc`. Two reasons: a scaffolded kit may already have an `.npmrc` # this would clobber, and a token that never touches the working tree cannot # be committed by a later job or packed into a tarball. NPM_CONFIG_USERCONFIG: "${CI_PROJECT_DIR}/.npmrc.ci" # Publishing a version that is already on the registry is a 409. The NuGet # job passes `--skip-duplicate`; npm has no such flag, so the publish step # checks first and exits green. Set to "false" to make a re-publish fail. SKIP_DUPLICATE: "true" npm_config_cache: "${CI_PROJECT_DIR}/.npm" npm_config_fund: "false" npm_config_audit: "false" cache: key: files: - package.json paths: - .npm/ # `npm install`, not `npm ci`: this kit ships without a lockfile, and a kit # installed with pnpm has none either. If you commit a package-lock.json, # switch this to `npm ci` — it is faster and reproducible. # # Pulled in with `extends:` rather than a YAML `<<:` merge anchor. Both work in # GitLab, but merge keys are an optional feature of the YAML spec that several # parsers (including the one you would lint this file with locally) ignore # silently — leaving a job with no install step and a confusing "command not # found" three lines later. .install: before_script: - node --version && npm --version - npm install --no-fund --no-audit # ── check ──────────────────────────────────────────────────────────────────── # Ordered cheapest-first so a typo fails in seconds rather than after a bundle. # lint token rules (no raw colours, no `--ui-*` in components) and the # import rules (no `radix-ui` barrel, no replaced packages) # typecheck two programs — the kit, and the showcase, which has its own # test:run the whole suite, including the axe sweep and the SSR sweep check: stage: check tags: - kubernetes extends: .install script: - npm run lint - npm run typecheck - npm run test:run rules: - if: '$CI_PIPELINE_SOURCE == "merge_request_event"' - if: '$CI_COMMIT_BRANCH' # `brand:check` reports whatever still belongs to the design system this kit was # forked from: the package name, the licence holder, the Storybook chrome, the # vendored logo. It does not block, and for *this* kit it never can — the check # reads `brandTitle: 'CBAR Design System'` as "not yet rebranded", which is the # right answer for a team forking the kit and the wrong one for CBAR, who owns # that mark. So read the output, do not gate on it. # # A team that forks this kit should run `npm run brand` and then delete # `allow_failure` here, so the kit cannot drift back to somebody else's identity. identity: stage: check tags: - kubernetes extends: .install allow_failure: true script: - npm run brand:check rules: - if: '$CI_PIPELINE_SOURCE == "merge_request_event"' - if: '$CI_COMMIT_BRANCH' # ── package ────────────────────────────────────────────────────────────────── # build tsup + add-use-client + the Tailwind CLI + the generated token export # verify the export map, the 'use client' directives, publint, attw # size per-subpath byte budgets — the guard on an accidental barrel import # pack the exact tarball the publish stage uploads, nothing rebuilt in between package: stage: package tags: - kubernetes extends: .install script: - npm run build - npm run verify - npm run size - mkdir -p artifacts - npm pack --pack-destination artifacts - ls -l artifacts # What a consumer will actually receive. `files` is dist-only, so anything # else in this listing is a leak worth stopping for. - tar -tzf artifacts/*.tgz | head -40 artifacts: paths: - artifacts/*.tgz expire_in: 1 day rules: - if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH' - if: '$CI_COMMIT_TAG' # ── publish ────────────────────────────────────────────────────────────────── publish: stage: publish tags: - kubernetes dependencies: - package before_script: # `//host/path/` — npm keys its credentials by the registry URL with the # scheme stripped, so this must match NPM_REGISTRY_URL character for # character, trailing slash included. - REGISTRY_KEY="${NPM_REGISTRY_URL#http*:}" - | if [ -n "$NEXUS_NPM_TOKEN" ]; then AUTH_LINE="${REGISTRY_KEY}:_authToken=${NEXUS_NPM_TOKEN}" elif [ -n "$NEXUS_USERNAME" ] && [ -n "$NEXUS_PASSWORD" ]; then AUTH_LINE="${REGISTRY_KEY}:_auth=$(printf '%s:%s' "$NEXUS_USERNAME" "$NEXUS_PASSWORD" | base64 | tr -d '\n')" else echo "No credentials: set NEXUS_NPM_TOKEN, or NEXUS_USERNAME + NEXUS_PASSWORD." echo "See the comments at the top of .gitlab-ci.yml." exit 1 fi # Written, never echoed. `always-auth` makes npm send the credential on # reads too, which Nexus wants for `npm view` against a hosted repo. - | { echo "registry=${NPM_REGISTRY_URL}" if [ -n "$NPM_SCOPE" ]; then echo "${NPM_SCOPE}:registry=${NPM_REGISTRY_URL}"; fi echo "${AUTH_LINE}" echo "always-auth=true" } > "$NPM_CONFIG_USERCONFIG" - chmod 600 "$NPM_CONFIG_USERCONFIG" script: - PKG_NAME=$(node -p "require('./package.json').name") - PKG_VERSION=$(node -p "require('./package.json').version") - TARBALL=$(ls artifacts/*.tgz | head -1) - echo "Publishing ${PKG_NAME}@${PKG_VERSION} → ${NPM_REGISTRY_URL}" - | if [ "$SKIP_DUPLICATE" = "true" ] \ && npm view "${PKG_NAME}@${PKG_VERSION}" version --registry "$NPM_REGISTRY_URL" >/dev/null 2>&1; then echo "${PKG_NAME}@${PKG_VERSION} is already on the registry — nothing to do." echo "Bump the version (npm run changeset && npx changeset version) to publish again." exit 0 fi # `--tag` keeps a prerelease (1.2.0-rc.1) off `latest`, so an install with # no version specifier never resolves to it. - | case "$PKG_VERSION" in *-*) DIST_TAG="next" ;; *) DIST_TAG="latest" ;; esac npm publish "$TARBALL" --registry "$NPM_REGISTRY_URL" --tag "$DIST_TAG" - echo "Published ${PKG_NAME}@${PKG_VERSION} (${DIST_TAG})." after_script: - rm -f "$NPM_CONFIG_USERCONFIG" rules: - if: '$CI_COMMIT_TAG' # Push to the default branch publishes, and skips silently when the version # in package.json is already on the registry — so the pipeline is green on # every commit and does real work only when the version moves. Change to # `when: manual` if you would rather press the button yourself. - if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH'