diff --git a/.github/actions/setup-npm/action.yml b/.github/actions/setup-npm/action.yml new file mode 100644 index 000000000..b8e997099 --- /dev/null +++ b/.github/actions/setup-npm/action.yml @@ -0,0 +1,62 @@ +name: 'Setup npm with Nexus' +description: 'Starts Nexus, publishes patched packages, then runs npm install' + +inputs: + node-version: + description: 'Node.js version' + required: false + default: '20' + +runs: + using: composite + steps: + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: ${{ inputs.node-version }} + + - name: Cache Nexus data + uses: actions/cache@v4 + with: + path: /tmp/nexus-data + key: nexus-data-v1-${{ hashFiles('deployment/npm-patches/**', 'deployment/nexus-init.sh') }} + restore-keys: | + nexus-data-v1- + + - name: Start Nexus + shell: bash + run: | + docker run -d --name nexus \ + -p 8091:8081 \ + -v /tmp/nexus-data:/nexus-data \ + --platform linux/amd64 \ + sonatype/nexus3:3.75.0 + echo "Nexus starting..." + + - name: Wait for Nexus + shell: bash + run: | + echo "Waiting for Nexus to be healthy (up to 3 minutes)..." + timeout 180 bash -c ' + until curl -sf http://localhost:8091/service/rest/v1/status -u admin:nexus >/dev/null 2>&1 || \ + curl -sf http://localhost:8091/service/rest/v1/status >/dev/null 2>&1; do + echo " still waiting..." + sleep 10 + done + ' + echo "Nexus is up" + + - name: Initialise Nexus (npm repos) + shell: bash + env: + NEXUS_URL: http://localhost:8091 + NEXUS_ADMIN_NEW_PASS: nexus + run: bash deployment/nexus-ci-init.sh + + - name: Publish patched npm packages + shell: bash + run: bash deployment/publish-npm-patches.sh + + - name: Install npm dependencies + shell: bash + run: npm install diff --git a/.github/workflows/gated-pipeline.yml b/.github/workflows/gated-pipeline.yml index 28273468c..9e3f321cc 100644 --- a/.github/workflows/gated-pipeline.yml +++ b/.github/workflows/gated-pipeline.yml @@ -398,13 +398,10 @@ jobs: - name: Checkout code uses: actions/checkout@v4 - - name: Setup Node - uses: actions/setup-node@v4 + - name: Setup npm with Nexus + uses: ./.github/actions/setup-npm with: - node-version: 20 - - - name: Install dependencies - run: npm install + node-version: '20' - name: Build workspace packages run: npm run build --workspaces --if-present 2>&1 || true @@ -435,13 +432,10 @@ jobs: - name: Checkout code uses: actions/checkout@v4 - - name: Setup Node - uses: actions/setup-node@v4 + - name: Setup npm with Nexus + uses: ./.github/actions/setup-npm with: - node-version: 20 - - - name: Install dependencies - run: npm install + node-version: '20' - name: Build workspace packages run: npm run build --workspaces --if-present 2>&1 || true @@ -483,13 +477,10 @@ jobs: - name: Checkout code uses: actions/checkout@v4 - - name: Setup Node - uses: actions/setup-node@v4 + - name: Setup npm with Nexus + uses: ./.github/actions/setup-npm with: - node-version: 20 - - - name: Install dependencies - run: npm install + node-version: '20' - name: Run dependency audit run: | @@ -696,13 +687,10 @@ jobs: - name: Checkout code uses: actions/checkout@v4 - - name: Setup Node - uses: actions/setup-node@v4 + - name: Setup npm with Nexus + uses: ./.github/actions/setup-npm with: - node-version: 20 - - - name: Install dependencies - run: npm install + node-version: '20' - name: Run unit tests with coverage run: | @@ -769,13 +757,10 @@ jobs: - name: Checkout code uses: actions/checkout@v4 - - name: Setup Node - uses: actions/setup-node@v4 + - name: Setup npm with Nexus + uses: ./.github/actions/setup-npm with: - node-version: 20 - - - name: Install dependencies - run: npm install + node-version: '20' - name: Install Playwright Browsers run: npx playwright install --with-deps chromium @@ -819,13 +804,10 @@ jobs: - name: Checkout code uses: actions/checkout@v4 - - name: Setup Node - uses: actions/setup-node@v4 + - name: Setup npm with Nexus + uses: ./.github/actions/setup-npm with: - node-version: 20 - - - name: Install dependencies - run: npm install + node-version: '20' - name: Install Playwright Browsers run: npx playwright install --with-deps chromium @@ -932,13 +914,10 @@ jobs: - name: Checkout code uses: actions/checkout@v4 - - name: Setup Node - uses: actions/setup-node@v4 + - name: Setup npm with Nexus + uses: ./.github/actions/setup-npm with: - node-version: 20 - - - name: Install dependencies - run: npm install + node-version: '20' - name: Build workspace packages run: npm run build --workspaces --if-present 2>&1 || true @@ -1226,13 +1205,10 @@ jobs: - name: Checkout code uses: actions/checkout@v4 - - name: Setup Node - uses: actions/setup-node@v4 + - name: Setup npm with Nexus + uses: ./.github/actions/setup-npm with: - node-version: 20 - - - name: Install dependencies - run: npm install + node-version: '20' - name: Build for staging run: npm run build -w frontends/nextjs @@ -1279,13 +1255,10 @@ jobs: - name: Checkout code uses: actions/checkout@v4 - - name: Setup Node - uses: actions/setup-node@v4 + - name: Setup npm with Nexus + uses: ./.github/actions/setup-npm with: - node-version: 20 - - - name: Install dependencies - run: npm install + node-version: '20' - name: Build for production run: npm run build -w frontends/nextjs diff --git a/.npmrc b/.npmrc index 3d3102336..9ea09f42a 100644 --- a/.npmrc +++ b/.npmrc @@ -42,9 +42,8 @@ workspaces-update=true # These are documented in package.json engines field # Current: Node 22.22.1, npm 11.11.0 - # SCOPED NEXUS REGISTRY - @esbuild-kit patched packages -# Start Nexus: cd deployment && docker compose -f docker-compose.nexus.yml up -d -# Publish patches: cd deployment && ./publish-npm-patches.sh +# Local dev: cd deployment && docker compose -f docker-compose.nexus.yml up -d +# CI: started automatically via .github/actions/setup-npm composite action @esbuild-kit:registry=http://localhost:8091/repository/npm-group/ //localhost:8091/repository/npm-group/:_auth=YWRtaW46bmV4dXM= diff --git a/deployment/nexus-ci-init.sh b/deployment/nexus-ci-init.sh new file mode 100755 index 000000000..e61d7f081 --- /dev/null +++ b/deployment/nexus-ci-init.sh @@ -0,0 +1,88 @@ +#!/bin/sh +# Lightweight Nexus initialisation for CI — npm repos only (no Docker, no Artifactory). +# Full local dev setup uses nexus-init.sh via docker compose. +set -e + +NEXUS_URL="${NEXUS_URL:-http://localhost:8091}" +NEW_PASS="${NEXUS_ADMIN_NEW_PASS:-nexus}" +PASS_FILE="/tmp/nexus-data/admin.password" + +log() { echo "[nexus-ci-init] $*"; } + +# ── Resolve admin password ────────────────────────────────────────────────── +HTTP=$(curl -s -o /dev/null -w "%{http_code}" \ + "$NEXUS_URL/service/rest/v1/status" -u "admin:$NEW_PASS") +if [ "$HTTP" = "200" ]; then + log "Already initialised with password '$NEW_PASS'" +elif [ -f "$PASS_FILE" ]; then + INIT_PASS=$(cat "$PASS_FILE") + log "First run: setting admin password..." + curl -sf -X PUT \ + "$NEXUS_URL/service/rest/v1/security/users/admin/change-password" \ + -u "admin:$INIT_PASS" -H "Content-Type: text/plain" -d "$NEW_PASS" + log "Admin password set" +else + log "ERROR: cannot authenticate and no password file found" + exit 1 +fi + +AUTH="admin:$NEW_PASS" + +# ── Enable anonymous access ──────────────────────────────────────────────── +curl -sf -X PUT "$NEXUS_URL/service/rest/v1/security/anonymous" \ + -u "$AUTH" -H "Content-Type: application/json" \ + -d '{"enabled":true,"userId":"anonymous","realmName":"NexusAuthorizingRealm"}' || true +log "Anonymous access enabled" + +# Enable npm token realm +curl -sf -X PUT "$NEXUS_URL/service/rest/v1/security/realms/active" \ + -u "$AUTH" -H "Content-Type: application/json" \ + -d '["NexusAuthenticatingRealm","NpmToken"]' || true + +# ── npm-hosted (patched packages) ───────────────────────────────────────── +HTTP=$(curl -s -o /dev/null -w "%{http_code}" -X POST \ + "$NEXUS_URL/service/rest/v1/repositories/npm/hosted" \ + -u "$AUTH" -H "Content-Type: application/json" -d '{ + "name": "npm-hosted", + "online": true, + "storage": {"blobStoreName": "default", "strictContentTypeValidation": true, "writePolicy": "allow"} +}') +case "$HTTP" in + 201) log "npm-hosted repo created" ;; + 400) log "npm-hosted repo already exists" ;; + *) log "ERROR creating npm-hosted: HTTP $HTTP"; exit 1 ;; +esac + +# ── npm-proxy (npmjs.org cache) ─────────────────────────────────────────── +HTTP=$(curl -s -o /dev/null -w "%{http_code}" -X POST \ + "$NEXUS_URL/service/rest/v1/repositories/npm/proxy" \ + -u "$AUTH" -H "Content-Type: application/json" -d '{ + "name": "npm-proxy", + "online": true, + "storage": {"blobStoreName": "default", "strictContentTypeValidation": true}, + "proxy": {"remoteUrl": "https://registry.npmjs.org", "contentMaxAge": 1440, "metadataMaxAge": 1440}, + "httpClient": {"blocked": false, "autoBlock": true}, + "negativeCache": {"enabled": true, "timeToLive": 1440} +}') +case "$HTTP" in + 201) log "npm-proxy repo created" ;; + 400) log "npm-proxy repo already exists" ;; + *) log "ERROR creating npm-proxy: HTTP $HTTP"; exit 1 ;; +esac + +# ── npm-group (combines hosted + proxy) ────────────────────────────────── +HTTP=$(curl -s -o /dev/null -w "%{http_code}" -X POST \ + "$NEXUS_URL/service/rest/v1/repositories/npm/group" \ + -u "$AUTH" -H "Content-Type: application/json" -d '{ + "name": "npm-group", + "online": true, + "storage": {"blobStoreName": "default", "strictContentTypeValidation": true}, + "group": {"memberNames": ["npm-hosted", "npm-proxy"]} +}') +case "$HTTP" in + 201) log "npm-group repo created" ;; + 400) log "npm-group repo already exists" ;; + *) log "ERROR creating npm-group: HTTP $HTTP"; exit 1 ;; +esac + +log "Nexus CI init complete"