1 import Settings, { SettingsKey } from "../settings.mjs";
2 import Logger from "../logger.mjs";
3 import GithubApiCache, {
4 GithubApiCacheEntryDataType,
5 } from "./githubApiCache.mjs";
6 import { type RequestOptions, request } from "https";
8 const HTTP_STATUS_OK = 200;
9 const HTTP_STATUS_NOT_MODIFIED = 304;
10 export const EXT_USER_AGENT = "Raspberry-Pi Pico VS Code Extension";
11 export const GITHUB_API_BASE_URL = "https://api.github.com";
13 export enum GithubRepository {
21 export type GithubReleaseResponse = {
23 assets: GithubReleaseAssetData[];
26 export type GithubReleaseAssetData = {
28 // eslint-disable-next-line @typescript-eslint/naming-convention
29 browser_download_url: string;
33 // NOTE: The primary rate limit for unauthenticated requests is 60 requests per hour.
34 export const SDK_REPOSITORY_URL = "https://github.com/raspberrypi/pico-sdk.git";
35 export const NINJA_REPOSITORY_URL = "https://github.com/ninja-build/ninja.git";
36 export const CMAKE_REPOSITORY_URL = "https://github.com/Kitware/CMake.git";
37 export const PYENV_REPOSITORY_URL = "https://github.com/pyenv/pyenv.git";
39 export function ownerOfRepository(repository: GithubRepository): string {
41 case GithubRepository.picoSDK:
43 case GithubRepository.cmake:
45 case GithubRepository.ninja:
47 case GithubRepository.tools:
49 case GithubRepository.openocd:
50 return "xpack-dev-tools";
54 export function repoNameOfRepository(repository: GithubRepository): string {
56 case GithubRepository.picoSDK:
58 case GithubRepository.cmake:
60 case GithubRepository.ninja:
62 case GithubRepository.tools:
63 return "pico-sdk-tools";
64 case GithubRepository.openocd:
65 return "openocd-xpack";
69 interface AuthorizationHeaders {
70 // eslint-disable-next-line @typescript-eslint/naming-convention
71 Authorization?: string;
74 export function getAuthorizationHeaders(): AuthorizationHeaders {
75 const headers: AuthorizationHeaders = {};
76 // takes some time to execute (noticable in UI)
77 const githubPAT = Settings.getInstance()?.getString(SettingsKey.githubToken);
78 if (githubPAT && githubPAT.length > 0) {
79 Logger.log("Using GitHub Personal Access Token for authentication");
80 headers.Authorization = `Bearer ${githubPAT}`;
86 async function makeAsyncGetRequest<T>(
88 headers: { [key: string]: string }
92 headers: { etag?: string };
94 return new Promise((resolve, reject) => {
95 const urlObj = new URL(url);
96 const options: RequestOptions = {
99 ...getAuthorizationHeaders(),
101 // eslint-disable-next-line @typescript-eslint/naming-convention
102 "User-Agent": EXT_USER_AGENT,
104 hostname: urlObj.hostname,
105 path: urlObj.pathname,
106 minVersion: "TLSv1.2",
107 protocol: urlObj.protocol,
110 const req = request(options, res => {
111 const chunks: Buffer[] = [];
113 res.on("data", (chunk: Buffer) => {
117 res.on("end", () => {
118 const responseBody = Buffer.concat(chunks).toString();
121 res.statusCode && res.statusCode === HTTP_STATUS_OK
122 ? (JSON.parse(responseBody) as T)
125 status: res.statusCode ?? -1,
127 headers: { etag: res.headers.etag },
131 console.error("Error parsing JSON:", error);
137 req.on("error", error => {
138 console.error("Error making GET request:", error.message);
146 async function getReleases(repository: GithubRepository): Promise<string[]> {
148 const owner = ownerOfRepository(repository);
149 const repo = repoNameOfRepository(repository);
150 const lastEtag = await GithubApiCache.getInstance().getLastEtag(
152 GithubApiCacheEntryDataType.releases
154 const headers: { [key: string]: string } = {
155 // eslint-disable-next-line @typescript-eslint/naming-convention
156 "X-GitHub-Api-Version": "2022-11-28",
159 headers["if-none-match"] = lastEtag;
162 // eslint-disable-next-line @typescript-eslint/naming-convention
163 const response = await makeAsyncGetRequest<Array<{ tag_name: string }>>(
164 `${GITHUB_API_BASE_URL}/repos/${owner}/${repo}/releases`,
168 if (response.status === HTTP_STATUS_NOT_MODIFIED) {
169 Logger.log("Using cached response for", repo, "releases");
171 const cachedResponse = await GithubApiCache.getInstance().getResponse(
173 GithubApiCacheEntryDataType.releases
175 if (cachedResponse) {
176 return cachedResponse.data as string[];
178 } else if (response.status !== 200) {
179 throw new Error("Error http status code: " + response.status);
182 if (response.data !== null) {
183 const responseData = response.data
185 .flatMap(release => release.tag_name)
186 .filter(release => release !== null);
188 // store the response in the cache
189 await GithubApiCache.getInstance().saveResponse(
191 GithubApiCacheEntryDataType.releases,
194 response.headers.etag
199 throw new Error("response.data is null");
202 Logger.log("Error fetching", repoNameOfRepository(repository), "releases");
204 return (await GithubApiCache.getInstance().getDefaultResponse(
206 GithubApiCacheEntryDataType.releases
207 ))?.data as string[];
211 export async function getSDKReleases(): Promise<string[]> {
212 return getReleases(GithubRepository.picoSDK);
215 export async function getNinjaReleases(): Promise<string[]> {
216 return getReleases(GithubRepository.ninja);
219 export async function getCmakeReleases(): Promise<string[]> {
220 return getReleases(GithubRepository.cmake);
223 export async function getGithubReleaseByTag(
224 repository: GithubRepository,
226 ): Promise<GithubReleaseResponse | undefined> {
228 const owner = ownerOfRepository(repository);
229 const repo = repoNameOfRepository(repository);
230 const lastEtag = await GithubApiCache.getInstance().getLastEtag(
232 GithubApiCacheEntryDataType.tag,
235 const headers: { [key: string]: string } = {
236 // eslint-disable-next-line @typescript-eslint/naming-convention
237 "X-GitHub-Api-Version": "2022-11-28",
240 headers["if-none-match"] = lastEtag;
243 const response = await makeAsyncGetRequest<{
244 assets: GithubReleaseAssetData[];
245 // eslint-disable-next-line @typescript-eslint/naming-convention
248 `${GITHUB_API_BASE_URL}/repos/${owner}/${repo}/releases/tags/${tag}`,
252 if (response.status === HTTP_STATUS_NOT_MODIFIED) {
253 Logger.log("Using cached response for", repo, "release by tag", tag);
255 const cachedResponse = await GithubApiCache.getInstance().getResponse(
257 GithubApiCacheEntryDataType.tag,
260 if (cachedResponse) {
261 return cachedResponse.data as GithubReleaseResponse;
263 } else if (response.status !== 200) {
264 throw new Error("Error http status code: " + response.status);
267 if (response.data !== null) {
268 const responseData = {
269 assets: response.data.assets,
270 assetsUrl: response.data.assets_url,
273 // store the response in the cache
274 await GithubApiCache.getInstance().saveResponse(
276 GithubApiCacheEntryDataType.tag,
279 response.headers.etag
284 throw new Error("response.data is null");
287 Logger.log("Error fetching", repoNameOfRepository(repository), "releases");
289 return (await GithubApiCache.getInstance().getDefaultResponse(
291 GithubApiCacheEntryDataType.tag,
293 ))?.data as GithubReleaseResponse;