From f4d66203b97777cea99d2dfcbf9d588e6a85e6fa Mon Sep 17 00:00:00 2001 From: Emil Lerch Date: Thu, 17 Jul 2025 11:50:21 -0700 Subject: [PATCH] add commentary and (unused) check for token applicability --- README.md | 25 ++++++++++++--- src/providers/GitHub.zig | 69 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 90 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index fa833d9..f7a4b53 100644 --- a/README.md +++ b/README.md @@ -24,10 +24,10 @@ zig build 2. Run the application: ```bash -./zig-out/bin/release-tracker config.json +./zig-out/bin/release-tracker config.json [output filename] ``` -3. The RSS feed will be generated as `releases.xml` +3. The RSS feed will be generated as `releases.xml` by default ## Configuration @@ -36,7 +36,7 @@ Create a `config.json` file with your API tokens: ```json { "github_token": "your_github_token", - "gitlab_token": "your_gitlab_token", + "gitlab_token": "your_gitlab_token", "codeberg_token": "your_codeberg_token", "sourcehut": { "repositories": [ @@ -50,11 +50,28 @@ Create a `config.json` file with your API tokens: ### API Token Setup -- **GitHub**: Create a Personal Access Token with `public_repo` and `user` scopes +- **GitHub**: Create a Personal Access Token with and `user:read` scope. Classic is preferred (see note) - **GitLab**: Create a Personal Access Token with `read_api` scope - **Codeberg**: Create an Access Token in your account settings - **SourceHut**: No token required for public repositories. Specify repositories to track in the configuration. +Note on GitHub PATs. Some GitHub orgs will place additional restrictions on +PATs. If your token does not align with those policies, the GitHub stars API +will **silently discard** repos from that org. The only way to tell something +is off is by detecting the starred repositories count at +https://github.com/?tab=stars is more than what is reported in the +application output. + +There is a `checkForInaccessibleRepos` function in the GitHub provider that +can detect this, but is limited to the `aws` organization, which may or may +not apply in all situations. For this reason, it is included in the code, but +disabled. If needed, uncomment the call and choose a repo from your enterprise +of choice. + +These org policies do not seem to effect classic tokens, so the best approach +with GitHub is to create and use a classic token instead of the new fine-grained +tokens. + ## Testing Run the test suite: diff --git a/src/providers/GitHub.zig b/src/providers/GitHub.zig index c89e338..df214c8 100644 --- a/src/providers/GitHub.zig +++ b/src/providers/GitHub.zig @@ -54,6 +54,10 @@ pub fn fetchReleases(self: *Self, allocator: Allocator) !ArrayList(Release) { const starred_duration: u64 = @intCast(starred_end_time - starred_start_time); std.log.debug("GitHub: Found {} starred repositories in {}ms", .{ starred_repos.items.len, starred_duration }); + + // Check for potentially inaccessible repositories due to enterprise policies + // try checkForInaccessibleRepos(allocator, &client, self.token, starred_repos.items); + std.log.debug("GitHub: Processing {} starred repositories with thread pool...", .{starred_repos.items.len}); const thread_start_time = std.time.milliTimestamp(); @@ -423,6 +427,71 @@ fn compareReleasesByDate(context: void, a: Release, b: Release) bool { return a.published_at > b.published_at; } +fn checkForInaccessibleRepos(allocator: Allocator, client: *http.Client, token: []const u8, starred_repos: [][]const u8) !void { + const is_test = @import("builtin").is_test; + if (is_test) return; // Skip in tests + + // List of repositories that are commonly affected by enterprise policies + const problematic_repos = [_][]const u8{ + "aws/language-server-runtimes", + "aws/aws-cli", + "aws/aws-sdk-js", + "aws/aws-cdk", + }; + + const auth_header = try std.fmt.allocPrint(allocator, "Bearer {s}", .{token}); + defer allocator.free(auth_header); + + for (problematic_repos) |repo| { + // Check if this repo is in our starred list + var found_in_starred = false; + for (starred_repos) |starred_repo| { + if (std.mem.eql(u8, starred_repo, repo)) { + found_in_starred = true; + break; + } + } + + if (!found_in_starred) { + // Check if we can access this repository directly to see if it's a policy issue + const check_url = try std.fmt.allocPrint(allocator, "https://api.github.com/user/starred/{s}", .{repo}); + defer allocator.free(check_url); + + const uri = std.Uri.parse(check_url) catch continue; + + var server_header_buffer: [16 * 1024]u8 = undefined; + var req = client.open(.GET, uri, .{ + .server_header_buffer = &server_header_buffer, + .extra_headers = &.{ + .{ .name = "Authorization", .value = auth_header }, + .{ .name = "Accept", .value = "application/vnd.github.v3+json" }, + .{ .name = "User-Agent", .value = "release-tracker/1.0" }, + }, + }) catch continue; + defer req.deinit(); + + req.send() catch continue; + req.wait() catch continue; + + if (req.response.status == .forbidden) { + // Try to read the error response for more details + const error_body = req.reader().readAllAlloc(allocator, 4096) catch ""; + defer if (error_body.len > 0) allocator.free(error_body); + + const stderr = std.io.getStdErr().writer(); + if (std.mem.indexOf(u8, error_body, "enterprise") != null or + std.mem.indexOf(u8, error_body, "personal access token") != null or + std.mem.indexOf(u8, error_body, "fine-grained") != null) + { + stderr.print("GitHub: Repository '{s}' may be starred but is inaccessible due to enterprise policies: {s}\n", .{ repo, error_body }) catch {}; + } else { + stderr.print("GitHub: Repository '{s}' is not accessible (HTTP 403): {s}\n", .{ repo, error_body }) catch {}; + } + } + } + } +} + test "github provider" { const allocator = std.testing.allocator;