Skip to content

Commit 3c8e997

Browse files
authored
Merge pull request #1133 from CircleCI-Public/LIFE-1564-add-the-ability-to-create-a-project-via-the-cli
[LIFE-1564] Add the ability to create a project via the cli
2 parents 83b96d3 + 04ad264 commit 3c8e997

File tree

6 files changed

+179
-1
lines changed

6 files changed

+179
-1
lines changed

api/project/project.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,19 @@ type ProjectInfo struct {
1111
Id string
1212
}
1313

14+
type CreateProjectInfo struct {
15+
Id string
16+
Name string
17+
Slug string
18+
OrgName string
19+
}
20+
1421
// ProjectClient is the interface to interact with project and it's
1522
// components.
1623
type ProjectClient interface {
1724
ProjectInfo(vcs, org, project string) (*ProjectInfo, error)
1825
ListAllEnvironmentVariables(vcs, org, project string) ([]*ProjectEnvironmentVariable, error)
1926
GetEnvironmentVariable(vcs, org, project, envName string) (*ProjectEnvironmentVariable, error)
2027
CreateEnvironmentVariable(vcs, org, project string, v ProjectEnvironmentVariable) (*ProjectEnvironmentVariable, error)
28+
CreateProject(vcs, org, project string) (*CreateProjectInfo, error)
2129
}

api/project/project_rest.go

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,24 @@ type listAllProjectEnvVarsResponse struct {
3131
NextPageToken string `json:"next_page_token"`
3232
}
3333

34+
type createProjectRequest struct {
35+
Name string `json:"name"`
36+
}
37+
38+
type createProjectResponse struct {
39+
Slug string `json:"slug"`
40+
Name string `json:"name"`
41+
Id string `json:"id"`
42+
OrganizationName string `json:"organization_name"`
43+
OrganizationSlug string `json:"organization_slug"`
44+
OrganizationId string `json:"organization_id"`
45+
VcsInfo struct {
46+
VcsUrl string `json:"vcs_url"`
47+
Provider string `json:"provider"`
48+
DefaultBranch string `json:"default_branch"`
49+
} `json:"vcs_info"`
50+
}
51+
3452
type createProjectEnvVarRequest struct {
3553
Name string `json:"name"`
3654
Value string `json:"value"`
@@ -129,6 +147,35 @@ func (c *projectRestClient) GetEnvironmentVariable(vcs string, org string, proje
129147
}, nil
130148
}
131149

150+
func (c *projectRestClient) CreateProject(vcs string, org string, name string) (*CreateProjectInfo, error) {
151+
orgSlug := fmt.Sprintf("%s/%s", vcs, org)
152+
153+
path := fmt.Sprintf("organization/%s/project", orgSlug)
154+
155+
reqBody := createProjectRequest{
156+
Name: name,
157+
}
158+
159+
urlObj := &url.URL{Path: path}
160+
req, err := c.client.NewRequest("POST", urlObj, reqBody)
161+
if err != nil {
162+
return nil, err
163+
}
164+
165+
var resp createProjectResponse
166+
_, err = c.client.DoRequest(req, &resp)
167+
if err != nil {
168+
return nil, err
169+
}
170+
171+
return &CreateProjectInfo{
172+
Id: resp.Id,
173+
Name: resp.Name,
174+
Slug: resp.Slug,
175+
OrgName: resp.OrganizationName,
176+
}, nil
177+
}
178+
132179
// CreateEnvironmentVariable creates a variable on the given project.
133180
// This returns the variable if successfully created.
134181
func (c *projectRestClient) CreateEnvironmentVariable(vcs string, org string, project string, v ProjectEnvironmentVariable) (*ProjectEnvironmentVariable, error) {

api/project/project_rest_test.go

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,77 @@ func Test_projectRestClient_GetEnvironmentVariable(t *testing.T) {
241241
}
242242
}
243243

244+
func Test_projectRestClient_CreateProject(t *testing.T) {
245+
const (
246+
vcsType = "github"
247+
orgName = "test-org"
248+
projName = "test-proj"
249+
testId = "this-is-the-id"
250+
projectSlug = "github/test-org/test-proj"
251+
)
252+
tests := []struct {
253+
name string
254+
handler http.HandlerFunc
255+
want *project.CreateProjectInfo
256+
wantErr bool
257+
}{
258+
{
259+
name: "Should handle a successful request",
260+
handler: func(w http.ResponseWriter, r *http.Request) {
261+
orgSlug := fmt.Sprintf("%s/%s", vcsType, orgName)
262+
assert.Equal(t, r.Header.Get("circle-token"), "token")
263+
assert.Equal(t, r.Header.Get("accept"), "application/json")
264+
assert.Equal(t, r.Header.Get("user-agent"), version.UserAgent())
265+
assert.Equal(t, r.Method, "POST")
266+
assert.Equal(t, r.URL.Path, fmt.Sprintf("/api/v2/organization/%s/project", orgSlug))
267+
w.Header().Set("Content-Type", "application/json")
268+
w.WriteHeader(http.StatusOK)
269+
_, err := w.Write([]byte(`
270+
{
271+
"id": "` + testId + `",
272+
"name": "` + projName + `",
273+
"slug": "` + projectSlug + `",
274+
"organization_name": "` + orgName + `"
275+
}`))
276+
assert.NilError(t, err)
277+
},
278+
want: &project.CreateProjectInfo{
279+
Id: testId,
280+
Name: projName,
281+
Slug: projectSlug,
282+
OrgName: orgName,
283+
},
284+
},
285+
{
286+
name: "Should handle an error request",
287+
handler: func(w http.ResponseWriter, _ *http.Request) {
288+
w.Header().Set("content-type", "application/json")
289+
w.WriteHeader(http.StatusInternalServerError)
290+
_, err := w.Write([]byte(`{"message": "error"}`))
291+
assert.NilError(t, err)
292+
},
293+
wantErr: true,
294+
},
295+
}
296+
for _, tt := range tests {
297+
t.Run(tt.name, func(t *testing.T) {
298+
server := httptest.NewServer(tt.handler)
299+
defer server.Close()
300+
p, err := getProjectRestClient(server)
301+
assert.NilError(t, err)
302+
303+
got, err := p.CreateProject(vcsType, orgName, projName)
304+
if (err != nil) != tt.wantErr {
305+
t.Errorf("projectRestClient.CreateProject() error = %v, wantErr %v", err, tt.wantErr)
306+
return
307+
}
308+
if !reflect.DeepEqual(got, tt.want) {
309+
t.Errorf("projectRestClient.CreateProject() = %v, want %v", got, tt.want)
310+
}
311+
})
312+
}
313+
}
314+
244315
func Test_projectRestClient_CreateEnvironmentVariable(t *testing.T) {
245316
const (
246317
vcsType = "github"

cmd/project/create.go

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
package project
2+
3+
import (
4+
"github.com/spf13/cobra"
5+
6+
"github.com/CircleCI-Public/circleci-cli/cmd/validator"
7+
"github.com/CircleCI-Public/circleci-cli/prompt"
8+
)
9+
10+
var projectName string
11+
12+
func newProjectCreateCommand(ops *projectOpts, preRunE validator.Validator) *cobra.Command {
13+
cmd := &cobra.Command{
14+
Use: "create <vcs-type> <org-name> [--name <project-name>]",
15+
Short: "Create a new project in a CircleCI organization.",
16+
Long: `Create a new project in a CircleCI organization.
17+
18+
The project name can be provided using the --name flag. If not provided, you will be prompted to enter it.
19+
20+
Example:
21+
circleci project create github my-org --name my-new-project
22+
23+
Note: For those with the circleci vcs type, you must use the hashed organization name, not the human readable name.
24+
You can get this from the URL of the organization on the web.
25+
26+
Example:
27+
https://app.circleci.com/organization/circleci/GQERGDFG13454135 -> GQERGDFG13454135
28+
Not the org name like: "test-org"`,
29+
PreRunE: preRunE,
30+
RunE: func(cmd *cobra.Command, args []string) error {
31+
vcsType := args[0]
32+
orgName := args[1]
33+
if projectName == "" {
34+
projectName = prompt.ReadStringFromUser("Enter a name for the project", "")
35+
}
36+
res, err := ops.projectClient.CreateProject(vcsType, orgName, projectName)
37+
if err != nil {
38+
return err
39+
}
40+
41+
cmd.Printf("Project '%s' successfully created in organization '%s'\n", projectName, res.OrgName)
42+
cmd.Println("You may view your new project at: https://app.circleci.com/projects/" + res.Slug)
43+
return nil
44+
},
45+
Args: cobra.ExactArgs(2),
46+
}
47+
48+
cmd.Flags().StringVar(&projectName, "name", "", "Name of the project to create")
49+
50+
return cmd
51+
}

cmd/project/project.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ func NewProjectCommand(config *settings.Config, preRunE validator.Validator, opt
5858

5959
command.AddCommand(newProjectEnvironmentVariableCommand(&pos, preRunE))
6060
command.AddCommand(newProjectDLCCommand(config, &pos, preRunE))
61+
command.AddCommand(newProjectCreateCommand(&pos, preRunE))
6162

6263
return command
6364
}

cmd/root.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -356,7 +356,7 @@ func (helpCmd *helpCmd) helpTemplate(cmd *cobra.Command, s []string) {
356356
Foreground(lipgloss.AdaptiveColor{Light: `#003740`, Dark: `#3B6385`}).
357357
BorderBottom(true).
358358
Margin(1, 0, 1, 0).
359-
Padding(0, 1, 0, 1).Align(lipgloss.Center)
359+
Padding(0, 1, 0, 1).Align(lipgloss.Left)
360360
subCmdStyle := lipgloss.NewStyle().
361361
Foreground(lipgloss.AdaptiveColor{Light: `#161616`, Dark: `#FFFFFF`}).
362362
Padding(0, 4, 0, 4).Align(lipgloss.Left)

0 commit comments

Comments
 (0)