Skip to content

Commit 3041bf5

Browse files
authored
feat: fallback to "raw" endpoint for manifest when rate limit is reached (#496)
* feat: fallback to "raw" endpoint for manifest when rate limit is reached * add information about raw access to the README * prettier * update cross-spawn to 7.0.6 to fix vulnerability
1 parent 41dfa10 commit 3041bf5

File tree

5 files changed

+107
-20
lines changed

5 files changed

+107
-20
lines changed

README.md

+9-14
Original file line numberDiff line numberDiff line change
@@ -242,18 +242,14 @@ documentation.
242242

243243
## Using `setup-go` on GHES
244244

245-
`setup-go` comes pre-installed on the appliance with GHES if Actions is enabled. When dynamically downloading Go
246-
distributions, `setup-go` downloads distributions from [`actions/go-versions`](https://github.com/actions/go-versions)
247-
on github.com (outside of the appliance). These calls to `actions/go-versions` are made via unauthenticated requests,
248-
which are limited
249-
to [60 requests per hour per IP](https://docs.github.com/en/rest/overview/resources-in-the-rest-api#rate-limiting). If
250-
more requests are made within the time frame, then you will start to see rate-limit errors during downloading that looks
251-
like: `##[error]API rate limit exceeded for...`. After that error the action will try to download versions directly
252-
from https://storage.googleapis.com/golang, but it also can have rate limit so it's better to put token.
253-
254-
To get a higher rate limit, you
255-
can [generate a personal access token on github.com](https://github.com/settings/tokens/new) and pass it as the `token`
256-
input for the action:
245+
`setup-go` comes pre-installed on the appliance with GHES if Actions is enabled.
246+
When dynamically downloading Go distributions, `setup-go` downloads distributions from [`actions/go-versions`](https://github.com/actions/go-versions) on github.com (outside of the appliance).
247+
248+
These calls to `actions/go-versions` are made via unauthenticated requests, which are limited to [60 requests per hour per IP](https://docs.github.com/en/rest/overview/resources-in-the-rest-api#rate-limiting).
249+
If more requests are made within the time frame, then the action leverages the `raw API` to retrieve the version-manifest. This approach does not impose a rate limit and hence facilitates unrestricted consumption. This is particularly beneficial for GHES runners, which often share the same IP, to avoid the quick exhaustion of the unauthenticated rate limit.
250+
If that fails as well the action will try to download versions directly from https://storage.googleapis.com/golang.
251+
252+
If that fails as well you can get a higher rate limit with [generating a personal access token on github.com](https://github.com/settings/tokens/new) and passing it as the `token` input to the action:
257253

258254
```yaml
259255
uses: actions/setup-go@v5
@@ -262,8 +258,7 @@ with:
262258
go-version: '1.18'
263259
```
264260

265-
If the runner is not able to access github.com, any Go versions requested during a workflow run must come from the
266-
runner's tool cache.
261+
If the runner is not able to access github.com, any Go versions requested during a workflow run must come from the runner's tool cache.
267262
See "[Setting up the tool cache on self-hosted runners without internet access](https://docs.github.com/en/[email protected]/admin/github-actions/managing-access-to-actions-from-githubcom/setting-up-the-tool-cache-on-self-hosted-runners-without-internet-access)"
268263
for more information.
269264

__tests__/setup-go.test.ts

+23
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import osm, {type} from 'os';
77
import path from 'path';
88
import * as main from '../src/main';
99
import * as im from '../src/installer';
10+
import * as httpm from '@actions/http-client';
1011

1112
import goJsonData from './data/golang-dl.json';
1213
import matchers from '../matchers.json';
@@ -46,6 +47,7 @@ describe('setup-go', () => {
4647
let execSpy: jest.SpyInstance;
4748
let getManifestSpy: jest.SpyInstance;
4849
let getAllVersionsSpy: jest.SpyInstance;
50+
let httpmGetJsonSpy: jest.SpyInstance;
4951

5052
beforeAll(async () => {
5153
process.env['GITHUB_ENV'] = ''; // Stub out Environment file functionality so we can verify it writes to standard out (toolkit is backwards compatible)
@@ -90,6 +92,9 @@ describe('setup-go', () => {
9092
getManifestSpy = jest.spyOn(tc, 'getManifestFromRepo');
9193
getAllVersionsSpy = jest.spyOn(im, 'getManifest');
9294

95+
// httm
96+
httpmGetJsonSpy = jest.spyOn(httpm.HttpClient.prototype, 'getJson');
97+
9398
// io
9499
whichSpy = jest.spyOn(io, 'which');
95100
existsSpy = jest.spyOn(fs, 'existsSync');
@@ -151,6 +156,21 @@ describe('setup-go', () => {
151156
);
152157
});
153158

159+
it('should return manifest from repo', async () => {
160+
const manifest = await im.getManifest(undefined);
161+
expect(manifest).toEqual(goTestManifest);
162+
});
163+
164+
it('should return manifest from raw URL if repo fetch fails', async () => {
165+
getManifestSpy.mockRejectedValue(new Error('Fetch failed'));
166+
httpmGetJsonSpy.mockResolvedValue({
167+
result: goTestManifest
168+
});
169+
const manifest = await im.getManifest(undefined);
170+
expect(httpmGetJsonSpy).toHaveBeenCalled();
171+
expect(manifest).toEqual(goTestManifest);
172+
});
173+
154174
it('can find 1.9 from manifest on linux', async () => {
155175
os.platform = 'linux';
156176
os.arch = 'x64';
@@ -790,6 +810,9 @@ describe('setup-go', () => {
790810
getManifestSpy.mockImplementation(() => {
791811
throw new Error('Unable to download manifest');
792812
});
813+
httpmGetJsonSpy.mockRejectedValue(
814+
new Error('Unable to download manifest from raw URL')
815+
);
793816
getAllVersionsSpy.mockImplementationOnce(() => undefined);
794817

795818
dlSpy.mockImplementation(async () => '/some/temp/path');

dist/setup/index.js

+29-1
Original file line numberDiff line numberDiff line change
@@ -88259,6 +88259,10 @@ const sys = __importStar(__nccwpck_require__(5632));
8825988259
const fs_1 = __importDefault(__nccwpck_require__(7147));
8826088260
const os_1 = __importDefault(__nccwpck_require__(2037));
8826188261
const utils_1 = __nccwpck_require__(1314);
88262+
const MANIFEST_REPO_OWNER = 'actions';
88263+
const MANIFEST_REPO_NAME = 'go-versions';
88264+
const MANIFEST_REPO_BRANCH = 'main';
88265+
const MANIFEST_URL = `https://raw.githubusercontent.com/${MANIFEST_REPO_OWNER}/${MANIFEST_REPO_NAME}/${MANIFEST_REPO_BRANCH}/versions-manifest.json`;
8826288266
function getGo(versionSpec_1, checkLatest_1, auth_1) {
8826388267
return __awaiter(this, arguments, void 0, function* (versionSpec, checkLatest, auth, arch = os_1.default.arch()) {
8826488268
var _a;
@@ -88433,10 +88437,34 @@ function extractGoArchive(archivePath) {
8843388437
exports.extractGoArchive = extractGoArchive;
8843488438
function getManifest(auth) {
8843588439
return __awaiter(this, void 0, void 0, function* () {
88436-
return tc.getManifestFromRepo('actions', 'go-versions', auth, 'main');
88440+
try {
88441+
return yield getManifestFromRepo(auth);
88442+
}
88443+
catch (err) {
88444+
core.debug('Fetching the manifest via the API failed.');
88445+
if (err instanceof Error) {
88446+
core.debug(err.message);
88447+
}
88448+
}
88449+
return yield getManifestFromURL();
8843788450
});
8843888451
}
8843988452
exports.getManifest = getManifest;
88453+
function getManifestFromRepo(auth) {
88454+
core.debug(`Getting manifest from ${MANIFEST_REPO_OWNER}/${MANIFEST_REPO_NAME}@${MANIFEST_REPO_BRANCH}`);
88455+
return tc.getManifestFromRepo(MANIFEST_REPO_OWNER, MANIFEST_REPO_NAME, auth, MANIFEST_REPO_BRANCH);
88456+
}
88457+
function getManifestFromURL() {
88458+
return __awaiter(this, void 0, void 0, function* () {
88459+
core.debug('Falling back to fetching the manifest using raw URL.');
88460+
const http = new httpm.HttpClient('tool-cache');
88461+
const response = yield http.getJson(MANIFEST_URL);
88462+
if (!response.result) {
88463+
throw new Error(`Unable to get manifest from ${MANIFEST_URL}`);
88464+
}
88465+
return response.result;
88466+
});
88467+
}
8844088468
function getInfoFromManifest(versionSpec_1, stable_1, auth_1) {
8844188469
return __awaiter(this, arguments, void 0, function* (versionSpec, stable, auth, arch = os_1.default.arch(), manifest) {
8844288470
let info = null;

package-lock.json

+4-3
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/installer.ts

+42-2
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,11 @@ import fs from 'fs';
88
import os from 'os';
99
import {StableReleaseAlias} from './utils';
1010

11+
const MANIFEST_REPO_OWNER = 'actions';
12+
const MANIFEST_REPO_NAME = 'go-versions';
13+
const MANIFEST_REPO_BRANCH = 'main';
14+
const MANIFEST_URL = `https://raw.githubusercontent.com/${MANIFEST_REPO_OWNER}/${MANIFEST_REPO_NAME}/${MANIFEST_REPO_BRANCH}/versions-manifest.json`;
15+
1116
type InstallationType = 'dist' | 'manifest';
1217

1318
export interface IGoVersionFile {
@@ -274,8 +279,43 @@ export async function extractGoArchive(archivePath: string): Promise<string> {
274279
return extPath;
275280
}
276281

277-
export async function getManifest(auth: string | undefined) {
278-
return tc.getManifestFromRepo('actions', 'go-versions', auth, 'main');
282+
export async function getManifest(
283+
auth: string | undefined
284+
): Promise<tc.IToolRelease[]> {
285+
try {
286+
return await getManifestFromRepo(auth);
287+
} catch (err) {
288+
core.debug('Fetching the manifest via the API failed.');
289+
if (err instanceof Error) {
290+
core.debug(err.message);
291+
}
292+
}
293+
return await getManifestFromURL();
294+
}
295+
296+
function getManifestFromRepo(
297+
auth: string | undefined
298+
): Promise<tc.IToolRelease[]> {
299+
core.debug(
300+
`Getting manifest from ${MANIFEST_REPO_OWNER}/${MANIFEST_REPO_NAME}@${MANIFEST_REPO_BRANCH}`
301+
);
302+
return tc.getManifestFromRepo(
303+
MANIFEST_REPO_OWNER,
304+
MANIFEST_REPO_NAME,
305+
auth,
306+
MANIFEST_REPO_BRANCH
307+
);
308+
}
309+
310+
async function getManifestFromURL(): Promise<tc.IToolRelease[]> {
311+
core.debug('Falling back to fetching the manifest using raw URL.');
312+
313+
const http: httpm.HttpClient = new httpm.HttpClient('tool-cache');
314+
const response = await http.getJson<tc.IToolRelease[]>(MANIFEST_URL);
315+
if (!response.result) {
316+
throw new Error(`Unable to get manifest from ${MANIFEST_URL}`);
317+
}
318+
return response.result;
279319
}
280320

281321
export async function getInfoFromManifest(

0 commit comments

Comments
 (0)