Skip to content

Commit 0754754

Browse files
parkumanpete-woods
authored andcommitted
Add 'circleci open' command
The open command was present in the legacy (main) branch but was missed during the migration to the next branch. This restores the feature, adapted to the new project structure: - Detects the project from the git remote (GitHub, Bitbucket, GitLab) - Opens the CircleCI pipelines page in the default browser - Uses structured error handling consistent with the new CLI conventions
1 parent f46168b commit 0754754

6 files changed

Lines changed: 158 additions & 0 deletions

File tree

CLAUDE.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ internal/
7676
│ ├── config/ circleci config validate/process/pack/generate
7777
│ ├── context/ circleci context + circleci context secret
7878
│ ├── job/ circleci job artifacts (deep path; wraps internal/artifacts)
79+
│ ├── open/ circleci open (opens current project in the CircleCI web UI)
7980
│ ├── pipeline/ circleci pipeline list/get/trigger
8081
│ ├── workflow/ circleci workflow list/get/cancel/rerun
8182
│ ├── orb/ circleci orb list/info/validate/publish/...

internal/cmd/open/open.go

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
// Copyright (c) 2026 Circle Internet Services, Inc.
2+
//
3+
// Permission is hereby granted, free of charge, to any person obtaining a copy
4+
// of this software and associated documentation files (the "Software"), to deal
5+
// in the Software without restriction, including without limitation the rights
6+
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7+
// copies of the Software, and to permit persons to whom the Software is
8+
// furnished to do so, subject to the following conditions:
9+
//
10+
// The above copyright notice and this permission notice shall be included in
11+
// all copies or substantial portions of the Software.
12+
//
13+
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14+
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15+
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16+
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17+
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18+
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19+
// SOFTWARE.
20+
//
21+
// SPDX-License-Identifier: MIT
22+
23+
// Package open implements the "circleci open" command.
24+
package open
25+
26+
import (
27+
"fmt"
28+
"net/url"
29+
"strings"
30+
31+
"github.com/MakeNowJust/heredoc"
32+
"github.com/pkg/browser"
33+
"github.com/spf13/cobra"
34+
35+
clierrors "github.com/CircleCI-Public/circleci-cli-v2/internal/errors"
36+
"github.com/CircleCI-Public/circleci-cli-v2/internal/gitremote"
37+
)
38+
39+
// ProjectURL builds the CircleCI pipelines URL for the given project slug.
40+
func ProjectURL(slug string) (string, error) {
41+
parts := strings.SplitN(slug, "/", 3)
42+
if len(parts) != 3 {
43+
return "", fmt.Errorf("invalid slug: %q", slug)
44+
}
45+
return fmt.Sprintf("https://app.circleci.com/pipelines/%s/%s/%s",
46+
url.PathEscape(parts[0]),
47+
url.PathEscape(parts[1]),
48+
url.PathEscape(parts[2]),
49+
), nil
50+
}
51+
52+
// NewOpenCmd returns the "circleci open" command.
53+
func NewOpenCmd() *cobra.Command {
54+
return &cobra.Command{
55+
Use: "open",
56+
Short: "Open the current project in the browser",
57+
Long: heredoc.Doc(`
58+
Open the CircleCI pipelines page for the current project in your
59+
default web browser.
60+
61+
The project is inferred from the current git repository's remote.
62+
Supports GitHub, Bitbucket, and GitLab remotes.
63+
`),
64+
Example: heredoc.Doc(`
65+
# Open pipelines for the current repo
66+
$ circleci open
67+
`),
68+
RunE: func(_ *cobra.Command, _ []string) error {
69+
info, err := gitremote.Detect()
70+
if err != nil {
71+
return clierrors.New("git.detect_failed",
72+
"Could not detect project from git remote", err.Error()).
73+
WithSuggestions(
74+
"Run from inside a git repository with a GitHub, Bitbucket, or GitLab remote",
75+
).
76+
WithExitCode(clierrors.ExitBadArguments)
77+
}
78+
79+
projectURL, err := ProjectURL(info.Slug)
80+
if err != nil {
81+
return err
82+
}
83+
84+
return browser.OpenURL(projectURL)
85+
},
86+
}
87+
}

internal/cmd/open/open_test.go

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
// Copyright (c) 2026 Circle Internet Services, Inc.
2+
//
3+
// Permission is hereby granted, free of charge, to any person obtaining a copy
4+
// of this software and associated documentation files (the "Software"), to deal
5+
// in the Software without restriction, including without limitation the rights
6+
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7+
// copies of the Software, and to permit persons to whom the Software is
8+
// furnished to do so, subject to the following conditions:
9+
//
10+
// The above copyright notice and this permission notice shall be included in
11+
// all copies or substantial portions of the Software.
12+
//
13+
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14+
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15+
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16+
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17+
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18+
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19+
// SOFTWARE.
20+
//
21+
// SPDX-License-Identifier: MIT
22+
23+
package open
24+
25+
import (
26+
"testing"
27+
28+
"gotest.tools/v3/assert"
29+
"gotest.tools/v3/assert/cmp"
30+
)
31+
32+
func TestProjectURL(t *testing.T) {
33+
t.Run("github project", func(t *testing.T) {
34+
got, err := ProjectURL("gh/bar/foo")
35+
assert.NilError(t, err)
36+
assert.Check(t, cmp.Equal(got, "https://app.circleci.com/pipelines/gh/bar/foo"))
37+
})
38+
39+
t.Run("bitbucket project", func(t *testing.T) {
40+
got, err := ProjectURL("bb/myorg/myrepo")
41+
assert.NilError(t, err)
42+
assert.Check(t, cmp.Equal(got, "https://app.circleci.com/pipelines/bb/myorg/myrepo"))
43+
})
44+
45+
t.Run("gitlab project", func(t *testing.T) {
46+
got, err := ProjectURL("gl/my-group/my-project")
47+
assert.NilError(t, err)
48+
assert.Check(t, cmp.Equal(got, "https://app.circleci.com/pipelines/gl/my-group/my-project"))
49+
})
50+
51+
t.Run("invalid slug", func(t *testing.T) {
52+
_, err := ProjectURL("invalid")
53+
assert.Check(t, err != nil, "expected error for invalid slug")
54+
})
55+
}

internal/cmd/root/root.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import (
3636
"github.com/CircleCI-Public/circleci-cli-v2/internal/cmd/envvar"
3737
"github.com/CircleCI-Public/circleci-cli-v2/internal/cmd/job"
3838
cmdlogs "github.com/CircleCI-Public/circleci-cli-v2/internal/cmd/logs"
39+
cmdopen "github.com/CircleCI-Public/circleci-cli-v2/internal/cmd/open"
3940
"github.com/CircleCI-Public/circleci-cli-v2/internal/cmd/pipeline"
4041
"github.com/CircleCI-Public/circleci-cli-v2/internal/cmd/project"
4142
"github.com/CircleCI-Public/circleci-cli-v2/internal/cmd/runner"
@@ -98,6 +99,7 @@ func NewRootCmd(version string) *cobra.Command {
9899
cmd.AddCommand(envvar.NewEnvVarCmd())
99100
cmd.AddCommand(job.NewJobCmd())
100101
cmd.AddCommand(cmdlogs.NewLogsCmd())
102+
cmd.AddCommand(cmdopen.NewOpenCmd())
101103
cmd.AddCommand(pipeline.NewPipelineCmd())
102104
cmd.AddCommand(project.NewProjectCmd())
103105
cmd.AddCommand(runner.NewRunnerCmd())

internal/cmd/root/testdata/usage/circleci.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ Available Commands:
1111
job Manage jobs
1212
logs Fetch job logs
1313
mcp MCP server management
14+
open Open the current project in the browser
1415
pipeline Manage pipelines
1516
project Manage CircleCI projects
1617
runner Manage self-hosted runners
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
Usage:
2+
circleci open [flags]
3+
4+
Examples:
5+
# Open pipelines for the current repo
6+
$ circleci open
7+
8+
9+
Global Flags:
10+
-c, --config string path to config file (default: ~/.config/circleci/config.yml)
11+
--debug enable debug logging
12+
-q, --quiet suppress informational output; data on stdout is unaffected

0 commit comments

Comments
 (0)