1// SPDX-License-Identifier: AGPL-3.0-or-later
2
3package app
4
5import (
6 "fmt"
7 "strings"
8
9 "connectrpc.com/connect"
10 "github.com/spf13/cobra"
11
12 "bnema.dev/ermite-api/gen/ermite"
13 "bnema.dev/ermite/internal/adapters/client"
14)
15
16// jsonVersion is the stable JSON shape of `ermite version --json`.
17type jsonVersion struct {
18 Client jsonClientVersion `json:"client"`
19 Server *jsonServerVersion `json:"server"`
20}
21
22type jsonClientVersion struct {
23 Version string `json:"version"`
24}
25
26type jsonServerVersion struct {
27 URL string `json:"url"`
28 Version string `json:"version"`
29 Features []string `json:"features"`
30 MissingFeatures []string `json:"missing_features"`
31}
32
33func newVersionCmd(env *cliEnv) *cobra.Command {
34 cmd := &cobra.Command{
35 Use: "version",
36 Short: "Print the client version and, when a server URL is configured, the server's",
37 Long: "Print the version of this client and, when a server URL is configured (ERMITE_URL or the url\n" +
38 "of the config file, see `ermite config`), the version of the server and the optional features\n" +
39 "of the API it implements. A warning on standard error says which of the features this client\n" +
40 "uses the server lacks. Without a URL only the client is shown. The token command of the config\n" +
41 "file is not run.\n\n" +
42 "JSON fields (--json): client.version; server, null without a URL, with url, version,\n" +
43 "features (array of identifiers) and missing_features (the ones this client uses that the\n" +
44 "server lacks).",
45 Args: cobra.NoArgs,
46 }
47 asJSON := addJSONFlag(cmd)
48 cmd.RunE = func(cmd *cobra.Command, _ []string) error {
49 out := cmd.OutOrStdout()
50 clientLine := "ermite " + env.version
51 url, err := env.resolveURL(cmd, true)
52 if err != nil {
53 // The client version does not depend on the file: say what is wrong
54 // and go on as if no URL were configured.
55 warnf(cmd, "%s", err)
56 url = ""
57 }
58 if url == "" {
59 if *asJSON {
60 return writeJSON(out, jsonVersion{Client: jsonClientVersion{Version: env.version}})
61 }
62 _, err := fmt.Fprintln(out, clientLine)
63 return err
64 }
65
66 // GetVersion is anonymous: token.cmd is not run for it.
67 c, err := env.clientNoTokenCmd(cmd, true)
68 if err != nil {
69 return err
70 }
71 res, err := c.Server().GetVersion(env.ctx(cmd), connect.NewRequest(&ermite.GetVersionRequest{}))
72 if err != nil {
73 // Still show what is known locally before failing.
74 if !*asJSON {
75 _, _ = fmt.Fprintln(out, clientLine)
76 }
77 return c.Describe(err)
78 }
79 features := res.Msg.GetFeatures()
80 if features == nil {
81 features = []string{}
82 }
83 missing := client.MissingFeatures(features)
84 if *asJSON {
85 if missing == nil {
86 missing = []string{}
87 }
88 err = writeJSON(out, jsonVersion{
89 Client: jsonClientVersion{Version: env.version},
90 Server: &jsonServerVersion{URL: c.URL(), Version: res.Msg.GetVersion(), Features: features, MissingFeatures: missing},
91 })
92 } else {
93 err = fields(out, [][2]string{
94 {"client", clientLine},
95 {"server", res.Msg.GetVersion() + " at " + c.URL()},
96 {"features", strings.Join(features, ", ")},
97 })
98 }
99 if err != nil {
100 return err
101 }
102 for _, f := range missing {
103 warnf(cmd, "the server does not support %s, which %s", f, featureUse[f])
104 }
105 return nil
106 }
107 return cmd
108}
109
110// featureUse says what breaks without a feature, for the version warning.
111var featureUse = map[string]string{
112 client.FeatureRepoUpdate: "`ermite repo edit` needs: the command will fail on this server",
113 client.FeatureEventsResume: "`ermite watch` uses to resume after a reconnection: events during an outage may be missed",
114 client.FeatureMergeRequests: "`ermite mr` needs: the commands will fail on this server",
115 client.FeatureMergeDeleteBranch: "`ermite mr merge --delete-branch` needs: the merge works but the branch is kept",
116 client.FeatureIssues: "`ermite issue` needs: the commands will fail on this server, and `repo edit --issues` and `token create --scope issue:write` are refused",
117 client.FeatureSigningKey: "`ermite signing-key show` needs: the command will fail on this server",
118 client.FeatureSession: "`ermite web login` needs: the command will fail on this server",
119 client.FeatureTokenBranchPrefixes: "`ermite token create --branch-prefix` needs: the command is refused, and `token list` does not show the limits",
120 client.FeatureCIRuns: "`ermite run` needs: the commands will fail on this server",
121 client.FeatureCIRunners: "`ermite runner` needs: the commands will fail on this server",
122 client.FeatureCISecrets: "`ermite secret`, `variable` and `environment` need: the commands will fail on this server",
123 client.FeatureCIDispatch: "`ermite workflow run` needs: the command will fail on this server",
124 client.FeatureCIWorkflows: "`ermite workflow list`, `disable` and `enable` need: the commands will fail on this server",
125 client.FeatureCIArtifacts: "`ermite run artifacts` and `run download` need: the commands will fail on this server",
126 client.FeaturePasskeys: "`ermite web passkey` needs: the commands will fail on this server (passkeys need an https public URL with a DNS host name)",
127 client.FeatureCommitSignatures: "`ermite signing-key trust` needs: the commands will fail on this server",
128}