Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
180 changes: 104 additions & 76 deletions ts/packages/cli/src/services/upgrade-binary.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,84 +97,107 @@ export class UpgradeBinary extends Effect.Service<UpgradeBinary>()('services/Upg
)) as T;
});

const fetchLatestRelease = (): Effect.Effect<GitHubRelease, UpgradeBinaryError, never> =>
const fetchLatestRelease = (
tag: string
): Effect.Effect<GitHubRelease, UpgradeBinaryError, never> =>
Effect.gen(function* () {
const release = yield* githubConfig.TAG.pipe(
Option.match({
onNone: Effect.fn(function* () {
yield* Effect.logDebug(
'No tag specified, resolving latest package-scoped CLI release'
);
const url = `${githubConfig.API_BASE_URL}/repos/${githubConfig.OWNER}/${githubConfig.REPO}/releases?per_page=100`;
const releases = yield* fetchGitHubJson<unknown>({
url,
fetchErrorMessage: 'Failed to fetch releases from GitHub',
parseErrorMessage: 'Failed to parse GitHub releases JSON response',
});

if (!Array.isArray(releases)) {
return yield* Effect.fail(
new UpgradeBinaryError({
cause: new Error('GitHub releases response was not an array'),
message: 'Unexpected response while resolving latest CLI release',
})
);
}

const cliReleases = releases.filter(
(release): release is GitHubRelease =>
typeof release === 'object' &&
release !== null &&
'tag_name' in release &&
typeof release.tag_name === 'string' &&
('prerelease' in release ? release.prerelease === false : true) &&
('draft' in release ? release.draft === false : true) &&
CLI_RELEASE_TAG_PATTERN.test(release.tag_name)
);
yield* Effect.logDebug(`Using tag: ${tag}`);
const encodedTag = encodeURIComponent(tag);
const url = `${githubConfig.API_BASE_URL}/repos/${githubConfig.OWNER}/${githubConfig.REPO}/releases/tags/${encodedTag}`;
return yield* fetchGitHubJson<GitHubRelease>({
url,
fetchErrorMessage: `Failed to fetch tags/${tag} release from GitHub`,
parseErrorMessage: 'Failed to parse GitHub release JSON response',
});
});

if (cliReleases.length === 0) {
return yield* Effect.fail(
new UpgradeBinaryError({
cause: new Error('No package-scoped CLI releases found'),
message:
'Failed to determine latest CLI release from @composio/cli tags on GitHub',
})
);
}

let latest = cliReleases[0];
for (const release of cliReleases.slice(1)) {
const comparison = yield* semverComparator(latest.tag_name, release.tag_name).pipe(
Effect.mapError(
error =>
new UpgradeBinaryError({
cause: error,
message: 'Failed to compare CLI release versions',
})
)
);
const getBinaryAssetName = (platformArch: PlatformArch): string =>
`${CLI_BINARY_NAME}-${platformArch.platform}-${platformArch.arch}.zip`;

if (comparison < 0) {
latest = release;
}
}
const hasBinaryAssetForPlatform = (
release: GitHubRelease,
platformArch: PlatformArch
): boolean => {
const binaryName = getBinaryAssetName(platformArch);
return release.assets.some(asset => asset.name === binaryName);
};

yield* Effect.logDebug(`Resolved latest CLI release tag: ${latest.tag_name}`);
return latest;
}),
onSome: Effect.fn(function* (tag) {
yield* Effect.logDebug(`Using tag: ${tag}`);
const encodedTag = encodeURIComponent(tag);
const url = `${githubConfig.API_BASE_URL}/repos/${githubConfig.OWNER}/${githubConfig.REPO}/releases/tags/${encodedTag}`;
const release = yield* fetchGitHubJson<GitHubRelease>({
url,
fetchErrorMessage: `Failed to fetch tags/${tag} release from GitHub`,
parseErrorMessage: 'Failed to parse GitHub release JSON response',
});

return release as GitHubRelease;
}),
})
const fetchLatestReleaseWithRequiredAssets = (
platformArch: PlatformArch
): Effect.Effect<GitHubRelease, UpgradeBinaryError, never> =>
Effect.gen(function* () {
const url = `${githubConfig.API_BASE_URL}/repos/${githubConfig.OWNER}/${githubConfig.REPO}/releases?per_page=100`;
const releases = yield* fetchGitHubJson<unknown>({
url,
fetchErrorMessage: 'Failed to fetch releases from GitHub',
parseErrorMessage: 'Failed to parse GitHub releases JSON response',
});

if (!Array.isArray(releases)) {
return yield* Effect.fail(
new UpgradeBinaryError({
cause: new Error('GitHub releases response was not an array'),
message: 'Unexpected response while resolving latest CLI release',
})
);
}

const cliReleases = releases.filter(
(release): release is GitHubRelease =>
typeof release === 'object' &&
release !== null &&
'tag_name' in release &&
typeof release.tag_name === 'string' &&
('prerelease' in release ? release.prerelease === false : true) &&
('draft' in release ? release.draft === false : true) &&
CLI_RELEASE_TAG_PATTERN.test(release.tag_name)
);

if (cliReleases.length === 0) {
return yield* Effect.fail(
new UpgradeBinaryError({
cause: new Error('No package-scoped CLI releases found'),
message: 'Failed to determine latest CLI release from @composio/cli tags on GitHub',
})
);
}

let best: GitHubRelease | null = null;
for (const candidate of cliReleases) {
if (!hasBinaryAssetForPlatform(candidate, platformArch)) continue;
if (best === null) {
best = candidate;
continue;
}
const comparison = yield* semverComparator(best.tag_name, candidate.tag_name).pipe(
Effect.mapError(
error =>
new UpgradeBinaryError({
cause: error,
message: 'Failed to compare CLI release versions',
})
)
);
if (comparison < 0) {
best = candidate;
}
}

const release = best;

if (!release) {
return yield* Effect.fail(
new UpgradeBinaryError({
cause: new Error(
`No @composio/cli release contains ${getBinaryAssetName(platformArch)} asset`
),
message: `No binary available for ${platformArch.platform}-${platformArch.arch}`,
})
);
}

yield* Effect.logDebug(
`Resolved latest CLI release with required assets: ${release.tag_name}`
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Duplicated binary name construction not using new helper

Low Severity

The new getBinaryAssetName helper constructs the binary name string, but downloadBinary still builds the identical string inline at line 230. If the naming convention ever changes, one site could be updated while the other is missed, causing a silent mismatch between the asset-filtering logic and the download logic.

Additional Locations (1)

Fix in Cursor Fix in Web

);
return release;
});
Expand Down Expand Up @@ -405,9 +428,15 @@ export class UpgradeBinary extends Effect.Service<UpgradeBinary>()('services/Upg
return;
}

const platformArch = yield* detectPlatform;
const didUpgrade = yield* ui.useMakeSpinner('Checking for updates...', spinner =>
Effect.gen(function* () {
const release = yield* fetchLatestRelease();
const release = yield* githubConfig.TAG.pipe(
Option.match({
onNone: () => fetchLatestReleaseWithRequiredAssets(platformArch),
onSome: tag => fetchLatestRelease(tag),
})
);
const updateAvailable = yield* isUpdateAvailable(release);
if (!updateAvailable) {
yield* spinner.stop('You are already running the latest version!');
Expand All @@ -418,7 +447,6 @@ export class UpgradeBinary extends Effect.Service<UpgradeBinary>()('services/Upg
`New version available: ${release.tag_name} (current: ${APP_VERSION}). Downloading...`
);

const platformArch = yield* detectPlatform;
const { name, data } = yield* downloadBinary(release, platformArch);

yield* spinner.message('Extracting...');
Expand Down
Loading
Loading