Skip to content

Commit 7553022

Browse files
committed
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 1ec3ba6 commit 7553022

8 files changed

Lines changed: 162 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/...

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ require (
1414
github.com/gofrs/flock v0.13.0
1515
github.com/hashicorp/go-retryablehttp v0.7.8
1616
github.com/njayp/ophis v1.1.4
17+
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c
1718
github.com/spf13/cobra v1.10.2
1819
github.com/zalando/go-keyring v0.2.8
1920
golang.org/x/term v0.42.0

go.sum

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -538,6 +538,8 @@ github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3v
538538
github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
539539
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
540540
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
541+
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
542+
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
541543
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
542544
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
543545
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
@@ -878,6 +880,7 @@ golang.org/x/sys v0.0.0-20211105183446-c75c47738b0c/go.mod h1:oPkhp1MJrh7nUepCBc
878880
golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
879881
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
880882
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
883+
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
881884
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
882885
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
883886
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=

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
@@ -34,6 +34,7 @@ import (
3434
"github.com/CircleCI-Public/circleci-cli-v2/internal/cmd/envvar"
3535
"github.com/CircleCI-Public/circleci-cli-v2/internal/cmd/job"
3636
cmdlogs "github.com/CircleCI-Public/circleci-cli-v2/internal/cmd/logs"
37+
cmdopen "github.com/CircleCI-Public/circleci-cli-v2/internal/cmd/open"
3738
"github.com/CircleCI-Public/circleci-cli-v2/internal/cmd/pipeline"
3839
"github.com/CircleCI-Public/circleci-cli-v2/internal/cmd/project"
3940
"github.com/CircleCI-Public/circleci-cli-v2/internal/cmd/runner"
@@ -82,6 +83,7 @@ func NewRootCmd(version string) *cobra.Command {
8283
cmd.AddCommand(envvar.NewEnvVarCmd())
8384
cmd.AddCommand(job.NewJobCmd())
8485
cmd.AddCommand(cmdlogs.NewLogsCmd())
86+
cmd.AddCommand(cmdopen.NewOpenCmd())
8587
cmd.AddCommand(pipeline.NewPipelineCmd())
8688
cmd.AddCommand(project.NewProjectCmd())
8789
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
@@ -10,6 +10,7 @@ Available Commands:
1010
job Manage jobs
1111
logs Fetch job logs
1212
mcp MCP server management
13+
open Open the current project in the browser
1314
pipeline Manage pipelines
1415
project Manage CircleCI projects
1516
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)