diff --git a/.github/workflows/releaseVSCode.yml b/.github/workflows/releaseVSCode.yml index 771f5c2..73be97c 100644 --- a/.github/workflows/releaseVSCode.yml +++ b/.github/workflows/releaseVSCode.yml @@ -12,10 +12,10 @@ jobs: release: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 + - uses: actions/checkout@v6 + - uses: actions/setup-node@v6 with: - node-version: 20 + node-version: 24 - run: npm run install:all working-directory: ./FASTBuildMonitorVSCode - run: npm run build:all diff --git a/.github/workflows/validate-pr-title.yml b/.github/workflows/validate-pr-title.yml index 7e0da0b..d4022e0 100644 --- a/.github/workflows/validate-pr-title.yml +++ b/.github/workflows/validate-pr-title.yml @@ -11,6 +11,6 @@ jobs: validate-pr-title: runs-on: ubuntu-latest steps: - - uses: amannn/action-semantic-pull-request@v5 + - uses: amannn/action-semantic-pull-request@v6 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file diff --git a/FASTBuildMonitorVSCode/package-lock.json b/FASTBuildMonitorVSCode/package-lock.json index 8a4f566..288feb5 100644 --- a/FASTBuildMonitorVSCode/package-lock.json +++ b/FASTBuildMonitorVSCode/package-lock.json @@ -22,7 +22,7 @@ "devDependencies": { "@semantic-release/changelog": "^6.0.3", "@semantic-release/git": "^10.0.1", - "@types/node": "^20.0.0", + "@types/node": "^24.0.0", "@types/semver": "^7.7.1", "@types/vscode": "^1.85.0", "@vscode/vsce": "^3.2.1", @@ -33,7 +33,7 @@ "typescript": "^5.3.0" }, "engines": { - "node": "^20.0.0", + "node": "^24.0.0", "vscode": "^1.85.0" } }, @@ -2184,13 +2184,13 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "20.19.33", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.33.tgz", - "integrity": "sha512-Rs1bVAIdBs5gbTIKza/tgpMuG1k3U/UMJLWecIMxNdJFDMzcM5LOiLVRYh3PilWEYDIeUDv7bpiHPLPsbydGcw==", + "version": "24.12.4", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.12.4.tgz", + "integrity": "sha512-GUUEShf+PBCGW2KaXwcIt3Yk+e3pkKwWKb9GSyM9WQVE+ep2jzmHdGsHzu4wgcZy5fN9FBdVzjpBQsYlpfpgLA==", "dev": true, "license": "MIT", "dependencies": { - "undici-types": "~6.21.0" + "undici-types": "~7.16.0" } }, "node_modules/@types/normalize-package-data": { @@ -11610,9 +11610,9 @@ } }, "node_modules/undici-types": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", - "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", "dev": true, "license": "MIT" }, diff --git a/FASTBuildMonitorVSCode/package.json b/FASTBuildMonitorVSCode/package.json index 2bd24d7..62803b3 100644 --- a/FASTBuildMonitorVSCode/package.json +++ b/FASTBuildMonitorVSCode/package.json @@ -19,7 +19,7 @@ "workspace" ], "engines": { - "node": "^20.0.0", + "node": "^24.0.0", "vscode": "^1.85.0" }, "categories": [ @@ -121,7 +121,7 @@ "@semantic-release/git": "^10.0.1", "esbuild": "^0.25.11", "esbuild-plugin-copy": "^2.1.1", - "@types/node": "^20.0.0", + "@types/node": "^24.0.0", "@types/vscode": "^1.85.0", "@types/semver": "^7.7.1", "@vscode/vsce": "^3.2.1", diff --git a/FASTBuildMonitorVSCode/src/utilities/githubApiClient.ts b/FASTBuildMonitorVSCode/src/utilities/githubApiClient.ts deleted file mode 100644 index afcf543..0000000 --- a/FASTBuildMonitorVSCode/src/utilities/githubApiClient.ts +++ /dev/null @@ -1,200 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -import * as vscode from 'vscode'; -import { Logger } from './logger.js'; -import { RetryHelper } from './retryHelper.js'; - -export interface GitHubReleaseAsset { - id: number; - name: string; - url: string; - browser_download_url: string; -} - -export interface GitHubRelease { - version: string; - name: string; - prerelease: boolean; - html_url: string; - assets: GitHubReleaseAsset[]; -} - -interface GitHubApiRelease { - tag_name: string; - name: string; - prerelease: boolean; - html_url: string; - assets: GitHubReleaseAsset[]; -} - -export interface GitHubApiOptions { - scopes: string[]; -} - -/** - * Client for interacting with the GitHub API. - * Handles authentication, release fetching, and asset downloads. - */ -export class GitHubApiClient { - private static readonly API_VERSION = '2022-11-28'; - private readonly retryHelper: RetryHelper; - - /** - * Creates a new GitHubApiClient instance. - * @param logger - Logger instance for diagnostic output - * @param options - API options including required OAuth scopes - */ - constructor( - private logger: Logger, - private options: GitHubApiOptions - ) { - this.retryHelper = new RetryHelper(logger); - } - - /** - * Gets a GitHub authentication session. - * @param promptForLogin - Whether to prompt the user to login if not authenticated - * @returns Authentication session or undefined if not available - */ - async getAuthSession(promptForLogin: boolean): Promise { - try { - return await vscode.authentication.getSession('github', this.options.scopes, { - createIfNone: promptForLogin, - silent: !promptForLogin, - clearSessionPreference: false - }); - } catch (error) { - const errorMsg = error instanceof Error ? error.message : String(error); - this.logger.error(`GitHub authentication request failed: ${errorMsg}`, error); - return undefined; - } - } - - /** - * Fetches releases from a GitHub repository. - * @param owner - Repository owner - * @param repo - Repository name - * @returns Array of releases, or empty array if authentication fails - */ - async fetchReleases(owner: string, repo: string): Promise { - const session = await this.getAuthSession(true); - if (!session) { - this.logger.warn('GitHub authentication required to retrieve release information; no session available.'); - void vscode.window.showWarningMessage('Sign in to GitHub to check for FASTBuild Monitor updates.'); - return []; - } - - return await this.retryHelper.executeWithRetry( - async () => await this.fetchReleasesInternal(owner, repo, session), - 'Fetch GitHub releases' - ); - } - - private async fetchReleasesInternal( - owner: string, - repo: string, - session: vscode.AuthenticationSession - ): Promise { - const url = `https://api.github.com/repos/${owner}/${repo}/releases`; - this.logger.debug(`Fetching releases from: ${url}`); - - const headers: Record = { - 'Accept': 'application/vnd.github+json', - 'X-GitHub-Api-Version': GitHubApiClient.API_VERSION, - 'Authorization': `Bearer ${session.accessToken}` - }; - - // Log authentication without exposing token - this.logger.debug(`Using authenticated GitHub API request (account: ${session.account.label})`); - - const response = await fetch(url, { headers }); - this.logger.debug(`GitHub API response: ${response.status} ${response.statusText}`); - - if (!response.ok) { - const errorMsg = `GitHub API returned ${response.status} ${response.statusText}`; - this.logger.error(errorMsg); - throw new Error(errorMsg); - } - - const rawReleases = await response.json() as GitHubApiRelease[]; - this.logger.debug(`Fetched ${rawReleases.length} releases`); - - // Map GitHub API response to GitHubRelease interface - // GitHub API returns 'tag_name' but our interface expects 'version' - const releases: GitHubRelease[] = rawReleases.map(r => ({ - version: r.tag_name, - name: r.name, - prerelease: r.prerelease, - html_url: r.html_url, - assets: r.assets - })); - - return releases; - } - - /** - * Downloads a binary asset from GitHub. - * @param assetUrl - GitHub API URL for the asset - * @param session - Authenticated GitHub session - * @returns Binary content as ArrayBuffer - */ - async downloadAsset(assetUrl: string, session: vscode.AuthenticationSession): Promise { - return await this.retryHelper.executeWithRetry( - async () => await this.downloadAssetInternal(assetUrl, session), - 'Download GitHub asset' - ); - } - - private async downloadAssetInternal( - assetUrl: string, - session: vscode.AuthenticationSession - ): Promise { - const headers: Record = { - 'Accept': 'application/octet-stream', - 'Authorization': `Bearer ${session.accessToken}`, - 'X-GitHub-Api-Version': GitHubApiClient.API_VERSION - }; - - this.logger.debug(`Downloading asset from: ${assetUrl}`); - - const response = await fetch(assetUrl, { headers }); - this.logger.debug(`Asset download response: ${response.status} ${response.statusText}`); - - if (!response.ok) { - const errorMsg = `Asset download failed with status ${response.status} ${response.statusText}`; - this.logger.error(errorMsg); - throw new Error(errorMsg); - } - - const buffer = await response.arrayBuffer(); - this.logger.debug(`Downloaded ${buffer.byteLength} bytes`); - - return buffer; - } - - /** - * Downloads text content from GitHub. - * @param url - GitHub API URL for the text file - * @param session - Authenticated GitHub session - * @returns Text content as string - */ - async downloadText(url: string, session: vscode.AuthenticationSession): Promise { - const headers: Record = { - 'Accept': 'application/octet-stream', - 'Authorization': `Bearer ${session.accessToken}`, - 'X-GitHub-Api-Version': GitHubApiClient.API_VERSION - }; - - this.logger.debug(`Downloading text from: ${url}`); - - const response = await fetch(url, { headers }); - - if (!response.ok) { - this.logger.warn(`Text download failed with status ${response.status}`); - throw new Error(`Failed to download: ${response.status} ${response.statusText}`); - } - - return await response.text(); - } -} diff --git a/FASTBuildMonitorVSCode/src/utilities/retryHelper.ts b/FASTBuildMonitorVSCode/src/utilities/retryHelper.ts deleted file mode 100644 index b0a1579..0000000 --- a/FASTBuildMonitorVSCode/src/utilities/retryHelper.ts +++ /dev/null @@ -1,79 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -import { Logger } from './logger'; - -const RETRY_MAX_ATTEMPTS = 3; -const RETRY_INITIAL_DELAY_MS = 1000; -const RETRY_MAX_DELAY_MS = 30000; -const RETRY_BACKOFF_MULTIPLIER = 2; - -/** - * Configuration for retry behavior. - */ -export interface RetryOptions { - maxAttempts?: number; - initialDelayMs?: number; - maxDelayMs?: number; - backoffMultiplier?: number; -} - -/** - * Utility for retrying operations with exponential backoff. - */ -export class RetryHelper { - private readonly maxAttempts: number; - private readonly initialDelayMs: number; - private readonly maxDelayMs: number; - private readonly backoffMultiplier: number; - - constructor( - private logger: Logger, - options: RetryOptions = {} - ) { - this.maxAttempts = options.maxAttempts ?? RETRY_MAX_ATTEMPTS; - this.initialDelayMs = options.initialDelayMs ?? RETRY_INITIAL_DELAY_MS; - this.maxDelayMs = options.maxDelayMs ?? RETRY_MAX_DELAY_MS; - this.backoffMultiplier = options.backoffMultiplier ?? RETRY_BACKOFF_MULTIPLIER; - } - - /** - * Execute an operation with retry logic. - * @param operation - Async function to execute - * @param operationName - Name of the operation for logging - * @returns Result of the operation - * @throws Last error if all retries fail - */ - async executeWithRetry( - operation: () => Promise, - operationName: string - ): Promise { - let lastError: Error | undefined; - let delay = this.initialDelayMs; - - for (let attempt = 1; attempt <= this.maxAttempts; attempt++) { - try { - this.logger.debug(`${operationName} - Attempt ${attempt}/${this.maxAttempts}`); - return await operation(); - } catch (error) { - lastError = error instanceof Error ? error : new Error(String(error)); - this.logger.warn( - `${operationName} - Attempt ${attempt}/${this.maxAttempts} failed: ${lastError.message}` - ); - - if (attempt < this.maxAttempts) { - this.logger.debug(`Retrying in ${delay}ms...`); - await this.sleep(delay); - delay = Math.min(delay * this.backoffMultiplier, this.maxDelayMs); - } - } - } - - this.logger.error(`${operationName} - All ${this.maxAttempts} attempts failed`); - throw lastError || new Error(`${operationName} failed after ${this.maxAttempts} attempts`); - } - - private sleep(ms: number): Promise { - return new Promise(resolve => setTimeout(resolve, ms)); - } -}