Files
qwen-code/scripts/tests/desktop-oss-workflow.test.js
38b71299c8 feat(desktop): build and publish rpm installers (#16060)
* feat(desktop): build and publish rpm installers

Add Fedora 44 and RHEL 10 release roots, strict RPM smoke checks, and artifact naming that keeps each root distinct.

Co-authored-by: Qwen-Coder <[email protected]>

* fix(desktop): declare rpm runtime requirements and clear the yaml gate

The rpm legs built artifacts with no `Requires:` entries: Tauri's rpm bundler
writes them only from `bundle.linux.rpm.depends`, which nothing set, so
`verifyRpmDependencies` had no webkit soname and no glibc floor to find and the
release could not pass its own gate.

- Add `rpm-fedora44.conf.json` / `rpm-rhel10.conf.json` overlays declaring the
  webkit soname and each root's glibc floor (2.43 vs 2.39 cannot share one
  config), and pass the per-leg overlay to `tauri build --config`.
- Quote-free `container: ${{ matrix.container || null }}` stays as is with an
  inline yamllint disable: quoting the expression would turn a null container
  into an empty string. The shape test pinning that literal moves with it.

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmvy1jgjo76

* ci(desktop): record the rpm legs in the workflow size baseline

`desktop-release.yml` is now 49858 bytes, past its recorded 45089 plus the
4096 allowance, so `Lint & Static` fails on the ratchet before any linter runs.
The growth is the two rpm legs: their matrix entries, the per-leg overlay
wiring, and the roots' comments. Moving that prose out would not pay for it —
the legs themselves are the bytes — so the number moves with the file in the
same PR, as the gate asks.

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmvy1jgjo76

* style(desktop): apply prettier to the rpm legs and realign the workflow shape pin

Prettier flagged .github/workflows/desktop-release.yml and
packages/desktop/scripts/smoke-linux-installers.js on head 10acaadeba.
test-release.js pinned that container line with two spaces before the inline
yamllint disable comment, which prettier rewrites to one; .yamllint.yml has
min-spaces-from-content: 1, so the pin now follows the formatter instead of
blocking it.

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmvy3ombr79

---------

Co-authored-by: Qwen-Coder <[email protected]>
Co-authored-by: yiliang114 <[email protected]>
2026-11-02 20:50:05 +08:00

711 lines
24 KiB
JavaScript

/**
* @license
* Copyright 2026 Qwen Team
* SPDX-License-Identifier: Apache-2.0
*/
import { execFileSync } from 'node:child_process';
import {
chmodSync,
mkdirSync,
mkdtempSync,
readdirSync,
readFileSync,
rmSync,
writeFileSync,
} from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { describe, expect, it } from 'vitest';
import { parse } from 'yaml';
import { getWorkflowJob, getWorkflowStep } from './workflow-helpers.js';
const releaseWorkflow = readFileSync(
'.github/workflows/desktop-release.yml',
'utf8',
);
const releaseYaml = parse(releaseWorkflow);
const syncWorkflow = readFileSync(
'.github/workflows/sync-desktop-to-oss.yml',
'utf8',
);
const tauriConfig = JSON.parse(
readFileSync('packages/desktop/src-tauri/tauri.conf.json', 'utf8'),
);
const hasJq = (() => {
try {
execFileSync('jq', ['--version'], { stdio: 'ignore' });
return true;
} catch {
return false;
}
})();
const replayable = process.platform !== 'win32' && hasJq;
function runAlreadyPublishedProbe(overrides = {}) {
const directory = mkdtempSync(join(tmpdir(), 'desktop-release-probe-'));
const bin = join(directory, 'bin');
const output = join(directory, 'output');
mkdirSync(bin);
writeFileSync(output, '');
writeFileSync(
join(bin, 'gh'),
`#!/usr/bin/env bash
set -euo pipefail
if [ "$1 $2" = 'release view' ]; then
[ "$3" = 'desktop-v1.2.3' ]
[ "$4" = '--json' ]
[ "$5" = 'isDraft,isPrerelease' ]
case "$RELEASE_STATE" in
stable) printf '%s\n' '{"isDraft":false,"isPrerelease":false}' ;;
draft) printf '%s\n' '{"isDraft":true,"isPrerelease":false}' ;;
prerelease) printf '%s\n' '{"isDraft":false,"isPrerelease":true}' ;;
malformed) printf '%s\n' '{' ;;
missing) exit 1 ;;
esac
exit 0
fi
if [ "$1 $2" = 'release download' ]; then
[ "$3" = 'desktop-latest' ]
target=''
pattern=''
while [ "$#" -gt 0 ]; do
if [ "$1" = '--dir' ]; then
shift
target="$1/desktop-latest.json"
elif [ "$1" = '--pattern' ]; then
shift
pattern="$1"
fi
shift
done
[ "$pattern" = 'desktop-latest.json' ]
case "$GH_FEED_STATE" in
current) printf '%s\n' '{"version":"1.2.3"}' > "$target" ;;
different) printf '%s\n' '{"version":"1.2.2"}' > "$target" ;;
malformed) printf '%s\n' '{' > "$target" ;;
missing) exit 1 ;;
esac
exit 0
fi
exit 1
`,
);
writeFileSync(
join(bin, 'curl'),
`#!/usr/bin/env bash
set -euo pipefail
target=''
url=''
while [ "$#" -gt 0 ]; do
if [ "$1" = '-o' ]; then
shift
target="$1"
elif [[ "$1" == https://* ]]; then
url="$1"
fi
shift
done
[ "$url" = 'https://assets.example.test/desktop/latest/desktop-latest.json' ]
case "$OSS_FEED_STATE" in
current) printf '%s\n' '{"version":"1.2.3"}' > "$target" ;;
different) printf '%s\n' '{"version":"1.2.2"}' > "$target" ;;
malformed) printf '%s\n' '{' > "$target" ;;
missing) exit 1 ;;
esac
`,
);
chmodSync(join(bin, 'gh'), 0o755);
chmodSync(join(bin, 'curl'), 0o755);
const step = releaseYaml.jobs.prepare.steps.find(
({ name }) => name === 'Check whether stable release is already published',
);
try {
const stdout = execFileSync('bash', ['-c', step.run], {
env: {
...process.env,
ALIYUN_OSS_PUBLIC_BASE_URL: 'https://assets.example.test',
FEED_TAG: 'desktop-latest',
GH_FEED_STATE: 'current',
GITHUB_EVENT_NAME: 'workflow_dispatch',
GITHUB_OUTPUT: output,
OSS_FEED_STATE: 'current',
PATH: `${bin}:${process.env.PATH}`,
RELEASE_CLOBBER: 'false',
RELEASE_DRAFT: 'false',
RELEASE_DRY_RUN: 'false',
RELEASE_ELECTRON_BRIDGE: 'false',
RELEASE_PRERELEASE: 'false',
RELEASE_STATE: 'stable',
RELEASE_TAG: 'desktop-v1.2.3',
RELEASE_VERSION: '1.2.3',
...overrides,
},
encoding: 'utf8',
stdio: 'pipe',
});
return {
output: readFileSync(output, 'utf8').trim().split('\n').at(-1),
stdout,
};
} finally {
rmSync(directory, { recursive: true, force: true });
}
}
const feedUpdateStep = releaseYaml.jobs.publish.steps.find(
({ name }) => name === 'Update stable updater feed',
);
// Replays the feed step against a gh stub that records every call, so the
// order and arguments of upload/edit/create are checked, not the script text.
function runFeedUpdate(overrides) {
const directory = mkdtempSync(join(tmpdir(), 'desktop-feed-update-'));
const bin = join(directory, 'bin');
const calls = join(directory, 'calls');
mkdirSync(bin);
mkdirSync(calls);
writeFileSync(
join(bin, 'gh'),
`#!/usr/bin/env bash
set -euo pipefail
count="$(find "$GH_CALLS" -type f | wc -l | tr -d ' ')"
printf '%s\\0' "$@" > "$GH_CALLS/$count"
case "$1 $2" in
'release view') [ "$FEED_VERSION" != 'missing' ] ;;
'release download')
while [ "$1" != '--dir' ]; do shift; done
printf '{"version":"%s"}\\n' "$FEED_VERSION" > "$2/desktop-latest.json"
;;
'release edit') [ "$EDIT_FAILS" != 'true' ] ;;
esac
`,
);
chmodSync(join(bin, 'gh'), 0o755);
try {
const stdout = execFileSync('bash', ['-c', feedUpdateStep.run], {
cwd: directory,
env: {
...process.env,
EDIT_FAILS: 'false',
ELECTRON_BRIDGE: 'false',
FEED_TAG: 'desktop-latest',
GH_CALLS: calls,
GITHUB_REPOSITORY: 'QwenLM/qwen-code',
PATH: `${bin}:${process.env.PATH}`,
RELEASE_TAG: 'desktop-v1.2.3',
RELEASE_VERSION: '1.2.3',
...overrides,
},
encoding: 'utf8',
stdio: 'pipe',
});
return {
calls: readdirSync(calls)
.sort((left, right) => Number(left) - Number(right))
.map((file) =>
readFileSync(join(calls, file), 'utf8').split('\0').slice(0, -1),
),
stdout,
};
} finally {
rmSync(directory, { recursive: true, force: true });
}
}
describe('Desktop OSS mirror workflow', () => {
it('short-circuits only after the stable GitHub and OSS publication is complete', () => {
const prepare = releaseYaml.jobs.prepare;
const published = prepare.steps.find(
({ name }) =>
name === 'Check whether stable release is already published',
);
expect(published.id).toBe('published');
expect(published.env).toMatchObject({
ALIYUN_OSS_PUBLIC_BASE_URL:
"${{ vars.ALIYUN_OSS_PUBLIC_BASE_URL || 'https://qwen-code-assets.oss-cn-hangzhou.aliyuncs.com' }}",
FEED_TAG: '${{ env.DESKTOP_FEED_TAG }}',
GH_TOKEN: '${{ github.token }}',
RELEASE_CLOBBER: '${{ inputs.clobber }}',
RELEASE_DRAFT: '${{ inputs.draft }}',
RELEASE_DRY_RUN: '${{ inputs.dry_run }}',
RELEASE_ELECTRON_BRIDGE: '${{ inputs.electron_bridge }}',
RELEASE_PRERELEASE: '${{ inputs.prerelease }}',
RELEASE_TAG: '${{ steps.version.outputs.tag }}',
RELEASE_VERSION: '${{ steps.version.outputs.version }}',
});
expect(published.run).toContain(
'::notice::Desktop $RELEASE_VERSION is already published; skipping build, publish and sync-oss.',
);
expect(prepare.outputs.already_published).toBe(
'${{ steps.published.outputs.already_published }}',
);
expect(releaseYaml.jobs.build.needs).toBe('prepare');
expect(releaseYaml.jobs.build.if).toBe(
"${{ needs.prepare.outputs.already_published != 'true' }}",
);
expect(releaseYaml.jobs.publish.needs).toContain('prepare');
expect(releaseYaml.jobs.publish.if).toBe(
"${{ (github.event_name == 'release' || inputs.dry_run == false) && github.repository == 'QwenLM/qwen-code' && needs.prepare.outputs.already_published != 'true' }}",
);
expect(releaseYaml.jobs['sync-oss'].needs).toContain('prepare');
expect(releaseYaml.jobs['sync-oss'].if).toBe(
"${{ github.repository == 'QwenLM/qwen-code' && (github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && inputs.dry_run == false && inputs.draft == false && inputs.prerelease == false)) && needs.prepare.outputs.already_published != 'true' }}",
);
});
it.skipIf(!replayable).each([
['completed stable dispatch', {}, 'already_published=true'],
[
'completed stable release event with empty inputs',
{
GITHUB_EVENT_NAME: 'release',
RELEASE_CLOBBER: '',
RELEASE_DRAFT: '',
RELEASE_DRY_RUN: '',
RELEASE_PRERELEASE: '',
},
'already_published=true',
],
[
'missing versioned release',
{ RELEASE_STATE: 'missing' },
'already_published=false',
],
[
'draft versioned release',
{ RELEASE_STATE: 'draft' },
'already_published=false',
],
[
'prerelease versioned release',
{ RELEASE_STATE: 'prerelease' },
'already_published=false',
],
[
'malformed versioned release metadata',
{ RELEASE_STATE: 'malformed' },
'already_published=false',
],
[
'missing GitHub feed',
{ GH_FEED_STATE: 'missing' },
'already_published=false',
],
[
'different GitHub feed',
{ GH_FEED_STATE: 'different' },
'already_published=false',
],
[
'malformed GitHub feed',
{ GH_FEED_STATE: 'malformed' },
'already_published=false',
],
[
'missing OSS feed',
{ OSS_FEED_STATE: 'missing' },
'already_published=false',
],
[
'different OSS feed',
{ OSS_FEED_STATE: 'different' },
'already_published=false',
],
[
'malformed OSS feed',
{ OSS_FEED_STATE: 'malformed' },
'already_published=false',
],
['dry run', { RELEASE_DRY_RUN: 'true' }, 'already_published=false'],
['draft dispatch', { RELEASE_DRAFT: 'true' }, 'already_published=false'],
[
'prerelease dispatch',
{ RELEASE_PRERELEASE: 'true' },
'already_published=false',
],
[
'clobber dispatch',
{ RELEASE_CLOBBER: 'true' },
'already_published=false',
],
[
'Electron bridge dispatch',
{ RELEASE_ELECTRON_BRIDGE: 'true' },
'already_published=false',
],
])('reports %s conservatively', (_name, overrides, expected) => {
expect(runAlreadyPublishedProbe(overrides).output).toBe(expected);
});
it.skipIf(!replayable)('explains a successful short-circuit', () => {
expect(runAlreadyPublishedProbe().stdout).toContain(
'::notice::Desktop 1.2.3 is already published; skipping build, publish and sync-oss.',
);
});
it('mirrors only published stable Desktop releases', () => {
expect(syncWorkflow).not.toContain('pull_request:');
expect(releaseWorkflow).toContain(
"desktop-release-${{ inputs.dry_run && inputs.version || 'publish' }}",
);
const prepare = getWorkflowStep(
getWorkflowJob(releaseWorkflow, 'prepare'),
'Resolve version',
);
expect(prepare).toContain("IS_DRAFT: '${{ inputs.draft }}'");
expect(prepare).toContain("IS_DRY_RUN: '${{ inputs.dry_run }}'");
expect(prepare).toContain("IS_PRERELEASE: '${{ inputs.prerelease }}'");
expect(prepare).toContain(
'Published stable Desktop versions must use X.Y.Z',
);
expect(prepare).toContain(
'Desktop prereleases must use a SemVer prerelease suffix',
);
const syncOss = getWorkflowJob(releaseWorkflow, 'sync-oss');
expect(syncOss).toContain(
"if: \"${{ github.repository == 'QwenLM/qwen-code' && (github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && inputs.dry_run == false && inputs.draft == false && inputs.prerelease == false)) && needs.prepare.outputs.already_published != 'true' }}\"",
);
expect(syncOss).toContain("- 'publish'");
expect(syncOss).toContain("source: 'artifact'");
expect(syncOss).toContain(
'follows_release: "${{ github.event_name == \'release\' }}"',
);
expect(syncOss).not.toContain('secrets: inherit');
});
it('passes only the OSS credentials into the reusable workflow', () => {
expect(releaseWorkflow).toContain("permissions:\n contents: 'read'");
const syncOss = getWorkflowJob(releaseWorkflow, 'sync-oss');
expect(syncOss).toContain(
"permissions:\n actions: 'read'\n contents: 'read'",
);
for (const secret of [
'ALIYUN_OSS_ACCESS_KEY_ID',
'ALIYUN_OSS_ACCESS_KEY_SECRET',
]) {
expect(syncWorkflow).toContain(`${secret}:\n required: true`);
expect(syncOss).toContain(`${secret}: '\${{ secrets.${secret} }}'`);
}
});
it('publishes verified versioned assets before advancing the OSS feed', () => {
const sync = getWorkflowJob(syncWorkflow, 'sync');
const prepare = getWorkflowStep(sync, 'Verify and prepare mirror assets');
expect(prepare).toContain(
'--base-url "${ALIYUN_OSS_PUBLIC_BASE_URL}/desktop/v${VERSION}"',
);
// Only the re-mirror path may treat a platform as optional; the artifact
// path is a fresh build and must fail if a leg did not upload (#12806).
expect(prepare).toContain("SOURCE: '${{ steps.release.outputs.source }}'");
expect(prepare).toContain(`if [ "$SOURCE" = 'release' ]; then`);
expect(prepare).toContain(
'for suffix in linux-x64-rpm-fedora44 linux-x64-rpm-rhel10; do',
);
expect(prepare).toContain(
'find . -maxdepth 1 -name "*-${suffix}.rpm" -print -quit',
);
expect(prepare).toContain(
'manifest_args=(--allow-missing-platform linux-aarch64)',
);
// Pin the containment, not only the pieces: hoisting the tolerant
// assignment into the unconditional declaration above the guard leaves
// every string above present and the `if` block merely redundant, yet a
// SOURCE=artifact fresh build whose arm64 leg failed to upload then
// publishes the mirror feed without linux-aarch64 and exits 0 — #12806
// again, with this test green.
expect(prepare).toContain(
'manifest_args=()\n' +
` if [ "$SOURCE" = 'release' ]; then\n` +
' manifest_args=(--allow-missing-platform linux-aarch64)\n' +
' fi',
);
// Pin the effect, not only the pieces: the declaration and the `if` guard
// both survive a refactor that drops the expansion, and an unexpanded array
// is not an error under `set -u` — the mirror would then die with
// `found 0` on exactly the path this flag exists for.
expect(prepare).toMatch(
/create-desktop-update-manifest\.mjs[\s\S]*?"\$\{manifest_args\[@\]}"/,
);
expect(prepare).toContain('sha256sum -- * > SHA256SUMS.txt');
const upload = getWorkflowStep(
sync,
'Upload versioned assets to Aliyun OSS',
);
expect(upload).toContain('--prefix "desktop/v${VERSION}"');
const latest = getWorkflowStep(
sync,
'Publish latest manifest to Aliyun OSS',
);
expect(latest).toContain("--prefix 'desktop/latest'");
expect(latest).toContain('dist/desktop/desktop-latest.json');
expect(latest).not.toContain('.dmg');
const verifyIndex = sync.indexOf(
"name: 'Verify versioned assets on Aliyun OSS'",
);
expect(verifyIndex).toBeGreaterThan(0);
expect(verifyIndex).toBeLessThan(
sync.indexOf("name: 'Publish latest manifest to Aliyun OSS'"),
);
expect(
getWorkflowStep(sync, 'Verify versioned assets on Aliyun OSS'),
).toContain('sha256sum -c SHA256SUMS.txt');
expect(
getWorkflowStep(sync, 'Verify latest manifest on Aliyun OSS'),
).toContain('cmp ');
});
it('advances the OSS feed only for the current GitHub stable version', () => {
const publish = getWorkflowJob(releaseWorkflow, 'publish');
// The other side of the strictness split. A fresh build must never be able
// to tolerate a missing leg: an operator unblocking a release by adding the
// flag here would drop the key from the primary feed while the arm64 asset
// still ships — #12806 again, with every test green and no step failing.
const generateManifest = getWorkflowStep(
publish,
'Generate checksums and updater manifest',
);
expect(generateManifest).toContain('create-desktop-update-manifest.mjs');
expect(generateManifest).not.toContain('--allow-missing-platform');
const updateFeed = getWorkflowStep(publish, 'Update stable updater feed');
expect(updateFeed).toContain('sort -V');
expect(updateFeed).toContain(
'Desktop $RELEASE_VERSION will not replace newer stable feed $current',
);
// The page notes link to the versioned release; the replay below sets the
// tag itself, so only this pin notices the binding going missing.
expect(feedUpdateStep.env.RELEASE_TAG).toBe(
'${{ needs.prepare.outputs.tag }}',
);
const sync = getWorkflowJob(syncWorkflow, 'sync');
const check = getWorkflowStep(
sync,
'Check whether release matches GitHub stable feed',
);
expect(check).toContain("gh release download 'desktop-latest'");
expect(check).toContain("SOURCE: '${{ steps.release.outputs.source }}'");
expect(check).toContain('elif [ "$SOURCE" = \'artifact\' ]; then');
expect(check).toContain('sort -V');
expect(check).toContain('GitHub stable feed is already newer at $actual');
expect(check).toContain(
'GitHub stable feed is $actual after publishing Desktop $expected',
);
expect(check).toContain('echo \'matches=true\' >> "$GITHUB_OUTPUT"');
expect(check).toContain('echo \'matches=false\' >> "$GITHUB_OUTPUT"');
expect(check).toContain(
'expected="$(jq -r \'.version\' dist/desktop/desktop-latest.json)"',
);
expect(check).toContain(
'actual="$(jq -r \'.version\' "$directory/desktop-latest.json")"',
);
expect(
getWorkflowStep(sync, 'Publish latest manifest to Aliyun OSS'),
).toContain('if: "${{ steps.latest.outputs.matches == \'true\' }}"');
expect(
getWorkflowStep(sync, 'Verify latest manifest on Aliyun OSS'),
).toContain('if: "${{ steps.latest.outputs.matches == \'true\' }}"');
});
const feedPageTitle = 'Qwen Code Desktop latest (v1.2.3)';
const feedPageNotes = expect.stringContaining(
'https://github.com/QwenLM/qwen-code/releases/tag/desktop-v1.2.3',
);
const advanceCalls = [
['release', 'view', 'desktop-latest'],
[
'release',
'download',
'desktop-latest',
'--dir',
expect.any(String),
'--pattern',
'desktop-latest.json',
],
[
'release',
'upload',
'desktop-latest',
'release-assets/desktop-latest.json',
'--clobber',
],
[
'release',
'edit',
'desktop-latest',
'--title',
feedPageTitle,
'--notes',
feedPageNotes,
'--latest=false',
],
];
it.skipIf(!replayable).each([
['an older feed', '1.2.2'],
['a same-version rerun', '1.2.3'],
])('rewrites the feed page text after advancing %s', (_name, feedVersion) => {
expect(runFeedUpdate({ FEED_VERSION: feedVersion }).calls).toEqual(
advanceCalls,
);
});
it.skipIf(!replayable)(
'leaves a newer feed and its page text untouched',
() => {
const { calls, stdout } = runFeedUpdate({ FEED_VERSION: '1.2.4' });
expect(calls).toEqual(advanceCalls.slice(0, 2));
expect(stdout).toContain('will not replace newer stable feed 1.2.4');
},
);
it.skipIf(!replayable)(
'warns instead of failing when only the page text update fails',
() => {
const { calls, stdout } = runFeedUpdate({
EDIT_FAILS: 'true',
FEED_VERSION: '1.2.2',
});
expect(calls).toEqual(advanceCalls);
expect(stdout).toContain('::warning::');
},
);
it.skipIf(!replayable)(
'creates a missing feed with the same page text',
() => {
expect(runFeedUpdate({ FEED_VERSION: 'missing' }).calls).toEqual([
['release', 'view', 'desktop-latest'],
[
'release',
'create',
'desktop-latest',
'release-assets/desktop-latest.json',
'--title',
feedPageTitle,
'--notes',
feedPageNotes,
'--latest=false',
],
]);
},
);
it('sync workflow validates stable-only releases in the reusable job', () => {
expect(syncWorkflow).not.toContain('pull_request:');
const sync = getWorkflowJob(syncWorkflow, 'sync');
const resolve = getWorkflowStep(sync, 'Resolve release');
expect(resolve).toContain('^[0-9]+\\.[0-9]+\\.[0-9]+$');
expect(resolve).toContain("!= 'artifact'");
expect(resolve).toContain("!= 'release'");
expect(getWorkflowStep(sync, 'Download GitHub release assets')).toContain(
'.isDraft == false and .isPrerelease == false',
);
});
it('keeps the workflow default aligned with the shipped updater endpoint', () => {
const firstEndpoint = tauriConfig.plugins.updater.endpoints[0];
expect(syncWorkflow).toContain(new URL(firstEndpoint).origin);
});
it('admits the release path in the reusable sync job gate', () => {
const sync = getWorkflowJob(syncWorkflow, 'sync');
expect(sync).toContain(
"if: \"${{ github.repository == 'QwenLM/qwen-code' && (github.ref == 'refs/heads/main' || inputs.follows_release) }}\"",
);
expect(syncWorkflow).toContain(
" follows_release:\n default: false\n type: 'boolean'",
);
expect(syncWorkflow).not.toContain(
' follows_release:\n required: true',
);
});
it('resolves the release tag parent before the main ancestry check', () => {
const source = getWorkflowStep(
getWorkflowJob(releaseWorkflow, 'prepare'),
'Resolve Qwen Code source',
);
expect(source).toContain(
'if [ "$GITHUB_REF_NAME" != \'main\' ] && [ "$GITHUB_EVENT_NAME" != \'release\' ]; then',
);
expect(source).toContain(
'::error::Published desktop releases must run from main or follow a published release.',
);
expect(source).toContain('ancestor="$sha"');
expect(source).toContain('if [ "$GITHUB_EVENT_NAME" = \'release\' ]; then');
expect(source).toContain('ancestor="$(git rev-parse "${sha}^")"');
expect(source).toContain(
'git merge-base --is-ancestor "$ancestor" refs/remotes/origin/main',
);
expect(
source.indexOf('ancestor="$(git rev-parse "${sha}^")"'),
).toBeGreaterThan(source.indexOf('ancestor="$sha"'));
expect(
source.indexOf('ancestor="$(git rev-parse "${sha}^")"'),
).toBeLessThan(source.indexOf('git merge-base --is-ancestor "$ancestor"'));
expect(source).toContain('sha="$(git rev-parse FETCH_HEAD)"');
expect(source).toContain('echo "sha=$sha" >> "$GITHUB_OUTPUT"');
expect(source).not.toContain('sha="$ancestor"');
});
it('puts the feed-clobbering publish behind the deployment gate', () => {
const publish = getWorkflowJob(releaseWorkflow, 'publish');
expect(publish).toContain("environment:\n name: 'production-release'");
});
});
describe('Desktop release event', () => {
it('installs historical Qwen Code refs with their available lockfile', () => {
const install = getWorkflowStep(
getWorkflowJob(releaseWorkflow, 'build'),
'Install Qwen Code dependencies',
);
expect(install).toContain("shell: 'bash'");
expect(install).toContain('if [ -f pnpm-lock.yaml ]');
expect(install).toContain(
'corepack pnpm install --frozen-lockfile --prefer-offline',
);
expect(install).toContain('npm ci --no-audit --progress=false');
});
it('gates automatic publishing like the VS Code release workflow', () => {
expect(releaseWorkflow).toContain("release:\n types: ['published']");
const prepare = getWorkflowJob(releaseWorkflow, 'prepare');
expect(prepare).toContain(
"github.event_name != 'release' ||\n" +
' (\n' +
" github.repository == 'QwenLM/qwen-code' &&\n" +
" vars.RELEASE_DESKTOP_SYNC_PUBLISH == 'true' &&\n" +
" startsWith(github.event.release.tag_name, 'v') &&\n" +
' github.event.release.prerelease == false',
);
expect(prepare).toContain(
"INPUT_VERSION: '${{ github.event.release.tag_name || inputs.version }}'",
);
expect(prepare).toContain(
"INPUT_REF: '${{ github.event.release.tag_name || inputs.qwen_code_ref }}'",
);
expect(releaseWorkflow).not.toContain(
' follows_release:\n default:',
);
expect(prepare).toContain('$GITHUB_EVENT_NAME');
expect(prepare).toContain('if [ "$IS_DRY_RUN" != \'true\' ]; then');
expect(getWorkflowJob(releaseWorkflow, 'build')).toContain(
"github.event_name == 'release' || inputs.dry_run == false",
);
expect(getWorkflowJob(releaseWorkflow, 'publish')).toContain(
"github.event_name == 'release' || inputs.dry_run == false",
);
expect(getWorkflowJob(releaseWorkflow, 'sync-oss')).toContain(
'follows_release: "${{ github.event_name == \'release\' }}"',
);
});
});