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";
7 import { unknownToError } from "./catchHelper.mjs";
9 const HTTP_STATUS_OK = 200;
10 const HTTP_STATUS_NOT_MODIFIED = 304;
11 export const EXT_USER_AGENT = "Raspberry-Pi Pico VS Code Extension";
12 export const GITHUB_API_BASE_URL = "https://api.github.com";
14 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:
52 export function repoNameOfRepository(repository: GithubRepository): string {
54 case GithubRepository.picoSDK:
56 case GithubRepository.cmake:
58 case GithubRepository.ninja:
60 case GithubRepository.tools:
61 return "pico-sdk-tools";
65 interface AuthorizationHeaders {
66 // eslint-disable-next-line @typescript-eslint/naming-convention
67 Authorization?: string;
70 export function getAuthorizationHeaders(): AuthorizationHeaders {
71 const headers: AuthorizationHeaders = {};
72 // takes some time to execute (noticable in UI)
73 const githubPAT = Settings.getInstance()?.getString(SettingsKey.githubToken);
74 if (githubPAT && githubPAT.length > 0) {
75 Logger.log("Using GitHub Personal Access Token for authentication");
76 headers.Authorization = `Bearer ${githubPAT}`;
82 async function makeAsyncGetRequest<T>(
84 headers: { [key: string]: string }
88 headers: { etag?: string };
90 return new Promise((resolve, reject) => {
91 const urlObj = new URL(url);
92 const options: RequestOptions = {
95 ...getAuthorizationHeaders(),
97 // eslint-disable-next-line @typescript-eslint/naming-convention
98 "User-Agent": EXT_USER_AGENT,
100 hostname: urlObj.hostname,
101 path: urlObj.pathname,
102 minVersion: "TLSv1.2",
103 protocol: urlObj.protocol,
106 const req = request(options, res => {
107 const chunks: Buffer[] = [];
109 res.on("data", (chunk: Buffer) => {
113 res.on("end", () => {
114 const responseBody = Buffer.concat(chunks).toString();
117 res.statusCode && res.statusCode === HTTP_STATUS_OK
118 ? (JSON.parse(responseBody) as T)
121 status: res.statusCode ?? -1,
123 headers: { etag: res.headers.etag },
127 // TODO: replace with proper logging
128 console.error("Error parsing JSON:", error);
129 reject(unknownToError(error));
134 req.on("error", error => {
135 console.error("Error making GET request:", error.message);
143 async function getReleases(repository: GithubRepository): Promise<string[]> {
145 const owner = ownerOfRepository(repository);
146 const repo = repoNameOfRepository(repository);
147 const lastEtag = await GithubApiCache.getInstance().getLastEtag(
149 GithubApiCacheEntryDataType.releases
151 const headers: { [key: string]: string } = {
152 // eslint-disable-next-line @typescript-eslint/naming-convention
153 "X-GitHub-Api-Version": "2022-11-28",
156 headers["if-none-match"] = lastEtag;
159 // eslint-disable-next-line @typescript-eslint/naming-convention
160 const response = await makeAsyncGetRequest<Array<{ tag_name: string }>>(
161 `${GITHUB_API_BASE_URL}/repos/${owner}/${repo}/releases`,
165 if (response.status === HTTP_STATUS_NOT_MODIFIED) {
166 Logger.log("Using cached response for", repo, "releases");
168 const cachedResponse = await GithubApiCache.getInstance().getResponse(
170 GithubApiCacheEntryDataType.releases
172 if (cachedResponse) {
173 return cachedResponse.data as string[];
175 } else if (response.status !== 200) {
176 throw new Error("Error http status code: " + response.status);
179 if (response.data !== null) {
180 const responseData = response.data
182 .flatMap(release => release.tag_name)
183 .filter(release => release !== null);
185 // store the response in the cache
186 await GithubApiCache.getInstance().saveResponse(
188 GithubApiCacheEntryDataType.releases,
191 response.headers.etag
196 throw new Error("response.data is null");
199 Logger.log("Error fetching", repoNameOfRepository(repository), "releases");
202 await GithubApiCache.getInstance().getDefaultResponse(
204 GithubApiCacheEntryDataType.releases
210 export async function getSDKReleases(): Promise<string[]> {
211 return getReleases(GithubRepository.picoSDK);
214 export async function getNinjaReleases(): Promise<string[]> {
215 return getReleases(GithubRepository.ninja);
218 export async function getCmakeReleases(): Promise<string[]> {
219 return getReleases(GithubRepository.cmake);
222 export async function getGithubReleaseByTag(
223 repository: GithubRepository,
225 ): Promise<GithubReleaseResponse | undefined> {
227 const owner = ownerOfRepository(repository);
228 const repo = repoNameOfRepository(repository);
229 const lastEtag = await GithubApiCache.getInstance().getLastEtag(
231 GithubApiCacheEntryDataType.tag,
234 const headers: { [key: string]: string } = {
235 // eslint-disable-next-line @typescript-eslint/naming-convention
236 "X-GitHub-Api-Version": "2022-11-28",
239 headers["if-none-match"] = lastEtag;
242 const response = await makeAsyncGetRequest<{
243 assets: GithubReleaseAssetData[];
244 // eslint-disable-next-line @typescript-eslint/naming-convention
247 `${GITHUB_API_BASE_URL}/repos/${owner}/${repo}/releases/tags/${tag}`,
251 if (response.status === HTTP_STATUS_NOT_MODIFIED) {
252 Logger.log("Using cached response for", repo, "release by tag", tag);
254 const cachedResponse = await GithubApiCache.getInstance().getResponse(
256 GithubApiCacheEntryDataType.tag,
259 if (cachedResponse) {
260 return cachedResponse.data as GithubReleaseResponse;
262 } else if (response.status !== 200) {
263 throw new Error("Error http status code: " + response.status);
266 if (response.data !== null) {
267 const responseData = {
268 assets: response.data.assets,
269 assetsUrl: response.data.assets_url,
272 // store the response in the cache
273 await GithubApiCache.getInstance().saveResponse(
275 GithubApiCacheEntryDataType.tag,
278 response.headers.etag
283 throw new Error("response.data is null");
286 Logger.log("Error fetching", repoNameOfRepository(repository), "releases");
289 await GithubApiCache.getInstance().getDefaultResponse(
291 GithubApiCacheEntryDataType.tag,
294 )?.data as GithubReleaseResponse;