From 8ec402b80a7b1f80825cdba09cde08fc79cc62ae Mon Sep 17 00:00:00 2001 From: Tim Schindler Date: Mon, 17 Aug 2026 07:58:01 +0200 Subject: [PATCH 1/2] docs: prepare CHANGELOG for v0.10.0 release --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 998a66c..04ed64b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ All notable changes to this project will be documented in this file. ## [Unreleased] +## [0.10.0] - 2026-08-17 + ### Changed - An invalid `cache_ttl` (unparseable, zero or negative) now fails the command instead of silently defaulting; the error names the config file, the expected duration syntax and `--refresh` From 0e201feaf0dcbde37d8e68cdcaf7a20e29831715 Mon Sep 17 00:00:00 2001 From: Tim Schindler Date: Sun, 23 Aug 2026 19:41:22 +0200 Subject: [PATCH 2/2] fix(config): point the cache_ttl error at the config file, not --refresh --- CHANGELOG.md | 2 +- CLAUDE.md | 2 +- internal/config/config.go | 2 +- internal/config/config_test.go | 8 ++++---- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 04ed64b..0338cc3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,7 @@ All notable changes to this project will be documented in this file. ### Changed -- An invalid `cache_ttl` (unparseable, zero or negative) now fails the command instead of silently defaulting; the error names the config file, the expected duration syntax and `--refresh` +- An invalid `cache_ttl` (unparseable, zero or negative) now fails the command instead of silently defaulting; the error names the config file and the expected duration syntax ### Fixed diff --git a/CLAUDE.md b/CLAUDE.md index cd75563..628bf83 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -151,7 +151,7 @@ Custom `SCAAccessService` follows SDK conventions: ## Cache - Eligibility responses cached in `~/.grant/cache/` as JSON files (e.g., `eligibility_azure.json`, `groups_eligibility_azure.json`) - Default TTL: 4 hours, configurable via `cache_ttl` in `~/.grant/config.yaml` (Go duration syntax: `2h`, `30m`) -- `config.ParseCacheTTL` returns `(time.Duration, error)`. **Absent** means "use the default"; **any explicitly supplied** value that cannot serve as a TTL — unparseable, zero or negative — is an error. Treating those two the same way is the point: silently defaulting `garbage` while rejecting `0s` would validate one field by two opposite rules. `config.Load` validates it so a bad value surfaces at load, not when some command happens to build a cache. `buildCachedLister` (`cmd/root.go`) therefore returns an error too — its bad-TTL arm is reachable only for a `Config` assembled in memory. Both rejection messages must name a remedy: the unparseable arm names the expected syntax (`must be a positive Go duration such as 4h or 30m`) and still wraps the `time.ParseDuration` error with `%w`; the non-positive arm names `--refresh` as the way to bypass the cache for one command, since `0s` used to work as an accidental kill-switch. Neither may point at `grant configure` (see Config) +- `config.ParseCacheTTL` returns `(time.Duration, error)`. **Absent** means "use the default"; **any explicitly supplied** value that cannot serve as a TTL — unparseable, zero or negative — is an error. Treating those two the same way is the point: silently defaulting `garbage` while rejecting `0s` would validate one field by two opposite rules. `config.Load` validates it so a bad value surfaces at load, not when some command happens to build a cache. `buildCachedLister` (`cmd/root.go`) therefore returns an error too — its bad-TTL arm is reachable only for a `Config` assembled in memory. Both rejection messages must name a remedy: the unparseable arm names the expected syntax (`must be a positive Go duration such as 4h or 30m`) and still wraps the `time.ParseDuration` error with `%w`; the non-positive arm names removing the setting, since `0s` used to work as an accidental kill-switch and `--refresh` does not exist on every cache-consuming command (`status`, `revoke`, `favorites add`) — nor would it help, because `config.Load` rejects the value before any flag is read. Neither may point at `grant configure` (see Config) - `--refresh` flag on `grant` and `grant env` bypasses cache reads but still writes fresh data - `internal/cache/cache.go` — generic `Store` with `Get[T]`/`Set[T]`, injectable clock for testing - `internal/cache/cached_eligibility.go` — `CachedEligibilityLister` decorator implementing `eligibilityLister` + `groupsEligibilityLister` diff --git a/internal/config/config.go b/internal/config/config.go index 2d38e49..dd55542 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -130,7 +130,7 @@ func ParseCacheTTL(cfg *Config) (time.Duration, error) { return 0, fmt.Errorf("invalid cache_ttl %q: must be a positive Go duration such as 4h or 30m: %w", cfg.CacheTTL, err) } if d <= 0 { - return 0, fmt.Errorf("invalid cache_ttl %q: must be greater than zero; use --refresh to bypass the cache for a single command", cfg.CacheTTL) + return 0, fmt.Errorf("invalid cache_ttl %q: must be greater than zero; remove the setting to use the default (%s)", cfg.CacheTTL, DefaultCacheTTL) } return d, nil } diff --git a/internal/config/config_test.go b/internal/config/config_test.go index 882eef3..0443543 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -519,21 +519,21 @@ func TestParseCacheTTL(t *testing.T) { }, }, { - name: "zero is rejected and names the --refresh alternative", + name: "zero is rejected and names the remedy", value: "0s", wantErrContains: []string{ `invalid cache_ttl "0s"`, "must be greater than zero", - "use --refresh to bypass the cache for a single command", + "remove the setting to use the default", }, }, { - name: "negative is rejected and names the --refresh alternative", + name: "negative is rejected and names the remedy", value: "-1h", wantErrContains: []string{ `invalid cache_ttl "-1h"`, "must be greater than zero", - "use --refresh to bypass the cache for a single command", + "remove the setting to use the default", }, }, }