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
4 changes: 4 additions & 0 deletions .mockery.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,7 @@ packages:
config:
all: true
interfaces:
github.com/codesphere-cloud/cs-go/pkg/deploy:
config:
all: true
interfaces:
26 changes: 26 additions & 0 deletions cli/cmd/deploy.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// Copyright (c) Codesphere Inc.
// SPDX-License-Identifier: Apache-2.0

package cmd

import (
"github.com/spf13/cobra"
)

type DeployCmd struct {
cmd *cobra.Command
}

func AddDeployCmd(rootCmd *cobra.Command, opts GlobalOptions) {
deploy := DeployCmd{
cmd: &cobra.Command{
Use: "deploy",
Short: "Deploy workspaces from CI/CD",
Long: `Deploy workspaces from CI/CD pipelines. Supports creating, updating, and deleting workspaces tied to git provider events like pull requests.`,
},
}
rootCmd.AddCommand(deploy.cmd)

// Add provider-specific subcommands
AddDeployGitHubCmd(deploy.cmd, opts)
}
199 changes: 199 additions & 0 deletions cli/cmd/deploy_github.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
// Copyright (c) Codesphere Inc.
// SPDX-License-Identifier: Apache-2.0

package cmd

import (
"encoding/json"
"fmt"
"os"
"strconv"
"strings"
"time"

"github.com/codesphere-cloud/cs-go/pkg/deploy"
"github.com/codesphere-cloud/cs-go/pkg/io"
"github.com/spf13/cobra"
)

type DeployGitHubCmd struct {
cmd *cobra.Command
Opts DeployGitHubOpts
}

type DeployGitHubOpts struct {
GlobalOptions
PlanId *int
Env *[]string
VpnConfig *string
Branch *string
Stages *string
Timeout *time.Duration
}

func (c *DeployGitHubCmd) RunE(_ *cobra.Command, args []string) error {
client, err := NewClient(c.Opts.GlobalOptions)
if err != nil {
return fmt.Errorf("failed to create Codesphere client: %w", err)
}

teamId, err := c.Opts.GetTeamId()
if err != nil {
return fmt.Errorf("failed to get team ID: %w", err)
}

// Load GitHub context
eventName := os.Getenv("GITHUB_EVENT_NAME")
prAction, prNumber := loadGitHubEvent()
repository := os.Getenv("GITHUB_REPOSITORY")
serverUrl := os.Getenv("GITHUB_SERVER_URL")

// Determine workspace name: <repo>-#<pr>
parts := strings.Split(repository, "/")
repo := parts[len(parts)-1]
wsName := fmt.Sprintf("%s-#%s", repo, prNumber)

// Resolve branch
branch := c.resolveBranch()

// Resolve repo URL
repoUrl := fmt.Sprintf("%s/%s.git", serverUrl, repository)

// Parse stages
var stages []string
for _, s := range strings.Fields(*c.Opts.Stages) {
if s != "" {
stages = append(stages, s)
}
}

// Parse env vars
envVars := make(map[string]string)
for _, e := range *c.Opts.Env {
if idx := strings.Index(e, "="); idx > 0 {
envVars[e[:idx]] = e[idx+1:]
}
}

cfg := deploy.Config{
TeamId: teamId,
PlanId: *c.Opts.PlanId,
Name: wsName,
EnvVars: envVars,
VpnConfig: *c.Opts.VpnConfig,
Branch: branch,
Stages: stages,
RepoUrl: repoUrl,
Timeout: *c.Opts.Timeout,
}

// Determine if this is a delete operation
isDelete := eventName == "pull_request" && prAction == "closed"

deployer := deploy.NewDeployer(client)
result, err := deployer.Deploy(cfg, isDelete)
if err != nil {
return err
}

// Write GitHub-specific outputs
if result != nil {
setGitHubOutputs(result.WorkspaceId, result.WorkspaceURL)
}

return nil
}

// resolveBranch determines the branch to deploy with priority:
// flag > GITHUB_HEAD_REF > GITHUB_REF_NAME > "main"
func (c *DeployGitHubCmd) resolveBranch() string {
if c.Opts.Branch != nil && *c.Opts.Branch != "" {
return *c.Opts.Branch
}
if headRef := os.Getenv("GITHUB_HEAD_REF"); headRef != "" {
return headRef
}
if refName := os.Getenv("GITHUB_REF_NAME"); refName != "" {
return refName
}
return "main"
}

// loadGitHubEvent reads the PR action and number from GITHUB_EVENT_PATH.
func loadGitHubEvent() (action string, number string) {
path := os.Getenv("GITHUB_EVENT_PATH")
if path == "" {
return "", ""
}
data, err := os.ReadFile(path)
if err != nil {
return "", ""
}
var event struct {
Action string `json:"action"`
Number int `json:"number"`
}
if json.Unmarshal(data, &event) == nil {
return event.Action, strconv.Itoa(event.Number)
}
return "", ""
}

// setGitHubOutputs writes deployment results to GitHub Actions output files.
func setGitHubOutputs(wsId int, url string) {
if f := os.Getenv("GITHUB_OUTPUT"); f != "" {
appendToFile(f, fmt.Sprintf("deployment-url=%s\nworkspace-id=%d\n", url, wsId))
}

if f := os.Getenv("GITHUB_STEP_SUMMARY"); f != "" {
appendToFile(f, fmt.Sprintf(
"### 🚀 Codesphere Deployment\n\n| Property | Value |\n|----------|-------|\n| **URL** | [%s](%s) |\n| **Workspace** | `%d` |\n",
url, url, wsId,
))
}
}

func appendToFile(path, content string) {
f, err := os.OpenFile(path, os.O_APPEND|os.O_WRONLY, 0644)
if err != nil {
return
}
defer f.Close() //nolint:errcheck // best-effort append
_, _ = f.WriteString(content)
}

func AddDeployGitHubCmd(deployCmd *cobra.Command, opts GlobalOptions) {
github := DeployGitHubCmd{
cmd: &cobra.Command{
Use: "github",
Short: "Deploy from GitHub Actions",
Long: io.Long(`Deploy workspaces from GitHub Actions.

Automatically detects the PR context from GitHub Actions environment
variables (GITHUB_EVENT_NAME, GITHUB_HEAD_REF, GITHUB_REPOSITORY, etc.)
and creates, updates, or deletes workspaces accordingly.

On PR open/synchronize: creates or updates a workspace.
On PR close: deletes the workspace.

Designed to be used from GitHub Actions workflows.`),
Example: io.FormatExampleCommands("deploy github", []io.Example{
{Cmd: "", Desc: "Deploy using GitHub Actions environment variables"},
{Cmd: "--plan-id 20", Desc: "Deploy with a specific plan"},
{Cmd: "--stages 'prepare test run'", Desc: "Deploy and run specific pipeline stages"},
{Cmd: "--branch feature-x", Desc: "Override the branch to deploy"},
}),
},
Opts: DeployGitHubOpts{GlobalOptions: opts},
}

github.Opts.PlanId = github.cmd.Flags().Int("plan-id", 8, "Plan ID for the workspace")
github.Opts.Env = github.cmd.Flags().StringArray("env", []string{}, "Environment variables in KEY=VALUE format")
github.Opts.VpnConfig = github.cmd.Flags().String("vpn-config", "", "VPN config name to connect the workspace to")
github.Opts.Branch = github.cmd.Flags().StringP("branch", "b", "", "Git branch to deploy (auto-detected from GitHub context if not set)")
github.Opts.Stages = github.cmd.Flags().String("stages", "prepare run", "Pipeline stages to run (space-separated: prepare test run)")
github.Opts.Timeout = github.cmd.Flags().Duration("timeout", 5*time.Minute, "Timeout for workspace creation/readiness")

deployCmd.AddCommand(github.cmd)
github.cmd.RunE = github.RunE
}
1 change: 1 addition & 0 deletions cli/cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ func GetRootCmd() *cobra.Command {
AddGoCmd(rootCmd)
AddWakeUpCmd(rootCmd, opts)
AddCurlCmd(rootCmd, opts)
AddDeployCmd(rootCmd, opts)

return rootCmd
}
Expand Down
Loading
Loading