Skip to content
Merged
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
2 changes: 1 addition & 1 deletion docs/site/docs-en/configuration/config-file.md
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,7 @@ Global settings that apply to all packages.
| `releaseCommitMode` | string | `"commit"` | How to handle the release commit: `"commit"`, `"pr"`, or `"none"` |
| `releaseCommitScope` | string | `"grouped"` | In a monorepo where several packages are bumped at once, whether to create a single `"grouped"` commit or one commit `"per-package"`. Only matters when multiple packages bump. |
| `releaseCommitBody` | string | `"none"` | What goes in the body of the release commit, below the subject. `"none"` keeps the single-line subject. `"summary"` lists one line per released package with its commit count. `"full"` embeds the changelog section written for each package — under `"grouped"` scope each section is headed `## <package> <version>`. |
| `forge` | string | `"auto"` | Git forge override: `"auto"` detects from the remote URL, and for an unrecognised host it probes the API over HTTPS to auto-detect a self-hosted **GitLab**, **GitHub Enterprise**, or **Gitea / Forgejo** instance (cached, ~2s, best-effort). Set `"github"`, `"gitlab"`, `"gitea"` (Gitea / Forgejo / Codeberg), or `"bitbucket"` (Bitbucket Cloud) to force a forge — needed only when the host isn't reachable over HTTPS or you want to skip probing. Gitea auth uses `GITEA_TOKEN` / `FORGEJO_TOKEN`; Bitbucket uses `BITBUCKET_TOKEN`. All cover release creation — on Bitbucket, which has no release object, the release is the annotated tag FerrFlow pushes. PR mode is GitHub/GitLab only. |
| `forge` | string | `"auto"` | Git forge override: `"auto"` detects from the remote URL, and for an unrecognised host it probes the API over HTTPS to auto-detect a self-hosted **GitLab**, **GitHub Enterprise**, or **Gitea / Forgejo** instance (cached, ~2s, best-effort). The same answer picks the credential git pushes with, so a self-hosted GitLab pushes with `GITLAB_TOKEN` whatever its host name. Set `"github"`, `"gitlab"`, `"gitea"` (Gitea / Forgejo / Codeberg), or `"bitbucket"` (Bitbucket Cloud) to force a forge — needed only when the host isn't reachable over HTTPS or you want to skip probing. Gitea auth uses `GITEA_TOKEN` / `FORGEJO_TOKEN`; Bitbucket uses `BITBUCKET_TOKEN`. All cover release creation — on Bitbucket, which has no release object, the release is the annotated tag FerrFlow pushes. PR mode is GitHub/GitLab only. |
| `skipCi` | boolean | depends on mode | Add `[skip ci]` to release commits. Defaults to `true` when mode is `"commit"`, `false` otherwise. |
| `commitSkipMarkers` | array | `["[skip ci]", "[ci skip]", "[no ci]", "[skip actions]", "[actions skip]"]` | Markers that cause FerrFlow to skip a commit when computing the next version. Matched case-insensitively, subject line only. |
| `commitFormats` | object | permissive conventional | Which commit subjects map to which bump level. Each of `major` / `minor` / `patch` takes a pattern string, a list of patterns, or `"all"` as a catch-all; `*` matches any run of characters (including `/`) and `?` exactly one. Resolution is major → minor → patch, first match wins. `caseSensitive` (default `true`) lowercases both sides when false. Defaults also accept capitalised and slash-separated variants (`Feat:`, `feat/`, `feature:`, `Fix/`, `Perf:`, `Refactor/`, and so on), listed in full under [permissive defaults](/docs/reference/conventional-commits). Breaking markers (`feat!:`, `fix(api)!:`, a `BREAKING CHANGE:` footer) are always detected regardless of what is configured. |
Expand Down
2 changes: 1 addition & 1 deletion docs/site/docs-fr/configuration/config-file.md
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,7 @@ Paramètres globaux qui s'appliquent à tous les packages.
| `releaseCommitMode` | string | `"commit"` | Gestion du commit de release : `"commit"`, `"pr"` ou `"none"` |
| `releaseCommitScope` | string | `"grouped"` | Dans un monorepo où plusieurs packages sont bumpés en même temps, créer un seul commit `"grouped"` ou un commit `"per-package"`. N'a d'effet que quand plusieurs packages bumpent. |
| `releaseCommitBody` | string | `"none"` | Ce que contient le corps du commit de release, sous la ligne de sujet. `"none"` conserve le sujet sur une seule ligne. `"summary"` liste une ligne par package publié avec son nombre de commits. `"full"` intègre la section de changelog écrite pour chaque package — en scope `"grouped"`, chaque section est titrée `## <package> <version>`. |
| `forge` | string | `"auto"` | Forçage du forge git : `"auto"` détecte depuis l'URL du remote, et pour un hôte non reconnu il sonde l'API en HTTPS pour auto-détecter une instance auto-hébergée de **GitLab**, **GitHub Enterprise** ou **Gitea / Forgejo** (mis en cache, ~2s, best-effort). Renseignez `"github"`, `"gitlab"`, `"gitea"` (Gitea / Forgejo / Codeberg) ou `"bitbucket"` (Bitbucket Cloud) pour forcer un forge — nécessaire uniquement si l'hôte n'est pas joignable en HTTPS ou pour éviter le sondage. L'auth Gitea utilise `GITEA_TOKEN` / `FORGEJO_TOKEN` ; Bitbucket utilise `BITBUCKET_TOKEN`. Tous couvrent la création de release — sur Bitbucket, qui n'a pas d'objet release, la release est le tag annoté que FerrFlow pousse. Le mode PR reste GitHub/GitLab. |
| `forge` | string | `"auto"` | Forçage du forge git : `"auto"` détecte depuis l'URL du remote, et pour un hôte non reconnu il sonde l'API en HTTPS pour auto-détecter une instance auto-hébergée de **GitLab**, **GitHub Enterprise** ou **Gitea / Forgejo** (mis en cache, ~2s, best-effort). Ce même résultat choisit l'identifiant avec lequel git pousse : une instance GitLab auto-hébergée pousse avec `GITLAB_TOKEN`, quel que soit son nom d'hôte. Renseignez `"github"`, `"gitlab"`, `"gitea"` (Gitea / Forgejo / Codeberg) ou `"bitbucket"` (Bitbucket Cloud) pour forcer un forge — nécessaire uniquement si l'hôte n'est pas joignable en HTTPS ou pour éviter le sondage. L'auth Gitea utilise `GITEA_TOKEN` / `FORGEJO_TOKEN` ; Bitbucket utilise `BITBUCKET_TOKEN`. Tous couvrent la création de release — sur Bitbucket, qui n'a pas d'objet release, la release est le tag annoté que FerrFlow pousse. Le mode PR reste GitHub/GitLab. |
| `skipCi` | boolean | dépend du mode | Ajouter `[skip ci]` aux commits de release. Par défaut `true` en mode `"commit"`, `false` sinon. |
| `commitSkipMarkers` | array | `["[skip ci]", "[ci skip]", "[no ci]", "[skip actions]", "[actions skip]"]` | Marqueurs qui font ignorer un commit par FerrFlow lors du calcul de la prochaine version. Comparaison insensible à la casse, sur la ligne de sujet uniquement. |
| `commitFormats` | object | conventionnel permissif | Quels sujets de commit correspondent à quel niveau de bump. Chacun de `major` / `minor` / `patch` accepte un motif, une liste de motifs, ou `"all"` comme fourre-tout ; `*` correspond à n'importe quelle suite de caractères (y compris `/`) et `?` à exactement un. Résolution : major → minor → patch, premier motif gagnant. `caseSensitive` (défaut `true`) met les deux côtés en minuscules quand il vaut `false`. Les défauts acceptent aussi les variantes capitalisées et séparées par une barre oblique (`Feat:`, `feat/`, `feature:`, `Fix/`, `Perf:`, `Refactor/`, etc.), listées en entier dans [défauts permissifs](/fr/docs/reference/conventional-commits). Les marqueurs de rupture (`feat!:`, `fix(api)!:`, un pied de page `BREAKING CHANGE:`) sont toujours détectés quelle que soit la configuration. |
Expand Down
39 changes: 26 additions & 13 deletions src/git/auth.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use std::process::Command;

use super::repo::Repository;
use crate::config::ForgeKind;
use crate::forge::gitlab::GitLabToken;

#[cfg(test)]
Expand Down Expand Up @@ -32,24 +33,36 @@ fn is_gitlab(url: &str) -> bool {
host_of(url).is_some_and(|host| host.contains("gitlab"))
}

pub(super) fn token_for_url(url: &str) -> Option<(String, String)> {
if let Ok(token) = std::env::var("FERRFLOW_TOKEN") {
let user = if is_gitlab(url) {
fn forge_of(url: &str, configured: ForgeKind) -> Option<ForgeKind> {
match configured {
ForgeKind::Auto if is_gitlab(url) => Some(ForgeKind::Gitlab),
ForgeKind::Auto => crate::forge::detect_forge_with_probe(url),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: detect_forge_with_probe falls through to detect_forge_from_url, which matches github.com / gitlab.com / codeberg.org anywhere in the string, path included. This file deliberately went the other way: is_gitlab matches on host_of(url) only, and a_repository_name_does_not_pick_the_forge asserts a repo name must not select the credential. The auto branch now gives that up.

Concretely, remote https://git.acme.com/tools/gitlab.com-exporter.git with forge: auto resolves to GitLab off the path: with FERRFLOW_TOKEN set the push username flips from x-access-token to oauth2, and with only GITHUB_TOKEN set token_for_url returns None, so the push goes out with no credential at all. The existing test does not catch it because all four of its URLs are on github.com.

Detecting on the host keeps the probe cache key identical and leaves the SaaS short-circuits working:

Suggested change
ForgeKind::Auto => crate::forge::detect_forge_with_probe(url),
ForgeKind::Auto => crate::forge::extract_host(url)
.and_then(|host| crate::forge::detect_forge_with_probe(&format!("https://{host}/"))),

(forge_target in preview.rs passes the full URL too, so the API side has the same quirk. Worth fixing there in the same pass, or inside detect_forge_from_url.)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Taken in #1132 (this PR had already merged).

explicit => Some(explicit),
}
}

pub(super) fn token_for_url(url: &str, forge: ForgeKind) -> Option<(String, String)> {
let ferrflow_token = std::env::var("FERRFLOW_TOKEN").ok();
let gitlab_token = std::env::var("GITLAB_TOKEN").ok();
let github_token = std::env::var("GITHUB_TOKEN").ok();
Comment on lines +45 to +47

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: an empty variable counts as present here. GITHUB_TOKEN: ${{ secrets.MISSING }} exports "", which passes the new guard and buys a network probe (up to 3 × 2s on a self-hosted host) for a credential that is then handed to git as an empty password. forge::resolve_token already filters empties; matching it makes the guard mean what it says.

Suggested change
let ferrflow_token = std::env::var("FERRFLOW_TOKEN").ok();
let gitlab_token = std::env::var("GITLAB_TOKEN").ok();
let github_token = std::env::var("GITHUB_TOKEN").ok();
let ferrflow_token = std::env::var("FERRFLOW_TOKEN").ok().filter(|t| !t.is_empty());
let gitlab_token = std::env::var("GITLAB_TOKEN").ok().filter(|t| !t.is_empty());
let github_token = std::env::var("GITHUB_TOKEN").ok().filter(|t| !t.is_empty());

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Taken in #1132 (this PR had already merged).

if ferrflow_token.is_none() && gitlab_token.is_none() && github_token.is_none() {
return None;
}
let is_gitlab = forge_of(url, forge) == Some(ForgeKind::Gitlab);
if let Some(token) = ferrflow_token {
let user = if is_gitlab {
GitLabToken::of(&token).git_username()
} else {
"x-access-token"
};
return Some((user.to_string(), token));
}
if is_gitlab(url) {
if let Ok(token) = std::env::var("GITLAB_TOKEN") {
let user = GitLabToken::of(&token).git_username();
return Some((user.to_string(), token));
}
} else if let Ok(token) = std::env::var("GITHUB_TOKEN") {
return Some(("x-access-token".to_string(), token));
if is_gitlab {
let token = gitlab_token?;
let user = GitLabToken::of(&token).git_username();
return Some((user.to_string(), token));
}
None
github_token.map(|token| ("x-access-token".to_string(), token))
}

/// Where the credential reaches git. `/proc/<pid>/cmdline` is world-readable,
Expand All @@ -68,9 +81,9 @@ pub(super) const GIT_PASSWORD_VAR: &str = "FERRFLOW_GIT_PASSWORD";
const CREDENTIAL_HELPER: &str =
r#"!f() { echo "username=$FERRFLOW_GIT_USER"; echo "password=$FERRFLOW_GIT_PASSWORD"; }; f"#;

pub(super) fn configure_git_command(cmd: &mut Command, url: &str) {
pub(super) fn configure_git_command(cmd: &mut Command, url: &str, forge: ForgeKind) {
scrub_trace_env(cmd);
if let Some((user, token)) = token_for_url(url) {
if let Some((user, token)) = token_for_url(url, forge) {
cmd.env(GIT_USER_VAR, user);
cmd.env(GIT_PASSWORD_VAR, token);
cmd.arg("-c")
Expand Down
9 changes: 7 additions & 2 deletions src/git/fetch.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
use anyhow::{Context, Result, anyhow};

use super::auth::{configure_git_command, get_remote_url};
use super::remote::Remote;
use super::repo::Repository;

pub fn fetch_tags(repo: &Repository, remote_name: &str) -> Result<()> {
pub fn fetch_tags(repo: &Repository, remote: Remote<'_>) -> Result<()> {
let Remote {
name: remote_name,
forge,
} = remote;
let workdir = repo
.workdir()
.ok_or_else(|| anyhow!("Bare repositories are not supported"))?;
Expand All @@ -12,7 +17,7 @@ pub fn fetch_tags(repo: &Repository, remote_name: &str) -> Result<()> {

let mut cmd = std::process::Command::new("git");
cmd.current_dir(workdir);
configure_git_command(&mut cmd, &url);
configure_git_command(&mut cmd, &url, forge);
cmd.args(["fetch", "--tags", remote_name]);

let output = cmd
Expand Down
2 changes: 2 additions & 0 deletions src/git/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ mod commits;
mod diff;
mod fetch;
mod push;
mod remote;
mod repo;
mod retry;
mod shell;
Expand Down Expand Up @@ -32,6 +33,7 @@ pub use push::{
delete_tag_if_unchanged, force_push_branch, force_push_tags, push, push_tags,
release_branch_foreign_commit, reset_branch_to_remote, revert_commit, verify_remote_branch,
};
pub use remote::Remote;
pub use repo::{Repository, get_repo_root, open_repo, resolve_current_branch};
pub use retry::is_push_rejected_error;
pub use tags::{
Expand Down
Loading
Loading