From e8734d837e6379f381a89b0cb576defe4129fce3 Mon Sep 17 00:00:00 2001 From: wanyongstar Date: Sat, 18 Jul 2026 13:06:12 +0800 Subject: [PATCH] fix(ci): bound plugin-npm-release git fetch and npm publish with timeout (#110279) * fix(ci): bound plugin-npm-release git fetch and npm publish with timeout * fix(ci): bound plugin-npm-release git fetch and npm publish with timeout ci: retrigger checks * test(ci): guard plugin release timeouts Co-authored-by: wanyongstar * docs(ci): explain npm timeout recovery Co-authored-by: wanyongstar --------- Co-authored-by: Peter Steinberger --- .github/workflows/plugin-npm-release.yml | 16 +++++++++------- .../plugin-npm-extended-stable-workflow.test.ts | 16 ++++++++++++++++ 2 files changed, 25 insertions(+), 7 deletions(-) diff --git a/.github/workflows/plugin-npm-release.yml b/.github/workflows/plugin-npm-release.yml index f84c7ae61087..12dc26ef287e 100644 --- a/.github/workflows/plugin-npm-release.yml +++ b/.github/workflows/plugin-npm-release.yml @@ -131,14 +131,14 @@ jobs: release_year="${BASH_REMATCH[1]}" release_month="${BASH_REMATCH[2]}" extended_stable_branch="extended-stable/${release_year}.${release_month}.33" - git fetch --no-tags origin "+refs/heads/${extended_stable_branch}:refs/remotes/origin/${extended_stable_branch}" + timeout --signal=TERM --kill-after=10s 120s git fetch --no-tags origin "+refs/heads/${extended_stable_branch}:refs/remotes/origin/${extended_stable_branch}" if [[ "${WORKFLOW_REF}" == "refs/heads/${extended_stable_branch}" ]] && [[ "$(git rev-parse HEAD)" == "$(git rev-parse "refs/remotes/origin/${extended_stable_branch}")" ]]; then exit 0 fi echo "Extended-stable plugin npm publishes must run from ${extended_stable_branch} at its exact branch tip." >&2 exit 1 fi - git fetch --no-tags origin \ + timeout --signal=TERM --kill-after=10s 120s git fetch --no-tags origin \ +refs/heads/main:refs/remotes/origin/main \ '+refs/heads/release/*:refs/remotes/origin/release/*' if [[ "${PREFLIGHT_ONLY}" == "true" ]] && ! git merge-base --is-ancestor "${WORKFLOW_SHA}" origin/main; then @@ -159,7 +159,7 @@ jobs: fi if [[ "${WORKFLOW_REF}" =~ ^refs/heads/tideclaw/alpha/[0-9]{4}-[0-9]{2}-[0-9]{2}-[0-9]{4}Z$ ]]; then alpha_branch="${WORKFLOW_REF#refs/heads/}" - git fetch --no-tags origin "+refs/heads/${alpha_branch}:refs/remotes/origin/${alpha_branch}" + timeout --signal=TERM --kill-after=10s 120s git fetch --no-tags origin "+refs/heads/${alpha_branch}:refs/remotes/origin/${alpha_branch}" if git merge-base --is-ancestor HEAD "refs/remotes/origin/${alpha_branch}"; then exit 0 fi @@ -619,7 +619,7 @@ jobs: RUN_ATTEMPT: ${{ github.run_attempt }} run: | set -euo pipefail - git fetch --no-tags --depth=1 origin "${SOURCE_SHA}" + timeout --signal=TERM --kill-after=10s 120s git fetch --no-tags --depth=1 origin "${SOURCE_SHA}" source_package_json="${RUNNER_TEMP}/${EXTENSION_ID}-source-package.json" git show "${SOURCE_SHA}:${PACKAGE_DIR}/package.json" > "${source_package_json}" @@ -1099,7 +1099,7 @@ jobs: echo "npm token bootstrap requires trusted main tooling or a protected SHA-pinned release-publish tag." >&2 exit 1 } - git fetch --no-tags origin +refs/heads/main:refs/remotes/origin/main + timeout --signal=TERM --kill-after=10s 120s git fetch --no-tags origin +refs/heads/main:refs/remotes/origin/main git merge-base --is-ancestor "$WORKFLOW_SHA" origin/main || { echo "npm token bootstrap workflow revision is not reachable from current main." >&2 exit 1 @@ -1137,7 +1137,7 @@ jobs: "https://api.github.com/repos/${GITHUB_REPOSITORY}/actions/artifacts/${artifact_id}/zip" \ -o "$artifact_zip" - git fetch --no-tags --depth=1 origin "$TARGET_SHA" + timeout --signal=TERM --kill-after=10s 120s git fetch --no-tags --depth=1 origin "$TARGET_SHA" source_package_json="${RUNNER_TEMP}/${EXTENSION_ID}-source-package.json" git show "${TARGET_SHA}:${PACKAGE_DIR}/package.json" > "$source_package_json" source_package_json_sha256="$(sha256sum "$source_package_json" | awk '{print $1}')" @@ -1321,12 +1321,14 @@ jobs: } > "$npmrc" chmod 0600 "$npmrc" unset NODE_AUTH_TOKEN NPM_TOKEN NODE_OPTIONS + # A timeout can race a committed publish. On rerun, the preceding check + # accepts only this tarball's exact integrity and shasum before skipping. HOME="$publish_home" \ NPM_CONFIG_GLOBALCONFIG=/dev/null \ NPM_CONFIG_IGNORE_SCRIPTS=true \ NPM_CONFIG_REGISTRY=https://registry.npmjs.org/ \ NPM_CONFIG_USERCONFIG="$npmrc" \ - npm publish "$TARBALL_PATH" \ + timeout --signal=TERM --kill-after=10s 300s npm publish "$TARBALL_PATH" \ --access public \ --ignore-scripts \ --provenance \ diff --git a/test/scripts/plugin-npm-extended-stable-workflow.test.ts b/test/scripts/plugin-npm-extended-stable-workflow.test.ts index 9c6d39b77be3..2deecbf3ab2e 100644 --- a/test/scripts/plugin-npm-extended-stable-workflow.test.ts +++ b/test/scripts/plugin-npm-extended-stable-workflow.test.ts @@ -321,6 +321,22 @@ describe("plugin npm extended-stable workflow", () => { expect(pluginManifest.id).toBe("meta"); }); + it("bounds external git fetch and npm publish operations", () => { + const source = readFileSync(workflowPath, "utf8"); + const gitFetchLines = source.split("\n").filter((line) => line.includes("git fetch")); + const npmPublishLines = source + .split("\n") + .filter((line) => line.includes('npm publish "$TARBALL_PATH"')); + + expect(gitFetchLines).toHaveLength(6); + expect( + gitFetchLines.every((line) => line.includes("timeout --signal=TERM --kill-after=10s 120s")), + ).toBe(true); + expect(npmPublishLines).toEqual([ + ' timeout --signal=TERM --kill-after=10s 300s npm publish "$TARBALL_PATH" \\', + ]); + }); + it("publishes extended-stable with OIDC only and verifies every package tag", () => { const parsed = workflow(); const publish = step(parsed.jobs?.publish_plugins_npm, "Publish with trusted publisher");