Skip to content

Commit f1f8064

Browse files
feat(p1): add upgrade channels and pinning
1 parent b5ebf20 commit f1f8064

File tree

4 files changed

+91
-15
lines changed

4 files changed

+91
-15
lines changed

docs/commands.md

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -317,17 +317,21 @@ Usage: cloudsqlctl upgrade [options]
317317
Upgrade cloudsqlctl to the latest version
318318
319319
Options:
320-
--check-only Only check for updates, do not download or install
321-
--no-install Download only, do not install
322-
--asset <mode> Asset type to download (auto, installer, exe) (default:
323-
"auto")
324-
--dir <path> Download directory (default:
325-
"<USER_HOME>\\AppData\\Local\\CloudSQLCTL\\downloads\\updates")
326-
--force Force update even if version is same or older
327-
--no-silent Run installer in interactive mode (installer only)
328-
--no-elevate Do not attempt to elevate privileges (installer only)
329-
--json Output status in JSON format
330-
-h, --help display help for command
320+
--check-only Only check for updates, do not download or install
321+
--no-install Download only, do not install
322+
--asset <mode> Asset type to download (auto, installer, exe) (default:
323+
"auto")
324+
--dir <path> Download directory (default:
325+
"<USER_HOME>\\AppData\\Local\\CloudSQLCTL\\downloads\\updates")
326+
--force Force update even if version is same or older
327+
--no-silent Run installer in interactive mode (installer only)
328+
--no-elevate Do not attempt to elevate privileges (installer only)
329+
--channel <channel> Update channel (stable or beta)
330+
--version <version> Install a specific version (e.g. 0.4.14 or v0.4.14)
331+
--pin <version> Pin to a specific version for future upgrades
332+
--unpin Clear pinned version
333+
--json Output status in JSON format
334+
-h, --help display help for command
331335
```
332336

333337
### support

src/commands/upgrade.ts

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { Command } from 'commander';
22
import path from 'path';
33
import { logger } from '../core/logger.js';
4+
import { readConfig, writeConfig } from '../core/config.js';
45
import { USER_PATHS } from '../system/paths.js';
56
import {
67
checkForUpdates,
@@ -28,16 +29,38 @@ export const upgradeCommand = new Command('upgrade')
2829
.option('--force', 'Force update even if version is same or older')
2930
.option('--no-silent', 'Run installer in interactive mode (installer only)')
3031
.option('--no-elevate', 'Do not attempt to elevate privileges (installer only)')
32+
.option('--channel <channel>', 'Update channel (stable or beta)')
33+
.option('--version <version>', 'Install a specific version (e.g. 0.4.14 or v0.4.14)')
34+
.option('--pin <version>', 'Pin to a specific version for future upgrades')
35+
.option('--unpin', 'Clear pinned version')
3136
.option('--json', 'Output status in JSON format')
3237
.action(async (options) => {
3338
try {
3439
const currentVersion = process.env.CLOUDSQLCTL_VERSION || '0.0.0';
40+
const config = await readConfig();
41+
const channel = (options.channel || config.updateChannel || 'stable') as 'stable' | 'beta';
42+
43+
if (channel !== 'stable' && channel !== 'beta') {
44+
throw new Error(`Invalid channel '${channel}'. Use 'stable' or 'beta'.`);
45+
}
46+
47+
if (options.unpin) {
48+
await writeConfig({ pinnedVersion: undefined });
49+
}
50+
51+
if (options.pin) {
52+
await writeConfig({ pinnedVersion: options.pin, updateChannel: channel });
53+
} else if (options.channel) {
54+
await writeConfig({ updateChannel: channel });
55+
}
56+
const targetVersion = options.version || options.pin || (!options.unpin ? config.pinnedVersion : undefined);
3557

3658
if (!options.json) {
37-
logger.info(`Checking for updates (Current: v${currentVersion})...`);
59+
const suffix = targetVersion ? ` (target: ${targetVersion})` : '';
60+
logger.info(`Checking for updates (Current: v${currentVersion}, channel: ${channel})${suffix}...`);
3861
}
3962

40-
const status = await checkForUpdates(currentVersion);
63+
const status = await checkForUpdates(currentVersion, { channel, targetVersion });
4164

4265
if (options.json) {
4366
console.log(JSON.stringify(status, null, 2));

src/core/config.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ export interface AppConfig {
1010
lastUpdateCheck?: string;
1111
lastUpdateAvailableVersion?: string;
1212
gcloudPath?: string;
13+
updateChannel?: 'stable' | 'beta';
14+
pinnedVersion?: string;
1315
}
1416

1517
export async function ensureDirs() {

src/core/selfUpdate.ts

Lines changed: 49 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ import { writeConfig } from './config.js';
1010
const REPO_OWNER = 'Kinin-Code-Offical';
1111
const REPO_NAME = 'cloudsqlctl';
1212
const GITHUB_API_URL = `https://api.github.com/repos/${REPO_OWNER}/${REPO_NAME}/releases/latest`;
13+
const GITHUB_RELEASES_URL = `https://api.github.com/repos/${REPO_OWNER}/${REPO_NAME}/releases`;
14+
const GITHUB_RELEASE_TAG_URL = `https://api.github.com/repos/${REPO_OWNER}/${REPO_NAME}/releases/tags`;
1315
const TIMEOUT_MS = 60000;
1416
const MAX_RETRIES = 2;
1517

@@ -42,8 +44,53 @@ export async function getLatestRelease(): Promise<ReleaseInfo> {
4244
}
4345
}
4446

45-
export async function checkForUpdates(currentVersion: string): Promise<UpdateStatus> {
46-
const release = await getLatestRelease();
47+
function normalizeTag(tag: string): string {
48+
return tag.startsWith('v') ? tag : `v${tag}`;
49+
}
50+
51+
export async function getReleaseByTag(tag: string): Promise<ReleaseInfo> {
52+
try {
53+
const response = await axios.get(`${GITHUB_RELEASE_TAG_URL}/${normalizeTag(tag)}`, {
54+
timeout: TIMEOUT_MS,
55+
headers: { 'User-Agent': 'cloudsqlctl/upgrade' }
56+
});
57+
return response.data;
58+
} catch (error) {
59+
logger.error('Failed to fetch release by tag', error);
60+
throw error;
61+
}
62+
}
63+
64+
export async function getLatestPrerelease(): Promise<ReleaseInfo> {
65+
try {
66+
const response = await axios.get(GITHUB_RELEASES_URL, {
67+
timeout: TIMEOUT_MS,
68+
headers: { 'User-Agent': 'cloudsqlctl/upgrade' }
69+
});
70+
const releases = Array.isArray(response.data) ? response.data : [];
71+
const prerelease = releases.find((r: { prerelease?: boolean; draft?: boolean }) => r.prerelease && !r.draft);
72+
if (!prerelease) {
73+
throw new Error('No prerelease found');
74+
}
75+
return prerelease;
76+
} catch (error) {
77+
logger.error('Failed to fetch latest prerelease info', error);
78+
throw error;
79+
}
80+
}
81+
82+
export async function checkForUpdates(
83+
currentVersion: string,
84+
options: { channel?: 'stable' | 'beta'; targetVersion?: string } = {}
85+
): Promise<UpdateStatus> {
86+
let release: ReleaseInfo;
87+
if (options.targetVersion) {
88+
release = await getReleaseByTag(options.targetVersion);
89+
} else if (options.channel === 'beta') {
90+
release = await getLatestPrerelease();
91+
} else {
92+
release = await getLatestRelease();
93+
}
4794
// Remove 'v' prefix if present for semver comparison
4895
const latestVer = release.tag_name.replace(/^v/, '');
4996
const currentVer = currentVersion.replace(/^v/, '');

0 commit comments

Comments
 (0)