Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
112 changes: 107 additions & 5 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,11 +40,113 @@ jobs:
- name: Unit tests
run: cargo test --features telemetry-otel

- name: Integration tests
if: env.GITHUB_TOKEN != ''
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: cargo test --features telemetry-otel -- --include-ignored --skip jira_ --skip bitbucket_
integration_github:
name: Integration (GitHub, blocking)
needs: test
runs-on: ubuntu-latest
if: ${{ secrets.GITHUB_TOKEN }}
env:
TOKEN_GITHUB: ${{ secrets.GITHUB_TOKEN }}
steps:
- uses: actions/checkout@v6

- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable

- name: Cache cargo registry
uses: actions/cache@v5
with:
path: |
~/.cargo/registry
~/.cargo/git
target
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
restore-keys: ${{ runner.os }}-cargo-

- name: Integration tests (GitHub)
run: cargo test --features telemetry-otel --test integration -- --include-ignored github_

integration_gitlab:
name: Integration (GitLab, blocking)
needs: test
runs-on: ubuntu-latest
if: ${{ secrets.GITLAB_TOKEN }}
env:
TOKEN_GITLAB: ${{ secrets.GITLAB_TOKEN }}
steps:
- uses: actions/checkout@v6

- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable

- name: Cache cargo registry
uses: actions/cache@v5
with:
path: |
~/.cargo/registry
~/.cargo/git
target
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
restore-keys: ${{ runner.os }}-cargo-

- name: Integration tests (GitLab)
run: cargo test --features telemetry-otel --test integration -- --include-ignored gitlab_

integration_jira:
name: Integration (Jira, advisory)
needs: test
runs-on: ubuntu-latest
continue-on-error: true
if: ${{ secrets.JIRA_TOKEN }}
env:
TOKEN_JIRA: ${{ secrets.JIRA_TOKEN }}
steps:
- uses: actions/checkout@v6

- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable

- name: Cache cargo registry
uses: actions/cache@v5
with:
path: |
~/.cargo/registry
~/.cargo/git
target
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
restore-keys: ${{ runner.os }}-cargo-

- name: Integration tests (Jira)
run: cargo test --features telemetry-otel --test integration -- --include-ignored jira_

integration_bitbucket:
name: Integration (Bitbucket, advisory)
needs: test
runs-on: ubuntu-latest
continue-on-error: true
if: ${{ secrets.BITBUCKET_TOKEN && secrets.BITBUCKET_REPO && secrets.BITBUCKET_PR_ID }}
env:
TOKEN_BITBUCKET: ${{ secrets.BITBUCKET_TOKEN }}
BITBUCKET_REPO: ${{ secrets.BITBUCKET_REPO }}
BITBUCKET_PR_ID: ${{ secrets.BITBUCKET_PR_ID }}
steps:
- uses: actions/checkout@v6

- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable

- name: Cache cargo registry
uses: actions/cache@v5
with:
path: |
~/.cargo/registry
~/.cargo/git
target
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
restore-keys: ${{ runner.os }}-cargo-

- name: Integration tests (Bitbucket)
run: cargo test --features telemetry-otel --test integration -- --include-ignored bitbucket_

docs:
name: Docs
Expand Down
8 changes: 7 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,13 @@ cargo test
Ignored integration tests (live APIs):

```bash
cargo test -- --include-ignored --skip jira_
# blocking provider slices
cargo test --test integration -- --include-ignored github_
cargo test --test integration -- --include-ignored gitlab_

# advisory provider slices
cargo test --test integration -- --include-ignored jira_
cargo test --test integration -- --include-ignored bitbucket_
```

## Local Quality Gates
Expand Down
2 changes: 1 addition & 1 deletion docs/providers/bitbucket-cloud.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
- `platform = "bitbucket"`
- `deployment = "cloud"`
- `repo` format: `workspace/repo_slug`
- Token env var: `BITBUCKET_TOKEN`
- Token env var: `TOKEN_BITBUCKET` (legacy fallback: `BITBUCKET_TOKEN`)

## Examples

Expand Down
2 changes: 1 addition & 1 deletion docs/providers/bitbucket-datacenter.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
- `deployment = "selfhosted"`
- `url` must be provided.
- `repo` format: `project/repo_slug`
- Token env var: `BITBUCKET_TOKEN`
- Token env var: `TOKEN_BITBUCKET` (legacy fallback: `BITBUCKET_TOKEN`)

## Examples

Expand Down
2 changes: 1 addition & 1 deletion docs/providers/github.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
## Required Values

- `repo` format: `owner/repo`
- Token env var: `GITHUB_TOKEN`
- Token env var: `TOKEN_GITHUB` (legacy fallback: `GITHUB_TOKEN`)

## Examples

Expand Down
2 changes: 1 addition & 1 deletion docs/providers/gitlab.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

- `repo` format: `group/project`
- Nested groups are supported (for example `group/subgroup/project`).
- Token env var: `GITLAB_TOKEN`
- Token env var: `TOKEN_GITLAB` (legacy fallback: `GITLAB_TOKEN`)

## Examples

Expand Down
2 changes: 1 addition & 1 deletion docs/providers/jira.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
## Required Values

- `repo` is Jira project scope for search (for example `CPQ`).
- Token env var: `JIRA_TOKEN`
- Token env var: `TOKEN_JIRA` (legacy fallback: `JIRA_TOKEN`)
- Optional account email env var: `JIRA_ACCOUNT_EMAIL`

## Examples
Expand Down
8 changes: 4 additions & 4 deletions docs/reference/config-keys.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,11 @@ Use `instances.<alias>.<field>`:

## Environment Variables

- `GITHUB_TOKEN`
- `GITLAB_TOKEN`
- `JIRA_TOKEN`
- `TOKEN_GITHUB` (legacy fallback: `GITHUB_TOKEN`)
- `TOKEN_GITLAB` (legacy fallback: `GITLAB_TOKEN`)
- `TOKEN_JIRA` (legacy fallback: `JIRA_TOKEN`)
- `JIRA_ACCOUNT_EMAIL`
- `BITBUCKET_TOKEN`
- `TOKEN_BITBUCKET` (legacy fallback: `BITBUCKET_TOKEN`)

## Selection Behavior

Expand Down
4 changes: 2 additions & 2 deletions src/cmd/config/store.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use std::path::PathBuf;

use crate::cmd::config::key::{ConfigKey, InstanceField};
use crate::config::{
DotfileConfig, InstanceConfig, TelemetrySection, account_email_env_var, token_env_var,
DotfileConfig, InstanceConfig, TelemetrySection, account_email_env_var, token_from_env,
};

#[derive(Debug, Clone, Copy)]
Expand Down Expand Up @@ -363,7 +363,7 @@ fn merge_telemetry(
fn apply_env_overrides(cfg: &mut DotfileConfig) {
for instance in cfg.instances.values_mut() {
if let Some(platform) = instance.platform.as_deref()
&& let Ok(token) = std::env::var(token_env_var(platform))
&& let Some(token) = token_from_env(platform)
{
instance.token = Some(token);
}
Expand Down
46 changes: 40 additions & 6 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -301,7 +301,7 @@ fn resolve_instance_mode(merged: &DotfileConfig, opts: ResolveOptions<'_>) -> Re
let token = opts
.token
.map(std::borrow::ToOwned::to_owned)
.or_else(|| std::env::var(token_env_var(&platform)).ok())
.or_else(|| token_from_env(&platform))
.or_else(|| instance.token.clone());
let account_email = opts
.account_email
Expand Down Expand Up @@ -354,7 +354,7 @@ fn resolve_cli_only_mode(opts: ResolveOptions<'_>) -> Result<Config> {
let token = opts
.token
.map(std::borrow::ToOwned::to_owned)
.or_else(|| std::env::var(token_env_var(&platform)).ok());
.or_else(|| token_from_env(&platform));
let account_email = opts
.account_email
.map(std::borrow::ToOwned::to_owned)
Expand Down Expand Up @@ -523,14 +523,32 @@ fn validate_instance_keys(table: &toml::value::Table) -> Result<()> {
#[must_use]
pub fn token_env_var(platform: &str) -> &'static str {
match platform {
"github" => "GITHUB_TOKEN",
"gitlab" => "GITLAB_TOKEN",
"jira" => "JIRA_TOKEN",
"bitbucket" => "BITBUCKET_TOKEN",
"github" => "TOKEN_GITHUB",
"gitlab" => "TOKEN_GITLAB",
"jira" => "TOKEN_JIRA",
"bitbucket" => "TOKEN_BITBUCKET",
_ => "TOKEN",
}
}

#[must_use]
pub fn legacy_token_env_var(platform: &str) -> Option<&'static str> {
match platform {
"github" => Some("GITHUB_TOKEN"),
"gitlab" => Some("GITLAB_TOKEN"),
"jira" => Some("JIRA_TOKEN"),
"bitbucket" => Some("BITBUCKET_TOKEN"),
_ => None,
}
}

#[must_use]
pub fn token_from_env(platform: &str) -> Option<String> {
std::env::var(token_env_var(platform))
.ok()
.or_else(|| legacy_token_env_var(platform).and_then(|key| std::env::var(key).ok()))
}

#[must_use]
pub fn account_email_env_var(platform: &str) -> Option<&'static str> {
match platform {
Expand Down Expand Up @@ -614,6 +632,22 @@ mod tests {
assert!(err.contains("instances.work.foo"));
}

#[test]
fn token_env_var_prefers_canonical_names() {
assert_eq!(token_env_var("github"), "TOKEN_GITHUB");
assert_eq!(token_env_var("gitlab"), "TOKEN_GITLAB");
assert_eq!(token_env_var("jira"), "TOKEN_JIRA");
assert_eq!(token_env_var("bitbucket"), "TOKEN_BITBUCKET");
}

#[test]
fn legacy_token_env_var_remains_available_for_compat() {
assert_eq!(legacy_token_env_var("github"), Some("GITHUB_TOKEN"));
assert_eq!(legacy_token_env_var("gitlab"), Some("GITLAB_TOKEN"));
assert_eq!(legacy_token_env_var("jira"), Some("JIRA_TOKEN"));
assert_eq!(legacy_token_env_var("bitbucket"), Some("BITBUCKET_TOKEN"));
}

#[test]
fn auto_selects_single_instance() {
let cfg = resolve_with_opts(
Expand Down
2 changes: 1 addition & 1 deletion src/source/bitbucket/shared/http.rs
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,7 @@ fn auth_hint(token: Option<&str>) -> &'static str {
if token.is_some() {
"Check Bitbucket token credentials and scopes."
} else {
"No Bitbucket token detected. Set --token, BITBUCKET_TOKEN, or [instances.<alias>].token."
"No Bitbucket token detected. Set --token, TOKEN_BITBUCKET (or BITBUCKET_TOKEN), or [instances.<alias>].token."
}
}

Expand Down
2 changes: 1 addition & 1 deletion src/source/github/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,7 @@ impl GitHubSource {
if token.is_some() {
"GitHub token seems invalid or lacks required scope."
} else {
"No GitHub token detected. Set --token, GITHUB_TOKEN, or [instances.<alias>].token."
"No GitHub token detected. Set --token, TOKEN_GITHUB (or GITHUB_TOKEN), or [instances.<alias>].token."
}
} else {
""
Expand Down
4 changes: 2 additions & 2 deletions src/source/gitlab/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,7 @@ impl GitLabSource {
let hint = if token.is_some() {
"GitLab token seems invalid or lacks required scope (use read_api)."
} else {
"No GitLab token detected. Set --token, GITLAB_TOKEN, or [instances.<alias>].token."
"No GitLab token detected. Set --token, TOKEN_GITLAB (or GITLAB_TOKEN), or [instances.<alias>].token."
};
return Err(AppError::auth(format!(
"GitLab API auth error {}: {hint} {}",
Expand Down Expand Up @@ -282,7 +282,7 @@ impl GitLabSource {
let hint = if token.is_some() {
"GitLab token seems invalid or lacks required scope (use read_api)."
} else {
"No GitLab token detected. Set --token, GITLAB_TOKEN, or [instances.<alias>].token."
"No GitLab token detected. Set --token, TOKEN_GITLAB (or GITLAB_TOKEN), or [instances.<alias>].token."
};
return Err(AppError::auth(format!(
"GitLab API auth error {}: {hint} {}",
Expand Down
4 changes: 2 additions & 2 deletions src/source/jira/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -246,7 +246,7 @@ impl JiraSource {
" Check Jira permissions for this issue."
}
} else {
" Jira often returns 404 for unauthorized issues. Set --token, JIRA_TOKEN, or [instances.<alias>].token."
" Jira often returns 404 for unauthorized issues. Set --token, TOKEN_JIRA (or JIRA_TOKEN), or [instances.<alias>].token."
};
return Err(AppError::not_found(format!(
"Jira issue '{}' was not found or is not accessible.{} Response: {}",
Expand Down Expand Up @@ -587,7 +587,7 @@ impl JiraSource {
" Jira auth failed. If this is an Atlassian API token, also set account email (--account-email, JIRA_ACCOUNT_EMAIL, or [instances.<alias>].account_email), or pass --token as email:api_token."
}
} else {
" No Jira token detected. Set --token, JIRA_TOKEN, or [instances.<alias>].token."
" No Jira token detected. Set --token, TOKEN_JIRA (or JIRA_TOKEN), or [instances.<alias>].token."
}
} else {
""
Expand Down
Loading