From ee96c4f862dc5f4909c5bae98d3b3998cc34b3b2 Mon Sep 17 00:00:00 2001 From: Yifeng He Date: Thu, 7 May 2026 21:50:00 -0700 Subject: [PATCH 1/3] fix: support GH_TOKEN alongside GITHUB_TOKEN (#67) Match the gh CLI convention: GH_TOKEN takes precedence over GITHUB_TOKEN when both are set, and empty values are treated as unset. Updates the auth helper, user-facing tip/error messages, README, and CHANGELOG, and adds unit tests for precedence, fallback, unset, and empty-string cases. --- CHANGELOG.md | 8 +++++ README.md | 9 +++-- src/registry/github.rs | 81 +++++++++++++++++++++++++++++++++++------- 3 files changed, 83 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1e2d8a7..ddb9e1d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,14 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [Unreleased] + +### Changed + +- GitHub API authentication now reads `GH_TOKEN` in addition to `GITHUB_TOKEN`, + matching the `gh` CLI convention. `GH_TOKEN` takes precedence when both are + set. (#67) + ## [1.0.0] - 2026-03-24 ### Added diff --git a/README.md b/README.md index daf759b..db64556 100644 --- a/README.md +++ b/README.md @@ -140,7 +140,9 @@ skillshub star-list https://github.com/stars/username/lists/skills skillshub star-list https://github.com/stars/username/lists/skills --install ``` -This requires a `GITHUB_TOKEN` (the GraphQL API requires authentication). +This requires `GH_TOKEN` or `GITHUB_TOKEN` (the GraphQL API requires +authentication). `GH_TOKEN` takes precedence when both are set, matching the +`gh` CLI convention. ### Agent Linking @@ -222,9 +224,12 @@ The GitHub API is only used for: - **Gist skills** (`skillshub add https://gist.github.com/...`) - **Star list imports** (`skillshub star-list ...`) -For these operations, set a `GITHUB_TOKEN` to avoid rate limiting: +For these operations, set `GH_TOKEN` (preferred) or `GITHUB_TOKEN` to avoid +rate limiting. When both are set, `GH_TOKEN` wins, matching the `gh` CLI: ```bash +export GH_TOKEN=your_token_here +# or export GITHUB_TOKEN=your_token_here ``` diff --git a/src/registry/github.rs b/src/registry/github.rs index ed16efd..a6bd62c 100644 --- a/src/registry/github.rs +++ b/src/registry/github.rs @@ -113,8 +113,8 @@ fn print_rate_limit_wait(reason: &str, wait_secs: u64, attempt: u32) { " {} Waiting {}s before retrying (attempt {}/{})...", reason, wait_secs, attempt, MAX_RETRIES ); - if std::env::var("GITHUB_TOKEN").is_err() { - eprint!("\n Tip: Set GITHUB_TOKEN for higher rate limits (5000/hour vs 60/hour)."); + if github_token().is_none() { + eprint!("\n Tip: Set GH_TOKEN or GITHUB_TOKEN for higher rate limits (5000/hour vs 60/hour)."); } eprintln!(); } @@ -164,7 +164,7 @@ where if let Some(wait) = rate_info.wait_duration() { if wait.as_secs() > MAX_RATE_LIMIT_WAIT_SECS { anyhow::bail!( - "Rate limit reset is {}s away (>{} max). Set GITHUB_TOKEN for higher limits.", + "Rate limit reset is {}s away (>{} max). Set GH_TOKEN or GITHUB_TOKEN for higher limits.", wait.as_secs(), MAX_RATE_LIMIT_WAIT_SECS ); @@ -267,9 +267,24 @@ fn build_client() -> Result { }) } -/// Add GitHub token authentication to a request if GITHUB_TOKEN is set +/// Read the GitHub auth token from the environment. +/// +/// Checks `GH_TOKEN` first (matching the `gh` CLI convention), then falls +/// back to `GITHUB_TOKEN`. Empty values are treated as unset. +fn github_token() -> Option { + for var in ["GH_TOKEN", "GITHUB_TOKEN"] { + if let Ok(token) = std::env::var(var) { + if !token.is_empty() { + return Some(token); + } + } + } + None +} + +/// Add GitHub token authentication to a request if a token env var is set. fn with_auth(request: RequestBuilder) -> RequestBuilder { - if let Ok(token) = std::env::var("GITHUB_TOKEN") { + if let Some(token) = github_token() { request.bearer_auth(token) } else { request @@ -337,7 +352,7 @@ pub fn get_default_branch(owner: &str, repo: &str) -> Result { "Repository not found on GitHub: {}/{}\n\ Please check that:\n\ - The repository exists and is spelled correctly\n\ - - The repository is public (or GITHUB_TOKEN is set for private repos)", + - The repository is public (or GH_TOKEN/GITHUB_TOKEN is set for private repos)", owner, repo ); @@ -457,7 +472,7 @@ fn is_valid_repo_id(s: &str) -> bool { /// /// Uses the GitHub Tree API to recursively find all SKILL.md files in the repo, /// then fetches each one to extract metadata. -/// Set GITHUB_TOKEN environment variable to avoid rate limiting. +/// Set `GH_TOKEN` or `GITHUB_TOKEN` environment variable to avoid rate limiting. pub fn discover_skills_from_repo(github_url: &GitHubUrl, tap_name: &str) -> Result { let client = build_client()?; @@ -632,7 +647,7 @@ pub fn fetch_gist(gist_id: &str) -> Result { anyhow::bail!( "Gist not found: {}\n\ Please check that the gist ID is correct and the gist is public \ - (or GITHUB_TOKEN is set for secret gists)", + (or GH_TOKEN/GITHUB_TOKEN is set for secret gists)", gist_id ); } @@ -733,11 +748,11 @@ pub fn parse_star_list_url(url: &str) -> Result<(String, String)> { /// /// Returns a list of "owner/repo" identifiers. pub fn fetch_star_list_repos(username: &str, list_name: &str) -> Result> { - let token = std::env::var("GITHUB_TOKEN").with_context(|| { - "GITHUB_TOKEN is required for star list operations.\n\ + let token = github_token().context( + "GH_TOKEN or GITHUB_TOKEN is required for star list operations.\n\ The GraphQL API does not support unauthenticated requests.\n\ - Set GITHUB_TOKEN with a personal access token." - })?; + Set GH_TOKEN (preferred) or GITHUB_TOKEN with a personal access token.", + )?; let client = build_client()?; let gql_url = graphql_url(); @@ -768,7 +783,7 @@ pub fn fetch_star_list_repos(username: &str, list_name: &str) -> Result Date: Thu, 7 May 2026 22:19:37 -0700 Subject: [PATCH 2/3] fix(github): clear GH_TOKEN in star-list tests, sync cli-reference Address review on #73: - Update docs/cli-reference.md to mention both GH_TOKEN and GITHUB_TOKEN (was stale after the precedence change). - Clear GH_TOKEN in test_fetch_star_list_repos_with_mock and test_fetch_star_list_repos_list_not_found so a developer with GH_TOKEN set in their shell does not silently bind their real token to the wiremock server. - Add test_github_token_treats_empty_github_token_as_unset to lock in the symmetric empty-string contract. --- docs/cli-reference.md | 3 ++- src/registry/github.rs | 17 ++++++++++++++++- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/docs/cli-reference.md b/docs/cli-reference.md index 5a88c20..b7cb2fb 100644 --- a/docs/cli-reference.md +++ b/docs/cli-reference.md @@ -23,7 +23,8 @@ skillshub star-list # Add all repos from a star list as skillshub star-list --install # Also install all skills from each tap ``` -Requires `GITHUB_TOKEN` (GraphQL API requires authentication). +Requires `GH_TOKEN` or `GITHUB_TOKEN` (GraphQL API requires authentication). +`GH_TOKEN` is checked first, matching the `gh` CLI. ## Tap Management diff --git a/src/registry/github.rs b/src/registry/github.rs index a6bd62c..b7d5856 100644 --- a/src/registry/github.rs +++ b/src/registry/github.rs @@ -942,6 +942,16 @@ mod tests { assert_eq!(token.as_deref(), Some("github-value")); } + #[test] + #[serial] + fn test_github_token_treats_empty_github_token_as_unset() { + std::env::remove_var("GH_TOKEN"); + std::env::set_var("GITHUB_TOKEN", ""); + let token = github_token(); + std::env::remove_var("GITHUB_TOKEN"); + assert!(token.is_none()); + } + #[test] fn test_parse_skill_md_content() { let content = r#"--- @@ -1899,13 +1909,16 @@ name: minimal-skill // Point GraphQL URL to mock server let gql_url = format!("{}/graphql", server.uri()); std::env::set_var("SKILLSHUB_GITHUB_GRAPHQL_URL", &gql_url); - // Set a dummy token so GITHUB_TOKEN check succeeds + // Clear GH_TOKEN so a dev's real token doesn't get sent to the mock, + // then set a dummy GITHUB_TOKEN so the auth check succeeds. + std::env::remove_var("GH_TOKEN"); std::env::set_var("GITHUB_TOKEN", "test-token"); let result = fetch_star_list_repos("testuser", "skills"); // Clean up env vars std::env::remove_var("SKILLSHUB_GITHUB_GRAPHQL_URL"); + std::env::remove_var("GH_TOKEN"); std::env::remove_var("GITHUB_TOKEN"); assert!(result.is_ok(), "fetch should succeed: {:?}", result.err()); @@ -1946,11 +1959,13 @@ name: minimal-skill let gql_url = format!("{}/graphql", server.uri()); std::env::set_var("SKILLSHUB_GITHUB_GRAPHQL_URL", &gql_url); + std::env::remove_var("GH_TOKEN"); std::env::set_var("GITHUB_TOKEN", "test-token"); let result = fetch_star_list_repos("testuser", "nonexistent"); std::env::remove_var("SKILLSHUB_GITHUB_GRAPHQL_URL"); + std::env::remove_var("GH_TOKEN"); std::env::remove_var("GITHUB_TOKEN"); assert!(result.is_err()); From 6097ad2fe95588c1f3c379c7aea254141770753a Mon Sep 17 00:00:00 2001 From: Yifeng He Date: Thu, 7 May 2026 22:53:34 -0700 Subject: [PATCH 3/3] chore: bump version to 1.0.4 --- CHANGELOG.md | 2 +- Cargo.lock | 2 +- Cargo.toml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ddb9e1d..0234bfa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [Unreleased] +## [1.0.4] - 2026-05-07 ### Changed diff --git a/Cargo.lock b/Cargo.lock index cfe8d34..608eee8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1516,7 +1516,7 @@ checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "skillshub" -version = "1.0.3" +version = "1.0.4" dependencies = [ "anyhow", "chrono", diff --git a/Cargo.toml b/Cargo.toml index b25e452..4249c42 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "skillshub" -version = "1.0.3" +version = "1.0.4" edition = "2021" rust-version = "1.74.0" description = "A package manager for AI coding agent skills - like homebrew for skills"