diff --git a/README.md b/README.md index 8e03f0e1..ef33d992 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,11 @@ The official Infisical CLI: Inject secrets into applications and manage your Infisical infrastructure.

+> [!IMPORTANT] +> **The Infisical CLI Linux package repository is moving off Cloudsmith.** To keep up with download volume, we're migrating the Linux package repository to our own host at `artifacts-cli.infisical.com`. Cloudsmith downloads will stop being served on **September 16th, 2026**, after which installs and updates from the old URL will fail. +> +> Every release, including all older versions, is already available on the new host. If you're on an existing setup, you don't need to change anything else, just repoint your machine to the new artifact URL by following the [migration steps](https://infisical.com/docs/cli/cloudsmith-migration). + ## Introduction The **[Infisical CLI](https://infisical.com/docs/cli/overview)** is a powerful command-line tool for secret management that allows you to: diff --git a/packages/cmd/root.go b/packages/cmd/root.go index de1b0b82..e690ed6c 100644 --- a/packages/cmd/root.go +++ b/packages/cmd/root.go @@ -132,9 +132,9 @@ func init() { config.INFISICAL_URL = util.AppendAPIEndpoint(resolveDomain(cmd, config.INFISICAL_URL)) - // util.DisplayAptInstallationChangeBannerWithWriter(silent, cmd.ErrOrStderr()) if !util.IsRunningInDocker() && !silent && !isStructuredOutputRequested(cmd) { util.CheckForUpdateWithWriter(cmd.ErrOrStderr()) + util.DisplayPackageRepoMigrationNoticeWithWriter(silent, cmd.ErrOrStderr()) } loggedInDetails, err := util.GetCurrentLoggedInUserDetails(false) diff --git a/packages/util/check-for-update.go b/packages/util/check-for-update.go index d59b9528..dc90713b 100644 --- a/packages/util/check-for-update.go +++ b/packages/util/check-for-update.go @@ -209,27 +209,87 @@ func writeUpdateCheckCache(cache *UpdateCheckCache) error { return nil } -func DisplayAptInstallationChangeBanner(isSilent bool) { - DisplayAptInstallationChangeBannerWithWriter(isSilent, os.Stderr) +const migrationNoticeCacheTTL = 24 * time.Hour + +const migrationGuideURL = "https://infisical.com/docs/cli/cloudsmith-migration" + +const migrationCloudsmithSunset = "September 16, 2026" + +type migrationNoticeCache struct { + LastShownTime time.Time `json:"lastShownTime"` } -func DisplayAptInstallationChangeBannerWithWriter(isSilent bool, w io.Writer) { - if isSilent { +func getMigrationNoticeCachePath() (string, error) { + homeDir, err := GetHomeDir() + if err != nil { + return "", err + } + return filepath.Join(homeDir, CONFIG_FOLDER_NAME, MIGRATION_NOTICE_CACHE_FILE_NAME), nil +} + +// migrationNoticeRecentlyShown returns true if the notice was shown within the TTL. +func migrationNoticeRecentlyShown() bool { + path, err := getMigrationNoticeCachePath() + if err != nil { + return false + } + data, err := os.ReadFile(path) + if err != nil { + return false + } + var cache migrationNoticeCache + if err := json.Unmarshal(data, &cache); err != nil { + return false + } + return time.Since(cache.LastShownTime) < migrationNoticeCacheTTL +} + +// recordMigrationNoticeShown stamps the cache so the notice is throttled. +func recordMigrationNoticeShown() { + path, err := getMigrationNoticeCachePath() + if err != nil { + return + } + if err := os.MkdirAll(filepath.Dir(path), 0700); err != nil { return } + data, err := json.Marshal(migrationNoticeCache{LastShownTime: time.Now()}) + if err != nil { + return + } + // Best-effort write; a failure just means the notice may show again next run. + _ = os.WriteFile(path, data, 0600) +} - if runtime.GOOS == "linux" { - _, err := exec.LookPath("apt-get") - isApt := err == nil - if isApt { - yellow := color.New(color.FgYellow).SprintFunc() - msg := fmt.Sprintf("%s", - yellow("Update Required: Your current package installation script is outdated and will no longer receive updates.\nPlease update to the new installation script which can be found here https://infisical.com/docs/cli/overview#installation debian section\n"), - ) +func DisplayPackageRepoMigrationNotice(isSilent bool) { + DisplayPackageRepoMigrationNoticeWithWriter(isSilent, os.Stderr) +} - fmt.Fprintln(w, msg) - } +// DisplayPackageRepoMigrationNoticeWithWriter prints a one-time-per-day notice +// that the Linux package repository has moved off Cloudsmith. +// Stays quiet in --silent mode, and can be disabled with INFISICAL_DISABLE_MIGRATION_NOTICE. +func DisplayPackageRepoMigrationNoticeWithWriter(isSilent bool, w io.Writer) { + if isSilent { + return + } + if os.Getenv("INFISICAL_DISABLE_MIGRATION_NOTICE") != "" { + return } + if migrationNoticeRecentlyShown() { + return + } + + yellow := color.New(color.FgYellow).SprintFunc() + bold := color.New(color.FgYellow, color.Bold).SprintFunc() + fmt.Fprintln(w, bold("Important: the Infisical CLI Linux package repository is moving off Cloudsmith.")) + fmt.Fprintln(w, yellow( + "What's happening: Cloudsmith stops serving on "+migrationCloudsmithSunset+". After that, installing or\n"+ + "updating the CLI on Linux from the old Cloudsmith URL (apt, yum/dnf, apk) will fail.\n"+ + "What to do: repoint your machine to the new host (artifacts-cli.infisical.com).\n"+ + "Migration steps: "+migrationGuideURL+"\n", + )) + + recordMigrationNoticeShown() } func getLatestTag(repoOwner string, repoName string) (string, time.Time, bool, error) { diff --git a/packages/util/constants.go b/packages/util/constants.go index 22bdf606..50300e85 100644 --- a/packages/util/constants.go +++ b/packages/util/constants.go @@ -69,7 +69,8 @@ const ( KUBERNETES_SERVICE_ACCOUNT_CA_CERT_PATH = "/var/run/secrets/kubernetes.io/serviceaccount/ca.crt" KUBERNETES_SERVICE_ACCOUNT_TOKEN_PATH = "/var/run/secrets/kubernetes.io/serviceaccount/token" - UPDATE_CHECK_CACHE_FILE_NAME = "update-check.json" + UPDATE_CHECK_CACHE_FILE_NAME = "update-check.json" + MIGRATION_NOTICE_CACHE_FILE_NAME = "migration-notice.json" ) var ( diff --git a/upload_to_s3.sh b/upload_to_s3.sh index 0766c1fe..8e1653db 100755 --- a/upload_to_s3.sh +++ b/upload_to_s3.sh @@ -65,7 +65,20 @@ if ls *.apk 1> /dev/null 2>&1; then # Sync existing packages from S3 (to preserve old versions) echo "Syncing existing APK packages from S3..." aws s3 sync "s3://$INFISICAL_CLI_S3_BUCKET/apk/" apk-staging/ --exclude "*/APKINDEX.tar.gz" - + + # Integrity gate: the APKINDEX is rebuilt from staging, so a partial sync would + # silently drop versions. Staging = synced repo + new apks, so it must have at + # least as many .apk as S3 per arch; if fewer, the sync was incomplete, so abort. + for arch in x86_64 aarch64; do + s3_count=$(aws s3 ls "s3://$INFISICAL_CLI_S3_BUCKET/apk/stable/main/$arch/" 2>/dev/null | grep -c '\.apk$' || true) + local_count=$(ls apk-staging/stable/main/$arch/*.apk 2>/dev/null | wc -l | tr -d ' ') + if [ "$local_count" -lt "$s3_count" ]; then + echo "Error: APK sync incomplete for $arch (S3 has $s3_count, staged $local_count). Aborting to avoid publishing a stale APKINDEX." + exit 1 + fi + echo "APK integrity OK for $arch: staged $local_count >= S3 $s3_count" + done + # Validate APK private key exists if [ ! -f "$APK_PRIVATE_KEY_PATH" ]; then echo "Error: APK private key not found at $APK_PRIVATE_KEY_PATH"