From 91a4da96c99bacd6d74b73c15bf32f122d1e7165 Mon Sep 17 00:00:00 2001 From: Jesse_Chen Date: Wed, 5 Aug 2026 14:11:11 +0800 Subject: [PATCH] Run staging workflows with pinned Node tooling --- .gitea/workflows/deploy-staging.yml | 42 ++++++++++++++++++- .gitea/workflows/migrate-staging-database.yml | 42 ++++++++++++++++++- .../tests/staging-backend-workflows.test.ts | 25 +++++++++++ 3 files changed, 107 insertions(+), 2 deletions(-) diff --git a/.gitea/workflows/deploy-staging.yml b/.gitea/workflows/deploy-staging.yml index 0fc142f1..3f185d75 100644 --- a/.gitea/workflows/deploy-staging.yml +++ b/.gitea/workflows/deploy-staging.yml @@ -28,7 +28,7 @@ concurrency: jobs: deploy: if: gitea.event_name == 'workflow_dispatch' || (gitea.event.workflow_run.conclusion == 'success' && gitea.event.workflow_run.event == 'push' && gitea.event.workflow_run.head_branch == 'staging') - runs-on: xiaoxin + runs-on: manman-linux timeout-minutes: 30 env: GITEA_SHA: ${{ gitea.sha }} @@ -105,6 +105,46 @@ jobs: git checkout --detach --force origin/main git merge-base --is-ancestor "$DEPLOY_SHA" HEAD || { echo "staging revision is not in trusted main history" >&2; exit 1; } + - name: Prepare pinned Node tooling + env: + NODE_TOOL_SOURCE_IMAGE: swr.cn-north-4.myhuaweicloud.com/ddn-k8s/docker.io/library/node:22-bookworm-slim@sha256:ef343465b6a14bbdf2ab52f6e100ec0659a792464fcf72c462370d88b3df909c + NODE_TOOL_IMAGE: node:22-bookworm-slim + run: | + set -euo pipefail + if ! docker image inspect "$NODE_TOOL_SOURCE_IMAGE" >/dev/null 2>&1; then + for attempt in 1 2 3; do + if timeout 180 docker pull "$NODE_TOOL_SOURCE_IMAGE"; then + break + fi + if [ "$attempt" -eq 3 ]; then + echo "Failed to preload $NODE_TOOL_IMAGE after $attempt attempts" >&2 + exit 1 + fi + sleep $((attempt * 15)) + done + fi + docker tag "$NODE_TOOL_SOURCE_IMAGE" "$NODE_TOOL_IMAGE" + docker image inspect "$NODE_TOOL_IMAGE" >/dev/null + tool_dir="$(mktemp -d "${RUNNER_TEMP:-/tmp}/jyotisha-node-tools.XXXXXX")" + cat > "$tool_dir/node" <<'EOF' + #!/usr/bin/env bash + set -euo pipefail + workdir="$(pwd -P)" + exec docker run --rm \ + --user "$(id -u):$(id -g)" \ + --volume "$workdir:$workdir" \ + --workdir "$workdir" \ + --env HOME=/tmp \ + node:22-bookworm-slim "${0##*/}" "$@" + EOF + chmod 0755 "$tool_dir/node" + ln -s node "$tool_dir/npm" + test -n "${GITHUB_PATH:-}" + printf '%s\n' "$tool_dir" >> "$GITHUB_PATH" + export PATH="$tool_dir:$PATH" + node --version + npm --version + - name: Download gate-produced image manifest env: GATE_RUN_ID: ${{ steps.revision.outputs.gate_run_id }} diff --git a/.gitea/workflows/migrate-staging-database.yml b/.gitea/workflows/migrate-staging-database.yml index c985af96..32bb9f27 100644 --- a/.gitea/workflows/migrate-staging-database.yml +++ b/.gitea/workflows/migrate-staging-database.yml @@ -19,7 +19,7 @@ concurrency: jobs: migrate: - runs-on: xiaoxin + runs-on: manman-linux timeout-minutes: 20 env: GITEA_SHA: ${{ gitea.sha }} @@ -75,6 +75,46 @@ jobs: git checkout --detach --force origin/main git merge-base --is-ancestor "$DEPLOY_SHA" HEAD || { echo "staging revision is not in trusted main history" >&2; exit 1; } + - name: Prepare pinned Node tooling + env: + NODE_TOOL_SOURCE_IMAGE: swr.cn-north-4.myhuaweicloud.com/ddn-k8s/docker.io/library/node:22-bookworm-slim@sha256:ef343465b6a14bbdf2ab52f6e100ec0659a792464fcf72c462370d88b3df909c + NODE_TOOL_IMAGE: node:22-bookworm-slim + run: | + set -euo pipefail + if ! docker image inspect "$NODE_TOOL_SOURCE_IMAGE" >/dev/null 2>&1; then + for attempt in 1 2 3; do + if timeout 180 docker pull "$NODE_TOOL_SOURCE_IMAGE"; then + break + fi + if [ "$attempt" -eq 3 ]; then + echo "Failed to preload $NODE_TOOL_IMAGE after $attempt attempts" >&2 + exit 1 + fi + sleep $((attempt * 15)) + done + fi + docker tag "$NODE_TOOL_SOURCE_IMAGE" "$NODE_TOOL_IMAGE" + docker image inspect "$NODE_TOOL_IMAGE" >/dev/null + tool_dir="$(mktemp -d "${RUNNER_TEMP:-/tmp}/jyotisha-node-tools.XXXXXX")" + cat > "$tool_dir/node" <<'EOF' + #!/usr/bin/env bash + set -euo pipefail + workdir="$(pwd -P)" + exec docker run --rm \ + --user "$(id -u):$(id -g)" \ + --volume "$workdir:$workdir" \ + --workdir "$workdir" \ + --env HOME=/tmp \ + node:22-bookworm-slim "${0##*/}" "$@" + EOF + chmod 0755 "$tool_dir/node" + ln -s node "$tool_dir/npm" + test -n "${GITHUB_PATH:-}" + printf '%s\n' "$tool_dir" >> "$GITHUB_PATH" + export PATH="$tool_dir:$PATH" + node --version + npm --version + - name: Download gate-produced migration manifest env: GATE_RUN_ID: ${{ steps.revision.outputs.gate_run_id }} diff --git a/frontend/tests/staging-backend-workflows.test.ts b/frontend/tests/staging-backend-workflows.test.ts index 099fa915..a0bfda67 100644 --- a/frontend/tests/staging-backend-workflows.test.ts +++ b/frontend/tests/staging-backend-workflows.test.ts @@ -189,6 +189,31 @@ test("Gitea quality gate validates before publishing an immutable ACR manifest", assert.doesNotMatch(workflow, /(?:^|:)latest$/m); }); +test("Gitea staging mutation workflows use the available runner and pinned Node tooling", () => { + for (const workflow of [read(giteaDeployWorkflow), read(giteaMigrationWorkflow)]) { + assert.equal((workflow.match(/runs-on: manman-linux/g) ?? []).length, 1); + assert.doesNotMatch(workflow, /runs-on: xiaoxin/); + assert.equal((workflow.match(/NODE_TOOL_SOURCE_IMAGE: swr\.cn-north-4\.myhuaweicloud\.com\/ddn-k8s\/docker\.io\/library\/node:22-bookworm-slim@sha256:ef343465b6a14bbdf2ab52f6e100ec0659a792464fcf72c462370d88b3df909c/g) ?? []).length, 1); + assert.equal((workflow.match(/NODE_TOOL_IMAGE: node:22-bookworm-slim/g) ?? []).length, 1); + assert.match(workflow, /timeout 180 docker pull "\$NODE_TOOL_SOURCE_IMAGE"/); + assert.match(workflow, /docker tag "\$NODE_TOOL_SOURCE_IMAGE" "\$NODE_TOOL_IMAGE"/); + assert.match(workflow, /cat > "\$tool_dir\/node" <<'EOF'/); + assert.match(workflow, /--volume "\$workdir:\$workdir"/); + assert.match(workflow, /--workdir "\$workdir"/); + assert.match(workflow, /node:22-bookworm-slim "\$\{0##\*\/\}" "\$@"/); + assert.match(workflow, />> "\$GITHUB_PATH"/); + assert.match(workflow, /node frontend\/scripts\/staging-image-manifest\.mjs/); + assert.doesNotMatch(workflow, /packages\+=\((?:nodejs|npm)\)|apt-get install[^\n]*(?:nodejs|npm)/); + assert.doesNotMatch(workflow, /(?:--volume|-v)[^\n]*(?:\$HOME\/\.docker|DOCKER_CONFIG)/); + assertOrder(workflow, [ + "Checkout trusted main controller", + "Prepare pinned Node tooling", + "Download gate-produced", + "node frontend/scripts/staging-image-manifest.mjs", + ]); + } +}); + test("quality gate builds the Python package with its declared backend dependencies", () => { const workflow = read(qualityWorkflow);