From b869bcd379b6f4d8057a2eb96d99bd15598c2061 Mon Sep 17 00:00:00 2001 From: t8y2 <1156263951@qq.com> Date: Thu, 2 Jul 2026 12:11:15 +0800 Subject: [PATCH] fix(agents): limit common version bumps --- .github/scripts/bump-agent-versions.mjs | 212 ++++++++++++++++++++ .github/workflows/agents-release.yml | 84 +------- packages/app-tests/agentVersionBump.test.ts | 130 ++++++++++++ 3 files changed, 352 insertions(+), 74 deletions(-) create mode 100644 .github/scripts/bump-agent-versions.mjs create mode 100644 packages/app-tests/agentVersionBump.test.ts diff --git a/.github/scripts/bump-agent-versions.mjs b/.github/scripts/bump-agent-versions.mjs new file mode 100644 index 0000000000..a61dd5de78 --- /dev/null +++ b/.github/scripts/bump-agent-versions.mjs @@ -0,0 +1,212 @@ +#!/usr/bin/env node +import { execFileSync } from "node:child_process"; +import { appendFileSync, existsSync, readFileSync, writeFileSync } from "node:fs"; + +const VERSIONS_PATH = "agents/versions.json"; + +function bumpPatchVersion(version) { + const match = /^(\d+)\.(\d+)\.(\d+)(.*)$/.exec(version); + if (!match) { + throw new Error(`Agent version '${version}' is not a patchable semver version.`); + } + return `${match[1]}.${match[2]}.${Number(match[3]) + 1}${match[4]}`; +} + +function pathChanged(changedFiles, pathPrefix) { + const normalized = pathPrefix.endsWith("/") ? pathPrefix : `${pathPrefix}/`; + return changedFiles.some((file) => file === pathPrefix || file.startsWith(normalized)); +} + +function isCommonRuntimeChange(file) { + return file === "agents/common/build.gradle" || file.startsWith("agents/common/src/main/"); +} + +function parseLegacyStandaloneProjects(buildGradle) { + const match = /legacyStandaloneProjects\s*=\s*\[([^\]]*)\]/m.exec(buildGradle); + if (!match) return new Set(); + + return new Set( + [...match[1].matchAll(/['"]([^'"]+)['"]/g)] + .map((entry) => entry[1]) + .filter(Boolean), + ); +} + +function fileContainsCommonDependency(path, moduleExists, readModuleFile) { + if (!moduleExists(path)) return false; + const source = readModuleFile(path); + return /project\(\s*['"]:common['"]\s*\)/.test(source); +} + +function resolveAgentModule(moduleName, { legacyStandaloneModules, moduleExists, readModuleFile }) { + let checkDir = null; + if (moduleName === "oracle" && moduleExists("agents/drivers/oracle-go")) { + checkDir = "drivers/oracle-go"; + } else if (moduleExists(`agents/drivers/${moduleName}`)) { + checkDir = `drivers/${moduleName}`; + } else if (moduleExists(`agents/${moduleName}`) || moduleName === "common") { + checkDir = moduleName; + } + + if (!checkDir) return null; + + const modulePath = `agents/${checkDir}`; + const buildGradlePath = `${modulePath}/build.gradle`; + const hasBuildGradle = moduleExists(buildGradlePath); + const explicitlyDependsOnCommon = fileContainsCommonDependency(buildGradlePath, moduleExists, readModuleFile); + + return { + checkDir, + modulePath, + commonDependent: hasBuildGradle && (explicitlyDependsOnCommon || !legacyStandaloneModules.has(moduleName)), + }; +} + +export function evaluateAgentVersionBump({ + versions, + prevVersions = versions, + changedFiles, + legacyStandaloneModules = new Set(), + moduleExists = existsSync, + readModuleFile = (path) => readFileSync(path, "utf8"), + skipBump = false, + manualVersionsChanged = changedFiles.includes(VERSIONS_PATH), +}) { + const nextVersions = { ...versions }; + const logs = []; + let changed = false; + + if (manualVersionsChanged && !skipBump) { + logs.push("Manual agents/versions.json changes detected; preserving manually changed module versions and auto-bumping the rest."); + } + + if (skipBump) { + logs.push("Skipping automatic module version bump for migrated first release; versions.json was carried over from dbx-agents."); + return { changed, versions: nextVersions, prevVersions, logs }; + } + + const commonChanged = changedFiles.some(isCommonRuntimeChange); + if (commonChanged) { + logs.push("Common agent runtime changes detected; common-triggered bumps are limited to modules that package agents/common."); + } + + for (const moduleName of Object.keys(versions)) { + const module = resolveAgentModule(moduleName, { legacyStandaloneModules, moduleExists, readModuleFile }); + if (!module) continue; + + const moduleChanged = pathChanged(changedFiles, module.modulePath); + // Only modules that package agents/common need installer-visible updates + // for shared Java runtime changes; native and standalone agents do not. + const commonAffectsModule = commonChanged && module.commonDependent; + const oldVersion = nextVersions[moduleName] ?? "0.1.0"; + const prevVersion = prevVersions[moduleName] ?? ""; + const manuallyVersioned = manualVersionsChanged && (!prevVersion || prevVersion !== oldVersion); + + if (!moduleChanged && !commonAffectsModule) { + logs.push(` ${moduleName}: no changes`); + } else if (manuallyVersioned) { + if (!prevVersion) { + logs.push(` ${moduleName}: CHANGED, new module version kept at ${oldVersion}`); + } else { + logs.push(` ${moduleName}: CHANGED, manual version ${prevVersion} -> ${oldVersion}`); + } + } else { + const newVersion = bumpPatchVersion(oldVersion); + nextVersions[moduleName] = newVersion; + changed = true; + logs.push(` ${moduleName}: CHANGED`); + logs.push(` ${moduleName}: ${oldVersion} -> ${newVersion}`); + } + } + + return { changed, versions: nextVersions, prevVersions, logs }; +} + +function git(args) { + return execFileSync("git", args, { encoding: "utf8" }).trim(); +} + +function parseArgs(argv) { + const options = { + migratedFirstRelease: false, + prevTag: "", + prevVersionsFile: "", + skipBump: false, + write: false, + }; + + for (let index = 0; index < argv.length; index += 1) { + const arg = argv[index]; + if (arg === "--write") { + options.write = true; + } else if (arg === "--skip-bump") { + options.skipBump = true; + } else if (arg === "--prev-tag") { + options.prevTag = argv[++index] ?? ""; + } else if (arg === "--prev-versions-file") { + options.prevVersionsFile = argv[++index] ?? ""; + } else if (arg === "--migrated-first-release") { + options.migratedFirstRelease = (argv[++index] ?? "") === "true"; + } else { + throw new Error(`Unexpected argument: ${arg}`); + } + } + + if (!options.prevTag) { + throw new Error("--prev-tag is required."); + } + return options; +} + +function outputStepValues(result, prevTag, migratedFirstRelease) { + const outputPath = process.env.GITHUB_OUTPUT; + if (!outputPath) return; + + appendFileSync( + outputPath, + [ + `versions=${JSON.stringify(result.versions)}`, + `prev_versions=${JSON.stringify(result.prevVersions)}`, + `prev_tag=${prevTag}`, + `migrated_first_release=${migratedFirstRelease}`, + "", + ].join("\n"), + ); +} + +function main() { + const options = parseArgs(process.argv.slice(2)); + const versions = JSON.parse(readFileSync(VERSIONS_PATH, "utf8")); + const legacyStandaloneModules = parseLegacyStandaloneProjects(readFileSync("agents/build.gradle", "utf8")); + const changedFiles = options.skipBump ? [] : git(["diff", "--name-only", `${options.prevTag}..HEAD`]).split("\n").filter(Boolean); + const manualVersionsChanged = changedFiles.includes(VERSIONS_PATH); + const prevVersions = options.prevVersionsFile + ? JSON.parse(readFileSync(options.prevVersionsFile, "utf8")) + : manualVersionsChanged + ? JSON.parse(git(["show", `${options.prevTag}:${VERSIONS_PATH}`])) + : versions; + + const result = evaluateAgentVersionBump({ + versions, + prevVersions, + changedFiles, + legacyStandaloneModules, + skipBump: options.skipBump, + manualVersionsChanged, + }); + + for (const line of result.logs) { + console.log(line); + } + + const versionsJson = `${JSON.stringify(result.versions, null, 2)}\n`; + if (options.write) { + writeFileSync(VERSIONS_PATH, versionsJson); + } + console.log(versionsJson); + outputStepValues(result, options.prevTag, options.migratedFirstRelease); +} + +if (import.meta.url === `file://${process.argv[1]}`) { + main(); +} diff --git a/.github/workflows/agents-release.yml b/.github/workflows/agents-release.yml index c45e5d20fb..469b9155d9 100644 --- a/.github/workflows/agents-release.yml +++ b/.github/workflows/agents-release.yml @@ -31,10 +31,7 @@ jobs: PREV_TAG=$(git tag --sort=-creatordate | grep '^agents-v' | sed -n '2p') SKIP_BUMP=false MIGRATED_FIRST_RELEASE=false - - VERSIONS=$(cat agents/versions.json) - PREV_VERSIONS="$VERSIONS" - MANUAL_VERSIONS_CHANGED=false + PREV_VERSIONS_FILE="" if [ -z "$PREV_TAG" ]; then echo "No previous agents-v tag found; treating this as the first migrated agents release" @@ -42,7 +39,8 @@ jobs: TMP_LEGACY="$(mktemp -d)" git clone --depth 1 --branch "$LEGACY_BASE_TAG" "$LEGACY_REPO" "$TMP_LEGACY" - PREV_VERSIONS=$(cat "$TMP_LEGACY/versions.json") + PREV_VERSIONS_FILE="$(mktemp)" + cp "$TMP_LEGACY/versions.json" "$PREV_VERSIONS_FILE" rm -rf "$TMP_LEGACY" SKIP_BUMP=true @@ -50,79 +48,17 @@ jobs: fi echo "Comparing $PREV_TAG..HEAD" - if [ "$SKIP_BUMP" != "true" ] && ! git diff --quiet "$PREV_TAG"..HEAD -- agents/versions.json 2>/dev/null; then - echo "Manual agents/versions.json changes detected; preserving manually changed module versions and auto-bumping the rest." - PREV_VERSIONS=$(git show "${PREV_TAG}:agents/versions.json") - MANUAL_VERSIONS_CHANGED=true - fi - - CHANGED=false - + ARGS=(--prev-tag "$PREV_TAG" --migrated-first-release "$MIGRATED_FIRST_RELEASE" --write) if [ "$SKIP_BUMP" = "true" ]; then - echo "Skipping automatic module version bump for migrated first release; versions.json was carried over from dbx-agents." - else - if git diff --quiet "$PREV_TAG"..HEAD -- agents/common/ 2>/dev/null; then - COMMON_CHANGED=false - else - COMMON_CHANGED=true - fi - - for dir in $(echo "$VERSIONS" | python3 -c "import sys,json; [print(k) for k in json.load(sys.stdin)]"); do - if [ "$dir" = "oracle" ] && [ -d "agents/drivers/oracle-go" ]; then - CHECK_DIR="drivers/oracle-go" - elif [ -d "agents/drivers/$dir" ]; then - CHECK_DIR="drivers/$dir" - elif [ -d "agents/$dir" ] || [ "$dir" = "common" ]; then - CHECK_DIR="$dir" - else - continue - fi - DIFF_PATHS=("agents/$CHECK_DIR/") - OLD_VER=$(echo "$VERSIONS" | python3 -c "import sys,json; print(json.load(sys.stdin).get('$dir','0.1.0'))") - PREV_VER=$(PREV_VERSIONS_JSON="$PREV_VERSIONS" MODULE_NAME="$dir" python3 - <<'PY' - import json, os - versions = json.loads(os.environ["PREV_VERSIONS_JSON"]) - print(versions.get(os.environ["MODULE_NAME"], "")) - PY - ) - MANUALLY_VERSIONED=false - if [ "$MANUAL_VERSIONS_CHANGED" = "true" ] && { [ -z "$PREV_VER" ] || [ "$PREV_VER" != "$OLD_VER" ]; }; then - MANUALLY_VERSIONED=true - fi - - if git diff --quiet "$PREV_TAG"..HEAD -- "${DIFF_PATHS[@]}" 2>/dev/null && [ "$COMMON_CHANGED" = "false" ]; then - echo " $dir: no changes" - elif [ "$MANUALLY_VERSIONED" = "true" ]; then - if [ -z "$PREV_VER" ]; then - echo " $dir: CHANGED, new module version kept at $OLD_VER" - else - echo " $dir: CHANGED, manual version $PREV_VER -> $OLD_VER" - fi - else - echo " $dir: CHANGED" - IFS='.' read -r major minor patch <<< "$OLD_VER" - NEW_VER="$major.$minor.$((patch + 1))" - VERSIONS=$(echo "$VERSIONS" | python3 -c " - import sys, json - d = json.load(sys.stdin) - d['$dir'] = '$NEW_VER' - json.dump(d, sys.stdout, indent=2) - print() - ") - CHANGED=true - echo " $dir: $OLD_VER -> $NEW_VER" - fi - done + ARGS+=(--skip-bump) + fi + if [ -n "$PREV_VERSIONS_FILE" ]; then + ARGS+=(--prev-versions-file "$PREV_VERSIONS_FILE") fi - echo "$VERSIONS" > agents/versions.json - cat agents/versions.json - echo "versions=$(cat agents/versions.json | python3 -c 'import sys,json; print(json.dumps(json.load(sys.stdin)))')" >> "$GITHUB_OUTPUT" - echo "prev_versions=$(echo "$PREV_VERSIONS" | python3 -c 'import sys,json; print(json.dumps(json.load(sys.stdin)))')" >> "$GITHUB_OUTPUT" - echo "prev_tag=$PREV_TAG" >> "$GITHUB_OUTPUT" - echo "migrated_first_release=$MIGRATED_FIRST_RELEASE" >> "$GITHUB_OUTPUT" + node .github/scripts/bump-agent-versions.mjs "${ARGS[@]}" - if [ "$CHANGED" = "true" ]; then + if ! git diff --quiet -- agents/versions.json; then git config user.name "github-actions[bot]" git config user.email "github-actions[bot]@users.noreply.github.com" git add agents/versions.json diff --git a/packages/app-tests/agentVersionBump.test.ts b/packages/app-tests/agentVersionBump.test.ts new file mode 100644 index 0000000000..067309fab3 --- /dev/null +++ b/packages/app-tests/agentVersionBump.test.ts @@ -0,0 +1,130 @@ +import { strict as assert } from "node:assert"; +import { readFileSync } from "node:fs"; +import { resolve } from "node:path"; +import { test } from "vitest"; + +const { evaluateAgentVersionBump } = await importScript(".github/scripts/bump-agent-versions.mjs"); + +function importScript(path: string): Promise> { + const source = readFileSync(resolve(path), "utf8").replace(/^#!.*\r?\n/, ""); + return import(`data:text/javascript;base64,${Buffer.from(source).toString("base64")}`); +} + +function moduleFixture(paths: string[], files: Record = {}) { + const existing = new Set(paths); + return { + moduleExists: (path: string) => existing.has(path), + readModuleFile: (path: string) => files[path] ?? "", + }; +} + +test("common agent runtime changes bump only modules that package common", () => { + const fixture = moduleFixture( + [ + "agents/drivers/access", + "agents/drivers/access/build.gradle", + "agents/drivers/kafka", + "agents/drivers/kafka/build.gradle", + "agents/drivers/mongodb", + "agents/drivers/mongodb/build.gradle", + "agents/drivers/oracle-go", + "agents/drivers/xugu", + ], + { + "agents/drivers/mongodb/build.gradle": "dependencies { implementation project(':common') }", + }, + ); + + const result = evaluateAgentVersionBump({ + versions: { + access: "0.1.0", + kafka: "0.1.0", + mongodb: "0.1.0", + oracle: "0.1.0", + xugu: "0.1.0", + }, + changedFiles: ["agents/common/src/main/resources/agent-protocol-v1.json"], + legacyStandaloneModules: new Set(["kafka", "mongodb"]), + ...fixture, + }); + + assert.equal(result.changed, true); + assert.deepEqual(result.versions, { + access: "0.1.1", + kafka: "0.1.0", + mongodb: "0.1.1", + oracle: "0.1.0", + xugu: "0.1.0", + }); +}); + +test("common agent test changes do not bump driver versions", () => { + const fixture = moduleFixture(["agents/drivers/access", "agents/drivers/access/build.gradle"]); + + const result = evaluateAgentVersionBump({ + versions: { + access: "0.1.0", + }, + changedFiles: ["agents/common/src/test/java/com/dbx/agent/AbstractJdbcAgentTest.java"], + ...fixture, + }); + + assert.equal(result.changed, false); + assert.deepEqual(result.versions, { + access: "0.1.0", + }); +}); + +test("native agent source changes still bump native module versions", () => { + const fixture = moduleFixture([ + "agents/drivers/access", + "agents/drivers/access/build.gradle", + "agents/drivers/oracle-go", + "agents/drivers/xugu", + ]); + + const result = evaluateAgentVersionBump({ + versions: { + access: "0.1.0", + oracle: "0.1.0", + xugu: "0.1.0", + }, + changedFiles: ["agents/drivers/oracle-go/main.go", "agents/drivers/xugu/main.go"], + ...fixture, + }); + + assert.equal(result.changed, true); + assert.deepEqual(result.versions, { + access: "0.1.0", + oracle: "0.1.1", + xugu: "0.1.1", + }); +}); + +test("manual agent versions are preserved while other changed modules auto bump", () => { + const fixture = moduleFixture([ + "agents/drivers/access", + "agents/drivers/access/build.gradle", + "agents/drivers/dameng", + "agents/drivers/dameng/build.gradle", + ]); + + const result = evaluateAgentVersionBump({ + versions: { + access: "0.1.2", + dameng: "0.1.0", + }, + prevVersions: { + access: "0.1.1", + dameng: "0.1.0", + }, + changedFiles: ["agents/common/src/main/java/com/dbx/agent/JdbcExecutor.java", "agents/versions.json"], + ...fixture, + }); + + assert.equal(result.changed, true); + assert.deepEqual(result.versions, { + access: "0.1.2", + dameng: "0.1.1", + }); +});