-
Notifications
You must be signed in to change notification settings - Fork 382
version: add update notifications, json output
#2421
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Conversation
Implements version update notifications similar to Terraform's pattern. When running tflint --version, users see a notification if a newer version is available on GitHub Releases. ## Changes - Adds `versioncheck` package that checks GitHub Releases API for latest version - Modifies `--version` command to display update notifications when newer version available - Adds `--format json` support to version command with structured output including update status - Supports `TFLINT_DISABLE_VERSION_CHECK` environment variable to opt out - Caches version check results for 48 hours in platform-specific cache directory ## Implementation Details Version checking happens on every `--version` invocation with a 3-second timeout. Results are cached in the OS cache directory (`~/Library/Caches/tflint` on macOS, `~/.cache/tflint` on Linux) for 48 hours to minimize API calls. The check uses GitHub's public API (60 req/hour unauthenticated) or `GITHUB_TOKEN` environment variable for higher rate limits (5000 req/hour). Errors are logged but non-fatal - network timeouts or API failures don't break the version command. ## Testing Verified version checking against live GitHub API with both outdated and current versions. Cache behavior tested with 48-hour TTL validation. JSON output structure validated for both text and JSON formats. Closes #883
version: add update notifications
version: add update notificationsversion: add update notifications, json output
Clarify the usage of TFLINT_DISABLE_VERSION_CHECK.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
This PR implements version update notifications for TFLint, similar to Terraform's approach. When running tflint --version, users are notified if a newer version is available on GitHub Releases. The implementation includes JSON output support via --format json and can be disabled using the TFLINT_DISABLE_VERSION_CHECK environment variable.
Key changes:
- Adds version checking with GitHub Releases API integration (60 req/hour unauthenticated, 5000 req/hour with
GITHUB_TOKEN) - Implements 48-hour caching in platform-specific cache directories to minimize API calls
- Adds JSON output format for version command with structured update information
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 9 comments.
Show a summary per file
| File | Description |
|---|---|
| versioncheck/github.go | Implements GitHub API integration to fetch latest release version with optional authentication and rate limit handling |
| versioncheck/check.go | Core version checking logic with caching support and version comparison |
| versioncheck/check_test.go | Unit tests for Enabled() and compareVersions() functions |
| versioncheck/cache.go | Cache management with 48-hour TTL using platform-specific cache directories |
| versioncheck/cache_test.go | Unit tests for cache expiration logic |
| cmd/version.go | Integrates version checking into --version command with text and JSON output formats |
| docs/user-guide/environment_variables.md | Documents TFLINT_DISABLE_VERSION_CHECK environment variable |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| func fetchLatestRelease(ctx context.Context) (string, error) { | ||
| // Create GitHub client with optional authentication | ||
| hc := &http.Client{Transport: http.DefaultTransport} | ||
| if token := os.Getenv("GITHUB_TOKEN"); token != "" { | ||
| hc = oauth2.NewClient(ctx, oauth2.StaticTokenSource(&oauth2.Token{ | ||
| AccessToken: token, | ||
| })) | ||
| } | ||
| client := github.NewClient(hc) | ||
|
|
||
| log.Printf("[DEBUG] Fetching latest release from GitHub API") | ||
| release, resp, err := client.Repositories.GetLatestRelease(ctx, repoOwner, repoName) | ||
| if err != nil { | ||
| // Check if it's a rate limit error | ||
| if resp != nil && resp.StatusCode == http.StatusForbidden { | ||
| if resp.Rate.Remaining == 0 { | ||
| log.Printf("[ERROR] GitHub API rate limited, consider setting GITHUB_TOKEN") | ||
| } | ||
| } | ||
| return "", fmt.Errorf("failed to fetch latest release: %w", err) | ||
| } | ||
|
|
||
| if release.TagName == nil { | ||
| return "", fmt.Errorf("latest release has no tag name") | ||
| } | ||
|
|
||
| log.Printf("[DEBUG] Latest release: %s", *release.TagName) | ||
| return *release.TagName, nil | ||
| } |
Copilot
AI
Nov 26, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The fetchLatestRelease function lacks test coverage. Consider adding tests using a mock GitHub client or HTTP test server to verify behavior for:
- Successful API responses
- Rate limit scenarios
- Network timeouts (via context)
- Missing tag names in releases
This would improve reliability and make debugging easier.
| // CheckForUpdate checks if a new version of tflint is available | ||
| // It returns UpdateInfo indicating if an update is available and the latest version string | ||
| // Errors are logged but not returned - failures should not break the version command | ||
| func CheckForUpdate(ctx context.Context, current *version.Version) (*UpdateInfo, error) { |
Copilot
AI
Nov 26, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The function comment on line 37 states "Errors are logged but not returned - failures should not break the version command", but the function does return errors (line 53). This inconsistency between documentation and implementation is misleading. Either update the comment to reflect that errors are returned, or change the implementation to match the documented behavior of only logging errors.
| func TestCompareVersions(t *testing.T) { | ||
| tests := []struct { | ||
| name string | ||
| current string | ||
| latest string | ||
| wantAvailable bool | ||
| wantError bool | ||
| }{ | ||
| { | ||
| name: "update available", | ||
| current: "0.59.0", | ||
| latest: "0.60.0", | ||
| wantAvailable: true, | ||
| wantError: false, | ||
| }, | ||
| { | ||
| name: "v prefix stripped", | ||
| current: "0.59.0", | ||
| latest: "v0.60.0", | ||
| wantAvailable: true, | ||
| wantError: false, | ||
| }, | ||
| { | ||
| name: "already latest", | ||
| current: "0.60.0", | ||
| latest: "0.60.0", | ||
| wantAvailable: false, | ||
| wantError: false, | ||
| }, | ||
| { | ||
| name: "invalid latest version", | ||
| current: "0.60.0", | ||
| latest: "invalid", | ||
| wantAvailable: false, | ||
| wantError: true, | ||
| }, | ||
| } |
Copilot
AI
Nov 26, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The test suite lacks a test case for when the current version is newer than the latest release (e.g., running a development build like version 0.61.0 when latest is 0.60.0). Consider adding a test case like:
{
name: "current newer than latest",
current: "0.61.0",
latest: "0.60.0",
wantAvailable: false,
wantError: false,
}This ensures the version comparison works correctly for all scenarios.
| func TestEnabled(t *testing.T) { | ||
| tests := []struct { | ||
| name string | ||
| envValue string | ||
| want bool | ||
| }{ | ||
| { | ||
| name: "not set - enabled by default", | ||
| envValue: "", | ||
| want: true, | ||
| }, | ||
| { | ||
| name: "disabled", | ||
| envValue: "1", | ||
| want: false, | ||
| }, | ||
| { | ||
| name: "invalid value - enabled by default", | ||
| envValue: "invalid", | ||
| want: true, | ||
| }, | ||
| } |
Copilot
AI
Nov 26, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The test coverage for Enabled() could be more comprehensive. Consider adding test cases for other valid strconv.ParseBool values:
- "0" (should enable, as it means "false" for disable)
- "true"/"True"/"TRUE" (should disable)
- "false"/"False"/"FALSE" (should enable)
This would ensure the function handles all valid boolean string representations correctly.
versioncheck/check_test.go
Outdated
| } | ||
|
|
||
| expectedLatest := tt.latest | ||
| if expectedLatest[0] == 'v' { |
Copilot
AI
Nov 26, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Accessing expectedLatest[0] without checking if the string is empty could cause a panic if an empty string test case is added. Consider adding a length check:
if len(expectedLatest) > 0 && expectedLatest[0] == 'v' {
expectedLatest = expectedLatest[1:]
}This makes the test more robust against future modifications.
| if expectedLatest[0] == 'v' { | |
| if len(expectedLatest) > 0 && expectedLatest[0] == 'v' { |
| - `TFLINT_PLUGIN_DIR` | ||
| - Configure the plugin directory. See [Configuring Plugins](./plugins.md). | ||
| - `TFLINT_DISABLE_VERSION_CHECK` | ||
| - Disable version update notifications when running `tflint --version`. Set to `1` to disable. |
Copilot
AI
Nov 26, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The GITHUB_TOKEN environment variable is used for authenticated GitHub API requests (as mentioned in lines 23-27 of versioncheck/github.go) but is not documented in the environment variables documentation. Consider adding documentation for this optional variable explaining:
- Its purpose (to increase GitHub API rate limits from 60 to 5000 req/hour)
- When it's needed
- How to obtain a token
| - Disable version update notifications when running `tflint --version`. Set to `1` to disable. | |
| - Disable version update notifications when running `tflint --version`. Set to `1` to disable. | |
| - `GITHUB_TOKEN` | |
| - (Optional) Used for authenticated GitHub API requests to increase the rate limit from 60 to 5000 requests per hour. This is needed if you encounter rate limit errors when running commands that check for updates or interact with GitHub. You can obtain a token by creating a [GitHub personal access token](https://github.com/settings/tokens); no special scopes are required. |
| func (cli *CLI) printVersionJSON(opts Options, updateInfo *versioncheck.UpdateInfo) int { | ||
| // Build output | ||
| output := VersionOutput{ | ||
| Version: tflint.Version.String(), | ||
| Plugins: getPluginVersions(opts), | ||
| } | ||
|
|
||
| if updateInfo != nil { | ||
| output.UpdateAvailable = updateInfo.Available | ||
| if updateInfo.Available { | ||
| output.LatestVersion = updateInfo.Latest | ||
| } | ||
| } |
Copilot
AI
Nov 26, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The JSON output structure may be ambiguous when version checking fails or is disabled. The update_available field will be false in three different scenarios:
- Version check succeeded and no update is available
- Version check was disabled via
TFLINT_DISABLE_VERSION_CHECK - Version check failed (network error, API error, etc.)
Consider adding an additional field to distinguish these cases, such as:
type VersionOutput struct {
Version string `json:"version"`
Plugins []PluginVersion `json:"plugins"`
UpdateCheckEnabled bool `json:"update_check_enabled"`
UpdateAvailable bool `json:"update_available"`
LatestVersion string `json:"latest_version,omitempty"`
}This would make the JSON output clearer for programmatic consumers.
- Fix rate limit check in GitHub API error handling - Remove misleading comment about error handling - Add boundary checks in tests to prevent panics - Expand test coverage for version comparison and environment variables - Document GITHUB_TOKEN environment variable - Make GitHub client testable with httptest
Addresses errcheck linter violation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
Copilot reviewed 8 out of 8 changed files in this pull request and generated 6 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
cmd/version.go
Outdated
| // Check for updates (unless disabled) | ||
| var updateInfo *versioncheck.UpdateInfo | ||
| if versioncheck.Enabled() { | ||
| ctx, cancel := context.WithTimeout(context.Background(), versionCheckTimeout) | ||
| defer cancel() | ||
|
|
||
| info, err := versioncheck.CheckForUpdate(ctx, tflint.Version) | ||
| if err != nil { | ||
| log.Printf("[ERROR] Failed to check for updates: %s", err) | ||
| } else { | ||
| updateInfo = info | ||
| } | ||
| } | ||
|
|
||
| // If JSON format requested, output JSON | ||
| if opts.Format == "json" { | ||
| return cli.printVersionJSON(opts, updateInfo) | ||
| } | ||
|
|
||
| // Print version | ||
| fmt.Fprintf(cli.outStream, "TFLint version %s\n", tflint.Version) | ||
|
|
||
| // Print update notification if available | ||
| if updateInfo != nil && updateInfo.Available { | ||
| fmt.Fprintf(cli.outStream, "\n") | ||
| fmt.Fprintf(cli.outStream, "Your version of TFLint is out of date! The latest version\n") | ||
| fmt.Fprintf(cli.outStream, "is %s. You can update by downloading from https://github.com/terraform-linters/tflint/releases\n", updateInfo.Latest) | ||
| } |
Copilot
AI
Nov 27, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[nitpick] The version check is performed synchronously before printing the version (lines 37-49), which delays the version output by up to 3 seconds even when the cache is valid. Consider performing the version check and output in parallel: print the version immediately, then show the update notification after if needed. This would improve perceived responsiveness for users.
versioncheck/github_test.go
Outdated
| client := &http.Client{Transport: http.DefaultTransport} | ||
| ghClient := github.NewClient(client) | ||
| serverURL, _ := url.Parse(server.URL + "/") | ||
| ghClient.BaseURL = serverURL |
Copilot
AI
Nov 27, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The ghClient variable is created but never used in the test. These lines (79-82) create a GitHub client and set its base URL, but then a separate testClient is created on lines 85-89. Consider removing these unused lines to clean up the test code.
| client := &http.Client{Transport: http.DefaultTransport} | |
| ghClient := github.NewClient(client) | |
| serverURL, _ := url.Parse(server.URL + "/") | |
| ghClient.BaseURL = serverURL |
| - `GITHUB_TOKEN` | ||
| - (Optional) Used for authenticated GitHub API requests when checking for updates and downloading plugins. Increases the rate limit from 60 to 5000 requests per hour. Useful if you encounter rate limit errors. You can obtain a token by creating a [GitHub personal access token](https://github.com/settings/tokens); no special scopes are required. |
Copilot
AI
Nov 27, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[nitpick] The GITHUB_TOKEN documentation mentions it's used "when checking for updates and downloading plugins" but this placement under version-related variables might be confusing. Consider moving this documentation to a more general location since it already exists elsewhere in the docs and is used for multiple purposes, not just version checking. Also note that the existing plugin code already documents GITHUB_TOKEN usage in the install.go comments (lines 336-344).
cmd/version.go
Outdated
| func (cli *CLI) printVersion(opts Options) int { | ||
| // Check for updates (unless disabled) | ||
| var updateInfo *versioncheck.UpdateInfo | ||
| if versioncheck.Enabled() { | ||
| ctx, cancel := context.WithTimeout(context.Background(), versionCheckTimeout) | ||
| defer cancel() | ||
|
|
||
| info, err := versioncheck.CheckForUpdate(ctx, tflint.Version) | ||
| if err != nil { | ||
| log.Printf("[ERROR] Failed to check for updates: %s", err) | ||
| } else { | ||
| updateInfo = info | ||
| } | ||
| } | ||
|
|
||
| // If JSON format requested, output JSON | ||
| if opts.Format == "json" { | ||
| return cli.printVersionJSON(opts, updateInfo) | ||
| } | ||
|
|
||
| // Print version | ||
| fmt.Fprintf(cli.outStream, "TFLint version %s\n", tflint.Version) | ||
|
|
||
| // Print update notification if available | ||
| if updateInfo != nil && updateInfo.Available { | ||
| fmt.Fprintf(cli.outStream, "\n") | ||
| fmt.Fprintf(cli.outStream, "Your version of TFLint is out of date! The latest version\n") | ||
| fmt.Fprintf(cli.outStream, "is %s. You can update by downloading from https://github.com/terraform-linters/tflint/releases\n", updateInfo.Latest) | ||
| } |
Copilot
AI
Nov 27, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[nitpick] Consider adding integration tests for the new version check functionality and JSON output format. The existing integration tests in integrationtest/cli/cli_test.go test basic version output, but don't cover the new features like update notifications or --format json with the version command. This would help ensure the feature works end-to-end in real-world scenarios.
cmd/version.go
Outdated
| fmt.Fprintf(cli.outStream, "\n") | ||
| fmt.Fprintf(cli.outStream, "Your version of TFLint is out of date! The latest version\n") | ||
| fmt.Fprintf(cli.outStream, "is %s. You can update by downloading from https://github.com/terraform-linters/tflint/releases\n", updateInfo.Latest) |
Copilot
AI
Nov 27, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[nitpick] The update notification message uses fmt.Fprintf with multiple separate calls, which could lead to interleaved output in concurrent scenarios. Consider consolidating into a single fmt.Fprintf call or using a formatted string. For example:
fmt.Fprintf(cli.outStream, "\nYour version of TFLint is out of date! The latest version\nis %s. You can update by downloading from https://github.com/terraform-linters/tflint/releases\n", updateInfo.Latest)| fmt.Fprintf(cli.outStream, "\n") | |
| fmt.Fprintf(cli.outStream, "Your version of TFLint is out of date! The latest version\n") | |
| fmt.Fprintf(cli.outStream, "is %s. You can update by downloading from https://github.com/terraform-linters/tflint/releases\n", updateInfo.Latest) | |
| fmt.Fprintf(cli.outStream, "\nYour version of TFLint is out of date! The latest version\nis %s. You can update by downloading from https://github.com/terraform-linters/tflint/releases\n", updateInfo.Latest) |
| if err := os.WriteFile(cachePath, data, 0644); err != nil { | ||
| return err | ||
| } | ||
|
|
Copilot
AI
Nov 27, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[nitpick] The cache file operations in loadCache and saveCache are not protected against concurrent access. If multiple tflint processes run --version simultaneously, they could race on reading/writing the cache file. Consider using file locking (e.g., syscall.Flock on Unix or similar) or atomic file operations (write to temp file, then rename) to prevent potential corruption. However, since cache corruption would only result in an extra API call (which is not critical), this may be acceptable as-is.
| if err := os.WriteFile(cachePath, data, 0644); err != nil { | |
| return err | |
| } | |
| // Write to a temp file, then atomically rename | |
| tmpFile, err := os.CreateTemp(cacheDir, "version_check_cache_*.tmp") | |
| if err != nil { | |
| return err | |
| } | |
| defer func() { | |
| tmpFile.Close() | |
| os.Remove(tmpFile.Name()) // Clean up temp file if rename fails | |
| }() | |
| if _, err := tmpFile.Write(data); err != nil { | |
| return err | |
| } | |
| if err := tmpFile.Sync(); err != nil { | |
| return err | |
| } | |
| if err := tmpFile.Close(); err != nil { | |
| return err | |
| } | |
| if err := os.Rename(tmpFile.Name(), cachePath); err != nil { | |
| return err | |
| } |
- Add update_check_enabled field to JSON output for clarity - Remove unused test variables in github_test.go - Consolidate notification message for thread safety - Make version check async for text format to improve responsiveness
Implements version update notifications similar to Terraform. When running
tflint --version, users now see a notification if a newer version is available on GitHub Releases.Changes
versioncheckpackage that checks GitHub Releases API for latest version--versioncommand to display update notifications when newer version available--format jsonsupport to version command with structured output including update statusTFLINT_DISABLE_VERSION_CHECKenvironment variable to opt outImplementation Details
Version checking happens asynchronously for text output (prints version immediately, shows update after plugins). JSON format performs synchronous check to ensure complete data in single output.
Results are cached in the OS cache directory (
~/Library/Caches/tflinton macOS,~/.cache/tflinton Linux) for 48 hours to minimize API calls.The check uses GitHub's public API (60 req/hour unauthenticated) or
GITHUB_TOKENenvironment variable for higher rate limits (5000 req/hour).Errors are logged but non-fatal - network timeouts or API failures don't break the version command.
JSON Output
JSON output includes
update_check_enabledfield to distinguish between disabled checking vs. no update available:update_check_enabled: false→ checking disabled via env varupdate_check_enabled: true, update_available: false→ no update or check failedupdate_check_enabled: true, update_available: true→ update availableTesting
Verified version checking against live GitHub API with both outdated and current versions. Added unit testing against the internals.
References
Closes #883