mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-11-07 06:19:43 -10:00
ci: run Java and Desktop checks daily or on demand (#14205)
* ci: run Java and Desktop checks weekly or on demand Move Java compatibility and integration checks out of PR and push events. Group Desktop Shell checks with the existing packaging dry run, replace daily packaging with weekly validation, and retain manual dispatch. Co-authored-by: Qwen-Coder <[email protected]> * ci: report weekly Java and Desktop failures as issues Include both weekly suites in the existing main failure analyzer and reuse its issue deduplication and autofix routing. Co-authored-by: Qwen-Coder <[email protected]> * ci: run Java and Desktop checks daily Co-authored-by: Qwen-Coder <[email protected]> * ci: format failure reporting workflow list Co-authored-by: Qwen-Coder <[email protected]> * ci: preserve lightweight guards after daily suite migration Co-authored-by: Qwen-Coder <[email protected]> * ci: align lint permission assertion and Java concurrency Co-authored-by: Qwen-Coder <[email protected]> --------- Co-authored-by: Qwen-Coder <[email protected]>
This commit is contained in:
1 parent
355ac8032d
commit
70407feeb9
7 files changed
+193
-356
No files matched your search
+25
-110
@@ -889,6 +889,7 @@ jobs:
|
||||
timeout-minutes: '${{ fromJSON(contains(needs.classify_pr.outputs.ubuntu_runner || ''["ubuntu-latest"]'', ''ecs-qwen'') && ''90'' || ''45'') }}'
|
||||
permissions:
|
||||
contents: 'read'
|
||||
pull-requests: 'read'
|
||||
steps:
|
||||
# Pre-checkout cleanup: a previous containerised job (e.g. qwen-triage
|
||||
# verify) may leave root-owned, read-only files anywhere in the workspace.
|
||||
@@ -1088,6 +1089,30 @@ jobs:
|
||||
if: "${{ needs.classify_pr.outputs.skip_ci != 'true' }}"
|
||||
run: '.github/scripts/check-workflow-size.sh'
|
||||
|
||||
# Keep the cheap schema guards on PRs and main while Java runs daily.
|
||||
- name: 'Check Flyway migration versions are unique'
|
||||
if: "${{ needs.classify_pr.outputs.skip_ci != 'true' && steps.ci_profile.outputs.ci_profile != 'docs_only' }}"
|
||||
run: 'node scripts/check-flyway-migrations.js packages/sdk-java/managed-agent-server packages/sdk-java/runtime-broker packages/sdk-java/qwencode'
|
||||
|
||||
- name: 'Check the API contract version advances'
|
||||
if: "${{ needs.classify_pr.outputs.skip_ci != 'true' && github.event_name == 'pull_request' && steps.ci_profile.outputs.ci_profile != 'docs_only' }}"
|
||||
env:
|
||||
BASE_REF: '${{ github.event.pull_request.base.ref }}'
|
||||
GH_TOKEN: '${{ github.token }}'
|
||||
REPO: '${{ github.repository }}'
|
||||
PR_NUMBER: '${{ github.event.pull_request.number }}'
|
||||
run: |-
|
||||
set -euo pipefail
|
||||
DOCUMENT='packages/sdk-java/managed-agent-server/src/main/resources/openapi/managed-agent-public-api.openapi.json'
|
||||
gh api "repos/${REPO}/pulls/${PR_NUMBER}/files?per_page=100" --paginate --jq '.[].filename' > "${RUNNER_TEMP}/contract-pr-files.txt"
|
||||
if ! grep -qxF "${DOCUMENT}" "${RUNNER_TEMP}/contract-pr-files.txt"; then
|
||||
exit 0
|
||||
fi
|
||||
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}"
|
||||
|
||||
# A green lane proves the branch passes the gate AS THE BRANCH defines
|
||||
# it — the checkout above is refs/pull/N/head by deliberate trade-off —
|
||||
# not as the base defines it. #11117 flipped Prettier --write →
|
||||
@@ -2361,113 +2386,3 @@ jobs:
|
||||
- name: 'Run CLI Integration Tests'
|
||||
run: |-
|
||||
npm run test:integration:cli:sandbox:none
|
||||
|
||||
#
|
||||
# Desktop Shell: compile + test the Tauri crate in PR CI.
|
||||
#
|
||||
# The desktop-release workflow (workflow_dispatch only) is otherwise the sole
|
||||
# place this crate is built, so a compile error can land on a PR and stay
|
||||
# invisible until release time. This job compiles the crate and runs its
|
||||
# release-config tests on every PR that touches the shell. It does not need
|
||||
# the bundled runtime, so it is cheap. `cargo test` builds the crate and thus
|
||||
# catches compile failures (e.g. a moved-value error); fmt/clippy are not run
|
||||
# here because the release pipeline does not gate on them either.
|
||||
desktop_shell:
|
||||
name: 'Desktop Shell (${{ matrix.os }})'
|
||||
needs: 'classify_pr'
|
||||
if: "${{ !cancelled() && github.event_name != 'push' && github.event_name != 'schedule' && needs.classify_pr.outputs.skip_ci != 'true' }}"
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: ['ubuntu-22.04', 'windows-2022']
|
||||
runs-on: '${{ matrix.os }}'
|
||||
timeout-minutes: 45
|
||||
permissions:
|
||||
contents: 'read'
|
||||
pull-requests: 'read'
|
||||
steps:
|
||||
- name: 'Checkout'
|
||||
uses: 'actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10' # v6.0.3
|
||||
with:
|
||||
ref: "${{ github.event.inputs.branch_ref || (github.event_name == 'pull_request' && format('refs/pull/{0}/head', github.event.pull_request.number)) || (github.event_name == 'merge_group' && github.event.merge_group.head_sha) || github.ref }}"
|
||||
fetch-depth: 1
|
||||
|
||||
# Fail open: any uncertainty (non-PR event, API failure) runs the job.
|
||||
#
|
||||
# Ask GitHub which files the PR changed, the same call the CI profile
|
||||
# classifier makes above. The two-tree `git diff BASE HEAD` this replaced
|
||||
# is not the PR's diff: it reports everything the base gained since the
|
||||
# branch point as a change on the PR's side. #8132 added
|
||||
# packages/desktop-shell (since renamed to packages/desktop), so every
|
||||
# branch older than it saw 78 phantom desktop paths, ran this job, and
|
||||
# failed in a checkout of refs/pull/N/head that has no such directory.
|
||||
# The rename cuts the other way and is not caught by the guard below: a
|
||||
# pre-rename head's files sit under packages/desktop-shell, which the
|
||||
# path filter no longer matches, so the job skips and reports green
|
||||
# having compiled nothing. Such a branch needs a rebase to be gated
|
||||
# again. The Cargo.toml guard covers the remaining case -- a head the
|
||||
# filter matched because ci.yml itself changed, with no crate at the
|
||||
# new path.
|
||||
- name: 'Detect desktop changes'
|
||||
id: 'filter'
|
||||
env:
|
||||
GH_TOKEN: '${{ github.token }}'
|
||||
PR_NUMBER: "${{ github.event_name == 'pull_request' && github.event.pull_request.number || '' }}"
|
||||
run: |-
|
||||
changed=true
|
||||
if [[ "${GITHUB_EVENT_NAME}" == "pull_request" && -n "${PR_NUMBER}" ]]; then
|
||||
# `previous_filename` too: renaming a file out of the crate changes
|
||||
# it, and only the old path says so.
|
||||
#
|
||||
# Both feed scripts are listed because `scripts/test-release.js`
|
||||
# is the only test either of them has, and it runs in this job.
|
||||
if files="$(gh api --paginate "repos/${GITHUB_REPOSITORY}/pulls/${PR_NUMBER}/files" --jq '.[] | .filename, (.previous_filename // empty)')"; then
|
||||
if grep -Eq '^(packages/desktop/|\.github/scripts/create-desktop-update-manifest\.mjs|\.github/scripts/create-electron-bridge-manifest\.mjs|\.github/workflows/ci\.yml|\.github/workflows/desktop-release\.yml)' <<<"${files}"; then
|
||||
changed=true
|
||||
else
|
||||
changed=false
|
||||
fi
|
||||
else
|
||||
echo "::warning::Unable to list the PR's changed files; compiling the crate."
|
||||
fi
|
||||
fi
|
||||
# Whatever the filter decided, a tree without the crate cannot have
|
||||
# regressed it, and cargo has no manifest to run against — the job
|
||||
# would report a missing working directory as a failure of the PR.
|
||||
if [[ "${changed}" == "true" && ! -f packages/desktop/src-tauri/Cargo.toml ]]; then
|
||||
echo "::notice::packages/desktop/src-tauri is absent from this head; nothing to compile."
|
||||
changed=false
|
||||
fi
|
||||
echo "changed=${changed}" >> "${GITHUB_OUTPUT}"
|
||||
echo "desktop changed: ${changed}"
|
||||
|
||||
# cargo test links the Tauri/wry webview, so the WebKit/GTK dev headers
|
||||
# must be present (mirrors the Linux build job in desktop-release.yml).
|
||||
- name: 'Install Linux dependencies'
|
||||
if: "${{ steps.filter.outputs.changed == 'true' && runner.os == 'Linux' }}"
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev libatk-bridge2.0-0 at-spi2-core dbus-x11 patchelf libfuse2 xdg-utils
|
||||
|
||||
- uses: 'actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e' # v6.4.0
|
||||
if: "${{ steps.filter.outputs.changed == 'true' && runner.os == 'Linux' }}"
|
||||
with:
|
||||
node-version: '22.x'
|
||||
|
||||
- uses: 'dtolnay/rust-toolchain@4cda84d5c5c54efe2404f9d843567869ab1699d4' # stable
|
||||
if: "${{ steps.filter.outputs.changed == 'true' }}"
|
||||
|
||||
- uses: 'Swatinem/rust-cache@42dc69e1aa15d09112580998cf2ef0119e2e91ae' # v2
|
||||
if: "${{ steps.filter.outputs.changed == 'true' }}"
|
||||
with:
|
||||
workspaces: 'packages/desktop/src-tauri -> target'
|
||||
|
||||
- name: 'Compile and test the desktop crate'
|
||||
if: "${{ steps.filter.outputs.changed == 'true' }}"
|
||||
working-directory: 'packages/desktop'
|
||||
run: 'cargo test --manifest-path src-tauri/Cargo.toml'
|
||||
|
||||
- name: 'Run desktop release tests'
|
||||
if: "${{ steps.filter.outputs.changed == 'true' && runner.os == 'Linux' }}"
|
||||
working-directory: 'packages/desktop'
|
||||
run: 'node scripts/test-release.js'
|
||||
@@ -1,14 +1,14 @@
|
||||
name: 'Desktop Packaging Check'
|
||||
name: 'Desktop Checks'
|
||||
|
||||
run-name: "Desktop packaging check (${{ inputs.qwen_code_ref || 'main' }})"
|
||||
run-name: "Desktop checks (${{ inputs.qwen_code_ref || 'main' }})"
|
||||
|
||||
# The desktop app bundles a Qwen Code runtime built from this repository's
|
||||
# `dist/`, so a change anywhere in the CLI can break desktop packaging. Until
|
||||
# this workflow existed, nothing exercised that path outside a manual release
|
||||
# dispatch: desktop release 0.2.3-preview.0 was the first run to see that a new
|
||||
# CLI dependency had shipped a native library the AppImage bundler chokes on,
|
||||
# two weeks after it landed. This runs the same jobs as a dry run so the
|
||||
# breakage surfaces on the day it lands instead of on release day.
|
||||
# two weeks after it landed. Daily dry runs exercise this path before release;
|
||||
# manual dispatch can validate a selected ref sooner.
|
||||
#
|
||||
# A dry run leaves the signing steps out (they are gated on `dry_run == false`),
|
||||
# so this does not cover macOS notarization or Windows Authenticode.
|
||||
@@ -51,3 +51,65 @@ jobs:
|
||||
draft: true
|
||||
prerelease: true
|
||||
clobber: false
|
||||
|
||||
desktop_shell:
|
||||
name: 'Desktop Shell (${{ matrix.os }})'
|
||||
if: "${{ github.repository == 'QwenLM/qwen-code' }}"
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: ['ubuntu-22.04', 'windows-2022']
|
||||
runs-on: '${{ matrix.os }}'
|
||||
timeout-minutes: 45
|
||||
defaults:
|
||||
run:
|
||||
shell: 'bash'
|
||||
permissions:
|
||||
contents: 'read'
|
||||
steps:
|
||||
- name: 'Checkout'
|
||||
uses: 'actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10' # v6.0.3
|
||||
with:
|
||||
ref: "${{ inputs.qwen_code_ref || 'main' }}"
|
||||
fetch-depth: 1
|
||||
|
||||
- name: 'Check desktop crate'
|
||||
id: 'filter'
|
||||
run: |-
|
||||
changed=true
|
||||
if [[ ! -f packages/desktop/src-tauri/Cargo.toml ]]; then
|
||||
echo "::notice::packages/desktop/src-tauri is absent from this head; nothing to compile."
|
||||
changed=false
|
||||
fi
|
||||
echo "changed=${changed}" >> "${GITHUB_OUTPUT}"
|
||||
|
||||
# cargo test links the Tauri/wry webview, so the WebKit/GTK dev headers
|
||||
# must be present (mirrors the Linux build job in desktop-release.yml).
|
||||
- name: 'Install Linux dependencies'
|
||||
if: "${{ steps.filter.outputs.changed == 'true' && runner.os == 'Linux' }}"
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev libatk-bridge2.0-0 at-spi2-core dbus-x11 patchelf libfuse2 xdg-utils
|
||||
|
||||
- uses: 'actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e' # v6.4.0
|
||||
if: "${{ steps.filter.outputs.changed == 'true' && runner.os == 'Linux' }}"
|
||||
with:
|
||||
node-version: '22.x'
|
||||
|
||||
- uses: 'dtolnay/rust-toolchain@4cda84d5c5c54efe2404f9d843567869ab1699d4' # stable
|
||||
if: "${{ steps.filter.outputs.changed == 'true' }}"
|
||||
|
||||
- uses: 'Swatinem/rust-cache@42dc69e1aa15d09112580998cf2ef0119e2e91ae' # v2
|
||||
if: "${{ steps.filter.outputs.changed == 'true' }}"
|
||||
with:
|
||||
workspaces: 'packages/desktop/src-tauri -> target'
|
||||
|
||||
- name: 'Compile and test the desktop crate'
|
||||
if: "${{ steps.filter.outputs.changed == 'true' }}"
|
||||
working-directory: 'packages/desktop'
|
||||
run: 'cargo test --manifest-path src-tauri/Cargo.toml'
|
||||
|
||||
- name: 'Run desktop release tests'
|
||||
if: "${{ steps.filter.outputs.changed == 'true' && runner.os == 'Linux' }}"
|
||||
working-directory: 'packages/desktop'
|
||||
run: 'node scripts/test-release.js'
|
||||
@@ -13,16 +13,14 @@ on:
|
||||
# to surface a regression as it lands; a failure there files an issue with
|
||||
# `type/bug`, `status/ready-for-agent` and `autofix/approved`, so expect
|
||||
# one per red merge, not per red nightly only.
|
||||
# 'SDK Java' is on the list for its post-merge push run. Its path filter
|
||||
# watches the SDK's embedding surfaces too (packages/cli/src/serve/**,
|
||||
# packages/acp-bridge/**, packages/cli/src/acp-integration/**), so it
|
||||
# fires on roughly half of recent merges to `main`, not only on SDK
|
||||
# changes — and that run is the only signal when a merge result is broken
|
||||
# in a way neither PR could see: two green branches whose Flyway
|
||||
# migrations claim one version collide only after both land (#12940: main
|
||||
# red for two hours while three more merges stacked on it). It has no
|
||||
# scheduled run, so the push arm covers it.
|
||||
workflows: ['E2E Tests', 'SDK Java', 'SDK Python', 'Qwen Code CI']
|
||||
# Daily Java and Desktop checks report main regressions through the same
|
||||
# deduplicated issue and autofix route as the nightly CLI checks.
|
||||
workflows:
|
||||
- 'E2E Tests'
|
||||
- 'SDK Java'
|
||||
- 'SDK Python'
|
||||
- 'Qwen Code CI'
|
||||
- 'Desktop Checks'
|
||||
types: ['completed']
|
||||
# Trigger-level branch filter, not only the `if` below: 'Qwen Code CI'
|
||||
# completes on every pull request, and without this each of those would
|
||||
@@ -42,7 +40,13 @@ jobs:
|
||||
# job as outputs.
|
||||
analyze:
|
||||
name: 'Identify the failing tests'
|
||||
if: "${{ github.repository == 'QwenLM/qwen-code' && github.event.workflow_run.conclusion == 'failure' && github.event.workflow_run.head_branch == 'main' && (github.event.workflow_run.event == 'push' || (github.event.workflow_run.event == 'schedule' && github.event.workflow_run.name == 'Qwen Code CI')) }}"
|
||||
if: >-
|
||||
${{ github.repository == 'QwenLM/qwen-code' &&
|
||||
github.event.workflow_run.conclusion == 'failure' &&
|
||||
github.event.workflow_run.head_branch == 'main' &&
|
||||
(github.event.workflow_run.event == 'push' ||
|
||||
(github.event.workflow_run.event == 'schedule' &&
|
||||
contains(fromJSON('["Qwen Code CI","SDK Java","Desktop Checks"]'), github.event.workflow_run.name))) }}
|
||||
runs-on: 'ubuntu-latest'
|
||||
timeout-minutes: 10
|
||||
permissions:
|
||||
|
||||
+10
-128
@@ -1,83 +1,9 @@
|
||||
name: 'SDK Java'
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '47 19 * * *'
|
||||
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/commands/channel/**'
|
||||
- 'packages/cli/src/commands/channel.ts'
|
||||
- 'packages/cli/src/serve/**'
|
||||
- 'packages/channels/email/**'
|
||||
- 'packages/channels/base/**'
|
||||
- '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/rocketmq-lite-broker.sh'
|
||||
- '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/commands/channel/**'
|
||||
- 'packages/cli/src/commands/channel.ts'
|
||||
- 'packages/cli/src/serve/**'
|
||||
- 'packages/channels/email/**'
|
||||
- 'packages/channels/base/**'
|
||||
- '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/rocketmq-lite-broker.sh'
|
||||
- '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'
|
||||
@@ -87,13 +13,15 @@ env:
|
||||
MAVEN_SHA512: 'bcfe4fe305c962ace56ac7b5fc7a08b87d5abd8b7e89027ab251069faebee516b0ded8961445d6d91ec1985dfe30f8153268843c89aa392733d1a3ec956c9978'
|
||||
|
||||
concurrency:
|
||||
group: 'sdk-java-${{ github.event.pull_request.number || github.run_id }}'
|
||||
cancel-in-progress: true
|
||||
group: 'sdk-java-${{ github.event.pull_request.number || github.ref }}'
|
||||
# Post-merge runs must not cancel each other: cancelled push runs would erase
|
||||
# the only post-merge regression evidence (see e2e.yml / pnpm-worktree-smoke.yml).
|
||||
cancel-in-progress: "${{ github.event_name == 'pull_request' }}"
|
||||
|
||||
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 }}'
|
||||
runs-on: '${{ (matrix.os == ''ubuntu-latest'' && 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"]'') || matrix.os }}'
|
||||
timeout-minutes: 60
|
||||
strategy:
|
||||
fail-fast: false
|
||||
@@ -225,18 +153,7 @@ jobs:
|
||||
- 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
|
||||
ref: '${{ github.sha }}'
|
||||
|
||||
# setup-java updates Maven files non-atomically. Keep them job-local
|
||||
# because runner instances on one ECS host share HOME.
|
||||
@@ -462,30 +379,6 @@ jobs:
|
||||
- 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'
|
||||
# Pool routing per the flyway job (#13245): hosted queue starvation fails
|
||||
@@ -1036,7 +929,7 @@ jobs:
|
||||
|
||||
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'' }}'
|
||||
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"]'') || ''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
|
||||
@@ -1162,18 +1055,7 @@ jobs:
|
||||
- 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
|
||||
ref: '${{ github.sha }}'
|
||||
|
||||
# Self-hosted can't reach nodejs.org reliably; reuse the machine's Node.
|
||||
- name: 'Set up Node.js 22 (hosted)'
|
||||
|
||||
@@ -376,7 +376,11 @@ describe('post-merge push lane', () => {
|
||||
// dispatch and push; widening this — say by copying `test`'s block when
|
||||
// adding a reporter — hands write tokens to that code. `test`'s own
|
||||
// wider block is legitimate: dorny/test-reporter needs `checks: write`.
|
||||
expect(ci.jobs.lint_and_static.permissions).toEqual({ contents: 'read' });
|
||||
// The contract guard reads the PR file list; it needs no write access.
|
||||
expect(ci.jobs.lint_and_static.permissions).toEqual({
|
||||
contents: 'read',
|
||||
'pull-requests': 'read',
|
||||
});
|
||||
});
|
||||
|
||||
it('keeps every shared prelude step identical between test and lint_and_static', () => {
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
// Three agreements nothing else in the suite reads, all of which fail green.
|
||||
// Two agreements nothing else in the suite reads, all of which fail green.
|
||||
//
|
||||
// 1. `scripts/check-desktop-isolation.js` keeps a `nativePrefixes` list, and
|
||||
// the root workspace manifests keep a `!packages/*` negation per native
|
||||
@@ -16,19 +16,8 @@
|
||||
// guard to keep covering the package. Deleting the added line changes
|
||||
// neither the guard's output nor its exit code, so only this file notices.
|
||||
//
|
||||
// 2. The `desktop_shell` job in `ci.yml` names the crate directory in five
|
||||
// places -- the changed-files filter, the `Cargo.toml` existence guard and
|
||||
// its `::notice::` text, the rust-cache `workspaces:`, and two
|
||||
// `working-directory:` values. They have to name the same directory as each
|
||||
// other and a directory that exists, or the job skips and reports success
|
||||
// having compiled nothing.
|
||||
//
|
||||
// 3. That job's changed-files filter lists both updater-feed scripts.
|
||||
// `packages/desktop/scripts/test-release.js` is the only test either of them
|
||||
// has and it runs in this job, so a filter naming one script and not the
|
||||
// other lets a PR that edits only the unlisted one skip its own test and
|
||||
// still report green. The filter is only worth pinning together with the
|
||||
// lane it gates, so the same test asserts that lane still runs the script.
|
||||
// 2. The daily Desktop job must agree on the crate directory across its
|
||||
// presence guard, cache and working directories, and retain release tests.
|
||||
//
|
||||
// The npm/pnpm mirror of the negation list is already pinned by
|
||||
// `package-scripts.test.js` ("mirrors the npm workspace boundaries in
|
||||
@@ -78,7 +67,10 @@ const npmNegations = JSON.parse(read('package.json'))
|
||||
.map((entry) => entry.slice(1));
|
||||
|
||||
const ci = parseYaml(read('.github', 'workflows', 'ci.yml'));
|
||||
const desktopJob = ci.jobs.desktop_shell;
|
||||
const desktop = parseYaml(
|
||||
read('.github', 'workflows', 'desktop-packaging-check.yml'),
|
||||
);
|
||||
const desktopJob = desktop.jobs.desktop_shell;
|
||||
const stepNamed = (name) =>
|
||||
desktopJob.steps.find((step) => step.name === name) ??
|
||||
desktopJob.steps.find((step) => String(step.uses ?? '').includes(name));
|
||||
@@ -133,9 +125,7 @@ describe('desktop_shell CI job — the crate path agrees with itself', () => {
|
||||
const filterRun = String(filterStep?.run ?? '');
|
||||
|
||||
it('still carries the Cargo.toml existence guard, and it clears changed', () => {
|
||||
// Not redundant with the filter: the filter also matches on ci.yml and
|
||||
// desktop-release.yml changing, which a head with no crate at all can do.
|
||||
// That is the #8132 failure the job's header comment describes.
|
||||
// Manual dispatch may select a ref that predates the desktop crate.
|
||||
// The condition is only half the guard: without the assignment inside its
|
||||
// own body, every gated step still runs against a crate that is not there.
|
||||
const guardBody =
|
||||
@@ -152,13 +142,7 @@ describe('desktop_shell CI job — the crate path agrees with itself', () => {
|
||||
).toMatch(/^\s*changed=false\s*$/mu);
|
||||
});
|
||||
|
||||
it('names one crate directory in all five places, and it exists', () => {
|
||||
const filterAlternative = /\^\(([^|]+)\|/u.exec(filterRun)?.[1];
|
||||
expect(
|
||||
filterAlternative,
|
||||
'changed-files filter lost its path alternative',
|
||||
).toBeDefined();
|
||||
|
||||
it('names one existing crate directory in every step', () => {
|
||||
const guardPath = /! -f (\S+)\/src-tauri\/Cargo\.toml/u.exec(
|
||||
filterRun,
|
||||
)?.[1];
|
||||
@@ -177,9 +161,7 @@ describe('desktop_shell CI job — the crate path agrees with itself', () => {
|
||||
.filter((value) => value !== undefined)
|
||||
.map(String);
|
||||
|
||||
expect(filterAlternative).toBeDefined();
|
||||
const sites = {
|
||||
'changed-files filter': filterAlternative.replace(/\/$/u, ''),
|
||||
'Cargo.toml guard': guardPath,
|
||||
'guard ::notice::': noticePath,
|
||||
'rust-cache workspaces': cacheRoot,
|
||||
@@ -189,7 +171,7 @@ describe('desktop_shell CI job — the crate path agrees with itself', () => {
|
||||
};
|
||||
|
||||
// Every site must resolve; an undefined one means the parser fell behind a
|
||||
// rewrite of ci.yml, which must not read as agreement.
|
||||
// rewrite of the workflow, which must not read as agreement.
|
||||
for (const [site, value] of Object.entries(sites)) {
|
||||
expect(
|
||||
value,
|
||||
@@ -201,62 +183,29 @@ describe('desktop_shell CI job — the crate path agrees with itself', () => {
|
||||
JSON.stringify(sites, null, 2),
|
||||
).toBe(1);
|
||||
|
||||
const crateDir = sites['changed-files filter'];
|
||||
const crateDir = sites['Cargo.toml guard'];
|
||||
expect(
|
||||
existsSync(join(root, crateDir, 'src-tauri', 'Cargo.toml')),
|
||||
`${crateDir}/src-tauri/Cargo.toml does not exist, so every gated step in desktop_shell skips and the job reports success having compiled nothing.`,
|
||||
).toBe(true);
|
||||
// The filter is `grep -Eq '^(<alternative>|...)'` over the PR's changed
|
||||
// file names, so the alternative is a bare path prefix and has to end in
|
||||
// `/`: without it, `packages/desktop` also matches a sibling such as
|
||||
// `packages/desktop-extra/`, and the job runs on heads that never touch
|
||||
// the crate. Building a path out of `crateDir` -- this same alternative
|
||||
// with the slash stripped -- and comparing it back proves nothing.
|
||||
expect(
|
||||
filterAlternative.endsWith('/'),
|
||||
`the filter alternative ${filterAlternative} has no trailing slash, so the changed-files filter also matches sibling paths that merely start with it`,
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('lists both updater-feed scripts, so either one triggers the lane that tests them', () => {
|
||||
// Read the alternatives out of the `grep -Eq '^(...)'` call instead of
|
||||
// searching the whole run script. A substring match cannot tell an
|
||||
// alternative of the anchored ERE from the same text in one of that step's
|
||||
// comment lines, and it does not pin the `-E` mode the pattern's `\.`, `|`
|
||||
// and `^(…)` depend on: under `grep -Fq` they are literals, the group
|
||||
// never matches, every PR gets changed=false, and the job reports success
|
||||
// having compiled nothing.
|
||||
const pattern = /grep -Eq '\^\((.*)\)'/u.exec(filterRun)?.[1];
|
||||
expect(
|
||||
pattern,
|
||||
"the changed-files filter is no longer an anchored `grep -Eq '^(...)'` alternation, so its `\\.` escapes and `|` separators are literals and no changed file ever matches",
|
||||
).toBeDefined();
|
||||
const alternatives = pattern.split('|');
|
||||
for (const script of [
|
||||
'create-desktop-update-manifest',
|
||||
'create-electron-bridge-manifest',
|
||||
]) {
|
||||
expect(
|
||||
alternatives,
|
||||
`the changed-files filter does not list ${script}.mjs as an alternative, so a PR touching only that script skips 'Run desktop release tests' and reports green having tested nothing`,
|
||||
).toContain(`\\.github/scripts/${script}\\.mjs`);
|
||||
}
|
||||
it('runs daily or manually without registering PR desktop jobs', () => {
|
||||
expect(ci.jobs.desktop_shell).toBeUndefined();
|
||||
expect(Object.keys(desktop.on).sort()).toEqual([
|
||||
'schedule',
|
||||
'workflow_dispatch',
|
||||
]);
|
||||
expect(desktop.on.schedule).toEqual([{ cron: '0 16 * * *' }]);
|
||||
expect(desktopJob.defaults.run.shell).toBe('bash');
|
||||
});
|
||||
|
||||
// The filter is only the first link: the mechanism that closes #12806 is
|
||||
// filter matches -> desktop_shell runs -> `Run desktop release tests`
|
||||
// executes test-release.js -> the matrix/feed parity assert fires. Pin the
|
||||
// last two links as well, or the filter can gate a lane that tests nothing.
|
||||
// `toContain` on the changed-files conjunct, not equality on the whole
|
||||
// `if:` -- that gate also carries `runner.os == 'Linux'`, which the
|
||||
// windows-2022 leg of the job matrix legitimately does not satisfy.
|
||||
it('runs release tests whenever the desktop crate is present', () => {
|
||||
expect(filterRun).toMatch(/^changed=true$/mu);
|
||||
const lane = stepNamed('Run desktop release tests');
|
||||
expect(
|
||||
String(lane?.run ?? ''),
|
||||
'desktop_shell no longer runs test-release.js, so the changed-files filter gates a lane that tests nothing',
|
||||
).toContain('node scripts/test-release.js');
|
||||
expect(
|
||||
String(lane?.if ?? ''),
|
||||
'the test-release.js lane is no longer gated by the filter this test pins, so the two can drift apart silently',
|
||||
).toContain("steps.filter.outputs.changed == 'true'");
|
||||
expect(lane.run).toBe('node scripts/test-release.js');
|
||||
expect(lane.if).toBe(
|
||||
"${{ steps.filter.outputs.changed == 'true' && runner.os == 'Linux' }}",
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -54,10 +54,7 @@ describe('SDK Java self-hosted workflow guards', () => {
|
||||
// NONE, '') fail contains() and stay hosted.
|
||||
'contains(fromJSON(\'\'["OWNER","MEMBER","COLLABORATOR"]\'\'), github.event.pull_request.author_association)',
|
||||
'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',
|
||||
"ref: '${{ github.sha }}'",
|
||||
]) {
|
||||
expect(block).toContain(fragment);
|
||||
}
|
||||
@@ -198,23 +195,32 @@ describe('SDK Java Flyway migration version guard', () => {
|
||||
}
|
||||
});
|
||||
|
||||
it('triggers the workflow when the guard script itself changes', () => {
|
||||
it('runs the full suite daily or on demand', () => {
|
||||
const yml = parse(workflow);
|
||||
for (const event of ['pull_request', 'push']) {
|
||||
expect(yml.on[event].paths).toContain(
|
||||
'scripts/check-flyway-migrations.js',
|
||||
);
|
||||
expect(yml.on[event].paths).toContain(
|
||||
'scripts/flyway-migration-utils.js',
|
||||
);
|
||||
// The contract version guard of #13804 rides the same lane.
|
||||
expect(yml.on[event].paths).toContain(
|
||||
'scripts/check-contract-version.js',
|
||||
);
|
||||
expect(yml.on[event].paths).toContain(
|
||||
'scripts/contract-version-utils.js',
|
||||
);
|
||||
}
|
||||
expect(Object.keys(yml.on).sort()).toEqual([
|
||||
'schedule',
|
||||
'workflow_dispatch',
|
||||
]);
|
||||
expect(yml.on.schedule).toEqual([{ cron: '47 19 * * *' }]);
|
||||
});
|
||||
|
||||
it('keeps the cheap migration guard in PR and post-merge CI', () => {
|
||||
const ci = parse(readFileSync('.github/workflows/ci.yml', 'utf8'));
|
||||
expect(ci.on.pull_request.branches).toContain('main');
|
||||
expect(ci.on.push.branches).toContain('main');
|
||||
expect(ci.on.pull_request.paths).toBeUndefined();
|
||||
expect(ci.on.push.paths).toBeUndefined();
|
||||
const guard = ci.jobs.lint_and_static.steps.find(
|
||||
(s) => s.name === 'Check Flyway migration versions are unique',
|
||||
);
|
||||
expect(guard.run).toBe(
|
||||
parse(workflow).jobs['flyway-migrations'].steps.find(
|
||||
(s) => s.name === 'Check Flyway migration versions are unique',
|
||||
).run,
|
||||
);
|
||||
expect(guard.if).toBe(
|
||||
"${{ needs.classify_pr.outputs.skip_ci != 'true' && steps.ci_profile.outputs.ci_profile != 'docs_only' }}",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -222,20 +228,30 @@ describe('SDK Java Flyway migration version guard', () => {
|
||||
// the base ref's whenever the document changed. Consumers key capability
|
||||
// detection on it, and a regressed or stalled number breaks nothing at
|
||||
// runtime to be caught by — both modes were seen in flight on 2026-10-09.
|
||||
// The check rides the same lane as the Flyway guard: a second job would
|
||||
// spend another pool checkout on a five-second scan.
|
||||
// The lightweight checks share the existing PR lint job.
|
||||
describe('SDK Java contract version guard', () => {
|
||||
it('runs the advance check on every pull request in the Flyway lane', () => {
|
||||
const parsed = parse(workflow).jobs['flyway-migrations'];
|
||||
it('runs the advance check in PR CI before dependency installation', () => {
|
||||
const ci = parse(readFileSync('.github/workflows/ci.yml', 'utf8'));
|
||||
expect(ci.on.pull_request.branches).toContain('main');
|
||||
expect(ci.on.pull_request.paths).toBeUndefined();
|
||||
const parsed = ci.jobs.lint_and_static;
|
||||
const names = parsed.steps.map((s) => s.name);
|
||||
const index = names.indexOf('Check the API contract version advances');
|
||||
expect(index).toBeGreaterThan(names.indexOf('Checkout'));
|
||||
const step = parsed.steps[index];
|
||||
// A push has no base ref to compare against; the races that remain
|
||||
// after a pull request's check are the re-check workflow's below.
|
||||
expect(step.if).toBe("${{ github.event_name == 'pull_request' }}");
|
||||
expect(step.if).toBe(
|
||||
"${{ needs.classify_pr.outputs.skip_ci != 'true' && github.event_name == 'pull_request' && steps.ci_profile.outputs.ci_profile != 'docs_only' }}",
|
||||
);
|
||||
expect(index).toBeLessThan(
|
||||
parsed.steps.findIndex((s) => s.run?.includes('pnpm install')),
|
||||
);
|
||||
expect(step.env).toEqual({
|
||||
BASE_REF: '${{ github.event.pull_request.base.ref }}',
|
||||
GH_TOKEN: '${{ github.token }}',
|
||||
REPO: '${{ github.repository }}',
|
||||
PR_NUMBER: '${{ github.event.pull_request.number }}',
|
||||
});
|
||||
// The default checkout is one commit deep, so the comparison copy comes
|
||||
// from a depth-one fetch of the base branch's head — not of a stale
|
||||
@@ -244,6 +260,11 @@ describe('SDK Java contract version guard', () => {
|
||||
'git fetch --quiet --depth=1 --filter=blob:none origin "refs/heads/${BASE_REF}"',
|
||||
);
|
||||
expect(step.run).toContain(CONTRACT_DOCUMENT);
|
||||
expect(parsed.permissions['pull-requests']).toBe('read');
|
||||
expect(step.run).toContain('--paginate');
|
||||
expect(step.run).toContain(
|
||||
'grep -qxF "${DOCUMENT}" "${RUNNER_TEMP}/contract-pr-files.txt"',
|
||||
);
|
||||
expect(step.run).toContain('node scripts/check-contract-version.js');
|
||||
});
|
||||
});
|
||||
|
||||
Reference in new issue
Block a user