Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
4 changes: 2 additions & 2 deletions docs/site/docs-en/reference/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -393,9 +393,9 @@ ferrflow doctor [OPTIONS]
| Flag | Description |
| ---------------- | --------------------------------------------------------------------- |
| `--format <FMT>` | `human` (default) or `json` |
| `--online` | Also probe the forge API (GitHub rate limit / auth); requires a token |
| `--online` | Also probe the forge API (GitHub rate limit / auth, self-hosted forge detection); requires a token |

The report groups checks into five sections — **Repo** (git repository, commit history, clean working tree, git identity, remote, tags), **Config** (which config file wins, whether it parses, plus the full `ferrflow validate` check suite), **Versioning** (strategy, each package's on-disk version, and whether its lockfile agrees with it), **Forge** (detected forge and whether an auth token is present in the environment), and **CI** (workflow files, and whether a workflow pins the `FerrLabs/FerrFlow` action). Every check reports green, a warning, or an error.
The report groups checks into five sections — **Repo** (git repository, commit history, clean working tree, git identity, remote, tags), **Config** (which config file wins, whether it parses, plus the full `ferrflow validate` check suite), **Versioning** (strategy, each package's on-disk version, and whether its lockfile agrees with it), **Forge** (detected forge, and whether the token that forge actually uses is in the environment; with `--online` the forge is also probed for a self-hosted host), and **CI** (workflow files, and whether a workflow pins the `FerrLabs/FerrFlow` action). Every check reports green, a warning, or an error.

Two of the **Versioning** checks are about lockfiles, and both matter for anything that builds with `--locked`. A lockfile recording a different version than the manifest beside it is reported outright, since that combination makes `cargo build --locked` refuse to run. A lockfile that is not covered by [`updateLockfiles`](/docs/configuration/config-file/) is flagged before it drifts, because nothing will keep it in step on the next release. Version comparison applies to `Cargo.lock`: pnpm, yarn, poetry, uv, bundler and mix lock dependencies without restating the version of the package that owns them, so there is nothing to compare, and those get the `updateLockfiles` check only.

Expand Down
4 changes: 2 additions & 2 deletions docs/site/docs-fr/reference/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -317,9 +317,9 @@ ferrflow doctor [OPTIONS]
| Option | Description |
| ---------------- | ------------------------------------------------------------------------------ |
| `--format <FMT>` | `human` (défaut) ou `json` |
| `--online` | Sonder aussi l'API de la forge (rate limit / auth GitHub) ; nécessite un token |
| `--online` | Sonder aussi l'API de la forge (rate limit / auth GitHub, détection d'une forge auto-hébergée) ; nécessite un token |

Le rapport groupe les vérifications en cinq sections : **Repo** (dépôt git, historique de commits, arbre de travail propre, identité git, remote, tags), **Config** (quel fichier de config l'emporte, s'il parse, plus toute la suite de vérifications de `ferrflow validate`), **Versioning** (stratégie, version sur disque de chaque package, et si son lockfile est d'accord avec elle), **Forge** (forge détectée et présence d'un token d'auth dans l'environnement) et **CI** (fichiers de workflow, et si un workflow épingle l'action `FerrLabs/FerrFlow`). Chaque vérification est verte, un avertissement, ou une erreur.
Le rapport groupe les vérifications en cinq sections : **Repo** (dépôt git, historique de commits, arbre de travail propre, identité git, remote, tags), **Config** (quel fichier de config l'emporte, s'il parse, plus toute la suite de vérifications de `ferrflow validate`), **Versioning** (stratégie, version sur disque de chaque package, et si son lockfile est d'accord avec elle), **Forge** (forge détectée, et présence dans l'environnement du token que cette forge utilise réellement ; avec `--online`, la forge d'un hôte auto-hébergé est aussi sondée) et **CI** (fichiers de workflow, et si un workflow épingle l'action `FerrLabs/FerrFlow`). Chaque vérification est verte, un avertissement, ou une erreur.

Deux des vérifications **Versioning** portent sur les lockfiles, et les deux comptent pour tout ce qui compile avec `--locked`. Un lockfile qui enregistre une version différente du manifeste voisin est signalé directement, cette combinaison faisant échouer `cargo build --locked`. Un lockfile que [`updateLockfiles`](/fr/docs/configuration/config-file/) ne couvre pas est signalé avant qu'il ne dérive, puisque rien ne le maintiendra à jour à la prochaine release. La comparaison de version s'applique à `Cargo.lock` : pnpm, yarn, poetry, uv, bundler et mix verrouillent les dépendances sans redonner la version du package qui les possède, il n'y a donc rien à comparer, et ceux-là n'ont que la vérification `updateLockfiles`.

Expand Down
68 changes: 45 additions & 23 deletions src/doctor/checks.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use std::path::Path;
use std::process::Command;

use crate::config::{Config, ForgeKind};
use crate::config::{Config, ForgeKind, GENERIC_TOKEN_ENV_VAR};
use crate::formats::lockfiles::{LockfileState, inspect_for_manifest};
use crate::formats::read_version;
use crate::git::{
Expand Down Expand Up @@ -259,7 +259,13 @@ pub(super) fn forge_section(
let remote = remote_name(config);
let url = repo.and_then(|r| get_remote_url(r, remote));
let configured = config.map(|c| c.workspace.forge).unwrap_or(ForgeKind::Auto);
let detected = url.as_deref().and_then(crate::forge::detect_forge_from_url);
let detected = url.as_deref().and_then(|url| {
if online {
crate::forge::detect_forge_with_probe(url)
} else {
crate::forge::detect_forge_from_url(url)
}
});
let forge = match configured {
ForgeKind::Auto => detected,
explicit => Some(explicit),
Comment thread
ferrfleet[bot] marked this conversation as resolved.
Resolved
Expand All @@ -279,21 +285,16 @@ pub(super) fn forge_section(
}
None => checks.push(Check::warn(
"forge",
Some("could not detect the forge from the remote URL. Set workspace.forge".into()),
Some(
"could not detect the forge from the remote URL. Set workspace.forge, or run with --online to probe the host"
.into(),
),
)),
}

let (var, present) = token_env(forge);
if present {
checks.push(Check::ok("auth token", Some(format!("{var} is set"))));
} else {
checks.push(Check::warn(
"auth token",
Some(format!(
"no token in env ({var} or FERRFLOW_TOKEN). API pushes and releases will fail"
)),
));
}
checks.push(token_check(forge, |var| {
std::env::var(var).is_ok_and(|value| !value.is_empty())
}));

if online {
checks.push(online_check(forge));
Expand Down Expand Up @@ -404,17 +405,38 @@ fn github_rate_limit() -> anyhow::Result<(u64, u64)> {
Ok((remaining, limit))
}

fn token_env(forge: Option<ForgeKind>) -> (&'static str, bool) {
let is_set = |var: &str| std::env::var(var).is_ok_and(|v| !v.is_empty());
if is_set("FERRFLOW_TOKEN") {
return ("FERRFLOW_TOKEN", true);
pub(super) fn token_check(forge: Option<ForgeKind>, is_set: impl Fn(&str) -> bool) -> Check {
if is_set(GENERIC_TOKEN_ENV_VAR) {
return Check::ok(
"auth token",
Some(format!("{GENERIC_TOKEN_ENV_VAR} is set")),
);
}
match forge {
Some(ForgeKind::Gitlab) => ("GITLAB_TOKEN", is_set("GITLAB_TOKEN")),
Some(ForgeKind::Gitea) => ("GITEA_TOKEN", is_set("GITEA_TOKEN")),
Some(ForgeKind::Bitbucket) => ("BITBUCKET_TOKEN", is_set("BITBUCKET_TOKEN")),
_ => ("GITHUB_TOKEN", is_set("GITHUB_TOKEN")),
let Some(kind) = forge else {
let detail = if is_set("GITHUB_TOKEN") {
"GITHUB_TOKEN is set, but it is only sent to a remote recognised as GitHub and this one is not. \
Set workspace.forge, or pass the token as FERRFLOW_TOKEN"
} else {
"no token in env. With the forge unknown, only FERRFLOW_TOKEN is used"
};
return Check::warn("auth token", Some(detail.into()));
Comment thread
ferrfleet[bot] marked this conversation as resolved.
Resolved
};
let vars = kind.token_env_vars();
if let Some(var) = vars.iter().find(|var| is_set(var)) {
return Check::ok("auth token", Some(format!("{var} is set")));
}
let expected = vars.join(" or ");
let detail = if kind == ForgeKind::Gitea && is_set("GITHUB_TOKEN") {
format!(
"only GITHUB_TOKEN is set. It still covers the push until the next major version, \
but releases need {expected}"
)
} else {
format!(
"no token in env ({expected} or {GENERIC_TOKEN_ENV_VAR}). API pushes and releases will fail"
)
};
Check::warn("auth token", Some(detail))
}

fn forge_label(kind: ForgeKind) -> &'static str {
Expand Down
62 changes: 62 additions & 0 deletions src/doctor/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -303,3 +303,65 @@ mod lockfiles {
);
}
}

fn only(vars: &'static [&'static str]) -> impl Fn(&str) -> bool {
move |var| vars.contains(&var)
}

fn token_status(
forge: Option<crate::config::ForgeKind>,
vars: &'static [&'static str],
) -> (Status, String) {
let check = super::checks::token_check(forge, only(vars));
(check.status, check.detail.unwrap_or_default())
}

#[test]
fn an_unrecognised_forge_does_not_count_the_github_token() {
let (status, detail) = token_status(None, &["GITHUB_TOKEN"]);
assert_eq!(status, Status::Warn);
assert!(
detail.contains("only sent to a remote recognised as GitHub"),
"{detail}"
);
}

#[test]
fn ferrflow_token_is_enough_whatever_the_forge() {
assert_eq!(token_status(None, &["FERRFLOW_TOKEN"]).0, Status::Ok);
assert_eq!(
token_status(Some(crate::config::ForgeKind::Gitea), &["FERRFLOW_TOKEN"]).0,
Status::Ok
);
}

#[test]
fn forgejo_token_counts_for_gitea() {
let (status, detail) = token_status(Some(crate::config::ForgeKind::Gitea), &["FORGEJO_TOKEN"]);
assert_eq!(status, Status::Ok);
assert_eq!(detail, "FORGEJO_TOKEN is set");
}

#[test]
fn gitea_with_only_the_github_token_warns_that_releases_need_their_own() {
let (status, detail) = token_status(Some(crate::config::ForgeKind::Gitea), &["GITHUB_TOKEN"]);
assert_eq!(status, Status::Warn);
assert!(
detail.contains("releases need GITEA_TOKEN or FORGEJO_TOKEN"),
"{detail}"
);
}

#[test]
fn a_known_forge_without_its_token_warns() {
let (status, detail) = token_status(Some(crate::config::ForgeKind::Gitlab), &["GITHUB_TOKEN"]);
assert_eq!(status, Status::Warn);
assert!(
detail.contains("GITLAB_TOKEN or FERRFLOW_TOKEN"),
"{detail}"
);
assert_eq!(
token_status(Some(crate::config::ForgeKind::Github), &["GITHUB_TOKEN"]).0,
Status::Ok
);
}
3 changes: 2 additions & 1 deletion src/git/auth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,8 @@ fn github_token_fallback(url: &str, forge: Option<ForgeKind>) -> Option<String>
GITEA_FALLBACK_WARNING.call_once(|| {
tracing::warn!(
"Warning: pushing to {} with GITHUB_TOKEN because neither GITEA_TOKEN nor FORGEJO_TOKEN is set. \
This fallback goes away in the next major version: pass the token as GITEA_TOKEN \
This fallback covers the push only: creating the release reads GITEA_TOKEN or FORGEJO_TOKEN, \
so it is skipped. The fallback goes away in the next major version: pass the token as GITEA_TOKEN \
(on Gitea or Forgejo Actions, `GITEA_TOKEN: ${{{{ secrets.GITHUB_TOKEN }}}}`).",
host()
);
Expand Down
Loading