Skip to content
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

Introducing JFrog CLI AI commands assistant #2703

Merged
merged 21 commits into from
Sep 24, 2024
Merged
Show file tree
Hide file tree
Changes from 11 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
30 changes: 29 additions & 1 deletion general/ai/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ type ApiCommand string
const (
cliAiAppApiUrl = "https://cli-ai-app-stg.jfrog.info/api/"
askRateLimitHeader = "X-JFrog-CLI-AI"
// The latest version of the terms and conditions for using the AI interface. (https://docs.jfrog-applications.jfrog.io/jfrog-applications/jfrog-cli/cli-ai/terms)
aiTermsRevision = 1
)

type ApiType string
Expand All @@ -41,9 +43,16 @@ func HowCmd(c *cli.Context) error {
if c.NArg() > 0 {
return cliutils.WrongNumberOfArgumentsHandler(c)
}
log.Output(coreutils.PrintLink("This AI-based interface converts your natural language inputs into fully functional JFrog CLI commands.\n" +
log.Output(coreutils.PrintLink("This AI-based interface converts natural language inputs into AI-generated JFrog CLI commands.\n" +
"For more information about this interface, see https://docs.jfrog-applications.jfrog.io/jfrog-applications/jfrog-cli/cli-ai\n" +
"NOTE: This is an experimental version and it supports mostly Artifactory and Xray commands.\n"))

// Ask the user to agree to the terms and conditions. If the user does not agree, the command will not proceed.
// Ask this only once per JFrog CLI installation, unless the terms are updated.
if agreed, err := handleAiTermsAgreement(); err != nil || !agreed {
return err
}

for {
var question string
scanner := bufio.NewScanner(os.Stdin)
Expand Down Expand Up @@ -176,3 +185,22 @@ func sendRestAPI(apiType ApiType, content interface{}) (response string, err err
response = strings.TrimSpace(string(body))
return
}

func handleAiTermsAgreement() (bool, error) {
latestTermsVer, err := cliutils.GetLatestAiTermsRevision()
if err != nil {
return false, err
}
if latestTermsVer == nil || *latestTermsVer < aiTermsRevision {
if !coreutils.AskYesNo("By using this interface, you agree to the terms of JFrog's AI Addendum on behalf of your organization as an active JFrog customer.\n"+
"Review these terms at "+coreutils.PrintLink("https://docs.jfrog-applications.jfrog.io/jfrog-applications/jfrog-cli/cli-ai/terms")+
"\nDo you agree?", false) {
return false, nil
}
if err = cliutils.SetLatestAiTermsRevision(aiTermsRevision); err != nil {
return false, err
}
log.Output()
}
return true, nil
}
12 changes: 6 additions & 6 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ replace (
require (
github.com/agnivade/levenshtein v1.1.1
github.com/buger/jsonparser v1.1.1
github.com/docker/docker v27.2.1+incompatible
github.com/docker/docker v27.3.1+incompatible
github.com/gocarina/gocsv v0.0.0-20240520201108-78e41c74b4b1
github.com/jfrog/archiver/v3 v3.6.1
github.com/jfrog/build-info-go v1.10.0
Expand Down Expand Up @@ -148,11 +148,11 @@ require (
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
github.com/yusufpapurcu/wmi v1.2.3 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 // indirect
go.opentelemetry.io/otel v1.29.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.29.0 // indirect
go.opentelemetry.io/otel/metric v1.29.0 // indirect
go.opentelemetry.io/otel/sdk v1.29.0 // indirect
go.opentelemetry.io/otel/trace v1.29.0 // indirect
go.opentelemetry.io/otel v1.30.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.30.0 // indirect
go.opentelemetry.io/otel/metric v1.30.0 // indirect
go.opentelemetry.io/otel/sdk v1.30.0 // indirect
go.opentelemetry.io/otel/trace v1.30.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
golang.org/x/crypto v0.27.0 // indirect
golang.org/x/mod v0.21.0 // indirect
Expand Down
40 changes: 20 additions & 20 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -707,8 +707,8 @@ github.com/dgryski/trifles v0.0.0-20200323201526-dd97f9abfb48 h1:fRzb/w+pyskVMQ+
github.com/dgryski/trifles v0.0.0-20200323201526-dd97f9abfb48/go.mod h1:if7Fbed8SFyPtHLHbg49SI7NAdJiC5WIA09pe59rfAA=
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
github.com/docker/docker v27.2.1+incompatible h1:fQdiLfW7VLscyoeYEBz7/J8soYFDZV1u6VW6gJEjNMI=
github.com/docker/docker v27.2.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/docker v27.3.1+incompatible h1:KttF0XoteNTicmUtBO0L2tP+J7FGRFTjaEF4k6WdhfI=
github.com/docker/docker v27.3.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c=
github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc=
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
Expand Down Expand Up @@ -1195,18 +1195,18 @@ go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E=
go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 h1:jq9TW8u3so/bN+JPT166wjOI6/vQPF6Xe7nMNIltagk=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0/go.mod h1:p8pYQP+m5XfbZm9fxtSKAbM6oIllS7s2AfxrChvc7iw=
go.opentelemetry.io/otel v1.29.0 h1:PdomN/Al4q/lN6iBJEN3AwPvUiHPMlt93c8bqTG5Llw=
go.opentelemetry.io/otel v1.29.0/go.mod h1:N/WtXPs1CNCUEx+Agz5uouwCba+i+bJGFicT8SR4NP8=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.29.0 h1:dIIDULZJpgdiHz5tXrTgKIMLkus6jEFa7x5SOKcyR7E=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.29.0/go.mod h1:jlRVBe7+Z1wyxFSUs48L6OBQZ5JwH2Hg/Vbl+t9rAgI=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.29.0 h1:JAv0Jwtl01UFiyWZEMiJZBiTlv5A50zNs8lsthXqIio=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.29.0/go.mod h1:QNKLmUEAq2QUbPQUfvw4fmv0bgbK7UlOSFCnXyfvSNc=
go.opentelemetry.io/otel/metric v1.29.0 h1:vPf/HFWTNkPu1aYeIsc98l4ktOQaL6LeSoeV2g+8YLc=
go.opentelemetry.io/otel/metric v1.29.0/go.mod h1:auu/QWieFVWx+DmQOUMgj0F8LHWdgalxXqvp7BII/W8=
go.opentelemetry.io/otel/sdk v1.29.0 h1:vkqKjk7gwhS8VaWb0POZKmIEDimRCMsopNYnriHyryo=
go.opentelemetry.io/otel/sdk v1.29.0/go.mod h1:pM8Dx5WKnvxLCb+8lG1PRNIDxu9g9b9g59Qr7hfAAok=
go.opentelemetry.io/otel/trace v1.29.0 h1:J/8ZNK4XgR7a21DZUAsbF8pZ5Jcw1VhACmnYt39JTi4=
go.opentelemetry.io/otel/trace v1.29.0/go.mod h1:eHl3w0sp3paPkYstJOmAimxhiFXPg+MMTlEh3nsQgWQ=
go.opentelemetry.io/otel v1.30.0 h1:F2t8sK4qf1fAmY9ua4ohFS/K+FUuOPemHUIXHtktrts=
go.opentelemetry.io/otel v1.30.0/go.mod h1:tFw4Br9b7fOS+uEao81PJjVMjW/5fvNCbpsDIXqP0pc=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.30.0 h1:lsInsfvhVIfOI6qHVyysXMNDnjO9Npvl7tlDPJFBVd4=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.30.0/go.mod h1:KQsVNh4OjgjTG0G6EiNi1jVpnaeeKsKMRwbLN+f1+8M=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.30.0 h1:umZgi92IyxfXd/l4kaDhnKgY8rnN/cZcF1LKc6I8OQ8=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.30.0/go.mod h1:4lVs6obhSVRb1EW5FhOuBTyiQhtRtAnnva9vD3yRfq8=
go.opentelemetry.io/otel/metric v1.30.0 h1:4xNulvn9gjzo4hjg+wzIKG7iNFEaBMX00Qd4QIZs7+w=
go.opentelemetry.io/otel/metric v1.30.0/go.mod h1:aXTfST94tswhWEb+5QjlSqG+cZlmyXy/u8jFpor3WqQ=
go.opentelemetry.io/otel/sdk v1.30.0 h1:cHdik6irO49R5IysVhdn8oaiR9m8XluDaJAs4DfOrYE=
go.opentelemetry.io/otel/sdk v1.30.0/go.mod h1:p14X4Ok8S+sygzblytT1nqG98QG2KYKv++HE0LY/mhg=
go.opentelemetry.io/otel/trace v1.30.0 h1:7UBkkYzeg3C7kQX8VAidWh2biiQbtAKjyIML8dQ9wmc=
go.opentelemetry.io/otel/trace v1.30.0/go.mod h1:5EyKqTzzmyqB9bwtCCq6pDLktPK6fmGf/Dph+8VI02o=
go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
go.opentelemetry.io/proto/otlp v0.15.0/go.mod h1:H7XAot3MsfNsj7EXtrA2q5xSNQ10UqI405h3+duxN4U=
go.opentelemetry.io/proto/otlp v0.19.0/go.mod h1:H7XAot3MsfNsj7EXtrA2q5xSNQ10UqI405h3+duxN4U=
Expand Down Expand Up @@ -1836,15 +1836,15 @@ google.golang.org/genproto/googleapis/api v0.0.0-20230525234020-1aefcd67740a/go.
google.golang.org/genproto/googleapis/api v0.0.0-20230525234035-dd9d682886f9/go.mod h1:vHYtlOoi6TsQ3Uk2yxR7NI5z8uoV+3pZtR4jmHIkRig=
google.golang.org/genproto/googleapis/api v0.0.0-20230526203410-71b5a4ffd15e/go.mod h1:vHYtlOoi6TsQ3Uk2yxR7NI5z8uoV+3pZtR4jmHIkRig=
google.golang.org/genproto/googleapis/api v0.0.0-20230530153820-e85fd2cbaebc/go.mod h1:vHYtlOoi6TsQ3Uk2yxR7NI5z8uoV+3pZtR4jmHIkRig=
google.golang.org/genproto/googleapis/api v0.0.0-20240822170219-fc7c04adadcd h1:BBOTEWLuuEGQy9n1y9MhVJ9Qt0BDu21X8qZs71/uPZo=
google.golang.org/genproto/googleapis/api v0.0.0-20240822170219-fc7c04adadcd/go.mod h1:fO8wJzT2zbQbAjbIoos1285VfEIYKDDY+Dt+WpTkh6g=
google.golang.org/genproto/googleapis/api v0.0.0-20240903143218-8af14fe29dc1 h1:hjSy6tcFQZ171igDaN5QHOw2n6vx40juYbC/x67CEhc=
google.golang.org/genproto/googleapis/api v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:qpvKtACPCQhAdu3PyQgV4l3LMXZEtft7y8QcarRsp9I=
google.golang.org/genproto/googleapis/bytestream v0.0.0-20230530153820-e85fd2cbaebc/go.mod h1:ylj+BE99M198VPbBh6A8d9n3w8fChvyLK3wwBOjXBFA=
google.golang.org/genproto/googleapis/rpc v0.0.0-20230525234015-3fc162c6f38a/go.mod h1:xURIpW9ES5+/GZhnV6beoEtxQrnkRGIfP5VQG2tCBLc=
google.golang.org/genproto/googleapis/rpc v0.0.0-20230525234030-28d5490b6b19/go.mod h1:66JfowdXAEgad5O9NnYcsNPLCPZJD++2L9X0PCMODrA=
google.golang.org/genproto/googleapis/rpc v0.0.0-20230526203410-71b5a4ffd15e/go.mod h1:66JfowdXAEgad5O9NnYcsNPLCPZJD++2L9X0PCMODrA=
google.golang.org/genproto/googleapis/rpc v0.0.0-20230530153820-e85fd2cbaebc/go.mod h1:66JfowdXAEgad5O9NnYcsNPLCPZJD++2L9X0PCMODrA=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240822170219-fc7c04adadcd h1:6TEm2ZxXoQmFWFlt1vNxvVOa1Q0dXFQD1m/rYjXmS0E=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240822170219-fc7c04adadcd/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 h1:pPJltXNxVzT4pK9yD8vR9X75DaWYYmLGMsEvBfFQZzQ=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
Expand Down Expand Up @@ -1886,8 +1886,8 @@ google.golang.org/grpc v1.52.0/go.mod h1:pu6fVzoFb+NBYNAvQL08ic+lvB2IojljRYuun5v
google.golang.org/grpc v1.53.0/go.mod h1:OnIrk0ipVdj4N5d9IUoFUx72/VlD7+jUsHwZgwSMQpw=
google.golang.org/grpc v1.54.0/go.mod h1:PUSEXI6iWghWaB6lXM4knEgpJNu2qUcKfDtNci3EC2g=
google.golang.org/grpc v1.55.0/go.mod h1:iYEXKGkEBhg1PjZQvoYEVPTDkHo1/bjTnfwTeGONTY8=
google.golang.org/grpc v1.65.0 h1:bs/cUb4lp1G5iImFFd3u5ixQzweKizoZJAwBNLR42lc=
google.golang.org/grpc v1.65.0/go.mod h1:WgYC2ypjlB0EiQi6wdKixMqukr6lBc0Vo+oOgjrM5ZQ=
google.golang.org/grpc v1.66.1 h1:hO5qAXR19+/Z44hmvIM4dQFMSYX9XcWsByfoxutBpAM=
google.golang.org/grpc v1.66.1/go.mod h1:s3/l6xSSCURdVfAnL+TqCNMyTDAGN6+lZeVxnZR128Y=
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
Expand Down
129 changes: 129 additions & 0 deletions utils/cliutils/persistence.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
package cliutils

import (
"encoding/json"
"github.com/jfrog/jfrog-cli-core/v2/utils/coreutils"
"github.com/jfrog/jfrog-client-go/utils/errorutils"
"github.com/jfrog/jfrog-client-go/utils/io/fileutils"
"os"
"path/filepath"
gosync "sync"
)

const persistenceFileName = "persistence.json"

// PersistenceFile holds varius indicators that need to be persisted between CLI runs inside the JFrog home directory
// for example, we keep the latest version check time to avoid checking for updates too frequently
type PersistenceFile struct {
LatestCliVersionCheckTime *int64 `json:"latestCliVersionCheckTime,omitempty"`
LatestAiTermsRevision *int `json:"latestAiTermsRevision,omitempty"`
}

var (
persistenceFilePath string
fileLock gosync.Mutex
)

// init initializes the persistence file path once, and stores it for future use
func init() {
homeDir, err := coreutils.GetJfrogHomeDir()
if err != nil {
panic("Failed to get JFrog home directory: : " + err.Error())
}
persistenceFilePath = filepath.Join(homeDir, persistenceFileName)
}

// setCliLatestVersionCheckTime updates the latest version check time in the persistence file
func setCliLatestVersionCheckTime(timestamp int64) error {
fileLock.Lock()
defer fileLock.Unlock()

info, err := getPersistenceInfo()
if err != nil {
return err
}

info.LatestCliVersionCheckTime = &timestamp
return setPersistenceInfo(info)
}

// getLatestCliVersionCheckTime retrieves the latest version check time from the persistence file
func getLatestCliVersionCheckTime() (*int64, error) {
fileLock.Lock()
defer fileLock.Unlock()

info, err := getPersistenceInfo()
if err != nil {
return nil, err
}

return info.LatestCliVersionCheckTime, nil
}

// SetLatestAiTermsRevision updates the AI terms version in the persistence file
func SetLatestAiTermsRevision(version int) error {
sverdlov93 marked this conversation as resolved.
Show resolved Hide resolved
fileLock.Lock()
defer fileLock.Unlock()

info, err := getPersistenceInfo()
if err != nil {
return err
}

info.LatestAiTermsRevision = &version
return setPersistenceInfo(info)
}

// GetLatestAiTermsRevision retrieves the AI terms version from the persistence file
func GetLatestAiTermsRevision() (*int, error) {
sverdlov93 marked this conversation as resolved.
Show resolved Hide resolved
fileLock.Lock()
defer fileLock.Unlock()

info, err := getPersistenceInfo()
if err != nil {
return nil, err
}

return info.LatestAiTermsRevision, nil
}

// getPersistenceInfo reads the persistence file, creates it if it doesn't exist, and returns the persisted info
func getPersistenceInfo() (*PersistenceFile, error) {
if exists, err := fileutils.IsFileExists(persistenceFilePath, false); err != nil || !exists {
if err != nil {
return nil, err
}
// Create an empty persistence file if it doesn't exist
pFile := &PersistenceFile{}
if err = setPersistenceInfo(pFile); err != nil {
return nil, errorutils.CheckErrorf("failed while attempting to initialize persistence file: " + err.Error())
}
return pFile, nil
}

data, err := os.ReadFile(persistenceFilePath)
if err != nil {
return nil, errorutils.CheckErrorf("failed while attempting to read persistence file: " + err.Error())
}

var info PersistenceFile
if err = json.Unmarshal(data, &info); err != nil {
return nil, errorutils.CheckErrorf("failed while attempting to parse persistence file: " + err.Error())
}

return &info, nil
}

// setPersistenceInfo writes the given info to the persistence file
func setPersistenceInfo(info *PersistenceFile) error {
data, err := json.MarshalIndent(info, "", " ")
if err != nil {
return errorutils.CheckErrorf("failed while attempting to create persistence file: " + err.Error())
}

err = os.WriteFile(persistenceFilePath, data, 0644)
if err != nil {
sverdlov93 marked this conversation as resolved.
Show resolved Hide resolved
return errorutils.CheckErrorf("failed while attempting to write persistence file: " + err.Error())
}
return nil
}
65 changes: 65 additions & 0 deletions utils/cliutils/persistence_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package cliutils

import (
"os"
"path/filepath"
"testing"
"time"

"github.com/stretchr/testify/assert"
)

// TestSetAndGetLatestVersionCheckTime tests setting and getting the LatestCliVersionCheckTime
func TestSetAndGetLatestVersionCheckTime(t *testing.T) {
// Setup temporary directory
persistenceFilePath = filepath.Join(t.TempDir(), persistenceFileName)

// Set the timestamp
timestamp := time.Now().UnixMilli()
err := setCliLatestVersionCheckTime(timestamp)
assert.NoError(t, err, "Failed to set LatestCliVersionCheckTime")

// Get the timestamp
storedTimestamp, err := getLatestCliVersionCheckTime()
assert.NoError(t, err, "Failed to get LatestCliVersionCheckTime")

// Assert equality
assert.Equal(t, timestamp, *storedTimestamp, "Stored timestamp does not match the set timestamp")
}

// TestSetAndGetAiTermsVersion tests setting and getting the LatestAiTermsRevision
func TestSetAndGetAiTermsVersion(t *testing.T) {
// Setup temporary directory
persistenceFilePath = filepath.Join(t.TempDir(), persistenceFileName)

// Set the AI terms version
version := 42
err := SetLatestAiTermsRevision(version)
assert.NoError(t, err, "Failed to set LatestAiTermsRevision")

// Get the AI terms version
storedVersion, err := GetLatestAiTermsRevision()
assert.NoError(t, err, "Failed to get LatestAiTermsRevision")

// Assert equality
assert.Equal(t, version, *storedVersion, "Stored AI terms version does not match the set version")
}

// TestPersistenceFileCreation tests if the persistence file is created when it doesn't exist
func TestPersistenceFileCreation(t *testing.T) {
// Setup temporary directory
persistenceFilePath = filepath.Join(t.TempDir(), persistenceFileName)

// Ensure the persistence file doesn't exist
_, err := os.Stat(persistenceFilePath)
assert.ErrorIs(t, err, os.ErrNotExist, "Expected error to be os.ErrNotExist")

// Trigger file creation by setting version check time
timestamp := time.Now().UnixMilli()
err = setCliLatestVersionCheckTime(timestamp)
assert.NoError(t, err, "Failed to set LatestCliVersionCheckTime")

// Verify the persistence file was created
_, err = os.Stat(persistenceFilePath)
assert.False(t, os.IsNotExist(err), "Expected file to exist, but it does not")
}
19 changes: 10 additions & 9 deletions utils/cliutils/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import (
"io"
"net/http"
"os"
"path"
"path/filepath"
"strconv"
"strings"
Expand Down Expand Up @@ -695,25 +694,27 @@ func GetDetailedSummary(c *cli.Context) bool {
return c.Bool("detailed-summary") || commandsummary.ShouldRecordSummary()
}

// Check if the latest CLI version should be checked via the GitHub API to let the user know if a newer version is available.
// To avoid checking this for every run, we check only if the last check was more than 6 hours ago.
// Also, if the user set the JFROG_CLI_AVOID_NEW_VERSION_WARNING environment variable to true, we won't check.
func shouldCheckLatestCliVersion() (shouldCheck bool, err error) {
if strings.ToLower(os.Getenv(JfrogCliAvoidNewVersionWarning)) == "true" {
return
}
homeDir, err := coreutils.GetJfrogHomeDir()
latestVersionCheckTime, err := getLatestCliVersionCheckTime()
if err != nil {
return
}
indicatorFile := path.Join(homeDir, "Latest_Cli_Version_Check_Indicator")
fileInfo, err := os.Stat(indicatorFile)
if err != nil && !os.IsNotExist(err) {
err = fmt.Errorf("couldn't get indicator file %s info: %s", indicatorFile, err.Error())
timeNow := time.Now().UnixMilli()
if latestVersionCheckTime != nil &&
(timeNow-*latestVersionCheckTime) < LatestCliVersionCheckInterval.Milliseconds() {
// Timestamp file exists and updated less than 6 hours ago, therefor no need to check version again
return
}
if err == nil && (time.Now().UnixMilli()-fileInfo.ModTime().UnixMilli()) < LatestCliVersionCheckInterval.Milliseconds() {
// Timestamp file exists and updated less than 6 hours ago, therefor no need to check version again
if err = setCliLatestVersionCheckTime(timeNow); err != nil {
return
}
return true, os.WriteFile(indicatorFile, []byte{}, 0666)
return true, nil
}

sverdlov93 marked this conversation as resolved.
Show resolved Hide resolved
func getLatestCliVersionFromGithubAPI() (githubVersionInfo githubResponse, err error) {
Expand Down
Loading
Loading