diff --git a/.github/workflows/build-desktop-tauri.yml b/.github/workflows/build-desktop-tauri.yml index 9b05a58..392a7ab 100644 --- a/.github/workflows/build-desktop-tauri.yml +++ b/.github/workflows/build-desktop-tauri.yml @@ -8,7 +8,7 @@ on: required: false default: https://github.com/AstrBotDevs/AstrBot.git source_git_ref: - description: Optional source ref override for `tag-poll` (branch/tag/commit SHA). Ignored in `nightly`. + description: Optional source ref override for `tag-poll` or required explicit source ref for `custom` (branch/tag/commit SHA). required: false default: "" publish_release: @@ -18,14 +18,15 @@ on: default: true build_mode: description: >- - Build mode (`tag-poll` | `nightly`): `nightly` (default) always builds latest upstream commit, - `tag-poll` builds latest upstream tag (or `source_git_ref` override) + Build mode (`tag-poll` | `nightly` | `custom`): `nightly` (default) always builds latest upstream commit, + `tag-poll` builds latest upstream tag (or `source_git_ref` override), `custom` builds the explicit `source_git_ref` required: false type: choice default: nightly options: - tag-poll - nightly + - custom schedule: # Hourly tag-poll (exclude the dedicated nightly window at 03:xx UTC). - cron: '0 0-2,4-23 * * *' @@ -130,7 +131,7 @@ jobs: run: | set -euo pipefail - changed_files="$(git status --porcelain -- package.json src-tauri/Cargo.toml src-tauri/tauri.conf.json)" + changed_files="$(git status --porcelain -- package.json src-tauri/Cargo.toml src-tauri/Cargo.lock src-tauri/tauri.conf.json)" if [ -z "${changed_files}" ]; then echo "Version files are already up to date. Nothing to commit." exit 0 @@ -138,7 +139,7 @@ jobs: git config user.name "github-actions[bot]" git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - git add package.json src-tauri/Cargo.toml src-tauri/tauri.conf.json + git add package.json src-tauri/Cargo.toml src-tauri/Cargo.lock src-tauri/tauri.conf.json git commit -m "chore(version): sync desktop version to v${ASTRBOT_VERSION}" git fetch origin "${TARGET_REF_NAME}" @@ -551,6 +552,7 @@ jobs: python3 scripts/ci/validate-release-artifacts.py release-artifacts - name: Generate Tauri updater manifest + if: ${{ needs.resolve_build_context.outputs.build_mode != 'custom' }} env: RELEASE_TAG: ${{ needs.resolve_build_context.outputs.release_tag }} RELEASE_VERSION: ${{ needs.resolve_build_context.outputs.astrbot_version }} @@ -600,7 +602,7 @@ jobs: fail_on_unmatched_files: true - name: Demote previous prerelease marker - if: ${{ needs.resolve_build_context.outputs.release_prerelease == 'true' }} + if: ${{ needs.resolve_build_context.outputs.release_prerelease == 'true' && needs.resolve_build_context.outputs.build_mode == 'nightly' }} env: GH_TOKEN: ${{ github.token }} CURRENT_RELEASE_TAG: ${{ needs.resolve_build_context.outputs.release_tag }} diff --git a/scripts/ci/fixtures/fake-git.sh b/scripts/ci/fixtures/fake-git.sh index 411676a..854a3ea 100644 --- a/scripts/ci/fixtures/fake-git.sh +++ b/scripts/ci/fixtures/fake-git.sh @@ -51,6 +51,13 @@ case "${command_name}" in version = "${ASTRBOT_TEST_FETCHED_VERSION:-4.19.0}" EOF ;; + rev-parse) + if [ -z "${1-}" ]; then + printf 'git rev-parse expected a ref argument\n' >&2 + exit 1 + fi + printf '%s\n' "${ASTRBOT_TEST_FETCHED_SHA:-3333333333333333333333333333333333333333}" + ;; *) printf 'unexpected git command: %s %s\n' "${command_name}" "$*" >&2 exit 1 diff --git a/scripts/ci/resolve-build-context.sh b/scripts/ci/resolve-build-context.sh index 323041b..05d9228 100755 --- a/scripts/ci/resolve-build-context.sh +++ b/scripts/ci/resolve-build-context.sh @@ -149,6 +149,10 @@ resolve_latest_upstream_tag() { printf '%s\n' "${latest_tag}" } +custom_build_mode_supported_for_event() { + [ "$1" = "workflow_dispatch" ] +} + source_git_url="${ASTRBOT_SOURCE_GIT_URL}" source_git_ref="${ASTRBOT_SOURCE_GIT_REF}" nightly_source_git_ref="${ASTRBOT_NIGHTLY_SOURCE_GIT_REF:-master}" @@ -179,10 +183,10 @@ workflow_source_git_ref_provided="false" latest_upstream_tag="" case "${requested_build_mode}" in - auto|tag-poll|nightly) ;; + auto|tag-poll|nightly|custom) ;; *) if [ "${GITHUB_EVENT_NAME}" = "workflow_dispatch" ]; then - echo "::error::invalid build_mode input '${requested_build_mode}'; expected tag-poll/nightly (auto is deprecated but still accepted and normalized to tag-poll for backward compatibility)." + echo "::error::invalid build_mode input '${requested_build_mode}'; expected tag-poll/nightly/custom (auto is deprecated but still accepted and normalized to tag-poll for backward compatibility)." else echo "::error::invalid build_mode input '${requested_build_mode}'; expected auto/tag-poll/nightly." fi @@ -221,6 +225,11 @@ if [ "${GITHUB_EVENT_NAME}" = "workflow_dispatch" ]; then fi fi +if [ "${requested_build_mode}" = "custom" ] && ! custom_build_mode_supported_for_event "${GITHUB_EVENT_NAME}"; then + echo "::error::${GITHUB_EVENT_NAME} runs do not support build_mode=custom." >&2 + exit 1 +fi + # Normalize build mode in one place to keep behavior explicit and predictable. case "${GITHUB_EVENT_NAME}" in workflow_dispatch) @@ -234,8 +243,14 @@ case "${GITHUB_EVENT_NAME}" in else build_mode="${requested_build_mode}" fi + if [ "${build_mode}" = "custom" ] && [ "${workflow_source_git_ref_provided}" != "true" ]; then + echo "::error::workflow_dispatch custom mode requires source_git_ref." >&2 + exit 1 + fi if [ "${build_mode}" = "tag-poll" ]; then echo "::notice::workflow_dispatch tag-poll selected. Prefer schedule runs for routine tag polling." + elif [ "${build_mode}" = "custom" ]; then + echo "::notice::workflow_dispatch custom mode selected. Build will use the explicit source ref override." fi ;; schedule) @@ -378,6 +393,15 @@ if [ "${should_build}" = "true" ]; then git -C "${repo_dir}" remote add origin "${source_git_url}" git -C "${repo_dir}" fetch --depth 1 origin "${source_git_ref}" git -C "${repo_dir}" checkout --detach FETCH_HEAD + if [ "${build_mode}" = "custom" ]; then + resolved_source_sha="$(git -C "${repo_dir}" rev-parse HEAD)" + if [ -z "${resolved_source_sha}" ]; then + echo "Unable to resolve a pinned commit SHA for custom source ref '${source_git_ref}'." >&2 + exit 1 + fi + echo "Custom source ref ${source_git_ref} resolved to ${resolved_source_sha}." + source_git_ref="${resolved_source_sha}" + fi version="$(python3 scripts/ci/read-project-version.py "${repo_dir}/pyproject.toml")" fi else @@ -396,6 +420,16 @@ if [ "${build_mode}" = "nightly" ] && [ "${should_build}" = "true" ]; then release_tag="nightly" release_name="AstrBot Desktop v${base_version}-nightly-${short_sha}" release_prerelease="true" +elif [ "${build_mode}" = "custom" ] && [ "${should_build}" = "true" ]; then + base_version="${version}" + custom_date="$(date -u +%Y%m%d)" + short_sha="$(printf '%s' "${source_git_ref}" | cut -c1-8)" + version="${version}-custom.${custom_date}.${short_sha}" + if [ "${publish_release}" = "true" ]; then + release_tag="custom-${custom_date}-${short_sha}" + release_name="AstrBot Desktop v${base_version}-custom-${short_sha}" + release_prerelease="false" + fi elif [ "${publish_release}" = "true" ] && [ "${should_build}" = "true" ]; then release_tag="v${version}" release_name="AstrBot Desktop v${version}" diff --git a/scripts/ci/resolve-build-context.test.mjs b/scripts/ci/resolve-build-context.test.mjs index 5b840c5..84ef501 100644 --- a/scripts/ci/resolve-build-context.test.mjs +++ b/scripts/ci/resolve-build-context.test.mjs @@ -48,6 +48,15 @@ const makeNightlyEnv = (overrides = {}) => ({ ...overrides, }); +const makeCustomEnv = (overrides = {}) => ({ + ...baseEnv, + WORKFLOW_BUILD_MODE: 'custom', + WORKFLOW_PUBLISH_RELEASE: 'true', + ASTRBOT_TEST_FETCHED_VERSION: '4.19.0', + ASTRBOT_TEST_FETCHED_SHA: '4444444444444444444444444444444444444444', + ...overrides, +}); + const parseGithubOutput = async (outputPath) => { const raw = await readFile(outputPath, 'utf8'); const entries = raw @@ -197,3 +206,71 @@ test('workflow_dispatch nightly never marks latest', async () => { assert.equal(outputs.release_prerelease, 'true'); assert.equal(outputs.release_make_latest, 'false'); }); + +test('workflow_dispatch custom resolves explicit source ref to a pinned commit SHA', async () => { + const { result, outputs } = await runResolveBuildContext(makeCustomEnv({ + WORKFLOW_SOURCE_GIT_REF: 'fix/windows-packaged-pip-build-env', + })); + + assert.equal(result.status, 0, result.stderr); + assert.equal(outputs.build_mode, 'custom'); + assert.equal(outputs.source_git_ref, '4444444444444444444444444444444444444444'); + assert.match( + outputs.astrbot_version, + /^4\.19\.0-custom\.\d{8}\.44444444$/, + ); + assert.equal(outputs.release_prerelease, 'false'); + assert.equal(outputs.release_make_latest, 'false'); + assert.match(outputs.release_tag, /^custom-\d{8}-44444444$/); +}); + +test('workflow_dispatch custom requires an explicit source ref', async () => { + const { result } = await runResolveBuildContext(makeCustomEnv({ + WORKFLOW_SOURCE_GIT_REF: '', + })); + + assert.notEqual(result.status, 0); + assert.match(result.stderr, /workflow_dispatch custom mode requires source_git_ref/i); +}); + +test('schedule runs reject build_mode=custom', async () => { + const { result } = await runResolveBuildContext({ + ...baseEnv, + GITHUB_EVENT_NAME: 'schedule', + WORKFLOW_BUILD_MODE: 'custom', + }); + + assert.notEqual(result.status, 0); + assert.match(result.stderr, /schedule runs do not support build_mode=custom/i); +}); + +test('non-workflow_dispatch runs reject build_mode=custom', async () => { + const { result } = await runResolveBuildContext({ + ...baseEnv, + GITHUB_EVENT_NAME: 'push', + WORKFLOW_BUILD_MODE: 'custom', + }); + + assert.notEqual(result.status, 0); + assert.match(result.stderr, /push runs do not support build_mode=custom/i); +}); + +test('fake git rev-parse returns the configured SHA for arbitrary refs', async () => { + await withSandbox( + { + ...baseEnv, + ASTRBOT_TEST_FETCHED_SHA: '5555555555555555555555555555555555555555', + }, + async (sandbox) => { + const gitPath = path.join(sandbox.tempDir, 'bin', 'git'); + const repoDir = path.join(sandbox.tempDir, 'repo'); + const result = spawnSync(gitPath, ['-C', repoDir, 'rev-parse', 'FETCH_HEAD'], { + encoding: 'utf8', + env: sandbox.env, + }); + + assert.equal(result.status, 0, result.stderr); + assert.equal(result.stdout.trim(), '5555555555555555555555555555555555555555'); + }, + ); +}); diff --git a/scripts/prepare-resources/version-sync.mjs b/scripts/prepare-resources/version-sync.mjs index 286dcf4..0dc62b8 100644 --- a/scripts/prepare-resources/version-sync.mjs +++ b/scripts/prepare-resources/version-sync.mjs @@ -2,6 +2,8 @@ import { existsSync } from 'node:fs'; import { readFile, writeFile } from 'node:fs/promises'; import path from 'node:path'; +export const DESKTOP_TAURI_CRATE_NAME = 'astrbot-desktop-tauri'; + export const normalizeDesktopVersionOverride = (version) => { const trimmed = typeof version === 'string' ? version.trim() : ''; if (!trimmed) { @@ -47,10 +49,112 @@ export const readAstrbotVersionFromPyproject = async ({ sourceDir }) => { throw new Error(`Cannot resolve [project].version from ${pyprojectPath}`); }; +const escapeRegExp = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); +const CARGO_LOCK_PACKAGE_HEADER = /^\s*\[\[package\]\]\s*(?:#.*)?$/; +const CARGO_LOCK_ANY_HEADER = /^\s*\[\[/; +const CARGO_LOCK_VERSION_LINE = /^\s*version\s*=/; +const escapeTomlBasicString = (value) => String(value).replace(/\\/g, '\\\\').replace(/"/g, '\\"'); + +const updateVersionLine = (line, version) => { + const commentIndex = line.indexOf('#'); + const beforeComment = commentIndex === -1 ? line : line.slice(0, commentIndex); + const comment = commentIndex === -1 ? '' : line.slice(commentIndex); + const separatorIndex = beforeComment.indexOf('='); + + if (separatorIndex === -1) { + return null; + } + + const left = beforeComment.slice(0, separatorIndex).trimEnd(); + const right = beforeComment.slice(separatorIndex + 1); + if (!right.trim()) { + return null; + } + + const trailingWhitespace = beforeComment.match(/\s*$/u)?.[0] ?? ''; + const updatedLine = `${left} = "${escapeTomlBasicString(version)}"`; + + if (!comment) { + return `${updatedLine}${trailingWhitespace}`; + } + + return `${updatedLine}${trailingWhitespace}${comment}`; +}; + +const updateCargoLockPackageVersion = ({ cargoLock, packageName, version }) => { + const lines = cargoLock.split(/\r?\n/); + const newline = cargoLock.includes('\r\n') ? '\r\n' : '\n'; + const packageNameLinePattern = new RegExp( + `^\\s*name\\s*=\\s*"${escapeRegExp(packageName)}"\\s*(?:#.*)?$`, + ); + + let inPackageBlock = false; + let inTargetPackage = false; + let foundTargetPackage = false; + + for (let index = 0; index < lines.length; index += 1) { + const line = lines[index]; + + if (CARGO_LOCK_PACKAGE_HEADER.test(line)) { + if (inTargetPackage) { + throw new Error( + `Cannot update Cargo.lock: version entry for package "${packageName}" not found or has an unexpected layout`, + ); + } + inPackageBlock = true; + inTargetPackage = false; + continue; + } + + if (inPackageBlock && CARGO_LOCK_ANY_HEADER.test(line)) { + if (inTargetPackage) { + throw new Error( + `Cannot update Cargo.lock: version entry for package "${packageName}" not found or has an unexpected layout`, + ); + } + inPackageBlock = false; + inTargetPackage = false; + } + + if (!inPackageBlock) { + continue; + } + + if (!inTargetPackage && packageNameLinePattern.test(line)) { + inTargetPackage = true; + foundTargetPackage = true; + continue; + } + + if (!inTargetPackage || !CARGO_LOCK_VERSION_LINE.test(line)) { + continue; + } + + const updatedLine = updateVersionLine(line, version); + if (updatedLine === null) { + throw new Error( + `Cannot update Cargo.lock: version entry for package "${packageName}" not found or has an unexpected layout`, + ); + } + + lines[index] = updatedLine; + return { content: lines.join(newline), updated: true, foundTargetPackage: true }; + } + + if (inTargetPackage) { + throw new Error( + `Cannot update Cargo.lock: version entry for package "${packageName}" not found or has an unexpected layout`, + ); + } + + return { content: cargoLock, updated: false, foundTargetPackage }; +}; + export const syncDesktopVersionFiles = async ({ projectRoot, version }) => { const packageJsonPath = path.join(projectRoot, 'package.json'); const tauriConfigPath = path.join(projectRoot, 'src-tauri', 'tauri.conf.json'); const cargoTomlPath = path.join(projectRoot, 'src-tauri', 'Cargo.toml'); + const cargoLockPath = path.join(projectRoot, 'src-tauri', 'Cargo.lock'); const packageJson = JSON.parse(await readFile(packageJsonPath, 'utf8')); if (packageJson.version !== version) { @@ -73,4 +177,21 @@ export const syncDesktopVersionFiles = async ({ projectRoot, version }) => { if (updatedCargoToml !== cargoToml) { await writeFile(cargoTomlPath, updatedCargoToml, 'utf8'); } + + if (existsSync(cargoLockPath)) { + const cargoLock = await readFile(cargoLockPath, 'utf8'); + const { content: updatedCargoLock, updated, foundTargetPackage } = updateCargoLockPackageVersion({ + cargoLock, + packageName: DESKTOP_TAURI_CRATE_NAME, + version, + }); + + if (!foundTargetPackage) { + console.warn( + `${cargoLockPath}: package "${DESKTOP_TAURI_CRATE_NAME}" not found. Skipping Cargo.lock version sync.`, + ); + } else if (updated && updatedCargoLock !== cargoLock) { + await writeFile(cargoLockPath, updatedCargoLock, 'utf8'); + } + } }; diff --git a/scripts/prepare-resources/version-sync.test.mjs b/scripts/prepare-resources/version-sync.test.mjs index cfc165b..9fb7504 100644 --- a/scripts/prepare-resources/version-sync.test.mjs +++ b/scripts/prepare-resources/version-sync.test.mjs @@ -5,11 +5,40 @@ import { test } from 'node:test'; import assert from 'node:assert/strict'; import { + DESKTOP_TAURI_CRATE_NAME, normalizeDesktopVersionOverride, readAstrbotVersionFromPyproject, syncDesktopVersionFiles, } from './version-sync.mjs'; +const createTempDesktopProject = async ({ cargoLockContents, version = '0.1.0' }) => { + const tempDir = await mkdtemp(path.join(os.tmpdir(), 'astrbot-sync-')); + const srcTauriDir = path.join(tempDir, 'src-tauri'); + + await mkdir(srcTauriDir, { recursive: true }); + await writeFile( + path.join(tempDir, 'package.json'), + `${JSON.stringify({ name: 'test', version }, null, 2)}\n`, + 'utf8', + ); + await writeFile( + path.join(srcTauriDir, 'tauri.conf.json'), + `${JSON.stringify({ version }, null, 2)}\n`, + 'utf8', + ); + await writeFile( + path.join(srcTauriDir, 'Cargo.toml'), + `[package]\nname = "${DESKTOP_TAURI_CRATE_NAME}"\nversion = "${version}"\n`, + 'utf8', + ); + + if (typeof cargoLockContents === 'string') { + await writeFile(path.join(srcTauriDir, 'Cargo.lock'), cargoLockContents, 'utf8'); + } + + return { tempDir, srcTauriDir }; +}; + test('normalizeDesktopVersionOverride trims and strips leading v', () => { assert.equal(normalizeDesktopVersionOverride(' v1.2.3 '), '1.2.3'); assert.equal(normalizeDesktopVersionOverride('2.0.0'), '2.0.0'); @@ -39,37 +68,224 @@ version = "1.9.1" } }); -test('syncDesktopVersionFiles updates package.json, tauri.conf.json and Cargo.toml', async () => { - const tempDir = await mkdtemp(path.join(os.tmpdir(), 'astrbot-sync-')); +test('syncDesktopVersionFiles updates package.json, tauri.conf.json, Cargo.toml and Cargo.lock', async () => { + let tempDir; + let srcTauriDir; try { - const srcTauriDir = path.join(tempDir, 'src-tauri'); - await mkdir(srcTauriDir, { recursive: true }); + ({ tempDir, srcTauriDir } = await createTempDesktopProject({ + cargoLockContents: `version = 4 - await writeFile( - path.join(tempDir, 'package.json'), - `${JSON.stringify({ name: 'test', version: '0.1.0' }, null, 2)}\n`, - 'utf8', +[[package]] +name = "${DESKTOP_TAURI_CRATE_NAME}" +version = "0.1.0" + +[[package]] +name = "dep" +version = "9.9.9" +`, + })); + + await syncDesktopVersionFiles({ projectRoot: tempDir, version: '2.3.4' }); + + const packageJson = JSON.parse(await readFile(path.join(tempDir, 'package.json'), 'utf8')); + const tauriConfig = JSON.parse(await readFile(path.join(srcTauriDir, 'tauri.conf.json'), 'utf8')); + const cargoToml = await readFile(path.join(srcTauriDir, 'Cargo.toml'), 'utf8'); + const cargoLock = await readFile(path.join(srcTauriDir, 'Cargo.lock'), 'utf8'); + + assert.equal(packageJson.version, '2.3.4'); + assert.equal(tauriConfig.version, '2.3.4'); + assert.match(cargoToml, /version\s*=\s*"2.3.4"/); + assert.match( + cargoLock, + new RegExp(`\\[\\[package\\]\\]\\nname = "${DESKTOP_TAURI_CRATE_NAME}"\\nversion = "2.3.4"`), ); - await writeFile( - path.join(srcTauriDir, 'tauri.conf.json'), - `${JSON.stringify({ version: '0.1.0' }, null, 2)}\n`, - 'utf8', + assert.match(cargoLock, /\[\[package\]\]\nname = "dep"\nversion = "9.9.9"/); + } finally { + await rm(tempDir, { recursive: true, force: true }); + } +}); + +test('syncDesktopVersionFiles tolerates extra Cargo.lock fields between package name and version', async () => { + let tempDir; + let srcTauriDir; + try { + ({ tempDir, srcTauriDir } = await createTempDesktopProject({ + cargoLockContents: `version = 4 + +[[package]] +name = "${DESKTOP_TAURI_CRATE_NAME}" +source = "path+file:///workspace/src-tauri" +version = "0.1.0" +dependencies = [ + "dep", +] + +[[package]] +name = "dep" +version = "9.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +`, + })); + + await syncDesktopVersionFiles({ projectRoot: tempDir, version: '2.3.4' }); + + const cargoLock = await readFile(path.join(srcTauriDir, 'Cargo.lock'), 'utf8'); + + assert.match( + cargoLock, + new RegExp( + String.raw`\[\[package\]\]\nname = "${DESKTOP_TAURI_CRATE_NAME}"\nsource = "path\+file:///workspace/src-tauri"\nversion = "2.3.4"`, + ), ); - await writeFile( - path.join(srcTauriDir, 'Cargo.toml'), - `[package]\nname = "astrbot-desktop-tauri"\nversion = "0.1.0"\n`, - 'utf8', + assert.match( + cargoLock, + /\[\[package\]\]\nname = "dep"\nversion = "9.9.9"\nsource = "registry\+https:\/\/github.com\/rust-lang\/crates.io-index"/, + ); + } finally { + await rm(tempDir, { recursive: true, force: true }); + } +}); + +test('syncDesktopVersionFiles preserves trailing Cargo.lock comments and spaces on name/version lines', async () => { + let tempDir; + let srcTauriDir; + try { + ({ tempDir, srcTauriDir } = await createTempDesktopProject({ + cargoLockContents: `version = 4 + +[[package]] # package header +name = "${DESKTOP_TAURI_CRATE_NAME}" # desktop package +version = "0.1.0" # keep this comment + +[[package]] +name = "dep" +version = "9.9.9" +`, + })); + + await syncDesktopVersionFiles({ projectRoot: tempDir, version: '2.3.4' }); + + const cargoLock = await readFile(path.join(srcTauriDir, 'Cargo.lock'), 'utf8'); + + assert.match( + cargoLock, + new RegExp( + String.raw`\[\[package\]\]\s+# package header\nname = "${DESKTOP_TAURI_CRATE_NAME}"\s+# desktop package\nversion = "2.3.4"\s+# keep this comment`, + ), ); + } finally { + await rm(tempDir, { recursive: true, force: true }); + } +}); + +test('syncDesktopVersionFiles rewrites Cargo.lock version lines using double quotes', async () => { + let tempDir; + let srcTauriDir; + try { + ({ tempDir, srcTauriDir } = await createTempDesktopProject({ + cargoLockContents: `version = 4 + +[[package]] +name = "${DESKTOP_TAURI_CRATE_NAME}" +version = '0.1.0' # keep this comment +`, + })); + + await syncDesktopVersionFiles({ projectRoot: tempDir, version: '2.3.4' }); + + const cargoLock = await readFile(path.join(srcTauriDir, 'Cargo.lock'), 'utf8'); + + assert.match(cargoLock, /version = "2.3.4"\s+# keep this comment/); + assert.doesNotMatch(cargoLock, /version = '2\.3\.4'/); + } finally { + await rm(tempDir, { recursive: true, force: true }); + } +}); + +test('syncDesktopVersionFiles only updates the version key, not similarly prefixed keys', async () => { + let tempDir; + let srcTauriDir; + try { + ({ tempDir, srcTauriDir } = await createTempDesktopProject({ + cargoLockContents: `version = 4 + +[[package]] +name = "${DESKTOP_TAURI_CRATE_NAME}" +versioned_dep = "keep-me" +version = "0.1.0" +`, + })); + + await syncDesktopVersionFiles({ projectRoot: tempDir, version: '2.3.4' }); + + const cargoLock = await readFile(path.join(srcTauriDir, 'Cargo.lock'), 'utf8'); + + assert.match(cargoLock, /versioned_dep = "keep-me"/); + assert.match(cargoLock, /version = "2.3.4"/); + } finally { + await rm(tempDir, { recursive: true, force: true }); + } +}); + +test('syncDesktopVersionFiles skips Cargo.lock updates when the desktop crate is missing', async () => { + let tempDir; + let srcTauriDir; + const warnings = []; + const originalWarn = console.warn; + console.warn = (message) => { + warnings.push(String(message)); + }; + + try { + const originalCargoLock = `version = 4 + +[[package]] +name = "dep" +version = "9.9.9" +`; + ({ tempDir, srcTauriDir } = await createTempDesktopProject({ + cargoLockContents: originalCargoLock, + })); await syncDesktopVersionFiles({ projectRoot: tempDir, version: '2.3.4' }); const packageJson = JSON.parse(await readFile(path.join(tempDir, 'package.json'), 'utf8')); const tauriConfig = JSON.parse(await readFile(path.join(srcTauriDir, 'tauri.conf.json'), 'utf8')); const cargoToml = await readFile(path.join(srcTauriDir, 'Cargo.toml'), 'utf8'); + const cargoLock = await readFile(path.join(srcTauriDir, 'Cargo.lock'), 'utf8'); assert.equal(packageJson.version, '2.3.4'); assert.equal(tauriConfig.version, '2.3.4'); assert.match(cargoToml, /version\s*=\s*"2.3.4"/); + assert.equal(cargoLock, originalCargoLock); + assert.equal(warnings.length, 1); + assert.match(warnings[0], new RegExp(`package "${DESKTOP_TAURI_CRATE_NAME}" not found`)); + } finally { + console.warn = originalWarn; + await rm(tempDir, { recursive: true, force: true }); + } +}); + +test('syncDesktopVersionFiles throws a specific error when the desktop crate version entry is malformed', async () => { + let tempDir; + let srcTauriDir; + try { + ({ tempDir, srcTauriDir } = await createTempDesktopProject({ + cargoLockContents: `version = 4 + +[[package]] +name = "${DESKTOP_TAURI_CRATE_NAME}" +source = "path+file:///workspace/src-tauri" +checksum = "unexpected-layout" +`, + })); + + await assert.rejects( + syncDesktopVersionFiles({ projectRoot: tempDir, version: '2.3.4' }), + new RegExp( + `version entry for package "${DESKTOP_TAURI_CRATE_NAME}" not found or has an unexpected layout`, + ), + ); } finally { await rm(tempDir, { recursive: true, force: true }); } diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 4625301..6e1867b 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -58,7 +58,7 @@ dependencies = [ [[package]] name = "astrbot-desktop-tauri" -version = "4.20.0" +version = "4.20.1" dependencies = [ "chrono", "home",