Skip to content

Commit baa85c9

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 5c136ec commit baa85c9

8 files changed

Lines changed: 161 additions & 0 deletions

File tree

CLAUDE.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ internal/
7272
│ ├── config/ circleci config validate/process/pack/generate
7373
│ ├── context/ circleci context + circleci context secret
7474
│ ├── job/ circleci job artifacts (deep path; wraps internal/artifacts)
75+
│ ├── open/ circleci open (opens current project in the CircleCI web UI)
7576
│ ├── pipeline/ circleci pipeline list/get/trigger
7677
│ ├── workflow/ circleci workflow list/get/cancel/rerun
7778
│ ├── orb/ circleci orb list/info/validate/publish/...

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ require (
1212
github.com/go-chi/chi/v5 v5.2.5
1313
github.com/go-chi/render v1.0.3
1414
github.com/hashicorp/go-retryablehttp v0.7.8
15+
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c
1516
github.com/spf13/cobra v1.10.2
1617
github.com/zalando/go-keyring v0.2.8
1718
golang.org/x/term v0.42.0

go.sum

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -529,6 +529,8 @@ github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3v
529529
github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
530530
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
531531
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
532+
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
533+
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
532534
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
533535
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
534536
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
@@ -864,6 +866,7 @@ golang.org/x/sys v0.0.0-20211105183446-c75c47738b0c/go.mod h1:oPkhp1MJrh7nUepCBc
864866
golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
865867
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
866868
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
869+
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
867870
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
868871
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
869872
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
@@ -33,6 +33,7 @@ import (
3333
"github.com/CircleCI-Public/circleci-cli-v2/internal/cmd/envvar"
3434
"github.com/CircleCI-Public/circleci-cli-v2/internal/cmd/job"
3535
cmdlogs "github.com/CircleCI-Public/circleci-cli-v2/internal/cmd/logs"
36+
cmdopen "github.com/CircleCI-Public/circleci-cli-v2/internal/cmd/open"
3637
"github.com/CircleCI-Public/circleci-cli-v2/internal/cmd/pipeline"
3738
"github.com/CircleCI-Public/circleci-cli-v2/internal/cmd/project"
3839
"github.com/CircleCI-Public/circleci-cli-v2/internal/cmd/runner"
@@ -73,6 +74,7 @@ func NewRootCmd(version string) *cobra.Command {
7374
cmd.AddCommand(envvar.NewEnvVarCmd())
7475
cmd.AddCommand(job.NewJobCmd())
7576
cmd.AddCommand(cmdlogs.NewLogsCmd())
77+
cmd.AddCommand(cmdopen.NewOpenCmd())
7678
cmd.AddCommand(pipeline.NewPipelineCmd())
7779
cmd.AddCommand(project.NewProjectCmd())
7880
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
@@ -9,6 +9,7 @@ Available Commands:
99
envvar Manage project environment variables
1010
job Manage jobs
1111
logs Fetch job logs
12+
open Open the current project in the browser
1213
pipeline Manage pipelines
1314
project Manage CircleCI projects
1415
runner Manage self-hosted runners
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
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+
--debug enable debug logging
11+
-q, --quiet suppress informational output; data on stdout is unaffected

0 commit comments

Comments
 (0)