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: 2 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -324,3 +324,5 @@ make ci # CI と同じ検査 (fmt-check + lint + test、書き換
- Tauri バージョン同期の「既に一致」判定は npm パッケージ**全件**で行う。`@tauri-apps/api` と `@tauri-apps/cli` はパッチ集合差や age フィルタで judge 結果が非対称になりうるため、先頭 1 件 (依存はキー順に並ぶので通常 `api`) の一致で打ち切ると残りのずれが同期されない。OSV フォールバックで脆弱な候補を退けた結果 (`osv_skipped` が非空) も同期の上書きから保護する — 同期先はレジストリの生の一覧から最新を選ぶため、保護しないと judge が除外した脆弱版を選び直して OSV チェックを無効化してしまう
- `--install` を走らせるディレクトリの順序はパス順に固定する。`HashMap` の `RandomState` 由来で実行のたびに入れ替わると、verbose 出力や失敗時の stderr の行順が変わって CI のログ比較で偽の差分になる
- Rust の post-install age 監査では、fetch 失敗時も進捗位置を進める (末尾の数件が失敗するとバーが手前で止まったままになる)。差し戻しの `cargo update` は残り予算が `MIN_CARGO_UPDATE_SLICE` (10 秒) 未満なら着手しない — タイムアウトは残り予算でクランプされるので予算末尾で着手すると必ず失敗し、試行済みとして記録された組合せは二度と再試行されないため、本来差し戻せた依存が恒久的に「失敗」で確定する

- 公開者による age 例外はグローバル設定の `[age_exempt].github` に閉じ、アカウント名を本体へ埋め込まない。`update/age_policy.rs` は版ごとの確認済み公開者(crates.io の GitHub ログイン一致 / Trusted Publishing)または Go・Swift の検証済み GitHub 取得元だけを照合する。自己申告 repository URL や現在の owner 一覧で過去の版を免除しない。候補選択・Tauri 同期・Rust post-install 監査(rollback 候補と最終 lock の照合を含む)で同じ `admits` 判定を共有する。例外は age のみを外し、プロジェクトの明示的な最小公開期間には優先しない。不正な例外設定は他のグローバル設定を維持したまま無効化する。
2 changes: 1 addition & 1 deletion README.ja.md
Original file line number Diff line number Diff line change
Expand Up @@ -210,7 +210,7 @@ depup は 3 か所から設定を読み取ります。どの設定も、コマ

| 設定 | 置き場所 | 用途 |
|---|---|---|
| グローバル設定 | `~/.config/depup/config.toml`(初回実行時に作成) | すべてのプロジェクトに共通する `age`・`osv`・`max_change` のデフォルト |
| グローバル設定 | `~/.config/depup/config.toml`(初回実行時に作成) | 共通する `age`・`osv`・`max_change` のデフォルトと、確認済み公開者の例外(`age_exempt`) |
| モノレポのディレクトリ | プロジェクトのルートの `.depup` | 追加で処理するディレクトリ |
| プロジェクトの age 設定 | pnpm・Bun の設定の `minimumReleaseAge`、mise の設定の `minimum_release_age` | そのプロジェクトの最小公開期間 |

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -210,7 +210,7 @@ depup reads settings from three places. For each setting, a command-line flag ov

| Setting | Location | Purpose |
|---|---|---|
| Global defaults | `~/.config/depup/config.toml`, created on the first run | Default `age`, `osv`, and `max_change` for every project |
| Global defaults | `~/.config/depup/config.toml`, created on the first run | Default `age`, `osv`, and `max_change`, plus verified publisher exceptions (`age_exempt`) |
| Monorepo directories | `.depup` at the project root | Additional directories to process |
| Project age policy | `minimumReleaseAge` in the pnpm or Bun settings, `minimum_release_age` in the mise settings | Minimum release age of that project |

Expand Down
26 changes: 26 additions & 0 deletions docs/configuration.ja.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,36 @@ osv = true
# "major"(デフォルト。すべて許可)のいずれかを指定します。
# 実行ごとに --max-change <LEVEL> で上書きできます。
# max_change = "minor"

# 確認済みの GitHub の身元で公開された版を、公開期間の制限から除外します。
# 自己申告の repository URL は根拠にしません。詳細は docs/configuration.ja.md を参照してください。
# [age_exempt]
# github = ["example-dev"]
```

ファイルにないキーには、組み込みデフォルト(`age = "1w"`、`osv = true`、`max_change` の制限なし)が使われます。ファイルを作成・解析できない場合は、警告を表示して組み込みデフォルトを使います。値が不正なキー(`age = "abc"` など)は、警告を表示したうえで、そのキーだけを組み込みデフォルトに戻します。

## 確認済みの公開者による age の例外

通常の待機期間を維持し、指定した GitHub アカウントだけを例外にするには、グローバル設定に次のように書きます。

```toml
age = "2w"

[age_exempt]
github = ["example-dev", "example-org"]
```

既定では例外は空です。ログイン名を完全一致で照合し、大文字・小文字は区別しません。不正な名前は警告して除外します。`age_exempt` テーブルの形式やキーが不正な場合も、例外だけを無効にし、他の設定は維持します。

対応する根拠は、crates.io の版ごとの公開者情報(GitHub ログイン名の一致が確認された `published_by`、または GitHub Trusted Publishing)と、Go・Swift の GitHub 取得元です。パッケージ名、別レジストリの同名アカウント、現在のクレート所有者一覧、自己申告の repository URL からは GitHub の身元を判断しません。他のレジストリには通常の age 制限を適用し、対象に含まれる場合は通知します。

Swift のタグには現状公開日の情報がなく、age フィルターはもともと実質適用されません。

例外が外すのは公開期間の制限だけです。ピン留め、バージョン範囲、プレリリースの扱い、`--max-change`、OSV チェックは引き続き適用します。プロジェクトに明示された `minimumReleaseAge` は優先され、グローバルの例外を無効にします。Git 依存は従来から公開期間の制限対象外です。

候補選択、Tauri の同期、`--install` 後の Rust lockfile 監査で同じ設定を使い、推移依存のクレートにも適用します。例外で更新する場合はテキスト出力に根拠を表示し、JSON 出力には `identity` と `evidence` を持つ `age_exemption` オブジェクトを追加します。

## `.depup` 設定ファイル

複数のサブディレクトリを持つモノレポでは、プロジェクトのルートに `.depup` ファイルを作成し、追加で処理するディレクトリを列挙できます。
Expand Down
26 changes: 26 additions & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,36 @@ osv = true
# or "major" (default — all bumps allowed).
# Override per-run with --max-change <LEVEL>.
# max_change = "minor"

# Exempt releases published by verified GitHub identities from age filtering.
# Self-declared repository URLs do not qualify. See docs/configuration.md.
# [age_exempt]
# github = ["example-dev"]
```

A key that is missing from the file falls back to the built-in default (`age = "1w"`, `osv = true`, no `max_change` limit). If the file cannot be created or parsed, depup prints a warning and uses the built-in defaults. If a single value is invalid (such as `age = "abc"`), depup prints a warning and uses the built-in default for that setting only.

## Age Exemptions by Verified Publisher

Keep the normal waiting period and exempt selected GitHub identities in your global configuration:

```toml
age = "2w"

[age_exempt]
github = ["example-dev", "example-org"]
```

The list is empty by default. Logins are matched as complete names, without case sensitivity. Invalid names are ignored with a warning; an invalid `age_exempt` table disables only the exemptions and preserves your other settings.

Supported evidence is version-specific crates.io publisher information (`published_by` with a confirmed GitHub login, or GitHub Trusted Publishing), and GitHub source paths for Go and Swift. A package name, a registry account with the same name, a current list of crate owners, or a self-declared repository URL does not establish a GitHub identity. Other registries keep the normal age filter; depup prints a notice when they are selected.

Swift tags currently have no release-date metadata, so their age filter already has no practical effect.

Exemptions affect only release age. Pinned dependencies, version ranges, prerelease rules, `--max-change`, and OSV checks still apply. An explicit project `minimumReleaseAge` takes precedence and disables global exemptions. Git dependencies already bypass release-age filtering.

The same policy applies to candidate selection, Tauri synchronization, and the Rust lockfile audit after `--install`, including transitive crates. Text output identifies an exempt update; JSON adds an `age_exemption` object with `identity` and `evidence` only when an update uses the exemption.

## `.depup` Configuration File

For monorepo projects with multiple subdirectories, create a `.depup` file at the project root to list additional directories to process:
Expand Down
12 changes: 12 additions & 0 deletions src/domain/update_result.rs
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,9 @@ pub enum UpdateResult {
/// API エラー時) を意味する。
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
osv_checked: bool,
/// age を免除した身元と、レジストリが確認した根拠。
#[serde(default, skip_serializing_if = "Option::is_none")]
age_exemption: Option<crate::update::AgeExemption>,
},
/// 依存関係の更新がスキップされた
Skip {
Expand All @@ -84,6 +87,13 @@ pub enum UpdateResult {
}

impl UpdateResult {
pub fn with_age_exemption(mut self, exemption: Option<crate::update::AgeExemption>) -> Self {
if let Self::Update { age_exemption, .. } = &mut self {
*age_exemption = exemption;
}
self
}

/// リリース日なしのUpdate結果を作成する (後方互換性のため)
pub fn update(dependency: Dependency, new_version: impl Into<String>) -> Self {
UpdateResult::Update {
Expand All @@ -92,6 +102,7 @@ impl UpdateResult {
released_at: None,
osv_skipped: Vec::new(),
osv_checked: false,
age_exemption: None,
}
}

Expand All @@ -107,6 +118,7 @@ impl UpdateResult {
released_at: Some(released_at),
osv_skipped: Vec::new(),
osv_checked: false,
age_exemption: None,
}
}

Expand Down
50 changes: 49 additions & 1 deletion src/global_config.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
//! グローバル設定ファイル (`~/.config/depup/config.toml`) のローダ
//!
//! ユーザー単位のデフォルトを定義する。現状は `age` と `osv` をサポート。
//! ユーザー単位の age・公開者の例外・OSV・変更レベルのデフォルトを定義する。
//! ファイルが無い場合は初回読み込み時にコメント付きの雛形を自動生成する
//! (生成に失敗してもツールの動作は続行され、組み込みデフォルトが使われる)。

Expand Down Expand Up @@ -51,6 +51,11 @@ osv = true
# or "major" (default — all bumps allowed).
# Override per-run with --max-change <LEVEL>.
# max_change = "minor"

# Exempt releases published by verified GitHub identities from age filtering.
# Self-declared repository URLs do not qualify. See docs/configuration.md.
# [age_exempt]
# github = ["example-dev"]
"#;

/// `~/.config/depup/config.toml` の内容。
Expand All @@ -73,6 +78,28 @@ pub struct GlobalConfig {
/// 未指定の場合は制限なし (= major bumps も許可)。
#[serde(default)]
pub max_change: Option<String>,

/// age だけを免除する確認済みの公開者。
#[serde(default, deserialize_with = "deserialize_age_exempt")]
pub age_exempt: crate::update::AgeExemptions,
}

fn deserialize_age_exempt<'de, D>(deserializer: D) -> Result<crate::update::AgeExemptions, D::Error>
where
D: serde::Deserializer<'de>,
{
let value = toml::Value::deserialize(deserializer)?;
match value.try_into::<crate::update::AgeExemptions>() {
Ok(mut exemptions) => {
exemptions.validate();
Ok(exemptions)
}
Err(error) => {
// 例外の typo で age=2w 等の他の設定まで失われないようにする。
eprintln!("Warning: invalid age_exempt configuration; exemptions disabled: {error}");
Ok(crate::update::AgeExemptions::default())
}
}
}

impl GlobalConfig {
Expand Down Expand Up @@ -210,6 +237,27 @@ pub fn resolve_osv(cli_osv: bool, no_osv: bool, config: Option<&GlobalConfig>) -

#[cfg(test)]
mod tests {
#[test]
fn age_exempt_normalizes_logins_and_preserves_other_settings_on_errors() {
let config: super::GlobalConfig = toml::from_str("age = '2w'\nosv = true\n[age_exempt]\ngithub = ['EXAMPLE-dev', 'example-dev', 'bad/name']").unwrap();
assert_eq!(config.age.as_deref(), Some("2w"));
assert_eq!(config.age_exempt.github, ["example-dev"]);
for nested in [
"github = 42",
"githb = ['example-dev']",
"github = ['example-dev', 42]",
] {
let config: super::GlobalConfig =
toml::from_str(&format!("age = '2w'\nosv = true\n[age_exempt]\n{nested}")).unwrap();
assert_eq!(
config.age_duration(),
Some(std::time::Duration::from_secs(14 * 86400))
);
assert_eq!(config.osv, Some(true));
assert!(config.age_exempt.is_empty());
}
}

use super::*;
use tempfile::TempDir;

Expand Down
Loading
Loading