feat(installer): restore hosted PowerShell entrypoint

This commit is contained in:
yiliang114 2026-05-13 18:53:45 +08:00
parent fedcbae1c9
commit af64da874f
4 changed files with 80 additions and 13 deletions

View file

@ -27,6 +27,10 @@ const HOSTED_INSTALLATION_ASSETS = [
output: 'install-qwen.bat',
lineEndings: 'crlf',
},
{
sourcePath: ['scripts', 'installation', 'install-qwen.ps1'],
output: 'install-qwen.ps1',
},
];
const HOSTED_INSTALLATION_ASSET_NAMES = HOSTED_INSTALLATION_ASSETS.map(
({ output }) => output,

View file

@ -23,7 +23,7 @@ are only required when the installer falls back to npm or when
## Installation Scripts
- Linux/macOS: `install-qwen.sh`
- Windows: `install-qwen.bat`
- Windows: `install-qwen.ps1`
## Release Artifacts
@ -37,7 +37,7 @@ GitHub releases publish these standalone archives:
- `SHA256SUMS`
The installer scripts (`install-qwen.sh`,
`install-qwen.bat`) are not republished per release. They are
`install-qwen.ps1`) are not republished per release. They are
served from a hosted installation endpoint and accept `--version` to pin a
specific standalone release. This keeps the public install command on a stable
hosted entrypoint while still allowing version pinning, rather than using
@ -58,16 +58,15 @@ curl -fsSL https://qwen-code-assets.oss-cn-hangzhou.aliyuncs.com/installation/in
```
```cmd
powershell -Command "Invoke-WebRequest 'https://qwen-code-assets.oss-cn-hangzhou.aliyuncs.com/installation/install-qwen.bat' -OutFile (Join-Path $env:TEMP 'install-qwen.bat'); & (Join-Path $env:TEMP 'install-qwen.bat')"
powershell -ExecutionPolicy Bypass -c "irm https://qwen-code-assets.oss-cn-hangzhou.aliyuncs.com/installation/install-qwen.ps1 | iex"
```
To pin a release with the hosted Windows entrypoint, download `install-qwen.bat`
and pass `--version`:
To pin a release with the hosted Windows entrypoint, set
`QWEN_INSTALL_VERSION` before invoking `install-qwen.ps1`:
```powershell
$installer = Join-Path $env:TEMP 'install-qwen.bat'
Invoke-WebRequest 'https://qwen-code-assets.oss-cn-hangzhou.aliyuncs.com/installation/install-qwen.bat' -OutFile $installer
& $installer --version vX.Y.Z
$env:QWEN_INSTALL_VERSION = 'vX.Y.Z'
irm https://qwen-code-assets.oss-cn-hangzhou.aliyuncs.com/installation/install-qwen.ps1 | iex
```
`QWEN_INSTALL_VERSION` is the equivalent environment variable when arguments
@ -76,7 +75,9 @@ cannot be passed through.
Hosted installer assets are staged separately from GitHub Release archives:
- `install-qwen.sh` is the Linux/macOS hosted entrypoint.
- `install-qwen.bat` is the Windows hosted entrypoint (also runnable directly).
- `install-qwen.ps1` is the Windows hosted entrypoint for `irm | iex`.
- `install-qwen.bat` is the Windows installer implementation used by
`install-qwen.ps1` and can also be downloaded and run directly.
Build them with:
@ -84,9 +85,11 @@ Build them with:
npm run package:hosted-installation -- --out-dir dist/installation
```
The staged `install-qwen.sh` and `install-qwen.bat` files map to the fixed
The staged `install-qwen.sh`, `install-qwen.ps1`, and `install-qwen.bat` files
map to the fixed
hosted URLs shown above. Upload their contents byte-for-byte to
`installation/install-qwen.sh` and `installation/install-qwen.bat`; the staging
`installation/install-qwen.sh`, `installation/install-qwen.ps1`, and
`installation/install-qwen.bat`; the staging
command also writes `SHA256SUMS` for upload verification. The hosted installers
intentionally default to `latest`; use `--version` or `QWEN_INSTALL_VERSION` to
pin a standalone release. OSS/CDN upload automation is still a follow-up release

View file

@ -0,0 +1,37 @@
# Qwen Code Windows hosted PowerShell entrypoint.
# Pairs with install-qwen.bat: this shim downloads the .bat into TEMP and runs
# it, so the documented one-liner can use the standard irm | iex pattern.
#
# Usage:
# powershell -ExecutionPolicy Bypass -c "irm https://qwen-code-assets.oss-cn-hangzhou.aliyuncs.com/installation/install-qwen.ps1 | iex"
#
# To pin a specific release, set $env:QWEN_INSTALL_VERSION before invoking,
# e.g. $env:QWEN_INSTALL_VERSION = 'vX.Y.Z'. This is equivalent to passing
# --version vX.Y.Z to install-qwen.bat directly.
$ErrorActionPreference = 'Stop'
$qwenInstallerUrl = 'https://qwen-code-assets.oss-cn-hangzhou.aliyuncs.com/installation/install-qwen.bat'
$qwenInstallerPath = Join-Path $env:TEMP 'install-qwen.bat'
try {
Invoke-WebRequest -Uri $qwenInstallerUrl `
-OutFile $qwenInstallerPath `
-UseBasicParsing `
-MaximumRedirection 10
} catch {
Write-Error "Failed to download Qwen Code installer from ${qwenInstallerUrl}: $($_.Exception.Message)"
exit 1
}
$qwenInstallerExitCode = 0
try {
& $qwenInstallerPath @args
$qwenInstallerExitCode = $LASTEXITCODE
} finally {
Remove-Item -Path $qwenInstallerPath -Force -ErrorAction SilentlyContinue
}
if ($qwenInstallerExitCode -ne 0) {
exit $qwenInstallerExitCode
}

View file

@ -264,6 +264,7 @@ describe('standalone release packaging', () => {
expect(hostedInstallScript).toContain('HOSTED_INSTALLATION_ASSETS');
expect(hostedInstallScript).toContain("output: 'install-qwen.sh'");
expect(hostedInstallScript).toContain("output: 'install-qwen.bat'");
expect(hostedInstallScript).toContain("output: 'install-qwen.ps1'");
expect(hostedInstallScript).not.toContain("output: 'install'");
const releaseVerifyScript = readScript(
@ -409,6 +410,15 @@ describe('standalone release packaging', () => {
);
expect(installBatchSource).toContain('"%~1"=="--version"');
expect(installBatchSource).toContain('--version requires a value');
const installPowerShellSource = readScript(
'scripts/installation/install-qwen.ps1',
);
expect(installPowerShellSource).toContain('install-qwen.bat');
expect(installPowerShellSource).toContain('Invoke-WebRequest');
expect(installPowerShellSource).toContain('QWEN_INSTALL_VERSION');
expect(installPowerShellSource).toContain('--version vX.Y.Z');
expect(installPowerShellSource).toContain('@args');
});
it('stages hosted installation assets with checksums', async () => {
@ -425,12 +435,14 @@ describe('standalone release packaging', () => {
const installSh = path.join(tmpDir, 'install-qwen.sh');
const installBat = path.join(tmpDir, 'install-qwen.bat');
const installPs1 = path.join(tmpDir, 'install-qwen.ps1');
const checksums = readScript(path.join(tmpDir, 'SHA256SUMS'));
const checksumLines = checksums.trim().split('\n');
expect(HOSTED_INSTALLATION_ASSET_NAMES).toEqual([
'install-qwen.sh',
'install-qwen.bat',
'install-qwen.ps1',
]);
expect(HOSTED_INSTALLATION_ASSETS.map(({ output }) => output)).toEqual(
HOSTED_INSTALLATION_ASSET_NAMES,
@ -444,14 +456,18 @@ describe('standalone release packaging', () => {
'\r\n',
),
);
expect(readScript(installPs1)).toBe(
readScript('scripts/installation/install-qwen.ps1'),
);
expect(existsSync(path.join(tmpDir, 'install'))).toBe(false);
expect(existsSync(path.join(tmpDir, 'install-qwen.ps1'))).toBe(false);
expect(checksumLines.map((line) => line.split(' ')[1])).toEqual([
'install-qwen.bat',
'install-qwen.ps1',
'install-qwen.sh',
]);
expect(checksums).toMatch(/^[0-9a-f]{64} {2}install-qwen\.sh$/m);
expect(checksums).toMatch(/^[0-9a-f]{64} {2}install-qwen\.bat$/m);
expect(checksums).toMatch(/^[0-9a-f]{64} {2}install-qwen\.ps1$/m);
if (process.platform !== 'win32') {
expect(lstatSync(installSh).mode & 0o111).not.toBe(0);
}
@ -485,6 +501,10 @@ describe('standalone release packaging', () => {
path.join(sourceDir, 'install-qwen.bat'),
'@echo off\r\nset "VERSION=latest"\r\n',
);
writeFileSync(
path.join(sourceDir, 'install-qwen.ps1'),
"# --version vX.Y.Z\n$env:QWEN_INSTALL_VERSION = 'latest'\n",
);
await expect(
buildHostedInstallationAssets(tmpDir, { root: tmpRoot }),
@ -598,7 +618,7 @@ describe('standalone release packaging', () => {
]);
}
for (const [url] of fetchedUrls) {
expect(url).not.toMatch(/install-qwen\.(sh|bat)$/);
expect(url).not.toMatch(/install-qwen\.(sh|bat|ps1)$/);
expect(url).not.toMatch(/\/install$/);
}
});
@ -831,6 +851,7 @@ describe('standalone release packaging', () => {
expect(workflow).not.toContain('package:installation-assets');
expect(workflow).not.toContain('install-qwen.sh');
expect(workflow).not.toContain('install-qwen.bat');
expect(workflow).not.toContain('install-qwen.ps1');
expect(workflow).not.toContain('verify_node_checksum()');
expect(workflow).not.toContain('download_node()');
expect(workflow).toContain('dist/standalone/qwen-code-*.tar.gz');
@ -855,6 +876,8 @@ describe('standalone release packaging', () => {
expect(guide).toContain('package:hosted-installation');
expect(guide).toContain('installation/install-qwen.sh');
expect(guide).toContain('installation/install-qwen.bat');
expect(guide).toContain('installation/install-qwen.ps1');
expect(guide).toContain('irm https://qwen-code-assets');
expect(guide).toContain('release operators must sync these staged files');
expect(guide).toContain('Hosted endpoint status');
expect(guide).toContain('legacy NVM-based installer');