From 8fd22880c31c3c3b7ebb2f37ee95361c74b4b187 Mon Sep 17 00:00:00 2001 From: "Christian G. Warden" Date: Tue, 20 Jan 2026 15:21:13 -0600 Subject: [PATCH] Add --release Flag To `login scratch` Command Allow specifying "preview" or "previous" release when creating scratch orgs. This sets the Release field on the ScratchOrgInfo record. --- command/login.go | 34 ++++++++++++++++++++++---- command/login_test.go | 48 +++++++++++++++++++++++++++++++++++++ docs/force_login_scratch.md | 7 ++++++ lib/auth.go | 6 ++++- lib/scratch.go | 7 ++++++ 5 files changed, 97 insertions(+), 5 deletions(-) diff --git a/command/login.go b/command/login.go index de53634..194bbb9 100644 --- a/command/login.go +++ b/command/login.go @@ -95,11 +95,26 @@ var ScratchSettingIds = map[ScratchSetting][]string{ PermsetsInFieldCreation: {"permsetsInFieldCreation"}, } +type ScratchRelease enumflag.Flag + +const ( + ReleaseDefault ScratchRelease = iota + ReleasePreview + ReleasePrevious +) + +var ScratchReleaseIds = map[ScratchRelease][]string{ + ReleaseDefault: {""}, + ReleasePreview: {"preview"}, + ReleasePrevious: {"previous"}, +} + var ( selectedFeatures []ScratchFeature selectedProducts []ScratchProduct selectedEdition ScratchEdition selectedSettings []ScratchSetting + selectedRelease ScratchRelease featureQuantities map[string]string ) @@ -142,6 +157,10 @@ logged in system. non-production server to login to (values are 'pre', enumflag.NewSlice(&selectedSettings, "setting", ScratchSettingIds, enumflag.EnumCaseInsensitive), "setting", "setting to enable (can be specified multiple times); see command help for available settings") + scratchCmd.Flags().Var( + enumflag.New(&selectedRelease, "release", ScratchReleaseIds, enumflag.EnumCaseInsensitive), + "release", + "Salesforce release for scratch org: preview (next release) or previous") loginCmd.AddCommand(scratchCmd) RootCmd.AddCommand(loginCmd) @@ -185,6 +204,10 @@ Available Settings (deployed after org creation): enableApexApprovalLockUnlock - Allow Apex to lock/unlock approval processes permsetsInFieldCreation - Allow assigning permission sets during field creation +Available Releases: + preview - Create scratch org on the next (preview) release + previous - Create scratch org on the previous release + Examples: force login scratch --product fsc force login scratch --feature PersonAccounts --feature StateAndCountryPicklist @@ -194,7 +217,9 @@ Examples: force login scratch --setting enableEnhancedNotes force login scratch --setting enableQuote force login scratch --product communities - force login scratch --product healthcloud`, + force login scratch --product healthcloud + force login scratch --release preview + force login scratch --release previous`, Run: func(cmd *cobra.Command, args []string) { scratchUser, _ := cmd.Flags().GetString("username") scratchNamespace, _ := cmd.Flags().GetString("namespace") @@ -202,7 +227,8 @@ Examples: allFeatures := expandProductsToFeatures(selectedProducts, selectedFeatures, quantities) edition := ScratchEditionIds[selectedEdition][0] allSettings := expandProductsToSettings(selectedProducts, selectedSettings) - scratchLogin(scratchUser, allFeatures, edition, allSettings, scratchNamespace) + release := ScratchReleaseIds[selectedRelease][0] + scratchLogin(scratchUser, allFeatures, edition, allSettings, scratchNamespace, release) }, } @@ -329,8 +355,8 @@ func expandProductsToSettings(products []ScratchProduct, settings []ScratchSetti return uniqueSettings } -func scratchLogin(scratchUser string, features []string, edition string, settings []string, namespace string) { - _, err := ForceScratchCreateLoginAndSaveWithNamespace(scratchUser, features, edition, settings, namespace, os.Stderr) +func scratchLogin(scratchUser string, features []string, edition string, settings []string, namespace string, release string) { + _, err := ForceScratchCreateLoginAndSaveWithRelease(scratchUser, features, edition, settings, namespace, release, os.Stderr) if err != nil { ErrorAndExit(err.Error()) } diff --git a/command/login_test.go b/command/login_test.go index b01c2ae..345fdbb 100644 --- a/command/login_test.go +++ b/command/login_test.go @@ -369,3 +369,51 @@ func TestScratchCommandHasNamespaceFlag(t *testing.T) { t.Fatal("Flag namespace not found") } } + +func TestScratchCommandHasReleaseFlag(t *testing.T) { + flag := scratchCmd.Flags().Lookup("release") + if flag == nil { + t.Fatal("Flag release not found") + } +} + +func TestScratchReleaseIds_AllReleasesDefined(t *testing.T) { + expectedReleases := map[string]bool{ + "": true, + "preview": true, + "previous": true, + } + + if len(ScratchReleaseIds) != len(expectedReleases) { + t.Errorf("Expected %d releases, got %d", len(expectedReleases), len(ScratchReleaseIds)) + } + + for _, ids := range ScratchReleaseIds { + if len(ids) != 1 { + t.Errorf("Expected 1 ID per release, got %d", len(ids)) + continue + } + releaseName := ids[0] + if !expectedReleases[releaseName] { + t.Errorf("Unexpected release: %s", releaseName) + } + } +} + +func TestScratchReleaseIds_Preview(t *testing.T) { + if ScratchReleaseIds[ReleasePreview][0] != "preview" { + t.Errorf("Expected 'preview' for ReleasePreview, got %s", ScratchReleaseIds[ReleasePreview][0]) + } +} + +func TestScratchReleaseIds_Previous(t *testing.T) { + if ScratchReleaseIds[ReleasePrevious][0] != "previous" { + t.Errorf("Expected 'previous' for ReleasePrevious, got %s", ScratchReleaseIds[ReleasePrevious][0]) + } +} + +func TestScratchReleaseIds_Default(t *testing.T) { + if ScratchReleaseIds[ReleaseDefault][0] != "" { + t.Errorf("Expected empty string for ReleaseDefault, got %s", ScratchReleaseIds[ReleaseDefault][0]) + } +} diff --git a/docs/force_login_scratch.md b/docs/force_login_scratch.md index a30c5aa..3ead281 100644 --- a/docs/force_login_scratch.md +++ b/docs/force_login_scratch.md @@ -39,6 +39,10 @@ Available Settings (deployed after org creation): enableApexApprovalLockUnlock - Allow Apex to lock/unlock approval processes permsetsInFieldCreation - Allow assigning permission sets during field creation +Available Releases: + preview - Create scratch org on the next (preview) release + previous - Create scratch org on the previous release + Examples: force login scratch --product fsc force login scratch --feature PersonAccounts --feature StateAndCountryPicklist @@ -49,6 +53,8 @@ Examples: force login scratch --setting enableQuote force login scratch --product communities force login scratch --product healthcloud + force login scratch --release preview + force login scratch --release previous ``` force login scratch [flags] @@ -63,6 +69,7 @@ force login scratch [flags] --namespace string namespace for the scratch org --product product product shortcut for features (can be specified multiple times); see command help for available products (default []) --quantity stringToString override default quantity for features (e.g., FinancialServicesUser=5); default quantity is 10 (default []) + --release release Salesforce release for scratch org: preview (next release) or previous --setting setting setting to enable (can be specified multiple times); see command help for available settings (default []) --username string username for scratch org user ``` diff --git a/lib/auth.go b/lib/auth.go index 788bafb..e1f97c7 100644 --- a/lib/auth.go +++ b/lib/auth.go @@ -135,13 +135,17 @@ func ForceScratchCreateLoginAndSave(scratchUser string, features []string, editi } func ForceScratchCreateLoginAndSaveWithNamespace(scratchUser string, features []string, edition string, settings []string, namespace string, output *os.File) (username string, err error) { + return ForceScratchCreateLoginAndSaveWithRelease(scratchUser, features, edition, settings, namespace, "", output) +} + +func ForceScratchCreateLoginAndSaveWithRelease(scratchUser string, features []string, edition string, settings []string, namespace string, release string, output *os.File) (username string, err error) { force, err := ActiveForce() if err != nil { err = errors.New("You must be logged into a Dev Hub org to authenticate as a scratch org user.") return } fmt.Fprintln(os.Stderr, "Creating new Scratch Org...") - scratchOrgId, err := force.CreateScratchOrgWithUserFeaturesEditionSettingsAndNamespace(scratchUser, features, edition, settings, namespace) + scratchOrgId, err := force.CreateScratchOrgWithRelease(scratchUser, features, edition, settings, namespace, release) if err != nil { return } diff --git a/lib/scratch.go b/lib/scratch.go index f6731ef..1ee44a8 100644 --- a/lib/scratch.go +++ b/lib/scratch.go @@ -277,6 +277,10 @@ func (f *Force) CreateScratchOrgWithUserFeaturesEditionAndSettings(username stri } func (f *Force) CreateScratchOrgWithUserFeaturesEditionSettingsAndNamespace(username string, features []string, edition string, settings []string, namespace string) (id string, err error) { + return f.CreateScratchOrgWithRelease(username, features, edition, settings, namespace, "") +} + +func (f *Force) CreateScratchOrgWithRelease(username string, features []string, edition string, settings []string, namespace string, release string) (id string, err error) { params := make(map[string]string) params["ConnectedAppCallbackUrl"] = "http://localhost:1717/OauthRedirect" params["ConnectedAppConsumerKey"] = "PlatformCLI" @@ -305,6 +309,9 @@ func (f *Force) CreateScratchOrgWithUserFeaturesEditionSettingsAndNamespace(user if username != "" { params["Username"] = username } + if release != "" { + params["Release"] = release + } id, err, messages := f.CreateRecord("ScratchOrgInfo", params) if err != nil { if len(messages) == 1 && messages[0].ErrorCode == "NOT_FOUND" {