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
This commit is contained in:
Tobias 2024-11-25 19:37:21 +01:00 committed by GitHub
parent 41dfa10bad
commit 3041bf56c9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 106 additions and 19 deletions

View File

@ -242,18 +242,14 @@ documentation.
## Using `setup-go` on GHES
`setup-go` comes pre-installed on the appliance with GHES if Actions is enabled. 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). 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). If
more requests are made within the time frame, then you will start to see rate-limit errors during downloading that looks
like: `##[error]API rate limit exceeded for...`. After that error the action will try to download versions directly
from https://storage.googleapis.com/golang, but it also can have rate limit so it's better to put token.
`setup-go` comes pre-installed on the appliance with GHES if Actions is enabled.
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).
To get a higher rate limit, you
can [generate a personal access token on github.com](https://github.com/settings/tokens/new) and pass it as the `token`
input for the action:
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).
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.
If that fails as well the action will try to download versions directly from https://storage.googleapis.com/golang.
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:
```yaml
uses: actions/setup-go@v5
@ -262,8 +258,7 @@ with:
go-version: '1.18'
```
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.
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.
See "[Setting up the tool cache on self-hosted runners without internet access](https://docs.github.com/en/enterprise-server@3.2/admin/github-actions/managing-access-to-actions-from-githubcom/setting-up-the-tool-cache-on-self-hosted-runners-without-internet-access)"
for more information.

View File

@ -7,6 +7,7 @@ import osm, {type} from 'os';
import path from 'path';
import * as main from '../src/main';
import * as im from '../src/installer';
import * as httpm from '@actions/http-client';
import goJsonData from './data/golang-dl.json';
import matchers from '../matchers.json';
@ -46,6 +47,7 @@ describe('setup-go', () => {
let execSpy: jest.SpyInstance;
let getManifestSpy: jest.SpyInstance;
let getAllVersionsSpy: jest.SpyInstance;
let httpmGetJsonSpy: jest.SpyInstance;
beforeAll(async () => {
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', () => {
getManifestSpy = jest.spyOn(tc, 'getManifestFromRepo');
getAllVersionsSpy = jest.spyOn(im, 'getManifest');
// httm
httpmGetJsonSpy = jest.spyOn(httpm.HttpClient.prototype, 'getJson');
// io
whichSpy = jest.spyOn(io, 'which');
existsSpy = jest.spyOn(fs, 'existsSync');
@ -151,6 +156,21 @@ describe('setup-go', () => {
);
});
it('should return manifest from repo', async () => {
const manifest = await im.getManifest(undefined);
expect(manifest).toEqual(goTestManifest);
});
it('should return manifest from raw URL if repo fetch fails', async () => {
getManifestSpy.mockRejectedValue(new Error('Fetch failed'));
httpmGetJsonSpy.mockResolvedValue({
result: goTestManifest
});
const manifest = await im.getManifest(undefined);
expect(httpmGetJsonSpy).toHaveBeenCalled();
expect(manifest).toEqual(goTestManifest);
});
it('can find 1.9 from manifest on linux', async () => {
os.platform = 'linux';
os.arch = 'x64';
@ -790,6 +810,9 @@ describe('setup-go', () => {
getManifestSpy.mockImplementation(() => {
throw new Error('Unable to download manifest');
});
httpmGetJsonSpy.mockRejectedValue(
new Error('Unable to download manifest from raw URL')
);
getAllVersionsSpy.mockImplementationOnce(() => undefined);
dlSpy.mockImplementation(async () => '/some/temp/path');

30
dist/setup/index.js vendored
View File

@ -88259,6 +88259,10 @@ const sys = __importStar(__nccwpck_require__(5632));
const fs_1 = __importDefault(__nccwpck_require__(7147));
const os_1 = __importDefault(__nccwpck_require__(2037));
const utils_1 = __nccwpck_require__(1314);
const MANIFEST_REPO_OWNER = 'actions';
const MANIFEST_REPO_NAME = 'go-versions';
const MANIFEST_REPO_BRANCH = 'main';
const MANIFEST_URL = `https://raw.githubusercontent.com/${MANIFEST_REPO_OWNER}/${MANIFEST_REPO_NAME}/${MANIFEST_REPO_BRANCH}/versions-manifest.json`;
function getGo(versionSpec_1, checkLatest_1, auth_1) {
return __awaiter(this, arguments, void 0, function* (versionSpec, checkLatest, auth, arch = os_1.default.arch()) {
var _a;
@ -88433,10 +88437,34 @@ function extractGoArchive(archivePath) {
exports.extractGoArchive = extractGoArchive;
function getManifest(auth) {
return __awaiter(this, void 0, void 0, function* () {
return tc.getManifestFromRepo('actions', 'go-versions', auth, 'main');
try {
return yield getManifestFromRepo(auth);
}
catch (err) {
core.debug('Fetching the manifest via the API failed.');
if (err instanceof Error) {
core.debug(err.message);
}
}
return yield getManifestFromURL();
});
}
exports.getManifest = getManifest;
function getManifestFromRepo(auth) {
core.debug(`Getting manifest from ${MANIFEST_REPO_OWNER}/${MANIFEST_REPO_NAME}@${MANIFEST_REPO_BRANCH}`);
return tc.getManifestFromRepo(MANIFEST_REPO_OWNER, MANIFEST_REPO_NAME, auth, MANIFEST_REPO_BRANCH);
}
function getManifestFromURL() {
return __awaiter(this, void 0, void 0, function* () {
core.debug('Falling back to fetching the manifest using raw URL.');
const http = new httpm.HttpClient('tool-cache');
const response = yield http.getJson(MANIFEST_URL);
if (!response.result) {
throw new Error(`Unable to get manifest from ${MANIFEST_URL}`);
}
return response.result;
});
}
function getInfoFromManifest(versionSpec_1, stable_1, auth_1) {
return __awaiter(this, arguments, void 0, function* (versionSpec, stable, auth, arch = os_1.default.arch(), manifest) {
let info = null;

7
package-lock.json generated
View File

@ -2504,10 +2504,11 @@
}
},
"node_modules/cross-spawn": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
"integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==",
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
"dev": true,
"license": "MIT",
"dependencies": {
"path-key": "^3.1.0",
"shebang-command": "^2.0.0",

View File

@ -8,6 +8,11 @@ import fs from 'fs';
import os from 'os';
import {StableReleaseAlias} from './utils';
const MANIFEST_REPO_OWNER = 'actions';
const MANIFEST_REPO_NAME = 'go-versions';
const MANIFEST_REPO_BRANCH = 'main';
const MANIFEST_URL = `https://raw.githubusercontent.com/${MANIFEST_REPO_OWNER}/${MANIFEST_REPO_NAME}/${MANIFEST_REPO_BRANCH}/versions-manifest.json`;
type InstallationType = 'dist' | 'manifest';
export interface IGoVersionFile {
@ -274,8 +279,43 @@ export async function extractGoArchive(archivePath: string): Promise<string> {
return extPath;
}
export async function getManifest(auth: string | undefined) {
return tc.getManifestFromRepo('actions', 'go-versions', auth, 'main');
export async function getManifest(
auth: string | undefined
): Promise<tc.IToolRelease[]> {
try {
return await getManifestFromRepo(auth);
} catch (err) {
core.debug('Fetching the manifest via the API failed.');
if (err instanceof Error) {
core.debug(err.message);
}
}
return await getManifestFromURL();
}
function getManifestFromRepo(
auth: string | undefined
): Promise<tc.IToolRelease[]> {
core.debug(
`Getting manifest from ${MANIFEST_REPO_OWNER}/${MANIFEST_REPO_NAME}@${MANIFEST_REPO_BRANCH}`
);
return tc.getManifestFromRepo(
MANIFEST_REPO_OWNER,
MANIFEST_REPO_NAME,
auth,
MANIFEST_REPO_BRANCH
);
}
async function getManifestFromURL(): Promise<tc.IToolRelease[]> {
core.debug('Falling back to fetching the manifest using raw URL.');
const http: httpm.HttpClient = new httpm.HttpClient('tool-cache');
const response = await http.getJson<tc.IToolRelease[]>(MANIFEST_URL);
if (!response.result) {
throw new Error(`Unable to get manifest from ${MANIFEST_URL}`);
}
return response.result;
}
export async function getInfoFromManifest(