mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-11-07 06:19:43 -10:00
The public Managed Agent contract's info.version is allocated by hand on every contract-changing pull request, and nothing checked it: on 2026-10-09 four such pull requests were in flight with 1.34.0 claimed twice while main was already at 1.35.0, and the stall had reached main once before — #13163 changed the document with no version movement. A duplicate Flyway version refuses to boot within minutes, but a regressed or stalled contract version breaks nothing at runtime; it silently misinforms every consumer that keys capability detection on it. On every pull request the Flyway lane now also compares the merge result's document with the base branch's head copy — fetched at depth one, since the default checkout is one commit deep — and fails when a changed document declares an equal or lower version, compared numerically per segment so 1.9.0 ranks below 1.37.0, naming whether the number stalled or regressed; a pull request arriving at the base ref's own bytes owes no bump. Each refusal arm has a test that fails when its guard is removed, and any path echoed into an ::error:: line is bound to the same byte alphabet check-flyway-migrations.js applies, so a hostile name cannot forge a second error line into a decoded log. After every push to main that changes the document, and every 30 minutes, a new workflow re-checks each open pull request's changed paths and document through the API — no pull request code is fetched, checked out or run — and sets the 'API contract version uniqueness (latest main)' status, the #13770 shape for the version number instead of the migrations: failure when the declared version no longer advances past main's, success otherwise, naming the other open pull requests claiming the same version, and error when a file list or head document could not be read in full. A head that already carries the status is re-evaluated even after it stops changing the document, so a stale failure clears. Both scripts ran live before this commit: the pull-request check over four merge shapes (stall, regression, advance, untouched) against a depth-one clone and filtered base fetch, and the re-check as a dry run against the open queue, whose six contract-changing pull requests it correctly read as stalled or regressed against main at 1.38.0. Fixes #13804
888 lines
48 KiB
YAML
888 lines
48 KiB
YAML
name: 'SDK Java'
|
||
|
||
on:
|
||
workflow_dispatch:
|
||
pull_request:
|
||
branches:
|
||
- 'main'
|
||
- 'release/**'
|
||
paths:
|
||
- 'packages/sdk-java/**'
|
||
- '**/.mvn/**'
|
||
- 'packages/core/src/config/approval-modes.json'
|
||
- 'packages/core/src/managed-runtime/**'
|
||
- 'packages/cli/src/config/**'
|
||
- 'packages/core/src/config/**'
|
||
- 'packages/core/src/core/**'
|
||
- 'packages/cli/src/commands/serve.ts'
|
||
- 'packages/cli/src/serve/**'
|
||
- 'packages/cli/src/acp-integration/**'
|
||
- 'packages/acp-bridge/**'
|
||
- 'integration-tests/fake-openai-server.ts'
|
||
- 'integration-tests/helpers/hosted-*'
|
||
- 'integration-tests/cli/hosted-latency-baseline.test.ts'
|
||
- 'integration-tests/baselines/hosted-latency.json'
|
||
- 'package.json'
|
||
- 'pnpm-lock.yaml'
|
||
- 'docs/developers/sdk-java.md'
|
||
- 'docs/design/java-daemon-sdk-alpha.md'
|
||
- 'scripts/run-java-daemon-sdk-e2e.ts'
|
||
- 'scripts/run-managed-agent-server-e2e.ts'
|
||
- 'scripts/check-failsafe-reports.js'
|
||
- 'scripts/check-flyway-migrations.js'
|
||
- 'scripts/flyway-migration-utils.js'
|
||
- 'scripts/check-contract-version.js'
|
||
- 'scripts/contract-version-utils.js'
|
||
- '.github/workflows/sdk-java.yml'
|
||
- '.github/workflows/release-sdk-java.yml'
|
||
push:
|
||
branches:
|
||
- 'main'
|
||
- 'release/**'
|
||
paths:
|
||
- 'packages/sdk-java/**'
|
||
- '**/.mvn/**'
|
||
- 'packages/core/src/config/approval-modes.json'
|
||
- 'packages/core/src/managed-runtime/**'
|
||
- 'packages/cli/src/config/**'
|
||
- 'packages/core/src/config/**'
|
||
- 'packages/core/src/core/**'
|
||
- 'packages/cli/src/commands/serve.ts'
|
||
- 'packages/cli/src/serve/**'
|
||
- 'packages/cli/src/acp-integration/**'
|
||
- 'packages/acp-bridge/**'
|
||
- 'integration-tests/fake-openai-server.ts'
|
||
- 'integration-tests/helpers/hosted-*'
|
||
- 'integration-tests/cli/hosted-latency-baseline.test.ts'
|
||
- 'integration-tests/baselines/hosted-latency.json'
|
||
- 'package.json'
|
||
- 'pnpm-lock.yaml'
|
||
- 'docs/developers/sdk-java.md'
|
||
- 'docs/design/java-daemon-sdk-alpha.md'
|
||
- 'scripts/run-java-daemon-sdk-e2e.ts'
|
||
- 'scripts/run-managed-agent-server-e2e.ts'
|
||
- 'scripts/check-failsafe-reports.js'
|
||
- 'scripts/check-flyway-migrations.js'
|
||
- 'scripts/flyway-migration-utils.js'
|
||
- 'scripts/check-contract-version.js'
|
||
- 'scripts/contract-version-utils.js'
|
||
- '.github/workflows/sdk-java.yml'
|
||
- '.github/workflows/release-sdk-java.yml'
|
||
|
||
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 == ''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 || contains(fromJSON(''["OWNER","MEMBER","COLLABORATOR"]''), github.event.pull_request.author_association))) && fromJSON(''["self-hosted", "linux", "x64", "ecs-qwen"]'') || matrix.os }}'
|
||
timeout-minutes: 30
|
||
strategy:
|
||
fail-fast: false
|
||
matrix:
|
||
include:
|
||
- os: 'ubuntu-latest'
|
||
java: '11'
|
||
- os: 'ubuntu-latest'
|
||
java: '17'
|
||
- os: 'ubuntu-latest'
|
||
java: '21'
|
||
- os: 'macos-latest'
|
||
java: '21'
|
||
- 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"
|
||
# A leftover workspace owned by a different uid than the step user
|
||
# (e.g. a runner-user checkout under root-run steps) makes every git
|
||
# call fail with "detected dubious ownership". The pinned
|
||
# actions/checkout adds safe.directory only to its own temporary
|
||
# global config, so later steps are uncovered; trust the workspace
|
||
# here instead. The --get-all guard keeps a persistent runner's
|
||
# gitconfig from accumulating a duplicate entry per job.
|
||
git config --global --get-all safe.directory 2>/dev/null | grep -qxF "$GITHUB_WORKSPACE" || git config --global --add safe.directory "$GITHUB_WORKSPACE" || echo "::warning::could not mark the workspace as a git safe.directory; git steps may fail on a mismatched-ownership checkout"
|
||
|
||
# ci.yml's pre-checkout sweep, byte-identical to the ci.yml copies
|
||
# (pinned in scripts/tests/sdk-java-workflow.test.js): residue left by
|
||
# review/autofix jobs on the shared ECS pool fails actions/checkout's
|
||
# clean — the #13506 failure. Hosted legs skip it on fresh VMs.
|
||
- name: 'Clean stale .qwen before checkout'
|
||
if: "${{ runner.environment == 'self-hosted' }}"
|
||
run: |-
|
||
set -uo pipefail
|
||
for stale_qwen in "$GITHUB_WORKSPACE/.qwen" "$GITHUB_WORKSPACE/.qwen.root-orig" "$GITHUB_WORKSPACE/trusted-ci-classifier"; do
|
||
if [ ! -e "$stale_qwen" ] && [ ! -L "$stale_qwen" ]; then
|
||
continue
|
||
fi
|
||
if [ -d "$stale_qwen" ] && [ ! -L "$stale_qwen" ]; then
|
||
chmod -R u+w "$stale_qwen" 2>/dev/null || true
|
||
fi
|
||
# A foreign-owned directory cannot always be renamed to a
|
||
# different parent: updating its .. entry can fail even when the
|
||
# workspace parent is writable. If that individual move fails,
|
||
# quarantine the runner-owned workspace itself, then recreate the
|
||
# empty checkout root. Warm contents are lost only on this
|
||
# otherwise unrecoverable path.
|
||
rm -rf -- "$stale_qwen" 2>/dev/null ||
|
||
sudo -n rm -rf -- "$stale_qwen" 2>/dev/null ||
|
||
{
|
||
quarantine="$(dirname -- "$GITHUB_WORKSPACE")/_qwen-quarantine"
|
||
mkdir -p "$quarantine" 2>/dev/null || true
|
||
stale_name="$(basename -- "$stale_qwen")"
|
||
if mv -- "$stale_qwen" "$quarantine/${stale_name#\.}-$(date -u +%Y%m%dT%H%M%SZ)-$$" 2>/dev/null; then
|
||
echo "::warning::could not delete leaked $stale_name; moved it to $quarantine so this checkout can proceed — that directory needs manual cleanup"
|
||
else
|
||
workspace_quarantine="$quarantine/workspace-$(date -u +%Y%m%dT%H%M%SZ)-$$"
|
||
if mv -- "$GITHUB_WORKSPACE" "$workspace_quarantine" 2>/dev/null &&
|
||
mkdir -p "$GITHUB_WORKSPACE" 2>/dev/null &&
|
||
cd "$GITHUB_WORKSPACE"; then
|
||
echo "::warning::could not delete leaked $stale_name; moved the whole workspace to $workspace_quarantine so this checkout can proceed — that directory needs manual cleanup"
|
||
break
|
||
else
|
||
echo "::warning::leaked $stale_name survived every recovery; runner needs manual cleanup"
|
||
fi
|
||
fi
|
||
}
|
||
done
|
||
# Interrupted reviews leave worktree registrations under .qwen/tmp/
|
||
# and qwen-review/* branches behind. prune drops registrations whose
|
||
# directories the rm above removed; worktree remove --force then
|
||
# clears any still-registered leftover directory (--force tolerates
|
||
# dirty contents), since a branch checked out in a live worktree
|
||
# cannot be deleted. If removal still fails, the registration
|
||
# survives and the branch delete below warns. The sweep deletes all
|
||
# review artifacts, not just the current PR's: safe because a runner
|
||
# executes one job at a time. Kept inline rather than a shared
|
||
# script: this runs pre-checkout on shared runners, where leftover
|
||
# workspace files are untrusted.
|
||
if [ -e "$GITHUB_WORKSPACE/.git" ]; then
|
||
GIT_SAFE=(git -c core.hooksPath=/dev/null -c core.fsmonitor= -C "$GITHUB_WORKSPACE")
|
||
"${GIT_SAFE[@]}" worktree prune -v || true
|
||
"${GIT_SAFE[@]}" worktree list --porcelain \
|
||
| awk '$1 == "worktree" && index($0, "/.qwen/tmp/review-pr-") > 0 { sub(/^worktree /, ""); print }' \
|
||
| while read -r worktree; do
|
||
[ -n "$worktree" ] || continue
|
||
# Registered paths come from leftover git metadata and are
|
||
# untrusted: the awk filter above matched by substring, so reject
|
||
# `..` traversal and re-anchor to the review prefix before the
|
||
# destructive remove.
|
||
case "$worktree" in
|
||
*/../*|../*|*/..)
|
||
echo "::warning::skipping suspicious review worktree path: $worktree"
|
||
continue
|
||
;;
|
||
"$GITHUB_WORKSPACE/.qwen/tmp/review-pr-"*) : ;;
|
||
*)
|
||
echo "::warning::skipping unexpected review worktree path: $worktree"
|
||
continue
|
||
;;
|
||
esac
|
||
"${GIT_SAFE[@]}" worktree remove --force "$worktree" ||
|
||
echo "::warning::could not remove review worktree: $worktree"
|
||
done || true
|
||
"${GIT_SAFE[@]}" worktree prune -v || true
|
||
"${GIT_SAFE[@]}" for-each-ref --format='%(refname:short)' 'refs/heads/qwen-review/*' \
|
||
| while read -r stale_ref; do
|
||
if [ -n "$stale_ref" ]; then
|
||
"${GIT_SAFE[@]}" branch -D "$stale_ref" ||
|
||
echo "::warning::could not remove review branch: $stale_ref"
|
||
fi
|
||
done || true
|
||
fi
|
||
|
||
- 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
|
||
|
||
# setup-java updates Maven files non-atomically. Keep them job-local
|
||
# because runner instances on one ECS host share HOME.
|
||
- name: 'Set up Java'
|
||
uses: 'actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654' # v5.2.0
|
||
with:
|
||
distribution: 'temurin'
|
||
java-version: '${{ matrix.java }}'
|
||
cache: 'maven'
|
||
cache-dependency-path: |-
|
||
packages/sdk-java/qwencode/pom.xml
|
||
packages/sdk-java/runtime-broker/pom.xml
|
||
settings-path: '${{ runner.temp }}/setup-java-m2'
|
||
|
||
# The runtime-broker process tests spawn the Node fixture worker.
|
||
# Self-hosted keeps relying on the runner's own Node, matching the
|
||
# daemon-e2e job's stance.
|
||
- name: 'Set up Node.js 22 (hosted)'
|
||
if: "${{ runner.environment == 'github-hosted' && matrix.java == '21' }}"
|
||
uses: 'actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e' # v6.4.0
|
||
with:
|
||
node-version: '22'
|
||
package-manager-cache: false
|
||
|
||
- 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 (self-hosted)'
|
||
if: "${{ runner.environment == 'self-hosted' }}"
|
||
working-directory: 'packages/sdk-java/qwencode'
|
||
env:
|
||
MAVEN_ARGS: '--settings ${{ runner.temp }}/setup-java-m2/settings.xml --toolchains ${{ runner.temp }}/setup-java-m2/toolchains.xml'
|
||
MATRIX_JAVA: '${{ matrix.java }}'
|
||
run: |-
|
||
mkdir -p "${HOME}/.cache/qwen-code-ci"
|
||
exec 9>"${HOME}/.cache/qwen-code-ci/sdk-java-tests.lock"
|
||
if ! flock --wait 1200 9; then
|
||
echo "::error::sdk-java host lock not acquired within 20 minutes"
|
||
exit 1
|
||
fi
|
||
mvn --batch-mode --no-transfer-progress clean test
|
||
if [ "${MATRIX_JAVA}" = "21" ]; then
|
||
cd ../runtime-broker
|
||
mvn --batch-mode --no-transfer-progress clean test
|
||
fi
|
||
|
||
- name: 'Run Java SDK tests (hosted)'
|
||
if: "${{ runner.environment == 'github-hosted' }}"
|
||
working-directory: 'packages/sdk-java/qwencode'
|
||
env:
|
||
MAVEN_ARGS: '--settings ${{ runner.temp }}/setup-java-m2/settings.xml --toolchains ${{ runner.temp }}/setup-java-m2/toolchains.xml'
|
||
run: 'mvn --batch-mode --no-transfer-progress clean test'
|
||
|
||
- name: 'Run Runtime Broker state tests'
|
||
if: "${{ runner.environment == 'github-hosted' && matrix.java == '21' }}"
|
||
working-directory: 'packages/sdk-java/runtime-broker'
|
||
env:
|
||
MAVEN_ARGS: '--settings ${{ runner.temp }}/setup-java-m2/settings.xml --toolchains ${{ runner.temp }}/setup-java-m2/toolchains.xml'
|
||
run: 'mvn --batch-mode --no-transfer-progress clean test'
|
||
|
||
- name: 'Run Java SDK Checkstyle'
|
||
if: "${{ matrix.os == 'ubuntu-latest' && matrix.java == '11' }}"
|
||
working-directory: 'packages/sdk-java/qwencode'
|
||
env:
|
||
MAVEN_ARGS: '--settings ${{ runner.temp }}/setup-java-m2/settings.xml --toolchains ${{ runner.temp }}/setup-java-m2/toolchains.xml'
|
||
run: 'mvn --batch-mode --no-transfer-progress checkstyle:check'
|
||
|
||
- name: 'Run Runtime Broker state Checkstyle'
|
||
if: "${{ matrix.os == 'ubuntu-latest' && matrix.java == '21' }}"
|
||
working-directory: 'packages/sdk-java/runtime-broker'
|
||
env:
|
||
MAVEN_ARGS: '--settings ${{ runner.temp }}/setup-java-m2/settings.xml --toolchains ${{ runner.temp }}/setup-java-m2/toolchains.xml'
|
||
run: 'mvn --batch-mode --no-transfer-progress checkstyle:check'
|
||
|
||
- name: 'Build release artifacts'
|
||
if: "${{ matrix.os == 'ubuntu-latest' && matrix.java == '11' }}"
|
||
working-directory: 'packages/sdk-java/qwencode'
|
||
env:
|
||
MAVEN_ARGS: '--settings ${{ runner.temp }}/setup-java-m2/settings.xml --toolchains ${{ runner.temp }}/setup-java-m2/toolchains.xml'
|
||
run: 'mvn --batch-mode --no-transfer-progress -DskipTests -Dgpg.skip=true package'
|
||
|
||
# The Flyway duplicate-version guard of #12940. managed-agent-server
|
||
# allocates migration versions by hand across two locations that share one
|
||
# sequence, so two green PRs can collide once both merge; the test that
|
||
# catches it boots a database and runs only in the slow lanes below. This
|
||
# job is the fast lane: a filename scan, seconds in, on every pull request —
|
||
# whose checkout is the prospective merge result — and on every push.
|
||
# runtime-broker and qwencode carry no migrations today but share the
|
||
# namespace: managed-agent-server depends on both at compile scope, so
|
||
# Flyway resolves classpath:db/migration across the jars and a version
|
||
# claimed in any of them collides with managed-agent-server's exactly like
|
||
# an in-module duplicate.
|
||
#
|
||
# Trusted triggers run on the ECS pool (#13245): a five-second scan sat 26
|
||
# minutes in the hosted queue. Untrusted fork PRs stay hosted, and the
|
||
# default merge-ref checkout is kept on both — the merge result is the point.
|
||
flyway-migrations:
|
||
name: 'Flyway migration version uniqueness'
|
||
runs-on: '${{ (github.repository == ''QwenLM/qwen-code'' && vars.MAINTAINER_ECS_RUNNER_DISABLED != ''true'' && (github.event_name != ''pull_request'' || github.event.pull_request.head.repo.full_name == github.repository || contains(fromJSON(''["OWNER","MEMBER","COLLABORATOR"]''), github.event.pull_request.author_association))) && fromJSON(''["self-hosted", "linux", "x64", "ecs-qwen"]'') || fromJSON(''["ubuntu-latest"]'') }}'
|
||
timeout-minutes: 5
|
||
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"
|
||
# A leftover workspace owned by a different uid than the step user
|
||
# (e.g. a runner-user checkout under root-run steps) makes every git
|
||
# call fail with "detected dubious ownership". The pinned
|
||
# actions/checkout adds safe.directory only to its own temporary
|
||
# global config, so later steps are uncovered; trust the workspace
|
||
# here instead. The --get-all guard keeps a persistent runner's
|
||
# gitconfig from accumulating a duplicate entry per job.
|
||
git config --global --get-all safe.directory 2>/dev/null | grep -qxF "$GITHUB_WORKSPACE" || git config --global --add safe.directory "$GITHUB_WORKSPACE" || echo "::warning::could not mark the workspace as a git safe.directory; git steps may fail on a mismatched-ownership checkout"
|
||
|
||
# ci.yml's pre-checkout sweep, byte-identical to the ci.yml copies
|
||
# (pinned in scripts/tests/sdk-java-workflow.test.js): residue left by
|
||
# review/autofix jobs on the shared ECS pool fails actions/checkout's
|
||
# clean — the #13506 failure. Hosted legs skip it on fresh VMs.
|
||
- name: 'Clean stale .qwen before checkout'
|
||
if: "${{ runner.environment == 'self-hosted' }}"
|
||
run: |-
|
||
set -uo pipefail
|
||
for stale_qwen in "$GITHUB_WORKSPACE/.qwen" "$GITHUB_WORKSPACE/.qwen.root-orig" "$GITHUB_WORKSPACE/trusted-ci-classifier"; do
|
||
if [ ! -e "$stale_qwen" ] && [ ! -L "$stale_qwen" ]; then
|
||
continue
|
||
fi
|
||
if [ -d "$stale_qwen" ] && [ ! -L "$stale_qwen" ]; then
|
||
chmod -R u+w "$stale_qwen" 2>/dev/null || true
|
||
fi
|
||
# A foreign-owned directory cannot always be renamed to a
|
||
# different parent: updating its .. entry can fail even when the
|
||
# workspace parent is writable. If that individual move fails,
|
||
# quarantine the runner-owned workspace itself, then recreate the
|
||
# empty checkout root. Warm contents are lost only on this
|
||
# otherwise unrecoverable path.
|
||
rm -rf -- "$stale_qwen" 2>/dev/null ||
|
||
sudo -n rm -rf -- "$stale_qwen" 2>/dev/null ||
|
||
{
|
||
quarantine="$(dirname -- "$GITHUB_WORKSPACE")/_qwen-quarantine"
|
||
mkdir -p "$quarantine" 2>/dev/null || true
|
||
stale_name="$(basename -- "$stale_qwen")"
|
||
if mv -- "$stale_qwen" "$quarantine/${stale_name#\.}-$(date -u +%Y%m%dT%H%M%SZ)-$$" 2>/dev/null; then
|
||
echo "::warning::could not delete leaked $stale_name; moved it to $quarantine so this checkout can proceed — that directory needs manual cleanup"
|
||
else
|
||
workspace_quarantine="$quarantine/workspace-$(date -u +%Y%m%dT%H%M%SZ)-$$"
|
||
if mv -- "$GITHUB_WORKSPACE" "$workspace_quarantine" 2>/dev/null &&
|
||
mkdir -p "$GITHUB_WORKSPACE" 2>/dev/null &&
|
||
cd "$GITHUB_WORKSPACE"; then
|
||
echo "::warning::could not delete leaked $stale_name; moved the whole workspace to $workspace_quarantine so this checkout can proceed — that directory needs manual cleanup"
|
||
break
|
||
else
|
||
echo "::warning::leaked $stale_name survived every recovery; runner needs manual cleanup"
|
||
fi
|
||
fi
|
||
}
|
||
done
|
||
# Interrupted reviews leave worktree registrations under .qwen/tmp/
|
||
# and qwen-review/* branches behind. prune drops registrations whose
|
||
# directories the rm above removed; worktree remove --force then
|
||
# clears any still-registered leftover directory (--force tolerates
|
||
# dirty contents), since a branch checked out in a live worktree
|
||
# cannot be deleted. If removal still fails, the registration
|
||
# survives and the branch delete below warns. The sweep deletes all
|
||
# review artifacts, not just the current PR's: safe because a runner
|
||
# executes one job at a time. Kept inline rather than a shared
|
||
# script: this runs pre-checkout on shared runners, where leftover
|
||
# workspace files are untrusted.
|
||
if [ -e "$GITHUB_WORKSPACE/.git" ]; then
|
||
GIT_SAFE=(git -c core.hooksPath=/dev/null -c core.fsmonitor= -C "$GITHUB_WORKSPACE")
|
||
"${GIT_SAFE[@]}" worktree prune -v || true
|
||
"${GIT_SAFE[@]}" worktree list --porcelain \
|
||
| awk '$1 == "worktree" && index($0, "/.qwen/tmp/review-pr-") > 0 { sub(/^worktree /, ""); print }' \
|
||
| while read -r worktree; do
|
||
[ -n "$worktree" ] || continue
|
||
# Registered paths come from leftover git metadata and are
|
||
# untrusted: the awk filter above matched by substring, so reject
|
||
# `..` traversal and re-anchor to the review prefix before the
|
||
# destructive remove.
|
||
case "$worktree" in
|
||
*/../*|../*|*/..)
|
||
echo "::warning::skipping suspicious review worktree path: $worktree"
|
||
continue
|
||
;;
|
||
"$GITHUB_WORKSPACE/.qwen/tmp/review-pr-"*) : ;;
|
||
*)
|
||
echo "::warning::skipping unexpected review worktree path: $worktree"
|
||
continue
|
||
;;
|
||
esac
|
||
"${GIT_SAFE[@]}" worktree remove --force "$worktree" ||
|
||
echo "::warning::could not remove review worktree: $worktree"
|
||
done || true
|
||
"${GIT_SAFE[@]}" worktree prune -v || true
|
||
"${GIT_SAFE[@]}" for-each-ref --format='%(refname:short)' 'refs/heads/qwen-review/*' \
|
||
| while read -r stale_ref; do
|
||
if [ -n "$stale_ref" ]; then
|
||
"${GIT_SAFE[@]}" branch -D "$stale_ref" ||
|
||
echo "::warning::could not remove review branch: $stale_ref"
|
||
fi
|
||
done || true
|
||
fi
|
||
|
||
- name: 'Checkout'
|
||
uses: 'actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10' # v6.0.3
|
||
|
||
- name: 'Check Flyway migration versions are unique'
|
||
run: 'node scripts/check-flyway-migrations.js packages/sdk-java/managed-agent-server packages/sdk-java/runtime-broker packages/sdk-java/qwencode'
|
||
|
||
# Contract version monotonicity, #13804: every pull request that
|
||
# changes the OpenAPI document must move info.version strictly past
|
||
# the base ref's — consumers key capability detection on it, and a
|
||
# regressed or stalled number breaks nothing at runtime to be caught
|
||
# by. The default checkout is one commit deep and holds the
|
||
# prospective merge result, so the base branch's head is fetched at
|
||
# depth one for the comparison copy. Pull requests only: a push has
|
||
# no base to compare, and the races that remain after this ran are
|
||
# the re-check workflow's. Comparing contents, not a changed-paths
|
||
# list, is what lets a merge result that already advanced (another
|
||
# pull request landed) read as regressed-or-stalled instead of
|
||
# passing on a stale branch point.
|
||
- name: 'Check the API contract version advances'
|
||
if: "${{ github.event_name == 'pull_request' }}"
|
||
env:
|
||
BASE_REF: '${{ github.event.pull_request.base.ref }}'
|
||
run: |-
|
||
set -uo pipefail
|
||
DOCUMENT='packages/sdk-java/managed-agent-server/src/main/resources/openapi/managed-agent-public-api.openapi.json'
|
||
BASE_DOCUMENT="${RUNNER_TEMP}/contract-base.json"
|
||
git fetch --quiet --depth=1 --filter=blob:none origin "refs/heads/${BASE_REF}"
|
||
git show "FETCH_HEAD:${DOCUMENT}" > "${BASE_DOCUMENT}" 2>/dev/null || : > "${BASE_DOCUMENT}"
|
||
node scripts/check-contract-version.js "${BASE_DOCUMENT}" "${DOCUMENT}"
|
||
|
||
mysql-integration:
|
||
name: 'Runtime Broker and Managed Agent MariaDB / Java 21'
|
||
runs-on: 'ubuntu-latest'
|
||
timeout-minutes: 15
|
||
services:
|
||
mariadb:
|
||
image: 'mariadb:10.11.18'
|
||
env:
|
||
MARIADB_DATABASE: 'runtime_broker_test'
|
||
MARIADB_ROOT_PASSWORD: 'runtime-broker'
|
||
ports:
|
||
- '3306:3306'
|
||
options: >-
|
||
--health-cmd "healthcheck.sh --connect --innodb_initialized"
|
||
--health-interval 5s
|
||
--health-timeout 5s
|
||
--health-retries 20
|
||
steps:
|
||
- name: 'Checkout'
|
||
uses: 'actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10' # v6.0.3
|
||
|
||
- name: 'Set up Java'
|
||
uses: 'actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654' # v5.2.0
|
||
with:
|
||
distribution: 'temurin'
|
||
java-version: '21'
|
||
cache: 'maven'
|
||
cache-dependency-path: |-
|
||
packages/sdk-java/qwencode/pom.xml
|
||
packages/sdk-java/runtime-broker/pom.xml
|
||
packages/sdk-java/managed-agent-server/pom.xml
|
||
settings-path: '${{ runner.temp }}/setup-java-m2'
|
||
|
||
- name: 'Run Runtime Broker MySQL integration tests'
|
||
working-directory: 'packages/sdk-java/runtime-broker'
|
||
env:
|
||
MAVEN_ARGS: '--settings ${{ runner.temp }}/setup-java-m2/settings.xml --toolchains ${{ runner.temp }}/setup-java-m2/toolchains.xml'
|
||
run: >-
|
||
mvn --batch-mode --no-transfer-progress -Pmysql-integration
|
||
-Dmysql.url='jdbc:mysql://127.0.0.1:3306/runtime_broker_test?allowPublicKeyRetrieval=true&useSSL=false'
|
||
-Dmysql.user=root -Dmysql.password=runtime-broker clean verify
|
||
|
||
- name: 'Install Managed Agent dependencies'
|
||
env:
|
||
MAVEN_ARGS: '--settings ${{ runner.temp }}/setup-java-m2/settings.xml --toolchains ${{ runner.temp }}/setup-java-m2/toolchains.xml'
|
||
run: |-
|
||
mvn --batch-mode --no-transfer-progress -f packages/sdk-java/qwencode/pom.xml -DskipTests -Dgpg.skip=true -Dmaven.javadoc.skip=true install
|
||
# These steps only stage jars; the SpotBugs gate lives on the
|
||
# `clean verify` invocations (pinned by scripts/tests/spotbugs-gate.test.js).
|
||
mvn --batch-mode --no-transfer-progress -f packages/sdk-java/runtime-broker/pom.xml -DskipTests -Dspotbugs.skip=true install
|
||
|
||
- name: 'Run Managed Agent tests, Checkstyle, and MySQL integration'
|
||
working-directory: 'packages/sdk-java/managed-agent-server'
|
||
env:
|
||
MAVEN_ARGS: '--settings ${{ runner.temp }}/setup-java-m2/settings.xml --toolchains ${{ runner.temp }}/setup-java-m2/toolchains.xml'
|
||
run: >-
|
||
mvn --batch-mode --no-transfer-progress -Pmysql-integration
|
||
-Dmysql.url='jdbc:mysql://127.0.0.1:3306/managed_agent_test?createDatabaseIfNotExist=true&allowPublicKeyRetrieval=true&useSSL=false'
|
||
-Dmysql.user=root -Dmysql.password=runtime-broker clean verify checkstyle:check
|
||
|
||
# Maven passes a selection narrowed by a profile, MAVEN_ARGS or
|
||
# .mvn/maven.config, so compare what ran with the source tree.
|
||
- name: 'Check that every non-Hosted integration test class ran'
|
||
run: 'node scripts/check-failsafe-reports.js non-hosted packages/sdk-java/runtime-broker packages/sdk-java/managed-agent-server'
|
||
|
||
hosted-harness-mysql:
|
||
name: 'Hosted process fault gates / MySQL 8.4 / Java 21'
|
||
runs-on: 'ubuntu-latest'
|
||
# The step ceilings (25 + 9×10 + 20 = 135 — the Verify and O4 steps
|
||
# sit above their failsafe fork timeouts) plus ~10 minutes for the
|
||
# uncapped setup steps (Checkout..Install MySQL binaries, the report
|
||
# checks) need ~145; 148 keeps a thin margin, and each arm buys its
|
||
# own 10 instead of sharing an envelope.
|
||
timeout-minutes: 148
|
||
services:
|
||
mysql:
|
||
image: 'mysql:8.4.6'
|
||
env:
|
||
MYSQL_DATABASE: 'hosted_harness_test'
|
||
MYSQL_ROOT_PASSWORD: 'hosted-fixture'
|
||
ports:
|
||
- '3306/tcp'
|
||
options: >-
|
||
--health-cmd "mysqladmin ping -h 127.0.0.1 -phosted-fixture"
|
||
--health-interval 5s
|
||
--health-timeout 5s
|
||
--health-retries 30
|
||
steps:
|
||
- name: 'Checkout'
|
||
uses: 'actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10' # v6.0.3
|
||
- name: 'Set up Java'
|
||
uses: 'actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654' # v5.2.0
|
||
with:
|
||
distribution: 'temurin'
|
||
java-version: '21'
|
||
cache: 'maven'
|
||
cache-dependency-path: 'packages/sdk-java/**/pom.xml'
|
||
settings-path: '${{ runner.temp }}/setup-java-m2'
|
||
- name: 'Set up Node.js'
|
||
uses: 'actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e' # v6.4.0
|
||
with:
|
||
node-version: '22'
|
||
package-manager-cache: false
|
||
- name: 'Enable Corepack'
|
||
run: 'corepack enable'
|
||
- name: 'Install, build and bundle CLI'
|
||
run: |-
|
||
corepack pnpm install --frozen-lockfile --prefer-offline --reporter=append-only
|
||
- name: 'Install Managed Agent dependencies'
|
||
id: 'managed_dependencies'
|
||
env:
|
||
MAVEN_ARGS: '--settings ${{ runner.temp }}/setup-java-m2/settings.xml --toolchains ${{ runner.temp }}/setup-java-m2/toolchains.xml'
|
||
run: |-
|
||
mvn --batch-mode --no-transfer-progress -f packages/sdk-java/qwencode/pom.xml -DskipTests -Dgpg.skip=true -Dmaven.javadoc.skip=true install
|
||
# These steps only stage jars; the SpotBugs gate lives on the
|
||
# `clean verify` invocations (pinned by scripts/tests/spotbugs-gate.test.js).
|
||
mvn --batch-mode --no-transfer-progress -f packages/sdk-java/runtime-broker/pom.xml -DskipTests -Dspotbugs.skip=true install
|
||
# The drivers' proxy header filter only shows up in the gates below as
|
||
# an intermittent socket-reuse failure, so pin it directly first.
|
||
- name: 'Check Hosted proxy header relay'
|
||
run: |-
|
||
cd integration-tests
|
||
npx vitest run helpers/hosted-relay-headers.test.ts
|
||
- name: 'Verify Hosted Java, Spring and MySQL processes'
|
||
# Must outlive the hosted-harness-mysql failsafe fork timeout (900 s)
|
||
# plus the wrapped compile/surefire/spotbugs/checkstyle work: a step
|
||
# killed before its fork leaves no per-test failure lines, so the
|
||
# main-CI failure analyzer can only file an undiagnosable per-commit
|
||
# issue (#13503). hosted-process-ci.test.js pins this ordering.
|
||
timeout-minutes: 25
|
||
env:
|
||
MAVEN_ARGS: '--settings ${{ runner.temp }}/setup-java-m2/settings.xml --toolchains ${{ runner.temp }}/setup-java-m2/toolchains.xml'
|
||
MYSQL_PORT: "${{ job.services.mysql.ports['3306'] }}"
|
||
run: >-
|
||
mvn --batch-mode --no-transfer-progress -f packages/sdk-java/managed-agent-server/pom.xml
|
||
-Phosted-harness-mysql
|
||
-Dnode.executable="$(command -v node)"
|
||
-Dqwen.cli.entry="${GITHUB_WORKSPACE}/dist/cli.js"
|
||
-Dmysql.url="jdbc:mysql://127.0.0.1:${MYSQL_PORT}/hosted_harness_test?allowPublicKeyRetrieval=true&useSSL=false"
|
||
-Dmysql.user=root -Dmysql.password=hosted-fixture clean verify checkstyle:check
|
||
- name: 'Check that every Hosted integration test class ran'
|
||
run: 'node scripts/check-failsafe-reports.js hosted packages/sdk-java/managed-agent-server'
|
||
- name: 'Check Hosted latency measurements'
|
||
run: |-
|
||
test -s packages/sdk-java/managed-agent-server/target/hosted-latency-baseline.json
|
||
cd integration-tests
|
||
npx vitest run cli/hosted-latency-baseline.test.ts
|
||
- name: 'Run Runtime Broker fault gates'
|
||
timeout-minutes: 10
|
||
env:
|
||
MAVEN_ARGS: '--settings ${{ runner.temp }}/setup-java-m2/settings.xml --toolchains ${{ runner.temp }}/setup-java-m2/toolchains.xml'
|
||
run: >-
|
||
mvn --batch-mode --no-transfer-progress -f packages/sdk-java/runtime-broker/pom.xml
|
||
-Pfault-gates -Dqwen.cli.entry="${GITHUB_WORKSPACE}/dist/cli.js" test
|
||
- name: 'Install MySQL binaries for the failover E2E'
|
||
run: |-
|
||
if ! command -v mysqld >/dev/null 2>&1; then
|
||
sudo apt-get update
|
||
sudo apt-get install --yes --no-install-recommends mysql-server mysql-client
|
||
fi
|
||
- name: 'Run session owner failover E2E'
|
||
timeout-minutes: 10
|
||
run: >-
|
||
npm run test:e2e:managed-session-failover
|
||
- name: 'Run in-flight owner failover E2E'
|
||
timeout-minutes: 10
|
||
run: >-
|
||
npm run test:e2e:managed-inflight-failover
|
||
- name: 'Run continuation owner failover E2E'
|
||
timeout-minutes: 10
|
||
run: >-
|
||
npm run test:e2e:managed-continuation-failover
|
||
- name: 'Run long-answer persistence E2E'
|
||
timeout-minutes: 10
|
||
run: >-
|
||
npm run test:e2e:managed-big-output
|
||
- name: 'Run Harness-restart session failover E2E'
|
||
timeout-minutes: 10
|
||
run: >-
|
||
npm run test:e2e:managed-harness-restart-failover
|
||
- name: 'Run Harness-restart in-flight failover E2E'
|
||
timeout-minutes: 10
|
||
run: >-
|
||
npm run test:e2e:managed-harness-restart-inflight-failover
|
||
- name: 'Run Harness-restart continuation failover E2E'
|
||
timeout-minutes: 10
|
||
run: >-
|
||
npm run test:e2e:managed-harness-restart-continuation-failover
|
||
- name: 'Run frozen former-owner fencing E2E'
|
||
timeout-minutes: 10
|
||
run: >-
|
||
npm run test:e2e:managed-continuation-frozen-owner-failover
|
||
- name: 'Upload Hosted process reports'
|
||
if: 'always()'
|
||
uses: 'actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a' # v7.0.1
|
||
with:
|
||
name: 'hosted-harness-mysql-reports'
|
||
path: |
|
||
packages/sdk-java/managed-agent-server/target/failsafe-reports/
|
||
packages/sdk-java/managed-agent-server/target/hosted-latency-baseline.json
|
||
retention-days: 7
|
||
|
||
- name: 'Verify O4 filesystem process and capacity gates'
|
||
id: 'o4_mysql'
|
||
if: "${{ !cancelled() && steps.managed_dependencies.outcome == 'success' }}"
|
||
# Must outlive the o4-mysql-gates failsafe fork timeout (600 s) plus
|
||
# the wrapped compile/surefire/spotbugs/checkstyle work — the same
|
||
# ordering the Hosted verify step needed at #13503: killed at 12
|
||
# minutes on a loaded runner, the step left no per-test failure
|
||
# lines (#13684). hosted-process-ci.test.js pins this ordering.
|
||
timeout-minutes: 20
|
||
env:
|
||
MAVEN_ARGS: '--settings ${{ runner.temp }}/setup-java-m2/settings.xml --toolchains ${{ runner.temp }}/setup-java-m2/toolchains.xml'
|
||
MYSQL_PORT: "${{ job.services.mysql.ports['3306'] }}"
|
||
MYSQL_PWD: 'hosted-fixture'
|
||
QWEN_O4_MYSQL_PASSWORD: 'hosted-fixture'
|
||
run: |-
|
||
mysql --no-defaults --protocol=TCP --host=127.0.0.1 --port="${MYSQL_PORT}" --user=root -e 'CREATE DATABASE qwen_o4_ci'
|
||
mvn --batch-mode --no-transfer-progress -f packages/sdk-java/managed-agent-server/pom.xml \
|
||
-Po4-mysql-gates \
|
||
-Dqwen.o4.mysql.url="jdbc:mysql://127.0.0.1:${MYSQL_PORT}/qwen_o4_ci?allowPublicKeyRetrieval=true&useSSL=false" \
|
||
-Dqwen.o4.mysql.user=root clean verify checkstyle:check && \
|
||
node scripts/check-failsafe-reports.js o4-mysql packages/sdk-java/managed-agent-server
|
||
|
||
- name: 'Upload O4 gate reports'
|
||
if: "${{ always() && steps.o4_mysql.outcome != 'skipped' }}"
|
||
uses: 'actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a' # v7.0.1
|
||
with:
|
||
name: 'o4-mysql-gate-reports'
|
||
path: 'packages/sdk-java/managed-agent-server/target/failsafe-reports/'
|
||
retention-days: 7
|
||
|
||
daemon-e2e:
|
||
name: 'Real daemon E2E / Java 11'
|
||
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 || contains(fromJSON(''["OWNER","MEMBER","COLLABORATOR"]''), github.event.pull_request.author_association))) && fromJSON(''["self-hosted", "linux", "x64", "ecs-qwen"]'') || ''ubuntu-latest'' }}'
|
||
# 45, not 30: this job is ~90% setup. A quiet run is 12.2 min end to end --
|
||
# npm ci 5.9 + build 5.2 -- and the Java daemon E2E it exists for costs
|
||
# about a minute. On the shared pool that setup is what stretches: npm ci
|
||
# measured 4.0-5.9 min across 11 green runs and 19.9/22.6 min on the two
|
||
# that died, which is the whole difference between them. At that factor the
|
||
# setup alone needs ~44 min, so 30 cancelled the job before a single
|
||
# assertion ran. Matches the release.yml quality_build tier
|
||
# (QWEN_RELEASE_BUILD_TIMEOUT_MINUTES, default 45).
|
||
timeout-minutes: 45
|
||
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"
|
||
# A leftover workspace owned by a different uid than the step user
|
||
# (e.g. a runner-user checkout under root-run steps) makes every git
|
||
# call fail with "detected dubious ownership". The pinned
|
||
# actions/checkout adds safe.directory only to its own temporary
|
||
# global config, so later steps are uncovered; trust the workspace
|
||
# here instead. The --get-all guard keeps a persistent runner's
|
||
# gitconfig from accumulating a duplicate entry per job.
|
||
git config --global --get-all safe.directory 2>/dev/null | grep -qxF "$GITHUB_WORKSPACE" || git config --global --add safe.directory "$GITHUB_WORKSPACE" || echo "::warning::could not mark the workspace as a git safe.directory; git steps may fail on a mismatched-ownership checkout"
|
||
|
||
# ci.yml's pre-checkout sweep, byte-identical to the ci.yml copies
|
||
# (pinned in scripts/tests/sdk-java-workflow.test.js): residue left by
|
||
# review/autofix jobs on the shared ECS pool fails actions/checkout's
|
||
# clean — the #13506 failure. Hosted legs skip it on fresh VMs.
|
||
- name: 'Clean stale .qwen before checkout'
|
||
if: "${{ runner.environment == 'self-hosted' }}"
|
||
run: |-
|
||
set -uo pipefail
|
||
for stale_qwen in "$GITHUB_WORKSPACE/.qwen" "$GITHUB_WORKSPACE/.qwen.root-orig" "$GITHUB_WORKSPACE/trusted-ci-classifier"; do
|
||
if [ ! -e "$stale_qwen" ] && [ ! -L "$stale_qwen" ]; then
|
||
continue
|
||
fi
|
||
if [ -d "$stale_qwen" ] && [ ! -L "$stale_qwen" ]; then
|
||
chmod -R u+w "$stale_qwen" 2>/dev/null || true
|
||
fi
|
||
# A foreign-owned directory cannot always be renamed to a
|
||
# different parent: updating its .. entry can fail even when the
|
||
# workspace parent is writable. If that individual move fails,
|
||
# quarantine the runner-owned workspace itself, then recreate the
|
||
# empty checkout root. Warm contents are lost only on this
|
||
# otherwise unrecoverable path.
|
||
rm -rf -- "$stale_qwen" 2>/dev/null ||
|
||
sudo -n rm -rf -- "$stale_qwen" 2>/dev/null ||
|
||
{
|
||
quarantine="$(dirname -- "$GITHUB_WORKSPACE")/_qwen-quarantine"
|
||
mkdir -p "$quarantine" 2>/dev/null || true
|
||
stale_name="$(basename -- "$stale_qwen")"
|
||
if mv -- "$stale_qwen" "$quarantine/${stale_name#\.}-$(date -u +%Y%m%dT%H%M%SZ)-$$" 2>/dev/null; then
|
||
echo "::warning::could not delete leaked $stale_name; moved it to $quarantine so this checkout can proceed — that directory needs manual cleanup"
|
||
else
|
||
workspace_quarantine="$quarantine/workspace-$(date -u +%Y%m%dT%H%M%SZ)-$$"
|
||
if mv -- "$GITHUB_WORKSPACE" "$workspace_quarantine" 2>/dev/null &&
|
||
mkdir -p "$GITHUB_WORKSPACE" 2>/dev/null &&
|
||
cd "$GITHUB_WORKSPACE"; then
|
||
echo "::warning::could not delete leaked $stale_name; moved the whole workspace to $workspace_quarantine so this checkout can proceed — that directory needs manual cleanup"
|
||
break
|
||
else
|
||
echo "::warning::leaked $stale_name survived every recovery; runner needs manual cleanup"
|
||
fi
|
||
fi
|
||
}
|
||
done
|
||
# Interrupted reviews leave worktree registrations under .qwen/tmp/
|
||
# and qwen-review/* branches behind. prune drops registrations whose
|
||
# directories the rm above removed; worktree remove --force then
|
||
# clears any still-registered leftover directory (--force tolerates
|
||
# dirty contents), since a branch checked out in a live worktree
|
||
# cannot be deleted. If removal still fails, the registration
|
||
# survives and the branch delete below warns. The sweep deletes all
|
||
# review artifacts, not just the current PR's: safe because a runner
|
||
# executes one job at a time. Kept inline rather than a shared
|
||
# script: this runs pre-checkout on shared runners, where leftover
|
||
# workspace files are untrusted.
|
||
if [ -e "$GITHUB_WORKSPACE/.git" ]; then
|
||
GIT_SAFE=(git -c core.hooksPath=/dev/null -c core.fsmonitor= -C "$GITHUB_WORKSPACE")
|
||
"${GIT_SAFE[@]}" worktree prune -v || true
|
||
"${GIT_SAFE[@]}" worktree list --porcelain \
|
||
| awk '$1 == "worktree" && index($0, "/.qwen/tmp/review-pr-") > 0 { sub(/^worktree /, ""); print }' \
|
||
| while read -r worktree; do
|
||
[ -n "$worktree" ] || continue
|
||
# Registered paths come from leftover git metadata and are
|
||
# untrusted: the awk filter above matched by substring, so reject
|
||
# `..` traversal and re-anchor to the review prefix before the
|
||
# destructive remove.
|
||
case "$worktree" in
|
||
*/../*|../*|*/..)
|
||
echo "::warning::skipping suspicious review worktree path: $worktree"
|
||
continue
|
||
;;
|
||
"$GITHUB_WORKSPACE/.qwen/tmp/review-pr-"*) : ;;
|
||
*)
|
||
echo "::warning::skipping unexpected review worktree path: $worktree"
|
||
continue
|
||
;;
|
||
esac
|
||
"${GIT_SAFE[@]}" worktree remove --force "$worktree" ||
|
||
echo "::warning::could not remove review worktree: $worktree"
|
||
done || true
|
||
"${GIT_SAFE[@]}" worktree prune -v || true
|
||
"${GIT_SAFE[@]}" for-each-ref --format='%(refname:short)' 'refs/heads/qwen-review/*' \
|
||
| while read -r stale_ref; do
|
||
if [ -n "$stale_ref" ]; then
|
||
"${GIT_SAFE[@]}" branch -D "$stale_ref" ||
|
||
echo "::warning::could not remove review branch: $stale_ref"
|
||
fi
|
||
done || true
|
||
fi
|
||
|
||
- 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
|
||
|
||
# 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'
|
||
package-manager-cache: false
|
||
|
||
- 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:
|
||
distribution: 'temurin'
|
||
java-version: '11'
|
||
cache: 'maven'
|
||
cache-dependency-path: 'packages/sdk-java/qwencode/pom.xml'
|
||
settings-path: '${{ runner.temp }}/setup-java-m2'
|
||
|
||
- 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: 'corepack pnpm install --frozen-lockfile --prefer-offline --reporter=append-only'
|
||
|
||
- name: 'Build Qwen Code'
|
||
run: 'npm run build'
|
||
|
||
- name: 'Bundle Qwen Code'
|
||
run: 'npm run bundle'
|
||
|
||
- name: 'Run Java daemon E2E'
|
||
env:
|
||
MAVEN_ARGS: '--settings ${{ runner.temp }}/setup-java-m2/settings.xml --toolchains ${{ runner.temp }}/setup-java-m2/toolchains.xml'
|
||
run: 'npx tsx scripts/run-java-daemon-sdk-e2e.ts'
|