Skip to content

Commit 851857a

Browse files
committed
fix: ignore prereleases for latest release
1 parent a929897 commit 851857a

9 files changed

Lines changed: 74 additions & 17 deletions

File tree

GraphQL/RepoSnapshot.graphql

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
query RepoSnapshot($owner: String!, $name: String!) {
22
repository(owner: $owner, name: $name) {
33
name
4-
releases(last: 1, orderBy: { field: CREATED_AT, direction: DESC }) {
4+
releases(first: 20, orderBy: { field: CREATED_AT, direction: DESC }) {
55
nodes {
66
name
77
tagName
88
publishedAt
9+
createdAt
910
url
11+
isDraft
12+
isPrerelease
1013
}
1114
}
1215
issues(states: OPEN) { totalCount }

Scripts/ghql.ts

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ const program = new Command()
9696
program
9797
.command('repo')
9898
.argument('<owner/repo>', 'Repository in owner/name form')
99-
.description('Run RepoSnapshot query to fetch issues, PRs, and latest release')
99+
.description('Run RepoSnapshot query to fetch issues, PRs, and latest stable release')
100100
.action(async (slug: string, opts: Record<string, unknown>, cmd: Command) => {
101101
const spinner = ora('Fetching repo snapshot').start();
102102
try {
@@ -113,7 +113,17 @@ program
113113
const { data, rateLimitReset } = await fetchGraphQL<{
114114
repository: {
115115
name: string;
116-
releases: { nodes?: { name?: string | null; tagName: string; publishedAt: string; url: string }[] };
116+
releases: {
117+
nodes?: {
118+
name?: string | null;
119+
tagName: string;
120+
publishedAt?: string | null;
121+
createdAt?: string | null;
122+
url: string;
123+
isDraft: boolean;
124+
isPrerelease: boolean;
125+
}[];
126+
};
117127
issues: { totalCount: number };
118128
pullRequests: { totalCount: number };
119129
} | null;
@@ -131,17 +141,23 @@ program
131141

132142
const repo = data.repository;
133143
if (!repo) throw new Error('Repository not found');
134-
const release = repo.releases.nodes?.[0];
144+
const release = repo.releases.nodes
145+
?.filter((node) => !node.isDraft && !node.isPrerelease)
146+
.sort((lhs, rhs) => {
147+
const lhsDate = lhs.publishedAt ?? lhs.createdAt ?? '0001-01-01T00:00:00Z';
148+
const rhsDate = rhs.publishedAt ?? rhs.createdAt ?? '0001-01-01T00:00:00Z';
149+
return rhsDate.localeCompare(lhsDate);
150+
})[0];
135151
const releaseLine = release
136-
? `${release.name ?? release.tagName} (${new Date(release.publishedAt).toLocaleDateString()})`
152+
? `${release.name ?? release.tagName} (${new Date(release.publishedAt ?? release.createdAt ?? 0).toLocaleDateString()})`
137153
: 'none';
138154

139155
console.log(
140156
[
141157
chalk.bold(`${owner}/${name}`),
142158
`Issues: ${repo.issues.totalCount}`,
143159
`PRs: ${repo.pullRequests.totalCount}`,
144-
`Latest release: ${releaseLine}`,
160+
`Latest stable release: ${releaseLine}`,
145161
].join('\n')
146162
);
147163
const rl = formatRateLimit(rateLimitReset);

Sources/RepoBarCore/API/GitHubClient.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -206,7 +206,7 @@ public actor GitHubClient {
206206
return Array(commits.prefix(max(limit, 0)))
207207
}
208208

209-
/// Latest release (including prereleases). Returns `nil` if the repo has no releases.
209+
/// Latest release, excluding drafts and prereleases. Returns `nil` if the repo has no releases.
210210
public func latestRelease(owner: String, name: String) async throws -> Release? {
211211
do {
212212
return try await self.restAPI.latestReleaseAny(owner: owner, name: name)

Sources/RepoBarCore/API/GitHubReleasePicker.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import Foundation
22

33
enum GitHubReleasePicker {
4-
/// Pick the newest non-draft release, preferring publishedAt over createdAt.
4+
/// Pick the newest stable release, preferring publishedAt over createdAt.
55
static func latestRelease(from responses: [ReleaseResponse]) -> Release? {
66
let candidates = responses
7-
.filter { $0.draft != true }
7+
.filter { $0.draft != true && $0.prerelease != true }
88
.sorted {
99
let lhsDate = $0.publishedAt ?? $0.createdAt ?? .distantPast
1010
let rhsDate = $1.publishedAt ?? $1.createdAt ?? .distantPast

Sources/RepoBarCore/API/GitHubRestAPI.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -630,7 +630,7 @@ struct GitHubRestAPI {
630630
)
631631
}
632632

633-
/// Most recent release (including prereleases) ordered by creation date; skips drafts.
633+
/// Most recent stable release ordered by creation date; skips drafts and prereleases.
634634
/// Returns `nil` if the repository has no releases.
635635
func latestReleaseAny(owner: String, name: String) async throws -> Release? {
636636
let token = try await tokenProvider()

Sources/RepoBarCore/API/GraphQLClient.swift

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ actor GraphQLClient {
4747
repository(owner: $owner, name: $name) {
4848
name
4949
releases(first: 20, orderBy: {field: CREATED_AT, direction: DESC}) {
50-
nodes { name tagName publishedAt createdAt url isDraft }
50+
nodes { name tagName publishedAt createdAt url isDraft isPrerelease }
5151
}
5252
issues(states: OPEN) { totalCount }
5353
pullRequests(states: OPEN) { totalCount }
@@ -122,7 +122,7 @@ actor GraphQLClient {
122122

123123
private nonisolated static func latestRelease(from nodes: [ReleaseNode]) -> Release? {
124124
let candidates = nodes
125-
.filter { !$0.isDraft }
125+
.filter { !$0.isDraft && !$0.isPrerelease }
126126
.sorted {
127127
let lhsDate = $0.publishedAt ?? $0.createdAt ?? .distantPast
128128
let rhsDate = $1.publishedAt ?? $1.createdAt ?? .distantPast
@@ -323,6 +323,7 @@ private struct ReleaseNode: Decodable {
323323
let createdAt: Date?
324324
let url: URL
325325
let isDraft: Bool
326+
let isPrerelease: Bool
326327
}
327328

328329
private struct CountContainer: Decodable {

Tests/RepoBarTests/GraphQLRepoSummaryParsingTests.swift

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,31 +17,44 @@ struct GraphQLRepoSummaryParsingTests {
1717
"publishedAt": null,
1818
"createdAt": "2026-01-03T00:00:00Z",
1919
"url": "https://example.com/v3",
20-
"isDraft": true
20+
"isDraft": true,
21+
"isPrerelease": false
22+
},
23+
{
24+
"name": "Prerelease",
25+
"tagName": "v2.1.0-beta",
26+
"publishedAt": "2026-01-05T00:00:00Z",
27+
"createdAt": "2026-01-05T00:00:00Z",
28+
"url": "https://example.com/v2-beta",
29+
"isDraft": false,
30+
"isPrerelease": true
2131
},
2232
{
2333
"name": "Created Later",
2434
"tagName": "v2.0.0-created",
2535
"publishedAt": "2026-01-02T00:00:00Z",
2636
"createdAt": "2026-01-02T00:00:00Z",
2737
"url": "https://example.com/v2-created",
28-
"isDraft": false
38+
"isDraft": false,
39+
"isPrerelease": false
2940
},
3041
{
3142
"name": "Published Later",
3243
"tagName": "v2.0.0",
3344
"publishedAt": "2026-01-04T00:00:00Z",
3445
"createdAt": "2026-01-01T00:00:00Z",
3546
"url": "https://example.com/v2",
36-
"isDraft": false
47+
"isDraft": false,
48+
"isPrerelease": false
3749
},
3850
{
3951
"name": "Old",
4052
"tagName": "v1.0.0",
4153
"publishedAt": "2026-01-01T00:00:00Z",
4254
"createdAt": "2026-01-01T00:00:00Z",
4355
"url": "https://example.com/v1",
44-
"isDraft": false
56+
"isDraft": false,
57+
"isPrerelease": false
4558
}
4659
]
4760
},

Tests/RepoBarTests/ReleaseSelectionTests.swift

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,30 @@ struct ReleaseSelectionTests {
5252
#expect(picked?.publishedAt == Date(timeIntervalSince1970: 1_700_150_000))
5353
}
5454

55+
@Test
56+
func `skips prereleases`() throws {
57+
let releases = try [
58+
releaseResponse(
59+
name: "v1.1.0-beta",
60+
tagName: "v1.1.0-beta",
61+
publishedAt: Date(timeIntervalSince1970: 1_700_300_000),
62+
createdAt: Date(timeIntervalSince1970: 1_700_250_000),
63+
prerelease: true,
64+
url: "https://example.com/1.1.0-beta"
65+
),
66+
releaseResponse(
67+
name: "v1.0.0",
68+
tagName: "v1.0.0",
69+
publishedAt: Date(timeIntervalSince1970: 1_700_100_000),
70+
createdAt: Date(timeIntervalSince1970: 1_700_050_000),
71+
url: "https://example.com/1.0.0"
72+
)
73+
]
74+
75+
let picked = GitHubClient.latestRelease(from: releases)
76+
#expect(picked?.tag == "v1.0.0")
77+
}
78+
5579
@Test
5680
func `returns nil when no releases`() {
5781
let picked = GitHubClient.latestRelease(from: [])

docs/ghql.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ Developer-only CLI to hit GitHub GraphQL quickly without touching the app.
1414
- Optional overrides: `GITHUB_GRAPHQL` for custom endpoints (e.g. GHE).
1515

1616
## Commands
17-
- `pnpm ghql repo <owner/repo>` – runs `GraphQL/RepoSnapshot.graphql`, prints issues/PRs/release.
17+
- `pnpm ghql repo <owner/repo>` – runs `GraphQL/RepoSnapshot.graphql`, prints issues/PRs/latest stable release.
1818
- `pnpm ghql contrib <login>` – flattens contribution calendar to day counts (heatmap helper).
1919
- `pnpm ghql run <file.graphql> --vars '{...}'` – run any query file with JSON vars.
2020

0 commit comments

Comments
 (0)