Skip to content
Draft
Show file tree
Hide file tree
Changes from all 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
10 changes: 10 additions & 0 deletions cli/cmd/deploy/deploy.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ import (

"github.com/rilldata/rill/cli/cmd/auth"
"github.com/rilldata/rill/cli/cmd/project"
"github.com/rilldata/rill/cli/cmd/project/deployment"
"github.com/rilldata/rill/cli/pkg/cmdutil"
"github.com/rilldata/rill/cli/pkg/gitutil"
"github.com/rilldata/rill/cli/pkg/local"
"github.com/spf13/cobra"
)
Expand Down Expand Up @@ -35,6 +37,14 @@ func DeployCmd(ch *cmdutil.Helper) *cobra.Command {
return err
}

if opts.CreateDeployment {
currentBranch, err := gitutil.CurrentBranch(opts.GitPath)
if err != nil {
return fmt.Errorf("failed to get current git branch: %w", err)
}
return deployment.CreateDeployment(cmd.Context(), ch, opts.PushToProject.Name, currentBranch, "", false)
}

if !opts.Managed && !opts.ArchiveUpload && !opts.Github {
if !ch.Interactive {
return fmt.Errorf("must specify --managed or --github in non-interactive mode")
Expand Down
2 changes: 1 addition & 1 deletion cli/cmd/project/connect_github.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ func ConnectGithubFlow(ctx context.Context, ch *cmdutil.Helper, opts *DeployOpts
localGitPath := opts.GitPath
localProjectPath := opts.LocalProjectPath()

if opts.pushToProject != nil {
if opts.PushToProject != nil {
return redeployProject(ctx, ch, opts)
}

Expand Down
90 changes: 56 additions & 34 deletions cli/cmd/project/deploy.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,10 +48,12 @@ type DeployOpts struct {
// SkipDeploy skips the runtime deployment step. Used for testing.
SkipDeploy bool

// CreateDeployment is set if a new deployment should be created. Set internally.
CreateDeployment bool
// PushToProject is set if the deploy should push current changes to this existing project. Set internally.
PushToProject *adminv1.Project
// remoteURL is the git remote url of the repository if detected. Set internally.
remoteURL string
// pushToProject is set if the deploy should push current changes to this existing project. Set internally.
pushToProject *adminv1.Project
}

func (o *DeployOpts) LocalProjectPath() string {
Expand Down Expand Up @@ -90,15 +92,13 @@ func (o *DeployOpts) ValidateAndApplyDefaults(ctx context.Context, ch *cmdutil.H
return err
}
if exists {
if ch.Interactive {
if err := cmdutil.ConfirmPrompt(fmt.Sprintf("Project with name %q already exists. Do you want to push current changes to the existing project?", o.Name), true); err != nil {
return err
}
o.PushToProject = p
if err := o.shouldCreateDeployment(ch); err != nil {
return err
}
o.pushToProject = p
o.Managed = o.pushToProject.ManagedGitId != ""
o.Github = o.pushToProject.ManagedGitId == "" && o.pushToProject.GitRemote != ""
o.ArchiveUpload = o.pushToProject.ArchiveAssetId != ""
o.Managed = o.PushToProject.ManagedGitId != ""
o.Github = o.PushToProject.ManagedGitId == "" && o.PushToProject.GitRemote != ""
o.ArchiveUpload = o.PushToProject.ArchiveAssetId != ""
return nil
}
}
Expand Down Expand Up @@ -127,35 +127,31 @@ func (o *DeployOpts) ValidateAndApplyDefaults(ctx context.Context, ch *cmdutil.H
}

// if there is a project already connected to this repo+subpath offer to push changes to it
if o.pushToProject != nil {
if o.pushToProject.ManagedGitId == "" && o.Managed {
ch.PrintfError("Project %s/%s is already connected to this GitHub repository. Cannot use --managed flag.\n", o.pushToProject.OrgName, o.pushToProject.Name)
if o.PushToProject != nil {
if o.PushToProject.ManagedGitId == "" && o.Managed {
ch.PrintfError("Project %s/%s is already connected to this GitHub repository. Cannot use --managed flag.\n", o.PushToProject.OrgName, o.PushToProject.Name)
return fmt.Errorf("aborting deploy")
}
if o.pushToProject.ManagedGitId != "" && o.Github {
ch.Printf("Found another rill managed project %s/%s connected to this folder\n", o.pushToProject.OrgName, o.pushToProject.Name)
if o.PushToProject.ManagedGitId != "" && o.Github {
ch.Printf("Found another rill managed project %s/%s connected to this folder\n", o.PushToProject.OrgName, o.PushToProject.Name)
ch.PrintfBold("Run `rill project edit --remote-url <github_remote>` to tranfer the project to GitHub.\n")
return fmt.Errorf("aborting deploy")
}
if o.pushToProject.OrgName != ch.Org {
ch.PrintfError("A project in another org deploys from this repository. Please switch to org %q to push changes to the project %q.\n", o.pushToProject.OrgName, o.pushToProject.Name)
if o.PushToProject.OrgName != ch.Org {
ch.PrintfError("A project in another org deploys from this repository. Please switch to org %q to push changes to the project %q.\n", o.PushToProject.OrgName, o.PushToProject.Name)
return fmt.Errorf("aborting deploy")
}
if subpath != "" && o.pushToProject.Subpath != subpath {
if subpath != "" && o.PushToProject.Subpath != subpath {
// just for verification confirm that subpath matches the one stored in project
return fmt.Errorf("current project subpath %q does not match the one stored in rill %q. Try doing deploy using rill cli from github repo root by passing explicit subpath using `rill deploy --subpath %s`", subpath, o.pushToProject.Subpath, o.pushToProject.Subpath)
return fmt.Errorf("current project subpath %q does not match the one stored in rill %q. Try doing deploy using rill cli from github repo root by passing explicit subpath using `rill deploy --subpath %s`", subpath, o.PushToProject.Subpath, o.PushToProject.Subpath)
}
// set flags based on existing project
o.Managed = o.pushToProject.ManagedGitId != ""
o.Github = o.pushToProject.ManagedGitId == "" && o.pushToProject.GitRemote != ""
o.ArchiveUpload = o.pushToProject.ArchiveAssetId != ""
o.Managed = o.PushToProject.ManagedGitId != ""
o.Github = o.PushToProject.ManagedGitId == "" && o.PushToProject.GitRemote != ""
o.ArchiveUpload = o.PushToProject.ArchiveAssetId != ""

ch.PrintfBold("\nFound existing project: ")
ch.Printf("%s/%s\n", o.pushToProject.OrgName, o.pushToProject.Name)
if !ch.Interactive {
return nil
}
return cmdutil.ConfirmPrompt("Do you want to push current changes to the existing project?", true)
// check if we need to create a new deployment or just push to existing one
return o.shouldCreateDeployment(ch)
}

if o.remoteURL == "" {
Expand Down Expand Up @@ -234,11 +230,11 @@ func (o *DeployOpts) detectGitRemoteAndProject(ctx context.Context, ch *cmdutil.
}
for _, p := range resp.Projects {
if p.ManagedGitId != "" {
o.pushToProject = p
o.PushToProject = p
o.remoteURL = p.GitRemote
return nil
}
o.pushToProject = p
o.PushToProject = p
o.remoteURL = p.GitRemote
// do not return yet, there might be a managed project
// this is not possible with new flow but keeping it for consistency
Expand All @@ -256,6 +252,32 @@ func (o *DeployOpts) detectGitRemoteAndProject(ctx context.Context, ch *cmdutil.
return nil
}

func (o *DeployOpts) shouldCreateDeployment(ch *cmdutil.Helper) error {
if o.PushToProject.ManagedGitId != "" || o.PushToProject.ArchiveAssetId != "" {
// for managed git and archive upload we allow pushing from any branch so we should not create a new deployment
return nil
}
currentBranch, err := gitutil.CurrentBranch(o.GitPath)
if err != nil {
return err
}
redeploy := currentBranch == o.PushToProject.PrimaryBranch
if ch.Interactive {
if redeploy {
if err := cmdutil.ConfirmPrompt(fmt.Sprintf("Found existing project: %q/%q. Do you want to push current changes to this project?\n", o.PushToProject.OrgName, o.PushToProject.Name), true); err != nil {
return err
}
} else {
ch.PrintfWarn("Found existing project: %q/%q. But current branch %q does not match the primary branch %q of the project.\n", o.PushToProject.OrgName, o.PushToProject.Name, currentBranch, o.PushToProject.PrimaryBranch)
if err := cmdutil.ConfirmPrompt("Do you want to create a new deployment instead?", true); err != nil {
return err
}
}
}
o.CreateDeployment = !redeploy
return nil
}

func DeployCmd(ch *cmdutil.Helper) *cobra.Command {
opts := &DeployOpts{
ProdVersion: "latest",
Expand Down Expand Up @@ -374,7 +396,7 @@ func DeployWithUploadFlow(ctx context.Context, ch *cmdutil.Helper, opts *DeployO
return fmt.Errorf("failed to set up .gitignore: %w", err)
}

if opts.pushToProject != nil {
if opts.PushToProject != nil {
return redeployProject(ctx, ch, opts)
}

Expand Down Expand Up @@ -462,7 +484,7 @@ func redeployProject(ctx context.Context, ch *cmdutil.Helper, opts *DeployOpts)
if err != nil {
return err
}
proj := opts.pushToProject
proj := opts.PushToProject
if proj.ManagedGitId != "" {
err := ch.GitHelper(ch.Org, proj.Name, opts.LocalProjectPath()).PushToManagedRepo(ctx)
if err != nil {
Expand All @@ -479,8 +501,8 @@ func redeployProject(ctx context.Context, ch *cmdutil.Helper, opts *DeployOpts)
return fmt.Errorf("current project subpath %q does not match the one stored in rill %q. Run rill cli from github repo root and pass explicit subpath using `rill deploy --subpath %s`", subpath, proj.Subpath, proj.Subpath)
}
config := &gitutil.Config{
Remote: opts.pushToProject.GitRemote,
DefaultBranch: opts.pushToProject.PrimaryBranch,
Remote: opts.PushToProject.GitRemote,
DefaultBranch: opts.PushToProject.PrimaryBranch,
Subpath: subpath,
}
author, err := ch.GitSignature(ctx, repoRoot)
Expand Down
75 changes: 49 additions & 26 deletions cli/cmd/project/deployment/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,49 +27,72 @@ func CreateCmd(ch *cmdutil.Helper) *cobra.Command {
branch = args[1]
}

client, err := ch.Client()
if err != nil {
return err
}

var err error
if project == "" {
project, err = ch.InferProjectName(cmd.Context(), path, "use --project to specify the name")
if err != nil {
return err
}
}

if environment == "prod" && editable {
return fmt.Errorf("prod deployments cannot be editable")
}
return CreateDeployment(cmd.Context(), ch, project, branch, environment, editable)
},
}

ch.PrintfBold("Creating %q deployment for branch %q...\n", environment, branch)
createCmd.Flags().StringVar(&project, "project", "", "Project name")
createCmd.Flags().StringVar(&path, "path", ".", "Project directory")
createCmd.Flags().StringVar(&environment, "environment", "dev", "Optional environment to create for (options: dev, prod)")
createCmd.Flags().BoolVar(&editable, "editable", false, "Make the deployment editable (changes are persisted back to git repo)")

resp, err := client.CreateDeployment(cmd.Context(), &adminv1.CreateDeploymentRequest{
Org: ch.Org,
Project: project,
Environment: environment,
Branch: branch,
Editable: editable,
})
return createCmd
}

// CreateDeployment creates a deployment for the specified branch in specified environment.
// If env is not set then it will prompt user to select environment (default: dev).
func CreateDeployment(ctx context.Context, ch *cmdutil.Helper, project, branch, env string, editable bool) error {
if env == "prod" && editable {
return fmt.Errorf("prod deployments cannot be editable")
}
var err error
if ch.Interactive && env == "" { // not set by caller
env, err = cmdutil.SelectPrompt("Environment to create deployment for?", []string{"dev", "prod"}, "dev")
if err != nil {
return err
}
if env != "prod" {
editable, err = cmdutil.YesNoPrompt("Do you want an editable deployment", false)
if err != nil {
return err
}
}
}
if env == "" { // still unset - default to dev
env = "dev"
}

ch.Printf("Deployment created with branch %q\n", resp.Deployment.Branch)
ch.Printf("Provisioning runtime (this may take a moment)")
client, err := ch.Client()
if err != nil {
return err
}

// Poll for deployment status and print result
return pollDeploymentStatus(cmd.Context(), client, ch, resp.Deployment.Id, project, branch, environment)
},
resp, err := client.CreateDeployment(ctx, &adminv1.CreateDeploymentRequest{
Org: ch.Org,
Project: project,
Environment: env,
Branch: branch,
Editable: editable,
})
if err != nil {
return err
}

createCmd.Flags().StringVar(&project, "project", "", "Project name")
createCmd.Flags().StringVar(&path, "path", ".", "Project directory")
createCmd.Flags().StringVar(&environment, "environment", "dev", "Optional environment to create for (options: dev, prod)")
createCmd.Flags().BoolVar(&editable, "editable", false, "Make the deployment editable (changes are persisted back to git repo)")
if ch.Interactive {
ch.Printf("Deployment created with branch %q\n", resp.Deployment.Branch)
ch.Printf("Provisioning runtime (this may take a moment)")
}

return createCmd
// Poll for deployment status and print result
return pollDeploymentStatus(ctx, client, ch, resp.Deployment.Id, project, branch, env)
}

// pollDeploymentStatus polls the deployment status until it's either running or errored, then prints the result
Expand Down
12 changes: 8 additions & 4 deletions cli/pkg/cmdutil/helper.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import (
runtimeclient "github.com/rilldata/rill/runtime/client"
"github.com/rilldata/rill/runtime/pkg/activity"
"github.com/rilldata/rill/runtime/pkg/fileutil"
rtgitutil "github.com/rilldata/rill/runtime/pkg/gitutil"
"go.uber.org/zap"
"golang.org/x/sync/errgroup"
"golang.org/x/term"
Expand Down Expand Up @@ -603,11 +604,11 @@ func (h *Helper) CommitAndSafePush(ctx context.Context, root string, config *git
}

// 2. Check status of the subpath
status, err := gitutil.RunGitStatus(root, config.Subpath, config.RemoteName(), "")
status, err := gitutil.RunGitStatus(root, config.Subpath, config.RemoteName(), fmt.Sprintf("%s/%s", config.RemoteName(), config.DefaultBranch))
if err != nil {
return fmt.Errorf("failed to get git status: %w", err)
}
if status.Branch != config.DefaultBranch {
if status.Branch != config.DefaultBranch && !config.ManagedRepo { // ensured upstream but just in case
return fmt.Errorf("current branch %q does not match expected branch %q", status.Branch, config.DefaultBranch)
}

Expand All @@ -632,10 +633,13 @@ func (h *Helper) CommitAndSafePush(ctx context.Context, root string, config *git
// The push can still fail if there were new remote commits since the fetch. But that's okay, the user can just retry.
switch choice {
case "1":
err := gitutil.RunUpstreamMerge(ctx, config.RemoteName(), root, status.Branch, false)
merged, err := rtgitutil.MergeWithBailOnConflict(root, fmt.Sprintf("%s/%s", config.RemoteName(), config.DefaultBranch))
if err != nil {
return fmt.Errorf("local is behind remote and failed to sync with remote: %w", err)
}
if !merged {
return fmt.Errorf("local is behind remote and has conflicts that must be resolved manually")
}
return gitutil.CommitAndPush(ctx, root, config, commitMsg, author)
case "2":
// Instead of a force push, we do a merge with favourLocal=true to ensure we don't lose history.
Expand All @@ -646,7 +650,7 @@ func (h *Helper) CommitAndSafePush(ctx context.Context, root string, config *git
// monorepo setups are advanced use cases and we can require users to manually resolve remote changes
return fmt.Errorf("cannot overwrite remote changes in a monorepo setup. Merge remote changes manually")
}
err := gitutil.RunUpstreamMerge(ctx, config.RemoteName(), root, status.Branch, true)
err := rtgitutil.MergeWithStrategy(root, fmt.Sprintf("%s/%s", config.RemoteName(), config.DefaultBranch), "ours")
if err != nil {
return fmt.Errorf("local is behind remote and failed to sync with remote: %w", err)
}
Expand Down
12 changes: 12 additions & 0 deletions cli/pkg/gitutil/gitcmdwrapper.go
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,18 @@ func InferGitRepoRoot(path string) (string, error) {
return filepath.Join(path, strings.TrimSpace(string(data))), nil
}

// CurrentBranch returns the current branch of the git repository at the specified path.
// Returns empty string and no error if in detached head state.
// Callers should ensure that it is a valid git repository.
func CurrentBranch(path string) (string, error) {
cmd := exec.Command("git", "-C", path, "branch", "--show-current")
data, err := cmd.CombinedOutput()
if err == nil {
return strings.TrimSpace(string(data)), nil
}
return "", fmt.Errorf("failed to determine current branch: %s: %w", string(data), err)
}

func isGitIgnored(repoRoot, subpath string) (bool, error) {
cmd := exec.Command("git", "-C", repoRoot, "check-ignore", subpath)
err := cmd.Run()
Expand Down
Loading
Loading