mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-11-07 06:19:43 -10:00
* fix(managed-agent): find the Flyway scripts in the module jar the server ships them in Since #15871 split the server into Maven modules, its Flyway scripts ship in BOOT-INF/lib/qwen-managed-agent-persistence-*.jar rather than in BOOT-INF/classes/db/. The upgrade command's check read only the classes, so on every archive since then its database row warned "cannot compare the Flyway scripts (no Flyway script found in ...)", and a release that changed or reordered a script the installed database ran went unflagged. migrationScripts() now also reads db/**/ at the root of each nested jar in BOOT-INF/lib/. Spring Boot stores those uncompressed, so their directories are read in place; a compressed one, which Spring Boot would not load, is skipped. A script keeps its path as its name wherever it ships, so one moved from the classes into a module compares as unchanged. The reference distribution workflow now reads the scripts out of the exact archive it built, so a later change of layout fails there rather than turning check's row into a standing WARN. * fix(managed-agent): read a Flyway script shipped twice with different contents as unreadable The reader keys the scripts on their classpath path, and the nested jars give that key a second source. Two copies of one path with different contents left whichever the central directory listed last in the map, so the database row compared one arbitrary copy, while the class loader would hand Flyway the other. Matching copies are still one script; differing ones now make the jar unreadable, which the row reports as "cannot compare the Flyway scripts". * fix(managed-agent): name both places that ship a differing Flyway script, and pin the local extra field - When one script path ships twice with different contents, the error now names the two places, BOOT-INF/classes or the nested jar, so the operator need not unpack the jar to find the second copy. - The test zips can carry a local-header extra field, as archivers add for timestamps or alignment, and the nested-jar case reads entries past one, so dropping that term from dataStart fails the suite.
537 lines
28 KiB
YAML
537 lines
28 KiB
YAML
name: 'Managed Agent Reference Distribution'
|
|
|
|
run-name: "Managed Agent reference ${{ inputs.version || format('PR #{0} dry run', github.event.pull_request.number) }}"
|
|
|
|
on:
|
|
pull_request:
|
|
paths:
|
|
- '.github/workflows/release-managed-agent-reference.yml'
|
|
- 'scripts/package-managed-agent-reference.mjs'
|
|
- 'scripts/generate-managed-agent-configuration-reference.mjs'
|
|
- 'scripts/contract-version-utils.js'
|
|
- 'scripts/flyway-migration-utils.js'
|
|
- 'scripts/tests/package-managed-agent-reference.test.mjs'
|
|
- 'scripts/tests/managed-agent-upgrade.test.mjs'
|
|
- 'scripts/tests/generate-managed-agent-configuration-reference.test.mjs'
|
|
- 'packages/sdk-java/qwencode/**'
|
|
- 'packages/managed-agent-server/**'
|
|
- 'packages/core/src/managed-runtime/contracts/**'
|
|
- 'packages/cli/src/serve/capabilities.ts'
|
|
- 'packages/cli/src/serve/broker-managed-runtime-provider.ts'
|
|
- 'packages/cli/src/serve/hosted-harness-contract.ts'
|
|
- 'docs/users/managed-agent-reference-deployment.md'
|
|
- 'docs/users/managed-agent-reference-deployment.zh-CN.md'
|
|
- 'docs/users/managed-agent-configuration-reference.md'
|
|
- 'docs/users/managed-agent-configuration-reference.zh-CN.md'
|
|
- 'docs/users/hosted-workspace-recovery.md'
|
|
- 'docs/users/hosted-workspace-recovery.zh-CN.md'
|
|
- 'LICENSE'
|
|
workflow_dispatch:
|
|
inputs:
|
|
version:
|
|
description: 'Experimental distribution version, for example 0.1.0-alpha.1.'
|
|
required: true
|
|
type: 'string'
|
|
dry_run:
|
|
description: 'Build and verify without creating a tag or GitHub prerelease.'
|
|
required: true
|
|
default: true
|
|
type: 'boolean'
|
|
|
|
permissions:
|
|
contents: 'read'
|
|
|
|
concurrency:
|
|
group: 'managed-agent-reference-${{ github.event.pull_request.number || github.run_id }}'
|
|
cancel-in-progress: false
|
|
|
|
jobs:
|
|
build:
|
|
name: 'Build and verify reference distribution'
|
|
# Only trusted PR dry runs use the persistent pool. A dispatch build feeds
|
|
# the published release, so it stays on a fresh hosted VM that never runs
|
|
# PR code.
|
|
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: 45
|
|
env:
|
|
MAVEN_VERSION: '3.9.11'
|
|
MAVEN_SHA512: 'bcfe4fe305c962ace56ac7b5fc7a08b87d5abd8b7e89027ab251069faebee516b0ded8961445d6d91ec1985dfe30f8153268843c89aa392733d1a3ec956c9978'
|
|
MYSQL_CONTAINER: 'qwen-code-reference-mysql-${{ github.run_id }}-${{ github.run_attempt }}'
|
|
outputs:
|
|
tag: '${{ steps.version.outputs.tag }}'
|
|
version: '${{ steps.version.outputs.version }}'
|
|
steps:
|
|
- name: 'Resolve experimental version'
|
|
id: 'version'
|
|
shell: 'bash'
|
|
env:
|
|
INPUT_VERSION: '${{ inputs.version }}'
|
|
PR_NUMBER: '${{ github.event.pull_request.number }}'
|
|
DRY_RUN: '${{ inputs.dry_run }}'
|
|
run: |-
|
|
set -euo pipefail
|
|
if [[ "${GITHUB_EVENT_NAME}" == 'pull_request' ]]; then
|
|
version="0.0.0-pr.${PR_NUMBER}"
|
|
else
|
|
version="${INPUT_VERSION#v}"
|
|
fi
|
|
invalid_version() {
|
|
echo '::error::The reference distribution requires an experimental SemVer with a prerelease.'
|
|
exit 1
|
|
}
|
|
if [[ ! "${version}" =~ ^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)-[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*$ ]]; then
|
|
invalid_version
|
|
fi
|
|
prerelease="${version#*-}"
|
|
IFS='.' read -r -a identifiers <<< "${prerelease}"
|
|
for identifier in "${identifiers[@]}"; do
|
|
if [[ "${identifier}" =~ ^[0-9]+$ && "${#identifier}" -gt 1 && "${identifier:0:1}" == '0' ]]; then
|
|
invalid_version
|
|
fi
|
|
done
|
|
if [[ "${GITHUB_EVENT_NAME}" == 'workflow_dispatch' && "${DRY_RUN}" == 'false' && "${GITHUB_REF}" != 'refs/heads/main' ]]; then
|
|
echo '::error::Published reference distributions must run from main.'
|
|
exit 1
|
|
fi
|
|
echo "version=${version}" >> "${GITHUB_OUTPUT}"
|
|
echo "tag=managed-agent-reference-v${version}" >> "${GITHUB_OUTPUT}"
|
|
|
|
- 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: 'Recreate runner Git metadata'
|
|
if: "${{ runner.environment == 'self-hosted' }}"
|
|
run: |-
|
|
rm -rf -- "$GITHUB_WORKSPACE/.git"
|
|
|
|
- name: 'Checkout source revision'
|
|
uses: 'actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10' # v6.0.3
|
|
with:
|
|
persist-credentials: false
|
|
|
|
- name: 'Set up Java 21'
|
|
uses: 'actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654' # v5.2.0
|
|
with:
|
|
distribution: 'temurin'
|
|
java-version: '21'
|
|
cache: "${{ runner.environment == 'github-hosted' && 'maven' || '' }}"
|
|
cache-dependency-path: |-
|
|
packages/sdk-java/qwencode/pom.xml
|
|
packages/managed-agent-server/**/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"
|
|
# Maven Central rate-limits the pool's shared egress for minutes at a
|
|
# time: 429 killed a lane before any test (run 40191430799), and a 2 s
|
|
# retry and then a 15 s one, 2.5 minutes in all, answered 429 on every
|
|
# attempt here. So a host keeps one copy and a cold fetch falls back
|
|
# to the mirror that serves the same bytes -- the pinned SHA-512 below
|
|
# still decides what gets extracted, so a bad mirror only fails.
|
|
cached="${HOME}/.cache/qwen-code-ci/apache-maven-${MAVEN_VERSION}-bin.tar.gz"
|
|
if ! { [ -f "${cached}" ] && cp -- "${cached}" "${archive}" &&
|
|
echo "${MAVEN_SHA512} ${archive}" | sha512sum --check --status; }; then
|
|
for base in \
|
|
'https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven' \
|
|
'https://maven.aliyun.com/repository/public/org/apache/maven/apache-maven'; do
|
|
if curl -fsSL --retry 2 --retry-delay 5 --retry-connrefused --retry-max-time 30 --max-time 300 \
|
|
"${base}/${MAVEN_VERSION}/apache-maven-${MAVEN_VERSION}-bin.tar.gz" -o "${archive}"; then
|
|
break
|
|
fi
|
|
done
|
|
if ! echo "${MAVEN_SHA512} ${archive}" | sha512sum --check --status; then
|
|
echo "::error::no checksum-verified Maven ${MAVEN_VERSION} from Maven Central or its mirror"
|
|
exit 1
|
|
fi
|
|
# Best-effort: the archive is already verified job-locally, so an
|
|
# unwritable shared cache must not abort the step before tar.
|
|
if ! { mkdir -p -- "$(dirname -- "${cached}")" &&
|
|
cp -- "${archive}" "${cached}.$$" &&
|
|
mv -- "${cached}.$$" "${cached}"; }; then
|
|
rm -f -- "${cached}.$$"
|
|
echo "::warning::Maven cache write-back skipped: the shared cache is not writable; the verified job-local archive is used instead"
|
|
fi
|
|
fi
|
|
tar -xzf "${archive}" -C "${RUNNER_TEMP}"
|
|
echo "${RUNNER_TEMP}/apache-maven-${MAVEN_VERSION}/bin" >> "${GITHUB_PATH}"
|
|
|
|
- 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' }}"
|
|
uses: './.github/actions/self-hosted-node'
|
|
|
|
- name: 'Check pool disk floor'
|
|
if: "${{ runner.environment == 'self-hosted' }}"
|
|
run: 'bash .github/scripts/check-disk-floor.sh "${GITHUB_WORKSPACE}" "${RUNNER_TEMP:-/tmp}"'
|
|
|
|
# Pool hosts share one host-wide fs.aio-nr budget across concurrent jobs,
|
|
# and a mysqld that reserves native AIO contexts can exhaust it and fail
|
|
# to boot with `io_setup() failed with EAGAIN` (#14416). Service
|
|
# containers cannot take a command, so MySQL starts here instead, with
|
|
# native AIO disabled: simulated AIO needs no io_setup() contexts.
|
|
- name: 'Start MySQL without native AIO'
|
|
shell: 'bash'
|
|
run: |-
|
|
set -euo pipefail
|
|
docker run --detach --name "${MYSQL_CONTAINER}" \
|
|
--publish '127.0.0.1::3306' \
|
|
--mount type=tmpfs,destination=/var/lib/mysql \
|
|
--env MYSQL_DATABASE='qwen_managed_agent' \
|
|
--env MYSQL_PASSWORD='qwen' \
|
|
--env MYSQL_ROOT_PASSWORD='root' \
|
|
--env MYSQL_USER='qwen' \
|
|
mysql:8.4 --innodb-use-native-aio=0
|
|
for attempt in {1..60}; do
|
|
if docker exec "${MYSQL_CONTAINER}" mysqladmin ping --host=127.0.0.1 --user=root --password=root --silent >/dev/null 2>&1; then
|
|
break
|
|
fi
|
|
if ! docker inspect --format '{{.State.Running}}' "${MYSQL_CONTAINER}" | grep -qx true; then
|
|
docker logs "${MYSQL_CONTAINER}" || true
|
|
exit 1
|
|
fi
|
|
if [[ "${attempt}" == '60' ]]; then
|
|
docker logs "${MYSQL_CONTAINER}" || true
|
|
exit 1
|
|
fi
|
|
sleep 2
|
|
done
|
|
MYSQL_PORT="$(docker inspect --format '{{(index (index .NetworkSettings.Ports "3306/tcp") 0).HostPort}}' "${MYSQL_CONTAINER}")"
|
|
echo "MYSQL_PORT=${MYSQL_PORT}" >> "${GITHUB_ENV}"
|
|
|
|
- name: 'Grant Flyway probe access'
|
|
shell: 'bash'
|
|
run: |-
|
|
set -euo pipefail
|
|
docker exec "${MYSQL_CONTAINER}" \
|
|
mysql -uroot -proot \
|
|
--execute="GRANT SELECT ON performance_schema.user_variables_by_thread TO 'qwen'@'%';"
|
|
|
|
- name: 'Test the packager and the upgrade command'
|
|
run: 'node --test scripts/tests/package-managed-agent-reference.test.mjs scripts/tests/managed-agent-upgrade.test.mjs scripts/tests/generate-managed-agent-configuration-reference.test.mjs'
|
|
|
|
- name: 'Build Java components'
|
|
run: |-
|
|
set -euo pipefail
|
|
export MAVEN_ARGS="--settings ${RUNNER_TEMP}/setup-java-m2/settings.xml --toolchains ${RUNNER_TEMP}/setup-java-m2/toolchains.xml"
|
|
if [[ "${RUNNER_ENVIRONMENT}" == 'self-hosted' ]]; then
|
|
# Both in-house artifacts use fixed release coordinates, so this
|
|
# PR's build must never reach the host-shared ~/.m2.
|
|
repo="${RUNNER_TEMP}/m2-repo"
|
|
cp -aln "${HOME}/.m2/repository" "${repo}" 2>/dev/null || true
|
|
rm -rf -- "${repo}/com/alibaba/qwencode-sdk" "${repo}/com/alibaba/"qwen-managed-agent-*
|
|
MAVEN_ARGS="${MAVEN_ARGS} -Dmaven.repo.local=${repo}"
|
|
fi
|
|
mvn --batch-mode --no-transfer-progress -f packages/sdk-java/qwencode/pom.xml -DskipTests -Dgpg.skip=true clean install
|
|
mvn --batch-mode --no-transfer-progress -f packages/managed-agent-server/pom.xml -DskipTests clean package
|
|
|
|
# The build wrote the configuration metadata the reference is made from.
|
|
- name: 'Check the configuration reference'
|
|
run: 'node scripts/generate-managed-agent-configuration-reference.mjs --check'
|
|
|
|
- name: 'Assemble reference distribution'
|
|
env:
|
|
RELEASE_VERSION: '${{ steps.version.outputs.version }}'
|
|
run: |-
|
|
# A pool runner can keep RUNNER_TEMP across jobs: a leftover
|
|
# extracted/ fails the verify step's mkdir, and another PR's
|
|
# archive would match the upload globs.
|
|
rm -rf -- "${RUNNER_TEMP}/managed-agent-reference"
|
|
node scripts/package-managed-agent-reference.mjs \
|
|
--version "${RELEASE_VERSION}" \
|
|
--source-revision "${GITHUB_SHA}" \
|
|
--output "${RUNNER_TEMP}/managed-agent-reference"
|
|
|
|
- name: 'Verify and boot the exact archive'
|
|
env:
|
|
RELEASE_VERSION: '${{ steps.version.outputs.version }}'
|
|
shell: 'bash'
|
|
run: |-
|
|
set -euo pipefail
|
|
release="${RUNNER_TEMP}/managed-agent-reference"
|
|
cd "${release}"
|
|
sha256sum --check SHA256SUMS.txt
|
|
mkdir extracted
|
|
tar -xzf "qwen-managed-agent-reference-${RELEASE_VERSION}.tar.gz" -C extracted
|
|
bundle="${release}/extracted/qwen-managed-agent-reference-${RELEASE_VERSION}"
|
|
cd "${bundle}"
|
|
sha256sum --check SHA256SUMS
|
|
# The upgrade command compares these scripts across releases, so it
|
|
# must find them where this archive's jar ships them.
|
|
node --input-type=module --eval "
|
|
import { readFile } from 'node:fs/promises';
|
|
import { migrationScripts } from '${bundle}/bin/qwen-managed-agent-upgrade.mjs';
|
|
const { components } = JSON.parse(await readFile('manifest.json', 'utf8'));
|
|
const jar = components.find((each) => each.role === 'control-plane').path;
|
|
const scripts = await migrationScripts(jar);
|
|
if (scripts.size === 0) throw new Error('no Flyway script found in ' + jar);
|
|
console.log('Flyway scripts in ' + jar + ': ' + [...scripts.keys()].join(', '));
|
|
"
|
|
server_port="$(node -e "const s=require('net').createServer().listen(0,'127.0.0.1',()=>{console.log(s.address().port);s.close()})")"
|
|
SERVER_PORT="${server_port}" \
|
|
SPRING_DATASOURCE_URL="jdbc:mysql://127.0.0.1:${MYSQL_PORT}/qwen_managed_agent?allowPublicKeyRetrieval=true&useSSL=false" \
|
|
SPRING_DATASOURCE_USERNAME='qwen' \
|
|
SPRING_DATASOURCE_PASSWORD='qwen' \
|
|
bin/qwen-managed-agent-server > "${RUNNER_TEMP}/managed-agent-reference-boot.log" 2>&1 &
|
|
server_pid=$!
|
|
trap 'kill "${server_pid}" 2>/dev/null || true; wait "${server_pid}" 2>/dev/null || true' EXIT
|
|
for attempt in {1..120}; do
|
|
if curl --fail --silent --show-error "http://127.0.0.1:${server_port}/actuator/health" > "${RUNNER_TEMP}/managed-agent-reference-health.json"; then
|
|
break
|
|
fi
|
|
if ! kill -0 "${server_pid}" 2>/dev/null; then
|
|
cat "${RUNNER_TEMP}/managed-agent-reference-boot.log"
|
|
exit 1
|
|
fi
|
|
if [[ "${attempt}" == '120' ]]; then
|
|
cat "${RUNNER_TEMP}/managed-agent-reference-boot.log"
|
|
exit 1
|
|
fi
|
|
sleep 1
|
|
done
|
|
grep -q '"status":"UP"' "${RUNNER_TEMP}/managed-agent-reference-health.json"
|
|
|
|
- name: 'Upload verified release assets'
|
|
uses: 'actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a' # v7.0.1
|
|
with:
|
|
name: 'managed-agent-reference-${{ steps.version.outputs.version }}'
|
|
path: |-
|
|
${{ runner.temp }}/managed-agent-reference/*.tar.gz
|
|
${{ runner.temp }}/managed-agent-reference/*.manifest.json
|
|
${{ runner.temp }}/managed-agent-reference/SHA256SUMS.txt
|
|
if-no-files-found: 'error'
|
|
retention-days: 14
|
|
|
|
# The host's docker cleanup service reaps qwen-code-* containers only
|
|
# after a strict >24h age gate. It fires once a day, and its 30m jitter
|
|
# spaces consecutive fires 23.5-24.5h apart, so a container the gate
|
|
# skips at one fire waits for a later one: a cancelled job can strand a
|
|
# running mysqld for up to ~48.5h.
|
|
- name: 'Remove the MySQL container'
|
|
if: '${{ always() }}'
|
|
shell: 'bash'
|
|
run: |-
|
|
set -uo pipefail
|
|
if docker inspect "${MYSQL_CONTAINER}" >/dev/null 2>&1; then
|
|
docker rm --force "${MYSQL_CONTAINER}" ||
|
|
echo "::warning::could not remove ${MYSQL_CONTAINER}; the host cleanup service reaps it within ~48.5h"
|
|
fi
|
|
|
|
publish:
|
|
name: 'Publish experimental reference distribution'
|
|
if: "${{ github.event_name == 'workflow_dispatch' && inputs.dry_run == false && github.repository == 'QwenLM/qwen-code' }}"
|
|
needs: 'build'
|
|
runs-on: 'ubuntu-latest'
|
|
timeout-minutes: 10
|
|
environment: 'production-release'
|
|
permissions:
|
|
contents: 'write'
|
|
steps:
|
|
- name: 'Download verified assets'
|
|
uses: 'actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c' # v8.0.1
|
|
with:
|
|
name: 'managed-agent-reference-${{ needs.build.outputs.version }}'
|
|
path: 'release-assets'
|
|
|
|
- name: 'Create immutable GitHub prerelease'
|
|
env:
|
|
GH_REPO: '${{ github.repository }}'
|
|
GH_TOKEN: '${{ github.token }}'
|
|
RELEASE_TAG: '${{ needs.build.outputs.tag }}'
|
|
RELEASE_VERSION: '${{ needs.build.outputs.version }}'
|
|
shell: 'bash'
|
|
run: |-
|
|
set -euo pipefail
|
|
archive="release-assets/qwen-managed-agent-reference-${RELEASE_VERSION}.tar.gz"
|
|
manifest="release-assets/qwen-managed-agent-reference-${RELEASE_VERSION}.manifest.json"
|
|
checksums='release-assets/SHA256SUMS.txt'
|
|
assets=("${archive}" "${manifest}" "${checksums}")
|
|
for asset in "${assets[@]}"; do
|
|
test -f "${asset}"
|
|
done
|
|
|
|
# Unanchored notes start at whichever release the API picks, often
|
|
# another tag family; anchor at the previous reference release.
|
|
previous_tag="$(
|
|
gh release list --limit 100 --exclude-drafts --json tagName --jq '.[].tagName' \
|
|
| grep -x 'managed-agent-reference-v.*' \
|
|
| grep -vxF "${RELEASE_TAG}" \
|
|
| head -n1 || true
|
|
)"
|
|
release_payload="$(jq -n \
|
|
--arg tag "${RELEASE_TAG}" \
|
|
--arg target "${GITHUB_SHA}" \
|
|
--arg name "Managed Agent reference ${RELEASE_VERSION}" \
|
|
--arg previous "${previous_tag}" \
|
|
'{tag_name:$tag,target_commitish:$target,name:$name,draft:true,prerelease:true}
|
|
+ if $previous == "" then {body:("Built from " + $target + ".")}
|
|
else {generate_release_notes:true,previous_tag_name:$previous} end')"
|
|
release_json="$(gh api --method POST "repos/${GH_REPO}/releases" --input - <<< "${release_payload}")"
|
|
release_id="$(jq -er '.id' <<< "${release_json}")"
|
|
|
|
gh release upload "${RELEASE_TAG}" "${assets[@]}"
|
|
release_json="$(gh api "repos/${GH_REPO}/releases/${release_id}")"
|
|
expected_names="$(printf '%s\n' \
|
|
'SHA256SUMS.txt' \
|
|
"qwen-managed-agent-reference-${RELEASE_VERSION}.manifest.json" \
|
|
"qwen-managed-agent-reference-${RELEASE_VERSION}.tar.gz" | sort)"
|
|
actual_names="$(jq -r '.assets[].name' <<< "${release_json}" | sort)"
|
|
if [[ "${actual_names}" != "${expected_names}" ]]; then
|
|
echo '::error::Draft release assets do not match the verified asset set.'
|
|
exit 1
|
|
fi
|
|
for asset in "${assets[@]}"; do
|
|
name="$(basename "${asset}")"
|
|
local_size="$(stat --format='%s' "${asset}")"
|
|
remote_size="$(jq -er --arg name "${name}" '.assets[] | select(.name == $name) | .size' <<< "${release_json}")"
|
|
if [[ "${remote_size}" != "${local_size}" ]]; then
|
|
echo "::error::Uploaded size mismatch for ${name}."
|
|
exit 1
|
|
fi
|
|
local_digest="sha256:$(sha256sum "${asset}" | awk '{print $1}')"
|
|
remote_digest="$(jq -r --arg name "${name}" '.assets[] | select(.name == $name) | .digest // empty' <<< "${release_json}")"
|
|
if [[ -z "${remote_digest}" ]]; then
|
|
asset_id="$(jq -er --arg name "${name}" '.assets[] | select(.name == $name) | .id' <<< "${release_json}")"
|
|
downloaded_asset="$(mktemp)"
|
|
trap 'rm -f "${downloaded_asset}"' EXIT
|
|
gh api \
|
|
-H 'Accept: application/octet-stream' \
|
|
"repos/${GH_REPO}/releases/assets/${asset_id}" \
|
|
> "${downloaded_asset}"
|
|
remote_digest="sha256:$(sha256sum "${downloaded_asset}" | awk '{print $1}')"
|
|
rm -f "${downloaded_asset}"
|
|
trap - EXIT
|
|
fi
|
|
if [[ "${remote_digest}" != "${local_digest}" ]]; then
|
|
echo "::error::Uploaded digest mismatch for ${name}."
|
|
exit 1
|
|
fi
|
|
done
|
|
|
|
tag_json="$(gh api --method POST "repos/${GH_REPO}/git/refs" \
|
|
-f ref="refs/tags/${RELEASE_TAG}" \
|
|
-f sha="${GITHUB_SHA}")"
|
|
if [[ "$(jq -er '.object.sha' <<< "${tag_json}")" != "${GITHUB_SHA}" ]]; then
|
|
echo '::error::The created release tag does not point to the verified source revision.'
|
|
exit 1
|
|
fi
|
|
|
|
published="$(gh api --method PATCH "repos/${GH_REPO}/releases/${release_id}" \
|
|
-F draft=false \
|
|
-F prerelease=true)"
|
|
jq -e \
|
|
--arg tag "${RELEASE_TAG}" \
|
|
'.draft == false and .prerelease == true and .tag_name == $tag' \
|
|
<<< "${published}" >/dev/null
|