diff --git a/.github/workflows/sdk-java.yml b/.github/workflows/sdk-java.yml index 071c522bcf7..61799ee961f 100644 --- a/.github/workflows/sdk-java.yml +++ b/.github/workflows/sdk-java.yml @@ -41,10 +41,18 @@ on: permissions: contents: 'read' +env: + MAVEN_VERSION: '3.9.11' + MAVEN_SHA512: 'bcfe4fe305c962ace56ac7b5fc7a08b87d5abd8b7e89027ab251069faebee516b0ded8961445d6d91ec1985dfe30f8153268843c89aa392733d1a3ec956c9978' + +concurrency: + group: 'sdk-java-${{ github.event.pull_request.number || github.run_id }}' + cancel-in-progress: true + jobs: test: name: '${{ matrix.os }} / Java ${{ matrix.java }}' - runs-on: '${{ matrix.os }}' + runs-on: '${{ (matrix.os == ''ubuntu-latest'' && github.repository == ''QwenLM/qwen-code'' && vars.MAINTAINER_ECS_RUNNER_DISABLED != ''true'' && (github.event_name == ''push'' || github.event.pull_request.head.repo.full_name == github.repository)) && fromJSON(''["self-hosted", "linux", "x64", "ecs-qwen"]'') || matrix.os }}' timeout-minutes: 30 strategy: fail-fast: false @@ -61,8 +69,32 @@ jobs: - os: 'windows-latest' java: '21' steps: + - name: 'Restore workspace ownership' + if: "${{ runner.environment == 'self-hosted' }}" + run: |- + set -uo pipefail + RUNNER_UID="$(id -u)" + RUNNER_GID="$(id -g)" + if [ "$RUNNER_UID" != "0" ]; then + chown -R "$RUNNER_UID:$RUNNER_GID" "$GITHUB_WORKSPACE" 2>/dev/null || sudo -n chown -R "$RUNNER_UID:$RUNNER_GID" "$GITHUB_WORKSPACE" || echo "::warning::could not restore workspace ownership; checkout may fail on leftover root-owned files" + fi + chmod -R u+rwX "$GITHUB_WORKSPACE" 2>/dev/null || sudo -n chmod -R u+rwX "$GITHUB_WORKSPACE" || echo "::warning::could not restore workspace write permissions; checkout may fail on leftover read-only files" + - name: 'Checkout' uses: 'actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10' # v6.0.3 + with: + ref: "${{ (runner.environment == 'self-hosted' && github.event_name == 'pull_request' && format('refs/pull/{0}/head', github.event.pull_request.number)) || github.ref }}" + + - name: 'Verify checkout includes expected head commit' + if: "${{ runner.environment == 'self-hosted' && github.event_name == 'pull_request' }}" + env: + EXPECTED_SHA: '${{ github.event.pull_request.head.sha }}' + run: |- + if ! git merge-base --is-ancestor "${EXPECTED_SHA}" HEAD; then + echo "::error::Checked out ref does not contain expected head ${EXPECTED_SHA}." + git log --oneline --decorate -5 + exit 1 + fi - name: 'Set up Java' uses: 'actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654' # v5.2.0 @@ -72,6 +104,15 @@ jobs: cache: 'maven' cache-dependency-path: 'packages/sdk-java/qwencode/pom.xml' + - name: 'Set up Maven (self-hosted)' + if: "${{ runner.environment == 'self-hosted' }}" + run: |- + archive="${RUNNER_TEMP}/apache-maven-${MAVEN_VERSION}-bin.tar.gz" + curl -fsSL "https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/${MAVEN_VERSION}/apache-maven-${MAVEN_VERSION}-bin.tar.gz" -o "${archive}" + echo "${MAVEN_SHA512} ${archive}" | sha512sum --check --status + tar -xzf "${archive}" -C "${RUNNER_TEMP}" + echo "${RUNNER_TEMP}/apache-maven-${MAVEN_VERSION}/bin" >> "${GITHUB_PATH}" + - name: 'Run Java SDK tests' working-directory: 'packages/sdk-java/qwencode' run: 'mvn --batch-mode --no-transfer-progress clean test' @@ -88,18 +129,56 @@ jobs: daemon-e2e: name: 'Real daemon E2E / Java 11' - runs-on: 'ubuntu-latest' + runs-on: '${{ (github.repository == ''QwenLM/qwen-code'' && vars.MAINTAINER_ECS_RUNNER_DISABLED != ''true'' && (github.event_name == ''push'' || github.event.pull_request.head.repo.full_name == github.repository)) && fromJSON(''["self-hosted", "linux", "x64", "ecs-qwen"]'') || ''ubuntu-latest'' }}' timeout-minutes: 30 steps: + - name: 'Restore workspace ownership' + if: "${{ runner.environment == 'self-hosted' }}" + run: |- + set -uo pipefail + RUNNER_UID="$(id -u)" + RUNNER_GID="$(id -g)" + if [ "$RUNNER_UID" != "0" ]; then + chown -R "$RUNNER_UID:$RUNNER_GID" "$GITHUB_WORKSPACE" 2>/dev/null || sudo -n chown -R "$RUNNER_UID:$RUNNER_GID" "$GITHUB_WORKSPACE" || echo "::warning::could not restore workspace ownership; checkout may fail on leftover root-owned files" + fi + chmod -R u+rwX "$GITHUB_WORKSPACE" 2>/dev/null || sudo -n chmod -R u+rwX "$GITHUB_WORKSPACE" || echo "::warning::could not restore workspace write permissions; checkout may fail on leftover read-only files" + - name: 'Checkout' uses: 'actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10' # v6.0.3 + with: + ref: "${{ (runner.environment == 'self-hosted' && github.event_name == 'pull_request' && format('refs/pull/{0}/head', github.event.pull_request.number)) || github.ref }}" - - name: 'Set up Node.js' + - name: 'Verify checkout includes expected head commit' + if: "${{ runner.environment == 'self-hosted' && github.event_name == 'pull_request' }}" + env: + EXPECTED_SHA: '${{ github.event.pull_request.head.sha }}' + run: |- + if ! git merge-base --is-ancestor "${EXPECTED_SHA}" HEAD; then + echo "::error::Checked out ref does not contain expected head ${EXPECTED_SHA}." + git log --oneline --decorate -5 + exit 1 + fi + + # Self-hosted can't reach nodejs.org reliably; reuse the machine's Node. + - name: 'Set up Node.js 22 (hosted)' + if: "${{ runner.environment == 'github-hosted' }}" uses: 'actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e' # v6.4.0 with: node-version: '22' cache: 'npm' + - name: 'Use pre-installed Node.js (self-hosted)' + if: "${{ runner.environment == 'self-hosted' }}" + run: |- + if ! command -v node >/dev/null 2>&1; then + echo "::error::Node.js is not on PATH for this self-hosted runner. Provision Node 22.x or set MAINTAINER_ECS_RUNNER_DISABLED=true." + exit 1 + fi + echo "Using pre-installed Node $(node -v) / npm $(npm -v)" + if [[ "$(node -p 'process.versions.node.split(".")[0]')" != "22" ]]; then + echo "::warning::Expected Node 22.x but found $(node -v); daemon E2E will run against the runner's Node." + fi + - name: 'Set up Java' uses: 'actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654' # v5.2.0 with: @@ -108,6 +187,15 @@ jobs: cache: 'maven' cache-dependency-path: 'packages/sdk-java/qwencode/pom.xml' + - name: 'Set up Maven (self-hosted)' + if: "${{ runner.environment == 'self-hosted' }}" + run: |- + archive="${RUNNER_TEMP}/apache-maven-${MAVEN_VERSION}-bin.tar.gz" + curl -fsSL "https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/${MAVEN_VERSION}/apache-maven-${MAVEN_VERSION}-bin.tar.gz" -o "${archive}" + echo "${MAVEN_SHA512} ${archive}" | sha512sum --check --status + tar -xzf "${archive}" -C "${RUNNER_TEMP}" + echo "${RUNNER_TEMP}/apache-maven-${MAVEN_VERSION}/bin" >> "${GITHUB_PATH}" + - name: 'Install Node.js dependencies' run: 'npm ci --prefer-offline --no-audit --progress=false' diff --git a/scripts/run-java-daemon-sdk-e2e.ts b/scripts/run-java-daemon-sdk-e2e.ts index d336e5344de..92f35786b64 100644 --- a/scripts/run-java-daemon-sdk-e2e.ts +++ b/scripts/run-java-daemon-sdk-e2e.ts @@ -131,7 +131,6 @@ const daemon = spawn( let stderr = ''; let javaTest: ChildProcess | undefined; let receivedSignal: NodeJS.Signals | undefined; -let succeeded = false; daemon.stderr?.on('data', (chunk) => { stderr += chunk.toString(); }); @@ -297,7 +296,6 @@ try { 'Java E2E completed without exercising the daemon model path; the integration tests may have been skipped', ); } - succeeded = true; } catch (error) { runFailure = error; } @@ -322,10 +320,14 @@ try { } process.off('SIGINT', handleSigint); process.off('SIGTERM', handleSigterm); -if (succeeded && cleanupFailures.length === 0) { - rmSync(temporary, { recursive: true, force: true }); -} else { +if (process.env.QWEN_DAEMON_E2E_KEEP_STATE === '1') { console.error(`Retained Java daemon E2E state at ${temporary}`); +} else { + try { + rmSync(temporary, { recursive: true, force: true }); + } catch (error) { + cleanupFailures.push(error); + } } if (receivedSignal !== undefined) { process.kill(process.pid, receivedSignal); diff --git a/scripts/tests/sdk-java-workflow.test.js b/scripts/tests/sdk-java-workflow.test.js new file mode 100644 index 00000000000..71fa309b50c --- /dev/null +++ b/scripts/tests/sdk-java-workflow.test.js @@ -0,0 +1,27 @@ +import { readFileSync } from 'node:fs'; +import { describe, expect, it } from 'vitest'; + +const workflow = readFileSync('.github/workflows/sdk-java.yml', 'utf8'); +const job = (name) => { + const start = workflow.indexOf(` ${name}:`); + const next = workflow.slice(start + 1).search(/\n {2}[a-z0-9-]+:\n/); + return workflow.slice(start, next < 0 ? undefined : start + 1 + next); +}; + +describe('SDK Java self-hosted workflow guards', () => { + it.each(['test', 'daemon-e2e'])('protects the %s job', (name) => { + const block = job(name); + for (const fragment of [ + "github.repository == ''QwenLM/qwen-code''", + 'github.event.pull_request.head.repo.full_name == github.repository', + "vars.MAINTAINER_ECS_RUNNER_DISABLED != ''true''", + 'fromJSON(\'\'["self-hosted", "linux", "x64", "ecs-qwen"]\'\')', + "format('refs/pull/{0}/head', github.event.pull_request.number)", + "EXPECTED_SHA: '${{ github.event.pull_request.head.sha }}'", + 'git merge-base --is-ancestor "${EXPECTED_SHA}" HEAD', + 'exit 1', + ]) { + expect(block).toContain(fragment); + } + }); +});