Skip to content

Commit f8c4ae7

Browse files
committed
feat: support pagination of tags
with short circuiting. Closes #3
1 parent 6da919e commit f8c4ae7

8 files changed

Lines changed: 110 additions & 182 deletions

File tree

.github/workflows/ci.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,8 @@ jobs:
3838
if: steps.cache-node-modules.outputs.cache-hit != 'true'
3939
run: npm ci --ignore-scripts
4040

41-
- name: Compile TypeScript
42-
run: npm run build
41+
- name: Type check
42+
run: ./node_modules/.bin/tsc --noEmit
4343

4444
- name: Test
4545
run: npm test

src/fetch.ts

Lines changed: 83 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import type { Octokit } from "./octokit";
2+
import { isEqual, none, Option, some } from "./option";
23

3-
import type {
4+
import {
5+
isExactSemanticVersion,
46
ExactSemanticVersion,
57
RepositorySlug,
68
SemanticVersion,
@@ -19,18 +21,87 @@ type Tag = {
1921

2022
export type TagsResponse = ReadonlyArray<Tag>;
2123

22-
// NOTE: there's a foot-gun here around lack of pagination
23-
export async function fetchRepoTags(
24+
function containsExactTag(
25+
tags: readonly SemanticVersion[] | undefined
26+
): ExactSemanticVersion | undefined {
27+
if (tags === undefined) {
28+
return undefined;
29+
}
30+
return tags.find(isExactSemanticVersion);
31+
}
32+
33+
// Find the exact semantic version tag that this tag maps to.
34+
//
35+
// We need an exact tag because that's the only accepted input
36+
// to GitHub's getReleaseByTag endpoint.
37+
export async function findExactSemanticVersionTag(
2438
octokit: Octokit,
25-
slug: RepositorySlug
26-
): Promise<TagsResponse> {
27-
const response = await octokit.rest.repos.listTags({
28-
owner: slug.owner,
29-
repo: slug.repository,
30-
per_page: 100,
31-
});
32-
// NOTE: we are not parsing here, so this is an unlawful type cast
33-
return response.data as unknown as TagsResponse;
39+
slug: RepositorySlug,
40+
givenTag: SemanticVersion
41+
): Promise<ExactSemanticVersion> {
42+
if (isExactSemanticVersion(givenTag)) {
43+
return givenTag;
44+
}
45+
46+
const versionsBySha: Record<Sha, SemanticVersion[]> = {};
47+
let givenTagSha: Option<Sha> = none();
48+
49+
// Conditions to stop looping are -- we know both the:
50+
//
51+
// - sha that the given tag points to
52+
// - exact version tag matching that sha
53+
//
54+
// These can be found in either order.
55+
56+
for await (const response of octokit.paginate.iterator(
57+
octokit.rest.repos.listTags,
58+
{
59+
owner: slug.owner,
60+
repo: slug.repository,
61+
per_page: 100,
62+
}
63+
)) {
64+
// NOTE: we are not parsing here, so this is an unlawful type cast
65+
for (const tag of response.data as unknown as TagsResponse) {
66+
const sha = tag.commit.sha;
67+
const version = tag.name;
68+
69+
// If we found the sha the given tag points to
70+
if (version === givenTag) {
71+
givenTagSha = some(sha);
72+
// check if we already knew the exact version tag matching that sha
73+
const maybeExactTag = containsExactTag(versionsBySha[sha]);
74+
if (maybeExactTag !== undefined) {
75+
return maybeExactTag;
76+
}
77+
}
78+
79+
// If we're not looking at the given tag, and we're not looking
80+
// at an exact version, this data is of no use to us.
81+
if (!isExactSemanticVersion(version)) {
82+
continue;
83+
}
84+
85+
// It is possible that we know the sha for the given tag,
86+
// we're just looking for exact version tag matching that sha.
87+
if (isEqual(givenTagSha, sha)) {
88+
return version;
89+
}
90+
91+
// Otherwise, record this map of sha -> exact version tag
92+
// so we can find it when we know the sha of the given tag.
93+
const associatedVersions = versionsBySha[sha];
94+
if (associatedVersions === undefined) {
95+
versionsBySha[sha] = [version];
96+
} else {
97+
associatedVersions.push(version);
98+
}
99+
}
100+
}
101+
102+
throw new Error(
103+
`Expected to find an exact semantic version tag matching ${givenTag}`
104+
);
34105
}
35106

36107
type ReleaseAssetMetadata = {

src/github.ts

Lines changed: 0 additions & 77 deletions
This file was deleted.

src/index.ts

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import * as core from "@actions/core";
66
import * as tc from "@actions/tool-cache";
77

88
import { getErrors, unwrap } from "./either";
9-
import { resolveReleaseTag } from "./github";
109
import { getOctokit, Octokit } from "./octokit";
1110
import {
1211
parseCacheDirectory,
@@ -15,7 +14,10 @@ import {
1514
TargetBinary,
1615
} from "./parse";
1716
import { getTargetTriple } from "./platform";
18-
import { fetchReleaseAssetMetadataFromTag, fetchRepoTags } from "./fetch";
17+
import {
18+
fetchReleaseAssetMetadataFromTag,
19+
findExactSemanticVersionTag,
20+
} from "./fetch";
1921
import type { ExactSemanticVersion, RepositorySlug } from "./types";
2022

2123
function getDestinationDirectory(
@@ -40,11 +42,13 @@ async function installGitHubReleaseBinary(
4042
cacheDirectory: string,
4143
token: string
4244
): Promise<void> {
43-
// NOTE: there's a foot-gun here cause by lack of pagination
44-
const repoTags = await fetchRepoTags(octokit, targetBinary.slug);
45-
const releaseTag = resolveReleaseTag(repoTags, targetBinary.tag);
4645
const targetTriple = getTargetTriple(arch(), platform());
4746

47+
const releaseTag = await findExactSemanticVersionTag(
48+
octokit,
49+
targetBinary.slug,
50+
targetBinary.tag
51+
);
4852
const releaseAsset = await fetchReleaseAssetMetadataFromTag(
4953
octokit,
5054
targetBinary.slug,

src/option.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
export type Option<A> = { tag: "none" } | { tag: "some"; value: A };
2+
3+
export function none<A>(): Option<A> {
4+
return { tag: "none" };
5+
}
6+
7+
export function some<A>(value: A): Option<A> {
8+
return { tag: "some", value };
9+
}
10+
11+
export function isEqual<A>(value: Option<A>, to: A): boolean {
12+
if (value.tag === "none") {
13+
return false;
14+
}
15+
return value.value === to;
16+
}

test/fixtures/README.md

Lines changed: 0 additions & 7 deletions
This file was deleted.

test/fixtures/tags.json

Lines changed: 0 additions & 52 deletions
This file was deleted.

test/test-select-main-release-tag.ts

Lines changed: 0 additions & 27 deletions
This file was deleted.

0 commit comments

Comments
 (0)