name: Offline npm caches on: workflow_dispatch: env: NODE_VERSION: '22.18.0' FRONTEND_SHA: c53a67831db98ea10f1aa168fcab80cca97134ed BACKEND_SHA: 866be223956ef5d3961b0d835b36adedfeb35c82 jobs: caches: name: offline npm caches runs-on: ubuntu-latest timeout-minutes: 40 steps: - name: Checkout workflow repo uses: actions/checkout@v4 with: fetch-depth: 0 - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: ${{ env.NODE_VERSION }} - name: Compiler tools for native smoke run: | set -euo pipefail if ! command -v python3 >/dev/null || ! command -v make >/dev/null || ! command -v g++ >/dev/null; then sudo apt-get update sudo apt-get install -y --no-install-recommends python3 make g++ fi - name: Extract pinned sources run: | set -euo pipefail mkdir -p "$RUNNER_TEMP/src/frontend" "$RUNNER_TEMP/src/backend" git archive "$FRONTEND_SHA" | tar -x -C "$RUNNER_TEMP/src/frontend" git clone --depth 1 --no-checkout https://get.feep.ir/gold_land/gold_land_server.git "$RUNNER_TEMP/backend-git" git -C "$RUNNER_TEMP/backend-git" fetch --depth 1 origin "$BACKEND_SHA" git -C "$RUNNER_TEMP/backend-git" checkout FETCH_HEAD git -C "$RUNNER_TEMP/backend-git" archive HEAD | tar -x -C "$RUNNER_TEMP/src/backend" test "$(git -C "$RUNNER_TEMP/backend-git" rev-parse HEAD)" = "$BACKEND_SHA" test -f "$RUNNER_TEMP/src/frontend/package-lock.json" test -f "$RUNNER_TEMP/src/backend/package-lock.json" - name: Fill empty npm caches and prove offline installs run: | set -euo pipefail node_dir="$(dirname "$(dirname "$(readlink -f "$(command -v node)")")")" if [ ! -f "$node_dir/include/node/node.h" ]; then echo "Node headers are not in $node_dir. node-gyp would download them outside the npm cache." exit 1 fi export npm_config_nodedir="$node_dir" export npm_config_build_from_source=true echo "CI npm_config_nodedir=$node_dir" node -v uname -s uname -m fill_and_prove() { name="$1" src="$2" cache="$3" shift 3 rm -rf "$cache" mkdir -p "$cache" echo "online npm ci: $name" npm ci --cache "$cache" --no-audit --no-fund --prefix "$src" clean="$RUNNER_TEMP/clean/$name" rm -rf "$clean" mkdir -p "$clean" tar -C "$src" --exclude node_modules -cf - . | tar -C "$clean" -xf - test ! -d "$clean/node_modules" echo "offline npm ci: $name" npm ci --offline --cache "$cache" --no-audit --no-fund --prefix "$clean" (cd "$clean" && npm run build) if [ "$name" = "backend" ]; then (cd "$clean" && node -e "const Database=require('better-sqlite3'); const db=new Database(':memory:'); const row=db.prepare('SELECT 1 AS ok').get(); if(row.ok!==1) process.exit(1); console.log('sqlite-smoke-ok')") fi } fill_and_prove backend "$RUNNER_TEMP/src/backend" "$RUNNER_TEMP/cache/backend" fill_and_prove frontend "$RUNNER_TEMP/src/frontend" "$RUNNER_TEMP/cache/frontend" if [ -d "$HOME/.cache/node-gyp" ]; then echo "node-gyp wrote $HOME/.cache/node-gyp outside the npm cache" exit 1 fi python3 - <<'PY' import os root=os.environ["RUNNER_TEMP"]+"/cache" needles=(":_authToken","_password=","BEGIN PRIVATE KEY","BEGIN RSA PRIVATE KEY") for dirpath, _, files in os.walk(root): for name in files: if name in {".npmrc","_auth"}: raise SystemExit(f"credential file in npm cache: {os.path.join(dirpath, name)}") path=os.path.join(dirpath, name) try: data=open(path,"rb").read() except OSError: continue text=data.decode("utf-8","ignore") for needle in needles: if needle in text: raise SystemExit(f"credential marker in npm cache: {path}") print("npm cache credential scan ok") PY - name: Pack archive run: | set -euo pipefail out="$RUNNER_TEMP/offline-pack" rm -rf "$out" mkdir -p "$out/offline-deps" cp -a "$RUNNER_TEMP/cache/backend" "$out/offline-deps/backend-npm-cache" cp -a "$RUNNER_TEMP/cache/frontend" "$out/offline-deps/frontend-npm-cache" cp offline/backend.Dockerfile offline/frontend.Dockerfile offline/SERVER.md "$out/offline-deps/" node_dir="$(dirname "$(dirname "$(readlink -f "$(command -v node)")")")" python3 - <