From 16d00f0768234735537befafcdafbeef369fbdfa Mon Sep 17 00:00:00 2001 From: Lukas Eipert Date: Fri, 18 Jul 2025 14:36:32 +0200 Subject: [PATCH 1/7] Add Linear external issue tracker integration This adds a new integration for the Linear issue tracker. It is a little simpler to configure than the other issue trackers because the URLs can be derived from a simple workspace URL. Changelog: added --- .../concerns/integrations/params.rb | 1 + .../concerns/integrations/base/integration.rb | 2 +- .../concerns/integrations/base/linear.rb | 80 +++++++++ app/models/integrations/linear.rb | 7 + app/models/project.rb | 1 + doc/api/graphql/reference/_index.md | 1 + doc/api/group_integrations.md | 39 +++++ doc/api/openapi/openapi_v2.yaml | 159 ++++++++++++++++++ doc/api/project_integrations.md | 39 +++++ doc/integration/external-issue-tracker.md | 1 + doc/user/project/integrations/_index.md | 1 + doc/user/project/integrations/linear.md | 52 ++++++ lib/api/helpers/integrations_helpers.rb | 2 + locale/gitlab.pot | 18 ++ spec/factories/integrations.rb | 7 + spec/models/integrations/linear_spec.rb | 7 + .../base/linear_shared_examples.rb | 75 +++++++++ 17 files changed, 491 insertions(+), 1 deletion(-) create mode 100644 app/models/concerns/integrations/base/linear.rb create mode 100644 app/models/integrations/linear.rb create mode 100644 doc/user/project/integrations/linear.md create mode 100644 spec/models/integrations/linear_spec.rb create mode 100644 spec/support/shared_examples/models/concerns/integrations/base/linear_shared_examples.rb diff --git a/app/controllers/concerns/integrations/params.rb b/app/controllers/concerns/integrations/params.rb index 65c33fbca0366f..fc173a629229ca 100644 --- a/app/controllers/concerns/integrations/params.rb +++ b/app/controllers/concerns/integrations/params.rb @@ -100,6 +100,7 @@ module Params :user_key, :username, :webhook, + :workspace_url, :zentao_product_xid ].freeze diff --git a/app/models/concerns/integrations/base/integration.rb b/app/models/concerns/integrations/base/integration.rb index 0279ebbaf59d9c..5ebb7e37a97327 100644 --- a/app/models/concerns/integrations/base/integration.rb +++ b/app/models/concerns/integrations/base/integration.rb @@ -11,7 +11,7 @@ module Integration INTEGRATION_NAMES = %w[ asana assembla bamboo bugzilla buildkite campfire clickup confluence custom_issue_tracker datadog diffblue_cover discord drone_ci emails_on_push ewm external_wiki - gitlab_slack_application hangouts_chat harbor irker jira matrix + gitlab_slack_application hangouts_chat harbor irker jira linear matrix mattermost mattermost_slash_commands microsoft_teams packagist phorge pipelines_email pivotaltracker prometheus pumble pushover redmine slack slack_slash_commands squash_tm teamcity telegram unify_circuit webex_teams youtrack zentao diff --git a/app/models/concerns/integrations/base/linear.rb b/app/models/concerns/integrations/base/linear.rb new file mode 100644 index 00000000000000..7f2e01d29a6718 --- /dev/null +++ b/app/models/concerns/integrations/base/linear.rb @@ -0,0 +1,80 @@ +# frozen_string_literal: true + +module Integrations + module Base + module Linear + extend ActiveSupport::Concern + + include Base::IssueTracker + include HasAvatar + + LINEAR_WORKSPACE_URI_REGEX = %r{\Ahttps://linear\.app/[a-z0-9\-]{3,32}/?\z} + # References like FEAT-123. + # The project key part is between one and 7 characters long and uppercase letters + digits + LINEAR_ISSUE_REFERENCE = %r{\b(?[A-Z0-9]{1,7}-\d+)\b} + + class_methods do + def title + 'Linear' + end + + def description + s_("LinearIntegration|Use Linear as this project's issue tracker.") + end + + def help + build_help_page_url( + 'user/project/integrations/linear.md', s_("LinearIntegration|Use Linear as this project's issue tracker.") + ) + end + + def to_param + 'linear' + end + + def attribution_notice + s_('LinearIntegration|The Linear logo is a trademark of Linear Orbit Inc. in the U.S. and other countries.') + end + end + + included do + field :workspace_url, + title: -> { s_('LinearIntegration|Workspace URL') }, + placeholder: -> { s_('LinearIntegration|https://linear.app/example') }, + help: -> { s_('LinearIntegration|Linear workspace URL (for example, https://linear.app/example)') }, + required: true + + with_options if: :activated? do + validates :workspace_url, presence: true, public_url: true, format: { + with: LINEAR_WORKSPACE_URI_REGEX, + message: ->(_object, _data) { s_('LinearIntegration|URL must point to a workspace URL like https://linear.app/example') } + } + end + + def reference_pattern(*) + @reference_pattern ||= LINEAR_ISSUE_REFERENCE + end + + def supports_data_fields? + false + end + + # Normally external issue trackers in GitLab save `project_url`, `issue_url` and `new_issue_url` + # in a separate DB table, but for the Linear integration we just use the `encrypted_properties` column + # + # For Linear only the workspace url is needed and the rest of the URLs can be derived + def handle_properties + # no op + end + + def project_url + workspace_url + end + + def issues_url + @issues_url ||= Gitlab::Utils.append_path(workspace_url, '/issue/:id') + end + end + end + end +end diff --git a/app/models/integrations/linear.rb b/app/models/integrations/linear.rb new file mode 100644 index 00000000000000..d7374ad48f1cf3 --- /dev/null +++ b/app/models/integrations/linear.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +module Integrations + class Linear < Integration + include Integrations::Base::Linear + end +end diff --git a/app/models/project.rb b/app/models/project.rb index f1e90aecbe0471..73b3ca17e3469e 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -243,6 +243,7 @@ def self.integration_association_name(name) has_one :jenkins_integration, class_name: 'Integrations::Jenkins' has_one :jira_integration, class_name: 'Integrations::Jira' has_one :jira_cloud_app_integration, class_name: 'Integrations::JiraCloudApp' + has_one :linear_integration, class_name: 'Integrations::Linear' has_one :mattermost_integration, class_name: 'Integrations::Mattermost' has_one :mattermost_slash_commands_integration, class_name: 'Integrations::MattermostSlashCommands' has_one :matrix_integration, class_name: 'Integrations::Matrix' diff --git a/doc/api/graphql/reference/_index.md b/doc/api/graphql/reference/_index.md index 8eed056d29ba2b..dce744392e6838 100644 --- a/doc/api/graphql/reference/_index.md +++ b/doc/api/graphql/reference/_index.md @@ -48414,6 +48414,7 @@ State of a Sentry error. | `JENKINS_SERVICE` | Jenkins integration. | | `JIRA_CLOUD_APP_SERVICE` | GitLab for Jira Cloud app integration. | | `JIRA_SERVICE` | Jira issues integration. | +| `LINEAR_SERVICE` | Linear integration. | | `MATRIX_SERVICE` | Matrix notifications integration. | | `MATTERMOST_SERVICE` | Mattermost notifications integration. | | `MATTERMOST_SLASH_COMMANDS_SERVICE` | Mattermost slash commands integration. | diff --git a/doc/api/group_integrations.md b/doc/api/group_integrations.md index fd9e503f0fc696..d6adaf7b86b5a9 100644 --- a/doc/api/group_integrations.md +++ b/doc/api/group_integrations.md @@ -1183,6 +1183,45 @@ Get the Jira integration settings for a group. GET /groups/:id/integrations/jira ``` +## Linear + +{{< history >}} + +- [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/198297) in GitLab 18.3. + +{{< /history >}} + +### Set up Linear + +Set up the Linear integration for a group. + +```plaintext +PUT /groups/:id/integrations/linear +``` + +Parameters: + +| Parameter | Type | Required | Description | +| ------------- | ------ | -------- | -------------- | +| `workspace_url` | string | yes | URL of the issue. | +| `use_inherited_settings` | boolean | no | Indicates whether or not to inherit default settings. Defaults to `false`. | + +### Disable Linear + +Disable the Linear integration for a group. Integration settings are reset. + +```plaintext +DELETE /groups/:id/integrations/linear +``` + +### Get Linear settings + +Get the Linear integration settings for a group. + +```plaintext +GET /groups/:id/integrations/linear +``` + ## Matrix notifications ### Set up Matrix notifications diff --git a/doc/api/openapi/openapi_v2.yaml b/doc/api/openapi/openapi_v2.yaml index 3d4d0e0472b17d..2932dd09c37198 100644 --- a/doc/api/openapi/openapi_v2.yaml +++ b/doc/api/openapi/openapi_v2.yaml @@ -5274,6 +5274,41 @@ paths: tags: - integrations operationId: putApiV4GroupsIdIntegrationsJiraCloudApp + "/api/v4/groups/{id}/integrations/linear": + put: + summary: Create/Edit Linear integration + description: Set Linear integration. + produces: + - application/json + consumes: + - application/json + parameters: + - in: path + name: id + type: integer + format: int32 + required: true + - name: putApiV4GroupsIdIntegrationsLinear + in: body + required: true + schema: + "$ref": "#/definitions/putApiV4GroupsIdIntegrationsLinear" + responses: + '200': + description: Create/Edit Linear integration + schema: + "$ref": "#/definitions/API_Entities_IntegrationBasic" + '400': + description: Bad request + '401': + description: Unauthorized + '404': + description: Not found + '422': + description: Unprocessable entity + tags: + - integrations + operationId: putApiV4GroupsIdIntegrationsLinear "/api/v4/groups/{id}/integrations/matrix": put: summary: Create/Edit Matrix integration @@ -6290,6 +6325,7 @@ paths: - jenkins - jira - jira-cloud-app + - linear - matrix - mattermost-slash-commands - slack-slash-commands @@ -6370,6 +6406,7 @@ paths: - jenkins - jira - jira-cloud-app + - linear - matrix - mattermost-slash-commands - slack-slash-commands @@ -20348,6 +20385,41 @@ paths: tags: - integrations operationId: putApiV4ProjectsIdServicesJiraCloudApp + "/api/v4/projects/{id}/services/linear": + put: + summary: Create/Edit Linear integration + description: Set Linear integration. + produces: + - application/json + consumes: + - application/json + parameters: + - in: path + name: id + type: integer + format: int32 + required: true + - name: putApiV4ProjectsIdServicesLinear + in: body + required: true + schema: + "$ref": "#/definitions/putApiV4ProjectsIdServicesLinear" + responses: + '200': + description: Create/Edit Linear integration + schema: + "$ref": "#/definitions/API_Entities_IntegrationBasic" + '400': + description: Bad request + '401': + description: Unauthorized + '404': + description: Not found + '422': + description: Unprocessable entity + tags: + - integrations + operationId: putApiV4ProjectsIdServicesLinear "/api/v4/projects/{id}/services/matrix": put: summary: Create/Edit Matrix integration @@ -21364,6 +21436,7 @@ paths: - jenkins - jira - jira-cloud-app + - linear - matrix - mattermost-slash-commands - slack-slash-commands @@ -21444,6 +21517,7 @@ paths: - jenkins - jira - jira-cloud-app + - linear - matrix - mattermost-slash-commands - slack-slash-commands @@ -22381,6 +22455,41 @@ paths: tags: - integrations operationId: putApiV4ProjectsIdIntegrationsJiraCloudApp + "/api/v4/projects/{id}/integrations/linear": + put: + summary: Create/Edit Linear integration + description: Set Linear integration. + produces: + - application/json + consumes: + - application/json + parameters: + - in: path + name: id + type: integer + format: int32 + required: true + - name: putApiV4ProjectsIdIntegrationsLinear + in: body + required: true + schema: + "$ref": "#/definitions/putApiV4ProjectsIdIntegrationsLinear" + responses: + '200': + description: Create/Edit Linear integration + schema: + "$ref": "#/definitions/API_Entities_IntegrationBasic" + '400': + description: Bad request + '401': + description: Unauthorized + '404': + description: Not found + '422': + description: Unprocessable entity + tags: + - integrations + operationId: putApiV4ProjectsIdIntegrationsLinear "/api/v4/projects/{id}/integrations/matrix": put: summary: Create/Edit Matrix integration @@ -23397,6 +23506,7 @@ paths: - jenkins - jira - jira-cloud-app + - linear - matrix - mattermost-slash-commands - slack-slash-commands @@ -23477,6 +23587,7 @@ paths: - jenkins - jira - jira-cloud-app + - linear - matrix - mattermost-slash-commands - slack-slash-commands @@ -47704,6 +47815,22 @@ definitions: description: Indicates whether to inherit the default settings. Defaults to `false`. description: Create/Edit Jira Cloud App integration + putApiV4GroupsIdIntegrationsLinear: + type: object + properties: + workspace_url: + type: string + description: Linear workspace URL (for example, https://linear.app/example) + push_events: + type: boolean + description: Trigger event for pushes to the repository. + use_inherited_settings: + type: boolean + description: Indicates whether to inherit the default settings. Defaults to + `false`. + required: + - workspace_url + description: Create/Edit Linear integration putApiV4GroupsIdIntegrationsMatrix: type: object properties: @@ -54238,6 +54365,22 @@ definitions: description: Indicates whether to inherit the default settings. Defaults to `false`. description: Create/Edit Jira Cloud App integration + putApiV4ProjectsIdServicesLinear: + type: object + properties: + workspace_url: + type: string + description: Linear workspace URL (for example, https://linear.app/example) + push_events: + type: boolean + description: Trigger event for pushes to the repository. + use_inherited_settings: + type: boolean + description: Indicates whether to inherit the default settings. Defaults to + `false`. + required: + - workspace_url + description: Create/Edit Linear integration putApiV4ProjectsIdServicesMatrix: type: object properties: @@ -56064,6 +56207,22 @@ definitions: description: Indicates whether to inherit the default settings. Defaults to `false`. description: Create/Edit Jira Cloud App integration + putApiV4ProjectsIdIntegrationsLinear: + type: object + properties: + workspace_url: + type: string + description: Linear workspace URL (for example, https://linear.app/example) + push_events: + type: boolean + description: Trigger event for pushes to the repository. + use_inherited_settings: + type: boolean + description: Indicates whether to inherit the default settings. Defaults to + `false`. + required: + - workspace_url + description: Create/Edit Linear integration putApiV4ProjectsIdIntegrationsMatrix: type: object properties: diff --git a/doc/api/project_integrations.md b/doc/api/project_integrations.md index 5606399ad390dc..3e93c30444eb41 100644 --- a/doc/api/project_integrations.md +++ b/doc/api/project_integrations.md @@ -1510,6 +1510,45 @@ Get the Jira issues integration settings for a project. GET /projects/:id/integrations/jira ``` +## Linear + +{{< history >}} + +- [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/198297) in GitLab 18.3. + +{{< /history >}} + +### Set up Linear + +Set up the Linear integration for a group. + +```plaintext +PUT /projects/:id/integrations/linear +``` + +Parameters: + +| Parameter | Type | Required | Description | +| ------------- | ------ | -------- | -------------- | +| `workspace_url` | string | yes | URL of the issue. | +| `use_inherited_settings` | boolean | no | Indicates whether or not to inherit default settings. Defaults to `false`. | + +### Disable Linear + +Disable the Linear integration for a group. Integration settings are reset. + +```plaintext +DELETE /projects/:id/integrations/linear +``` + +### Get Linear settings + +Get the Linear integration settings for a group. + +```plaintext +GET /projects/:id/integrations/linear +``` + ## Matrix notifications {{< history >}} diff --git a/doc/integration/external-issue-tracker.md b/doc/integration/external-issue-tracker.md index fa1c2381fe1c99..06f102436f86a6 100644 --- a/doc/integration/external-issue-tracker.md +++ b/doc/integration/external-issue-tracker.md @@ -52,6 +52,7 @@ You can configure any of the following external issue trackers: - [Custom issue tracker](../user/project/integrations/custom_issue_tracker.md) - [Engineering Workflow Management (EWM)](../user/project/integrations/ewm.md) - [Jira](jira/_index.md) +- [Linear](../user/project/integrations/linear.md) - [Phorge](../user/project/integrations/phorge.md) - [Redmine](../user/project/integrations/redmine.md) - [YouTrack](../user/project/integrations/youtrack.md) diff --git a/doc/user/project/integrations/_index.md b/doc/user/project/integrations/_index.md index 2be8adfd7cc9f9..d08a0fc4017152 100644 --- a/doc/user/project/integrations/_index.md +++ b/doc/user/project/integrations/_index.md @@ -173,6 +173,7 @@ The following integrations add links to [external issue trackers](../../../integ | [ClickUp](clickup.md) | Use ClickUp as an issue tracker. | {{< icon name="dotted-circle" >}} No | {{< icon name="dotted-circle" >}} No | {{< icon name="dotted-circle" >}} No | | [Custom issue tracker](custom_issue_tracker.md) | Use a custom issue tracker. | {{< icon name="dotted-circle" >}} No | {{< icon name="dotted-circle" >}} No | {{< icon name="dotted-circle" >}} No | | [Engineering Workflow Management (EWM)](ewm.md) | Use EWM as an issue tracker. | {{< icon name="dotted-circle" >}} No | {{< icon name="dotted-circle" >}} No | {{< icon name="check-circle" >}} Yes | +| [Linear](linear.md) | Use Linear as an issue tracker. | {{< icon name="dotted-circle" >}} No | {{< icon name="dotted-circle" >}} No | {{< icon name="dotted-circle" >}} No | | [Phorge](phorge.md) | Use Phorge as an issue tracker. | {{< icon name="dotted-circle" >}} No | {{< icon name="dotted-circle" >}} No | {{< icon name="check-circle" >}} Yes | | [Redmine](redmine.md) | Use Redmine as an issue tracker. | {{< icon name="dotted-circle" >}} No | {{< icon name="dotted-circle" >}} No | {{< icon name="check-circle" >}} Yes | | [YouTrack](youtrack.md) | Use JetBrains YouTrack as your project's issue tracker. | {{< icon name="dotted-circle" >}} No | {{< icon name="check-circle" >}} Yes | {{< icon name="dotted-circle" >}} No | diff --git a/doc/user/project/integrations/linear.md b/doc/user/project/integrations/linear.md new file mode 100644 index 00000000000000..cc2365c35c99aa --- /dev/null +++ b/doc/user/project/integrations/linear.md @@ -0,0 +1,52 @@ +--- +stage: Plan +group: Project Management +info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments +title: Linear +--- + +{{< details >}} + +- Tier: Free, Premium, Ultimate +- Offering: GitLab.com, GitLab Self-Managed, GitLab Dedicated + +{{< /details >}} + +{{< history >}} + +- [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/198297) in GitLab 18.3. + +{{< /history >}} + +You can use [Linear](https://linear.app/) as an external issue tracker. +To enable the Linear integration in a project: + +1. On the left sidebar, select **Search or go to** and find your project. +1. Select **Settings > Integrations**. +1. Select **Linear**. +1. Under **Enable integration**, select the **Active** checkbox. +1. Fill in the required fields: + + - **Workspace URL**: The URL to the Linear Workspace project to link to this GitLab project. + +1. Optional. Select **Test settings**. +1. Select **Save changes**. + +After you have configured and enabled Linear, you see the Linear link on the GitLab project pages, +which takes you to your Linear workspace. + +For example, this is a configuration for a workspace named `example`: + +- Workspace URL: `https://linear.app/example` + +You can also disable [GitLab internal issue tracking](../issues/_index.md) in this project. +For more information about the steps and consequences of disabling GitLab issues, see +Configure project [visibility](../../public_access.md#change-project-visibility), [features, and permissions](../settings/_index.md#configure-project-features-and-permissions). + +## Reference Linear issues in GitLab + +You can reference your Linear issues using: + +- `-`, for example `API-123`, where: + - `` is a Team identifier + - `` is a number. diff --git a/lib/api/helpers/integrations_helpers.rb b/lib/api/helpers/integrations_helpers.rb index 3ac444c4f85ce4..adb8d79402636a 100644 --- a/lib/api/helpers/integrations_helpers.rb +++ b/lib/api/helpers/integrations_helpers.rb @@ -115,6 +115,7 @@ def self.integrations 'jenkins' => ::Integrations::Jenkins.api_arguments, 'jira' => ::Integrations::Jira.api_arguments, 'jira-cloud-app' => ::Integrations::JiraCloudApp.api_arguments, + 'linear' => ::Integrations::Linear.api_arguments, 'matrix' => ::Integrations::Matrix.api_arguments, 'mattermost-slash-commands' => ::Integrations::MattermostSlashCommands.api_arguments, 'slack-slash-commands' => ::Integrations::SlackSlashCommands.api_arguments, @@ -173,6 +174,7 @@ def self.integration_classes ::Integrations::Jenkins, ::Integrations::Jira, ::Integrations::JiraCloudApp, + ::Integrations::Linear, ::Integrations::Matrix, ::Integrations::Mattermost, ::Integrations::MattermostSlashCommands, diff --git a/locale/gitlab.pot b/locale/gitlab.pot index bda11643f21bef..e7753601841380 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -37433,6 +37433,24 @@ msgstr[1] "" msgid "Line changes" msgstr "" +msgid "LinearIntegration|Linear workspace URL (for example, https://linear.app/example)" +msgstr "" + +msgid "LinearIntegration|The Linear logo is a trademark of Linear Orbit Inc. in the U.S. and other countries." +msgstr "" + +msgid "LinearIntegration|URL must point to a workspace URL like https://linear.app/example" +msgstr "" + +msgid "LinearIntegration|Use Linear as this project's issue tracker." +msgstr "" + +msgid "LinearIntegration|Workspace URL" +msgstr "" + +msgid "LinearIntegration|https://linear.app/example" +msgstr "" + msgid "Link" msgstr "" diff --git a/spec/factories/integrations.rb b/spec/factories/integrations.rb index 238b525dd1b2f0..d462ed11108a40 100644 --- a/spec/factories/integrations.rb +++ b/spec/factories/integrations.rb @@ -502,6 +502,13 @@ jira_cloud_app_service_ids { 'b:YXJpOmNsb3VkOmdyYXBoOjpzZXJ2aWNlLzI=' } end + factory :linear_integration, class: 'Integrations::Linear' do + project + active { true } + type { 'Integrations::Linear' } + workspace_url { 'https://linear.app/example' } + end + # this is for testing storing values inside properties, which is deprecated and will be removed in # https://gitlab.com/gitlab-org/gitlab/issues/29404 trait :without_properties_callback do diff --git a/spec/models/integrations/linear_spec.rb b/spec/models/integrations/linear_spec.rb new file mode 100644 index 00000000000000..b877e0fc8200b9 --- /dev/null +++ b/spec/models/integrations/linear_spec.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Integrations::Linear, :use_clean_rails_memory_store_caching, feature_category: :integrations do + it_behaves_like Integrations::Base::Linear +end diff --git a/spec/support/shared_examples/models/concerns/integrations/base/linear_shared_examples.rb b/spec/support/shared_examples/models/concerns/integrations/base/linear_shared_examples.rb new file mode 100644 index 00000000000000..f060f8a47b6a39 --- /dev/null +++ b/spec/support/shared_examples/models/concerns/integrations/base/linear_shared_examples.rb @@ -0,0 +1,75 @@ +# frozen_string_literal: true + +RSpec.shared_examples Integrations::Base::Linear do + describe 'Validations' do + context 'when integration is active' do + before do + subject.active = true + end + + it { is_expected.to validate_presence_of(:workspace_url) } + + it { is_expected.to allow_value("https://linear.app/test").for(:workspace_url) } + + it { is_expected.not_to allow_value("linear.app/test").for(:workspace_url) } + it { is_expected.not_to allow_value("https://linear.app/test/bar/baz").for(:workspace_url) } + it { is_expected.not_to allow_value('https://example.com').for(:workspace_url) } + it { is_expected.not_to allow_value('example.com').for(:workspace_url) } + it { is_expected.not_to allow_value('ftp://example.com').for(:workspace_url) } + it { is_expected.not_to allow_value('herp-and-derp').for(:workspace_url) } + end + + context 'when integration is inactive' do + before do + subject.active = false + end + + it { is_expected.not_to validate_presence_of(:workspace_url) } + end + end + + describe '#reference_pattern' do + it 'matches full issue reference (team prefix + id)' do + expect(subject.reference_pattern.match('PRJ-123')[:issue]).to eq('PRJ-123') + expect(subject.reference_pattern.match('123-123')[:issue]).to eq('123-123') + expect(subject.reference_pattern.match('ABCDEFG-123')[:issue]).to eq('ABCDEFG-123') + end + + it 'does not match invalid references' do + # too long + expect(subject.reference_pattern.match('12345678-123')).to be_nil + # no dash + expect(subject.reference_pattern.match('ABCD1234')).to be_nil + # other leading characters + expect(subject.reference_pattern.match('abcABC-123')).to be_nil + # other following characters + expect(subject.reference_pattern.match('ABC-123abc')).to be_nil + end + end + + describe '#project_url' do + before do + subject.workspace_url = 'https://linear.app/test' + end + + it 'returns the project URL' do + expect(subject.project_url).to eq('https://linear.app/test') + end + end + + describe '#issue_url' do + before do + subject.workspace_url = 'https://linear.app/test' + end + + it 'returns the project URL' do + expect(subject.issue_url('PRJ-123')).to eq('https://linear.app/test/issue/PRJ-123') + end + end + + describe '#fields' do + it 'only returns the workspace_url field' do + expect(subject.fields.pluck(:name)).to eq(%w[workspace_url]) + end + end +end -- GitLab From 18aae55b03bc453e59be379f7abcee4c1e4a37bf Mon Sep 17 00:00:00 2001 From: Lukas Eipert Date: Fri, 18 Jul 2025 17:08:43 +0200 Subject: [PATCH 2/7] Address reviewer feedback 1. Add some more inline comments 2. Disable avatar for now, so that we are not blocked on svgs needing to be merged first 3. minor changes to the specs --- app/models/concerns/integrations/base/linear.rb | 8 ++++++-- .../concerns/integrations/base/linear_shared_examples.rb | 4 ++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/app/models/concerns/integrations/base/linear.rb b/app/models/concerns/integrations/base/linear.rb index 7f2e01d29a6718..4d825453b9bf32 100644 --- a/app/models/concerns/integrations/base/linear.rb +++ b/app/models/concerns/integrations/base/linear.rb @@ -6,9 +6,13 @@ module Linear extend ActiveSupport::Concern include Base::IssueTracker - include HasAvatar + # TODO: Depends on https://gitlab.com/gitlab-org/gitlab-svgs/-/merge_requests/1219 + # include HasAvatar + # Workspace keys in the URL must be between 3 and 32 characters long and + # contain only lowercase letters, numbers, and hyphens. LINEAR_WORKSPACE_URI_REGEX = %r{\Ahttps://linear\.app/[a-z0-9\-]{3,32}/?\z} + # References like FEAT-123. # The project key part is between one and 7 characters long and uppercase letters + digits LINEAR_ISSUE_REFERENCE = %r{\b(?[A-Z0-9]{1,7}-\d+)\b} @@ -24,7 +28,7 @@ def description def help build_help_page_url( - 'user/project/integrations/linear.md', s_("LinearIntegration|Use Linear as this project's issue tracker.") + 'user/project/integrations/linear.md', description ) end diff --git a/spec/support/shared_examples/models/concerns/integrations/base/linear_shared_examples.rb b/spec/support/shared_examples/models/concerns/integrations/base/linear_shared_examples.rb index f060f8a47b6a39..c7d4b306f4c5ed 100644 --- a/spec/support/shared_examples/models/concerns/integrations/base/linear_shared_examples.rb +++ b/spec/support/shared_examples/models/concerns/integrations/base/linear_shared_examples.rb @@ -4,7 +4,7 @@ describe 'Validations' do context 'when integration is active' do before do - subject.active = true + subject.activate! end it { is_expected.to validate_presence_of(:workspace_url) } @@ -21,7 +21,7 @@ context 'when integration is inactive' do before do - subject.active = false + subject.deactivate! end it { is_expected.not_to validate_presence_of(:workspace_url) } -- GitLab From d55b77c3aba47cf9b083025f8661881782fbb231 Mon Sep 17 00:00:00 2001 From: Lukas Eipert Date: Fri, 18 Jul 2025 17:16:30 +0200 Subject: [PATCH 3/7] Some file needs some new line added Because otherwise, boom. --- db/docs/integrations.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/db/docs/integrations.yml b/db/docs/integrations.yml index ec141afe93f014..1bed92a3c68d62 100644 --- a/db/docs/integrations.yml +++ b/db/docs/integrations.yml @@ -33,6 +33,7 @@ classes: - Integrations::Jenkins - Integrations::Jira - Integrations::JiraCloudApp +- Integrations::Linear - Integrations::Matrix - Integrations::Mattermost - Integrations::MattermostSlashCommands -- GitLab From 83f084758f95bd774d17d5322188efb34d073fc0 Mon Sep 17 00:00:00 2001 From: Lukas Eipert Date: Fri, 18 Jul 2025 17:22:24 +0200 Subject: [PATCH 4/7] Acknowledge that the new model exists --- spec/lib/gitlab/import_export/all_models.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml index 99095ac909c317..5f11e3b641f1e5 100644 --- a/spec/lib/gitlab/import_export/all_models.yml +++ b/spec/lib/gitlab/import_export/all_models.yml @@ -668,6 +668,7 @@ project: - ewm_integration - clickup_integration - external_wiki_integration +- linear_integration - mock_ci_integration - mock_monitoring_integration - squash_tm_integration -- GitLab From e7dff5380bef42b76d082bc7ed8e8372bb905bb7 Mon Sep 17 00:00:00 2001 From: Lukas Eipert Date: Tue, 22 Jul 2025 17:11:01 +0200 Subject: [PATCH 5/7] Add metric definitions and fix a link in the docs --- .../20250622150122_projects_linear_active.yml | 21 +++++++++++++++++++ .../20250622150124_groups_linear_active.yml | 21 +++++++++++++++++++ ...20250622150127_instances_linear_active.yml | 21 +++++++++++++++++++ ...0129_projects_inheriting_linear_active.yml | 21 +++++++++++++++++++ ...150131_groups_inheriting_linear_active.yml | 21 +++++++++++++++++++ doc/development/integrations/_index.md | 2 +- 6 files changed, 106 insertions(+), 1 deletion(-) create mode 100644 config/metrics/counts_all/20250622150122_projects_linear_active.yml create mode 100644 config/metrics/counts_all/20250622150124_groups_linear_active.yml create mode 100644 config/metrics/counts_all/20250622150127_instances_linear_active.yml create mode 100644 config/metrics/counts_all/20250622150129_projects_inheriting_linear_active.yml create mode 100644 config/metrics/counts_all/20250622150131_groups_inheriting_linear_active.yml diff --git a/config/metrics/counts_all/20250622150122_projects_linear_active.yml b/config/metrics/counts_all/20250622150122_projects_linear_active.yml new file mode 100644 index 00000000000000..631070189b4e77 --- /dev/null +++ b/config/metrics/counts_all/20250622150122_projects_linear_active.yml @@ -0,0 +1,21 @@ +--- +data_category: optional +key_path: counts.projects_linear_active +description: Count of projects with active integrations for Linear +product_group: import +product_categories: + - integrations +value_type: number +status: active +time_frame: all +data_source: database +instrumentation_class: ActiveProjectIntegrationsMetric +options: + type: linear +tiers: + - free + - premium + - ultimate +performance_indicator_type: [] +milestone: '18.3' +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/198297 diff --git a/config/metrics/counts_all/20250622150124_groups_linear_active.yml b/config/metrics/counts_all/20250622150124_groups_linear_active.yml new file mode 100644 index 00000000000000..a47d09a4c06cc6 --- /dev/null +++ b/config/metrics/counts_all/20250622150124_groups_linear_active.yml @@ -0,0 +1,21 @@ +--- +data_category: optional +key_path: counts.groups_linear_active +description: Count of groups with active integrations for Linear +product_group: import +product_categories: + - integrations +value_type: number +status: active +time_frame: all +data_source: database +instrumentation_class: ActiveGroupIntegrationsMetric +options: + type: linear +tiers: + - free + - premium + - ultimate +performance_indicator_type: [] +milestone: '18.3' +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/198297 diff --git a/config/metrics/counts_all/20250622150127_instances_linear_active.yml b/config/metrics/counts_all/20250622150127_instances_linear_active.yml new file mode 100644 index 00000000000000..31484c18568133 --- /dev/null +++ b/config/metrics/counts_all/20250622150127_instances_linear_active.yml @@ -0,0 +1,21 @@ +--- +data_category: optional +key_path: counts.instances_linear_active +description: Count of active instance-level integrations for Linear +product_group: import +product_categories: + - integrations +value_type: number +status: active +time_frame: all +data_source: database +instrumentation_class: ActiveInstanceIntegrationsMetric +options: + type: linear +tiers: + - free + - premium + - ultimate +performance_indicator_type: [] +milestone: '18.3' +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/198297 diff --git a/config/metrics/counts_all/20250622150129_projects_inheriting_linear_active.yml b/config/metrics/counts_all/20250622150129_projects_inheriting_linear_active.yml new file mode 100644 index 00000000000000..866a4b2a8d30b8 --- /dev/null +++ b/config/metrics/counts_all/20250622150129_projects_inheriting_linear_active.yml @@ -0,0 +1,21 @@ +--- +data_category: optional +key_path: counts.projects_inheriting_linear_active +description: Count of active projects inheriting integrations for Linear +product_group: import +product_categories: + - integrations +value_type: number +status: active +time_frame: all +data_source: database +instrumentation_class: ActiveProjectsInheritingIntegrationsMetric +options: + type: linear +tiers: + - free + - premium + - ultimate +performance_indicator_type: [] +milestone: '18.3' +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/198297 diff --git a/config/metrics/counts_all/20250622150131_groups_inheriting_linear_active.yml b/config/metrics/counts_all/20250622150131_groups_inheriting_linear_active.yml new file mode 100644 index 00000000000000..8f3bc73b0a80e8 --- /dev/null +++ b/config/metrics/counts_all/20250622150131_groups_inheriting_linear_active.yml @@ -0,0 +1,21 @@ +--- +data_category: optional +key_path: counts.groups_inheriting_linear_active +description: Count of active groups inheriting integrations for Linear +product_group: import +product_categories: + - integrations +value_type: number +status: active +time_frame: all +data_source: database +instrumentation_class: ActiveGroupsInheritingIntegrationsMetric +options: + type: linear +tiers: + - free + - premium + - ultimate +performance_indicator_type: [] +milestone: '18.3' +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/198297 diff --git a/doc/development/integrations/_index.md b/doc/development/integrations/_index.md index 5edc05084757ed..be2e9ff27d7d9a 100644 --- a/doc/development/integrations/_index.md +++ b/doc/development/integrations/_index.md @@ -204,7 +204,7 @@ To create metric definitions: 1. Replace `milestone` with the current milestone and `introduced_by_url` with the merge request link. 1. Verify all other attributes have correct values by checking the [metrics guide](../internal_analytics/metrics/metrics_dictionary.md#metrics-definition-and-validation). -For example, to create metric definitions for the Slack integration, you copy the metrics [1](https://gitlab.com/gitlab-org/gitlab/blob/master/config/metrics/counts_all/20210216180122_projects_slack_active.yml), [2](https://gitlab.com/gitlab-org/gitlab/blob/master/config/metrics/counts_all/20210216180124_groups_slack_active.yml), [3](https://gitlab.com/gitlab-org/gitlab/blob/master/config/metrics/counts_all/20210216180127_instances_slack_active.yml), [4](https://gitlab.com/gitlab-org/gitlab/blob/master/config/metrics/counts_all/20210216180127_instances_slack_active.yml), and [5](https://gitlab.com/gitlab-org/gitlab/blob/master/config/metrics/counts_all/20210216180129_projects_inheriting_slack_active.yml)), then replace `Slack` with the name of the new integration. +For example, to create metric definitions for the Slack integration, you copy the metrics [1](https://gitlab.com/gitlab-org/gitlab/blob/master/config/metrics/counts_all/20210216180122_projects_slack_active.yml), [2](https://gitlab.com/gitlab-org/gitlab/blob/master/config/metrics/counts_all/20210216180124_groups_slack_active.yml), [3](https://gitlab.com/gitlab-org/gitlab/blob/master/config/metrics/counts_all/20210216180127_instances_slack_active.yml), [4](https://gitlab.com/gitlab-org/gitlab/blob/master/config/metrics/counts_all/20210216180131_groups_inheriting_slack_active.yml), and [5](https://gitlab.com/gitlab-org/gitlab/blob/master/config/metrics/counts_all/20210216180129_projects_inheriting_slack_active.yml)), then replace `Slack` with the name of the new integration. ### Security requirements -- GitLab From fbbda95706608ef8d39030956a25acbb8ccefccd Mon Sep 17 00:00:00 2001 From: Marcin Sedlak-Jakubowski Date: Thu, 24 Jul 2025 21:44:04 +0200 Subject: [PATCH 6/7] Apply TW review --- doc/development/integrations/_index.md | 9 ++++++++- doc/user/project/integrations/linear.md | 2 +- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/doc/development/integrations/_index.md b/doc/development/integrations/_index.md index be2e9ff27d7d9a..ad488e5a2516f7 100644 --- a/doc/development/integrations/_index.md +++ b/doc/development/integrations/_index.md @@ -204,7 +204,14 @@ To create metric definitions: 1. Replace `milestone` with the current milestone and `introduced_by_url` with the merge request link. 1. Verify all other attributes have correct values by checking the [metrics guide](../internal_analytics/metrics/metrics_dictionary.md#metrics-definition-and-validation). -For example, to create metric definitions for the Slack integration, you copy the metrics [1](https://gitlab.com/gitlab-org/gitlab/blob/master/config/metrics/counts_all/20210216180122_projects_slack_active.yml), [2](https://gitlab.com/gitlab-org/gitlab/blob/master/config/metrics/counts_all/20210216180124_groups_slack_active.yml), [3](https://gitlab.com/gitlab-org/gitlab/blob/master/config/metrics/counts_all/20210216180127_instances_slack_active.yml), [4](https://gitlab.com/gitlab-org/gitlab/blob/master/config/metrics/counts_all/20210216180131_groups_inheriting_slack_active.yml), and [5](https://gitlab.com/gitlab-org/gitlab/blob/master/config/metrics/counts_all/20210216180129_projects_inheriting_slack_active.yml)), then replace `Slack` with the name of the new integration. +For example, to create metric definitions for the Slack integration, you copy these metrics, and +then replace `Slack` with the name of the new integration: + +- [`20210216180122_projects_slack_active.yml`](https://gitlab.com/gitlab-org/gitlab/blob/master/config/metrics/counts_all/20210216180122_projects_slack_active.yml) +- [`20210216180124_groups_slack_active.yml`](https://gitlab.com/gitlab-org/gitlab/blob/master/config/metrics/counts_all/20210216180124_groups_slack_active.yml) +- [`20210216180127_instances_slack_active.yml`](https://gitlab.com/gitlab-org/gitlab/blob/master/config/metrics/counts_all/20210216180127_instances_slack_active.yml) +- [`20210216180131_groups_inheriting_slack_active.yml`](https://gitlab.com/gitlab-org/gitlab/blob/master/config/metrics/counts_all/20210216180131_groups_inheriting_slack_active.yml) +- [`20210216180129_projects_inheriting_slack_active.yml`](https://gitlab.com/gitlab-org/gitlab/blob/master/config/metrics/counts_all/20210216180129_projects_inheriting_slack_active.yml) ### Security requirements diff --git a/doc/user/project/integrations/linear.md b/doc/user/project/integrations/linear.md index cc2365c35c99aa..6a3b335a1efcd6 100644 --- a/doc/user/project/integrations/linear.md +++ b/doc/user/project/integrations/linear.md @@ -48,5 +48,5 @@ Configure project [visibility](../../public_access.md#change-project-visibility) You can reference your Linear issues using: - `-`, for example `API-123`, where: - - `` is a Team identifier + - `` is a team identifier - `` is a number. -- GitLab From d498d1d156bd53ea67c4fbbf2e759944818ac621 Mon Sep 17 00:00:00 2001 From: Lukas Eipert Date: Tue, 29 Jul 2025 12:18:46 +0200 Subject: [PATCH 7/7] Fix undercoverage issue And move the comment to the right place --- app/models/concerns/integrations/base/linear.rb | 11 +++++++---- .../integrations/base/linear_shared_examples.rb | 6 ++++++ 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/app/models/concerns/integrations/base/linear.rb b/app/models/concerns/integrations/base/linear.rb index 4d825453b9bf32..0e49dc8c25d4eb 100644 --- a/app/models/concerns/integrations/base/linear.rb +++ b/app/models/concerns/integrations/base/linear.rb @@ -59,14 +59,17 @@ def reference_pattern(*) @reference_pattern ||= LINEAR_ISSUE_REFERENCE end - def supports_data_fields? - false - end - # Normally external issue trackers in GitLab save `project_url`, `issue_url` and `new_issue_url` # in a separate DB table, but for the Linear integration we just use the `encrypted_properties` column # + # That's why we overwrite `supports_data_fields?` and `handle_properties` + # # For Linear only the workspace url is needed and the rest of the URLs can be derived + # + def supports_data_fields? + false + end + def handle_properties # no op end diff --git a/spec/support/shared_examples/models/concerns/integrations/base/linear_shared_examples.rb b/spec/support/shared_examples/models/concerns/integrations/base/linear_shared_examples.rb index c7d4b306f4c5ed..b67a3b1e40d1a2 100644 --- a/spec/support/shared_examples/models/concerns/integrations/base/linear_shared_examples.rb +++ b/spec/support/shared_examples/models/concerns/integrations/base/linear_shared_examples.rb @@ -72,4 +72,10 @@ expect(subject.fields.pluck(:name)).to eq(%w[workspace_url]) end end + + describe '#supports_data_fields?' do + it 'returns false' do + expect(subject.supports_data_fields?).to be(false) + end + end end -- GitLab