From 7c522e744108cfdbf586e813b58ba23a2b11dda7 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Sun, 28 Jul 2024 22:35:45 +0300 Subject: [PATCH] Default to v2 for Slack. Fixes #35 --- api/gitlab/build.go | 10 +- bridgeconfig/slack.tpl.yaml | 345 ++++++---------------------------- bridgeconfig/slackv2.tpl.yaml | 61 ------ cmd/bbctl/bridgeutil.go | 2 - cmd/bbctl/config.go | 3 +- cmd/bbctl/run.go | 12 +- 6 files changed, 67 insertions(+), 366 deletions(-) delete mode 100644 bridgeconfig/slackv2.tpl.yaml diff --git a/api/gitlab/build.go b/api/gitlab/build.go index 214ce89..137d873 100644 --- a/api/gitlab/build.go +++ b/api/gitlab/build.go @@ -172,9 +172,8 @@ func needsLibolmDylib(bridge string) bool { } } -func DownloadMautrixBridgeBinary(ctx context.Context, bridge, path string, noUpdate bool, branchOverride, currentCommit string) error { +func DownloadMautrixBridgeBinary(ctx context.Context, bridge, path string, v2, noUpdate bool, branchOverride, currentCommit string) error { domain := "mau.dev" - v2 := strings.HasSuffix(bridge, "v2") bridge = strings.TrimSuffix(bridge, "v2") repo := fmt.Sprintf("mautrix/%s", bridge) fileName := filepath.Base(path) @@ -202,13 +201,6 @@ func DownloadMautrixBridgeBinary(ctx context.Context, bridge, path string, noUpd if err != nil { return fmt.Errorf("failed to get last build info: %w", err) } - // TODO remove this hack after slackv2 is merged to main - if build.JobURL == "" && ref == "main" && repo == "mautrix/slack" { - build, err = GetLastBuild(domain, repo, "refactor", job) - if err != nil { - return fmt.Errorf("failed to get last build info: %w", err) - } - } if build.Commit == currentCommit { log.Printf("[cyan]%s[reset] is up to date (commit: %s)", fileName, linkifyCommit(repo, currentCommit)) return nil diff --git a/bridgeconfig/slack.tpl.yaml b/bridgeconfig/slack.tpl.yaml index 890e24e..484f4d5 100644 --- a/bridgeconfig/slack.tpl.yaml +++ b/bridgeconfig/slack.tpl.yaml @@ -1,292 +1,61 @@ -# Homeserver details. -homeserver: - # The address that this appservice can use to connect to the homeserver - address: {{ .HungryAddress }} - # The domain of the homeserver (for MXIDs, etc). - domain: beeper.local - - # What software is the homeserver running? - # Standard Matrix homeservers like Synapse, Dendrite and Conduit should just use "standard" here. - software: hungry - # The URL to push real-time bridge status to. - # If set, the bridge will make POST requests to this URL whenever a user's slack connection state changes. - # The bridge will use the appservice as_token to authorize requests. - status_endpoint: null - # Endpoint for reporting per-message status. - message_send_checkpoint_endpoint: null - # Does the homeserver support https://github.com/matrix-org/matrix-spec-proposals/pull/2246? - async_media: true - - # Should the bridge use a websocket for connecting to the homeserver? - # The server side is currently not documented anywhere and is only implemented by mautrix-wsproxy, - # mautrix-asmux (deprecated), and hungryserv (proprietary). - websocket: {{ .Websocket }} - # How often should the websocket be pinged? Pinging will be disabled if this is zero. - ping_interval_seconds: 180 - -# Application service host/registration related details. -# Changing these values requires regeneration of the registration. -appservice: - # The address that the homeserver can use to connect to this appservice. - address: null - - # The hostname and port where this appservice should listen. - hostname: {{ if .Websocket }}null{{ else }}{{ .ListenAddr }}{{ end }} - port: {{ if .Websocket }}null{{ else }}{{ .ListenPort }}{{ end }} - - # Database config. - database: - # The database type. "sqlite3-fk-wal" and "postgres" are supported. - type: sqlite3-fk-wal - # The database URI. - # SQLite: A raw file path is supported, but `file:?_txlock=immediate` is recommended. - # https://github.com/mattn/go-sqlite3#connection-string - # Postgres: Connection string. For example, postgres://user:password@host/database?sslmode=disable - # To connect via Unix socket, use something like postgres:///dbname?host=/var/run/postgresql - uri: file:{{.DatabasePrefix}}mautrix-slack.db?_txlock=immediate - # Maximum number of connections. Mostly relevant for Postgres. - max_open_conns: 5 - max_idle_conns: 2 - # Maximum connection idle time and lifetime before they're closed. Disabled if null. - # Parsed with https://pkg.go.dev/time#ParseDuration - max_conn_idle_time: null - max_conn_lifetime: null - - # The unique ID of this appservice. - id: {{ .AppserviceID }} - # Appservice bot details. - bot: - # Username of the appservice bot. - username: {{ .BridgeName }}bot - # Display name and avatar for bot. Set to "remove" to remove display name/avatar, leave empty - # to leave display name/avatar as-is. - displayname: Slack bridge bot - avatar: mxc://maunium.net/pVtzLmChZejGxLqmXtQjFxem - - # Whether or not to receive ephemeral events via appservice transactions. - # Requires MSC2409 support (i.e. Synapse 1.22+). - # You should disable bridge -> sync_with_custom_puppets when this is enabled. - ephemeral_events: true - - # Should incoming events be handled asynchronously? - # This may be necessary for large public instances with lots of messages going through. - # However, messages will not be guaranteed to be bridged in the same order they were sent in. - async_transactions: false - - # Authentication tokens for AS <-> HS communication. Autogenerated; do not modify. - as_token: {{ .ASToken }} - hs_token: {{ .HSToken }} - -# Bridge config -bridge: - # Localpart template of MXIDs for Slack users. - # {{.}} is replaced with the internal ID of the Slack user. - username_template: {{ .BridgeName }}_{{ "{{.}}" }} - # Displayname template for Slack users. - # TODO: document variables - displayname_template: {{ `"{{.RealName}}"` }} - bot_displayname_template: {{ `"{{.Name}}"` }} - channel_name_template: {{ `"#{{.Name}}"` }} - - portal_message_buffer: 128 - - # Should the bridge send a read receipt from the bridge bot when a message has been sent to Slack? - delivery_receipts: false - # Whether the bridge should send the message status as a custom com.beeper.message_send_status event. - message_status_events: true - # Whether the bridge should send error notices via m.notice events when a message fails to bridge. - message_error_notices: false +network: + # Displayname template for Slack users. Available variables: + # .Name - The username of the user + # .ID - The internal ID of the user + # .IsBot - Whether the user is a bot + # .Profile.DisplayName - The username or real name of the user (depending on settings) + # Variables only available for users (not bots): + # .TeamID - The internal ID of the workspace the user is in + # .TZ - The timezone region of the user (e.g. Europe/London) + # .TZLabel - The label of the timezone of the user (e.g. Greenwich Mean Time) + # .TZOffset - The UTC offset of the timezone of the user (e.g. 0) + # .Profile.RealName - The real name of the user + # .Profile.FirstName - The first name of the user + # .Profile.LastName - The last name of the user + # .Profile.Title - The job title of the user + # .Profile.Pronouns - The pronouns of the user + # .Profile.Email - The email address of the user + # .Profile.Phone - The formatted phone number of the user + displayname_template: '{{ `{{.Profile.DisplayName}}{{if .IsBot}} (bot){{end}}` }}' + # Channel name template for Slack channels (all types). Available variables: + # .Name - The name of the channel + # .TeamName - The name of the team the channel is in + # .TeamDomain - The Slack subdomain of the team the channel is in + # .ID - The internal ID of the channel + # .IsNoteToSelf - Whether the channel is a DM with yourself + # .IsGeneral - Whether the channel is the #general channel + # .IsChannel - Whether the channel is a channel (rather than a DM) + # .IsPrivate - Whether the channel is private + # .IsIM - Whether the channel is a one-to-one DM + # .IsMpIM - Whether the channel is a group DM + # .IsShared - Whether the channel is shared with another workspace. + # .IsExtShared - Whether the channel is shared with an external organization. + # .IsOrgShared - Whether the channel is shared with an organization in the same enterprise grid. + channel_name_template: '{{ `{{if or .IsNoteToSelf (not .IsIM)}}{{if and .IsChannel (not .IsPrivate)}}#{{end}}{{.Name}}{{if .IsNoteToSelf}} (you){{end}}{{end}}` }}' + # Displayname template for Slack workspaces. Available variables: + # .Name - The name of the team + # .Domain - The Slack subdomain of the team + # .ID - The internal ID of the team + team_name_template: '{{ `{{.Name}}` }}' # Should incoming custom emoji reactions be bridged as mxc:// URIs? # If set to false, custom emoji reactions will be bridged as the shortcode instead, and the image won't be available. custom_emoji_reactions: true - - # Should the bridge sync with double puppeting to receive EDUs that aren't normally sent to appservices. - sync_with_custom_puppets: false - # Should the bridge update the m.direct account data event when double puppeting is enabled. - # Note that updating the m.direct event is not atomic (except with mautrix-asmux) - # and is therefore prone to race conditions. - sync_direct_chat_list: false - # Whether or not created rooms should have federation enabled. - # If false, created portal rooms will never be federated. - federate_rooms: false - # Whether to explicitly set the avatar and room name for private chat portal rooms. - # If set to `default`, this will be enabled in encrypted rooms and disabled in unencrypted rooms. - # If set to `always`, all DM rooms will have explicit names and avatars set. - # If set to `never`, DM rooms will never have names and avatars set. - private_chat_portal_meta: default - - # Servers to always allow double puppeting from - double_puppet_server_map: - {{ .BeeperDomain }}: {{ .HungryAddress }} - # Allow using double puppeting from any server with a valid client .well-known file. - double_puppet_allow_discovery: false - # Shared secrets for https://github.com/devture/matrix-synapse-shared-secret-auth - # - # If set, double puppeting will be enabled automatically for local users - # instead of users having to find an access token and run `login-matrix` - # manually. - login_shared_secret_map: - {{ .BeeperDomain }}: appservice - - message_handling_timeout: - # Send an error message after this timeout, but keep waiting for the response until the deadline. - # This is counted from the origin_server_ts, so the warning time is consistent regardless of the source of delay. - # If the message is older than this when it reaches the bridge, the message won't be handled at all. - error_after: 10s - # Drop messages after this timeout. They may still go through if the message got sent to the servers. - # This is counted from the time the bridge starts handling the message. - deadline: 60s - - # The prefix for commands. Only required in non-management rooms. - command_prefix: '!slack' - # Messages sent upon joining a management room. - # Markdown is supported. The defaults are listed below. - management_room_text: - # Sent when joining a room. - welcome: "Hello, I'm a Slack bridge bot." - # Sent when joining a management room and the user is already logged in. - welcome_connected: "Use `help` for help." - # Sent when joining a management room and the user is not logged in. - welcome_unconnected: "Use `help` for help, or `login-token` or `login-password` to log in." - # Optional extra text sent when joining a management room. - additional_help: "" - + # Should channels and group DMs have the workspace icon as the Matrix room avatar? + workspace_avatar_in_rooms: false + # Number of participants to sync in channels (doesn't affect group DMs) + participant_sync_count: 5 + # Should channel participants only be synced when creating the room? + # If you want participants to always be accurately synced, set participant_sync_count to a high value and this to false. + participant_sync_only_on_create: true + # Options for backfilling messages from Slack. backfill: - # Allow backfilling at all? Requires MSC2716 support on homeserver. - enable: true - - # Maximum number of conversations to fetch from Slack when syncing team from Slack. - # Must be 0-999 - conversations_count: 200 - - # If a backfilled chat is older than this number of hours, mark it as read even if it's unread on Slack. - # Set to -1 to let any chat be unread. - unread_hours_threshold: 720 - - # Number of messages to immediately backfill when creating a portal. - immediate_messages: 10 - - # Settings for incremental backfill of history. - incremental: - # Maximum number of messages to backfill per batch. - messages_per_batch: 100 - # The number of seconds to wait after backfilling the batch of messages. - post_batch_delay: 20 - # The maximum number of messages to backfill per portal, split by the chat type. - # If set to -1, all messages in the chat will eventually be backfilled. - max_messages: - # Channels - channel: 1000 - # Group direct messages - group_dm: -1 - # 1:1 direct messages - dm: -1 - - # End-to-bridge encryption support options. - # - # See https://docs.mau.fi/bridges/general/end-to-bridge-encryption.html for more info. - encryption: - # Allow encryption, work in group chat rooms with e2ee enabled - allow: true - # Default to encryption, force-enable encryption in all portals the bridge creates - # This will cause the bridge bot to be in private chats for the encryption to work properly. - default: true - # Whether to use MSC2409/MSC3202 instead of /sync long polling for receiving encryption-related data. - appservice: true - # Require encryption, drop any unencrypted messages. - require: true - # Enable key sharing? If enabled, key requests for rooms where users are in will be fulfilled. - # You must use a client that supports requesting keys from other users to use this feature. - allow_key_sharing: true - # Options for deleting megolm sessions from the bridge. - delete_keys: - # Beeper-specific: delete outbound sessions when hungryserv confirms - # that the user has uploaded the key to key backup. - delete_outbound_on_ack: true - # Don't store outbound sessions in the inbound table. - dont_store_outbound: false - # Ratchet megolm sessions forward after decrypting messages. - ratchet_on_decrypt: true - # Delete fully used keys (index >= max_messages) after decrypting messages. - delete_fully_used_on_decrypt: true - # Delete previous megolm sessions from same device when receiving a new one. - delete_prev_on_new_session: true - # Delete megolm sessions received from a device when the device is deleted. - delete_on_device_delete: true - # Periodically delete megolm sessions when 2x max_age has passed since receiving the session. - periodically_delete_expired: true - # Delete inbound megolm sessions that don't have the received_at field used for - # automatic ratcheting and expired session deletion. This is meant as a migration - # to delete old keys prior to the bridge update. - delete_outdated_inbound: true - # What level of device verification should be required from users? - # - # Valid levels: - # unverified - Send keys to all device in the room. - # cross-signed-untrusted - Require valid cross-signing, but trust all cross-signing keys. - # cross-signed-tofu - Require valid cross-signing, trust cross-signing keys on first use (and reject changes). - # cross-signed-verified - Require valid cross-signing, plus a valid user signature from the bridge bot. - # Note that creating user signatures from the bridge bot is not currently possible. - # verified - Require manual per-device verification - # (currently only possible by modifying the `trust` column in the `crypto_device` database table). - verification_levels: - # Minimum level for which the bridge should send keys to when bridging messages from WhatsApp to Matrix. - receive: cross-signed-tofu - # Minimum level that the bridge should accept for incoming Matrix messages. - send: cross-signed-tofu - # Minimum level that the bridge should require for accepting key requests. - share: cross-signed-tofu - # Options for Megolm room key rotation. These options allow you to - # configure the m.room.encryption event content. See: - # https://spec.matrix.org/v1.3/client-server-api/#mroomencryption for - # more information about that event. - rotation: - # Enable custom Megolm room key rotation settings. Note that these - # settings will only apply to rooms created after this option is - # set. - enable_custom: true - # The maximum number of milliseconds a session should be used - # before changing it. The Matrix spec recommends 604800000 (a week) - # as the default. - milliseconds: 2592000000 - # The maximum number of messages that should be sent with a given a - # session before changing it. The Matrix spec recommends 100 as the - # default. - messages: 10000 - - # Disable rotating keys when a user's devices change? - # You should not enable this option unless you understand all the implications. - disable_device_change_key_rotation: true - - # Settings for provisioning API - provisioning: - # Prefix for the provisioning API paths. - prefix: /_matrix/provision - # Shared secret for authentication. If set to "generate", a random secret will be generated, - # or if set to "disable", the provisioning API will be disabled. - shared_secret: {{ .ProvisioningSecret }} - - # Permissions for using the bridge. - # Permitted values: - # relay - Talk through the relaybot (if enabled), no access otherwise - # user - Access to use the bridge to chat with a Slack account. - # admin - User level and some additional administration tools - # Permitted keys: - # * - All Matrix users - # domain - All users on that homeserver - # mxid - Specific user - permissions: - "{{ .UserID }}": admin - -# Logging config. See https://github.com/tulir/zeroconfig for details. -logging: - min_level: debug - writers: - - type: stdout - format: pretty-colored - - type: file - format: json - filename: ./logs/mautrix-slack.log - max_size: 100 - max_backups: 10 - compress: false + # Number of conversations to fetch from Slack when syncing workspace. + # This option applies even if message backfill is disabled below. + # If set to -1, all chats in the client.boot response will be bridged, and nothing will be fetched separately. + conversation_count: -1 + +{{ setfield . "CommandPrefix" "!slack" -}} +{{ setfield . "DatabaseFileName" "mautrix-slack" -}} +{{ setfield . "BridgeTypeName" "Slack" -}} +{{ setfield . "BridgeTypeIcon" "mxc://maunium.net/pVtzLmChZejGxLqmXtQjFxem" -}} +{{ template "bridgev2.tpl.yaml" . }} diff --git a/bridgeconfig/slackv2.tpl.yaml b/bridgeconfig/slackv2.tpl.yaml deleted file mode 100644 index 484f4d5..0000000 --- a/bridgeconfig/slackv2.tpl.yaml +++ /dev/null @@ -1,61 +0,0 @@ -network: - # Displayname template for Slack users. Available variables: - # .Name - The username of the user - # .ID - The internal ID of the user - # .IsBot - Whether the user is a bot - # .Profile.DisplayName - The username or real name of the user (depending on settings) - # Variables only available for users (not bots): - # .TeamID - The internal ID of the workspace the user is in - # .TZ - The timezone region of the user (e.g. Europe/London) - # .TZLabel - The label of the timezone of the user (e.g. Greenwich Mean Time) - # .TZOffset - The UTC offset of the timezone of the user (e.g. 0) - # .Profile.RealName - The real name of the user - # .Profile.FirstName - The first name of the user - # .Profile.LastName - The last name of the user - # .Profile.Title - The job title of the user - # .Profile.Pronouns - The pronouns of the user - # .Profile.Email - The email address of the user - # .Profile.Phone - The formatted phone number of the user - displayname_template: '{{ `{{.Profile.DisplayName}}{{if .IsBot}} (bot){{end}}` }}' - # Channel name template for Slack channels (all types). Available variables: - # .Name - The name of the channel - # .TeamName - The name of the team the channel is in - # .TeamDomain - The Slack subdomain of the team the channel is in - # .ID - The internal ID of the channel - # .IsNoteToSelf - Whether the channel is a DM with yourself - # .IsGeneral - Whether the channel is the #general channel - # .IsChannel - Whether the channel is a channel (rather than a DM) - # .IsPrivate - Whether the channel is private - # .IsIM - Whether the channel is a one-to-one DM - # .IsMpIM - Whether the channel is a group DM - # .IsShared - Whether the channel is shared with another workspace. - # .IsExtShared - Whether the channel is shared with an external organization. - # .IsOrgShared - Whether the channel is shared with an organization in the same enterprise grid. - channel_name_template: '{{ `{{if or .IsNoteToSelf (not .IsIM)}}{{if and .IsChannel (not .IsPrivate)}}#{{end}}{{.Name}}{{if .IsNoteToSelf}} (you){{end}}{{end}}` }}' - # Displayname template for Slack workspaces. Available variables: - # .Name - The name of the team - # .Domain - The Slack subdomain of the team - # .ID - The internal ID of the team - team_name_template: '{{ `{{.Name}}` }}' - # Should incoming custom emoji reactions be bridged as mxc:// URIs? - # If set to false, custom emoji reactions will be bridged as the shortcode instead, and the image won't be available. - custom_emoji_reactions: true - # Should channels and group DMs have the workspace icon as the Matrix room avatar? - workspace_avatar_in_rooms: false - # Number of participants to sync in channels (doesn't affect group DMs) - participant_sync_count: 5 - # Should channel participants only be synced when creating the room? - # If you want participants to always be accurately synced, set participant_sync_count to a high value and this to false. - participant_sync_only_on_create: true - # Options for backfilling messages from Slack. - backfill: - # Number of conversations to fetch from Slack when syncing workspace. - # This option applies even if message backfill is disabled below. - # If set to -1, all chats in the client.boot response will be bridged, and nothing will be fetched separately. - conversation_count: -1 - -{{ setfield . "CommandPrefix" "!slack" -}} -{{ setfield . "DatabaseFileName" "mautrix-slack" -}} -{{ setfield . "BridgeTypeName" "Slack" -}} -{{ setfield . "BridgeTypeIcon" "mxc://maunium.net/pVtzLmChZejGxLqmXtQjFxem" -}} -{{ template "bridgev2.tpl.yaml" . }} diff --git a/cmd/bbctl/bridgeutil.go b/cmd/bbctl/bridgeutil.go index f870754..bc53fb2 100644 --- a/cmd/bbctl/bridgeutil.go +++ b/cmd/bbctl/bridgeutil.go @@ -29,7 +29,6 @@ var officialBridges = []bridgeTypeToNames{ {"linkedin", []string{"linkedin"}}, {"signalv2", []string{"signalv2"}}, {"signal", []string{"signal"}}, - {"slackv2", []string{"slackv2"}}, {"slack", []string{"slack"}}, {"telegram", []string{"telegram"}}, {"twitter", []string{"twitter"}}, @@ -48,7 +47,6 @@ var websocketBridges = map[string]bool{ "imessagego": true, "signal": true, "signalv2": true, - "slackv2": true, "bridgev2": true, "meta": true, } diff --git a/cmd/bbctl/config.go b/cmd/bbctl/config.go index 2a89137..c776ac5 100644 --- a/cmd/bbctl/config.go +++ b/cmd/bbctl/config.go @@ -222,7 +222,6 @@ var bridgeIPSuffix = map[string]string{ "signalv2": "28", "discord": "34", "slack": "35", - "slackv2": "35", "gmessages": "36", "imessagego": "37", } @@ -357,7 +356,7 @@ func generateBridgeConfig(ctx *cli.Context) error { startupCommand += " -c " + outputPath } installInstructions = fmt.Sprintf("https://docs.mau.fi/bridges/go/setup.html?bridge=%s#installation", cfg.BridgeType) - case "signalv2", "slackv2": + case "signalv2": startupCommand = fmt.Sprintf("mautrix-%s-v2", strings.TrimSuffix(cfg.BridgeType, "v2")) if outputPath != "config.yaml" && outputPath != "" { startupCommand += " -c " + outputPath diff --git a/cmd/bbctl/run.go b/cmd/bbctl/run.go index 8e954ef..46cdd2a 100644 --- a/cmd/bbctl/run.go +++ b/cmd/bbctl/run.go @@ -105,7 +105,7 @@ type VersionJSONOutput struct { } } -func updateGoBridge(ctx context.Context, binaryPath, bridgeType string, noUpdate bool) error { +func updateGoBridge(ctx context.Context, binaryPath, bridgeType string, v2, noUpdate bool) error { var currentVersion VersionJSONOutput err := os.MkdirAll(filepath.Dir(binaryPath), 0700) @@ -120,7 +120,7 @@ func updateGoBridge(ctx context.Context, binaryPath, bridgeType string, noUpdate log.Printf("Failed to get parse bridge version: [red]%v[reset] - reinstalling", err) } } - return gitlab.DownloadMautrixBridgeBinary(ctx, bridgeType, binaryPath, noUpdate, "", currentVersion.Commit) + return gitlab.DownloadMautrixBridgeBinary(ctx, bridgeType, binaryPath, v2, noUpdate, "", currentVersion.Commit) } func compileGoBridge(ctx context.Context, buildDir, binaryPath, bridgeType string, noUpdate bool) error { @@ -317,9 +317,13 @@ func runBridge(ctx *cli.Context) error { var bridgeArgs []string var needsWebsocketProxy bool switch cfg.BridgeType { - case "imessage", "imessagego", "whatsapp", "discord", "slack", "slackv2", "gmessages", "signal", "signalv2", "meta": + case "imessage", "imessagego", "whatsapp", "discord", "slack", "gmessages", "signal", "signalv2", "meta": binaryName := fmt.Sprintf("mautrix-%s", cfg.BridgeType) v2 := false + switch cfg.BridgeType { + case "slack": + v2 = true + } if strings.HasSuffix(cfg.BridgeType, "v2") { binaryName = fmt.Sprintf("mautrix-%s", strings.TrimSuffix(cfg.BridgeType, "v2")) if cfg.BridgeType == "signalv2" { @@ -350,7 +354,7 @@ func runBridge(ctx *cli.Context) error { return fmt.Errorf("failed to compile bridge: %w", err) } } else if overrideBridgeCmd == "" { - err = updateGoBridge(ctx.Context, bridgeCmd, cfg.BridgeType, ctx.Bool("no-update")) + err = updateGoBridge(ctx.Context, bridgeCmd, cfg.BridgeType, v2, ctx.Bool("no-update")) if errors.Is(err, gitlab.ErrNotBuiltInCI) { return UserError{fmt.Sprintf("Binaries for %s are not built in the CI. Use --compile to tell bbctl to build the bridge locally.", binaryName)} } else if err != nil {