diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2023-09-09 00:11:20 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2023-09-09 00:11:20 +0300 |
commit | 6a315e2d9c09da0efffdabed91ee872e867a981c (patch) | |
tree | 177a685b4ee5929e0e0b3b7b29430e893321c959 | |
parent | 375b68fe813abba5e362aa6054eae7b325e92e03 (diff) |
Add latest changes from gitlab-org/gitlab@master
22 files changed, 1605 insertions, 160 deletions
@@ -93,7 +93,7 @@ gem 'timfel-krb5-auth', '~> 0.8', group: :kerberos # Spam and anti-bot protection gem 'recaptcha', '~> 5.12', require: 'recaptcha/rails' gem 'akismet', '~> 3.0' -gem 'invisible_captcha', '~> 2.0.0' +gem 'invisible_captcha', '~> 2.1.0' # Two-factor authentication gem 'devise-two-factor', '~> 4.0.2' diff --git a/Gemfile.checksum b/Gemfile.checksum index af1f01be1a8..6a85dc3ce67 100644 --- a/Gemfile.checksum +++ b/Gemfile.checksum @@ -305,7 +305,7 @@ {"name":"ice_cube","version":"0.16.4","platform":"ruby","checksum":"da117e5de24bdc33931be629f9b55048641924442c7e9b72fedc05e5592531b7"}, {"name":"ice_nine","version":"0.11.2","platform":"ruby","checksum":"5d506a7d2723d5592dc121b9928e4931742730131f22a1a37649df1c1e2e63db"}, {"name":"imagen","version":"0.1.8","platform":"ruby","checksum":"fde7b727d4fe79c6bb5ac46c1f7184bf87a6d54df54d712ad2be039d2f93a162"}, -{"name":"invisible_captcha","version":"2.0.0","platform":"ruby","checksum":"a381edcb1d1b8744e9dc398ecad142c3e2ab077604645f85eeb02f9ea535c042"}, +{"name":"invisible_captcha","version":"2.1.0","platform":"ruby","checksum":"02b452f3eb1b691d155ba3e8e97e1be0e6b6be62e8bc94957234b9cde0852b1e"}, {"name":"ipaddr","version":"1.2.5","platform":"ruby","checksum":"4e679c71d6d8ed99f925487082f70f9a958de155591caa0e7f6cef9aa160f17a"}, {"name":"ipaddress","version":"0.8.3","platform":"ruby","checksum":"85640c4f9194c26937afc8c78e3074a8e7c97d5d1210358d1440f01034d006f5"}, {"name":"jaeger-client","version":"1.1.0","platform":"ruby","checksum":"cb5e9b9bbee6ee8d6a82d03d947a5b04543d8c0a949c22e484254f18d8a458a8"}, diff --git a/Gemfile.lock b/Gemfile.lock index 605ee7339e8..b85acd88706 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -882,8 +882,8 @@ GEM ice_nine (0.11.2) imagen (0.1.8) parser (>= 2.5, != 2.5.1.1) - invisible_captcha (2.0.0) - rails (>= 5.0) + invisible_captcha (2.1.0) + rails (>= 5.2) ipaddr (1.2.5) ipaddress (0.8.3) jaeger-client (1.1.0) @@ -1873,7 +1873,7 @@ DEPENDENCIES html2text httparty (~> 0.21.0) icalendar - invisible_captcha (~> 2.0.0) + invisible_captcha (~> 2.1.0) ipaddr (~> 1.2.5) ipaddress (~> 0.8.3) ipynbdiff! diff --git a/app/assets/javascripts/entrypoints/analytics.js b/app/assets/javascripts/entrypoints/analytics.js index 75f7c41a930..8eb265cb1e8 100644 --- a/app/assets/javascripts/entrypoints/analytics.js +++ b/app/assets/javascripts/entrypoints/analytics.js @@ -6,6 +6,7 @@ if (appId && host) { window.glClient = glClientSDK({ appId, host, + hasCookieConsent: true, plugins: { clientHints: false, linkTracking: false, diff --git a/app/assets/javascripts/vue_shared/components/confidentiality_badge.vue b/app/assets/javascripts/vue_shared/components/confidentiality_badge.vue index 447820471b3..025e38a55ad 100644 --- a/app/assets/javascripts/vue_shared/components/confidentiality_badge.vue +++ b/app/assets/javascripts/vue_shared/components/confidentiality_badge.vue @@ -1,10 +1,11 @@ <script> -import { GlBadge, GlTooltipDirective } from '@gitlab/ui'; +import { GlBadge, GlTooltipDirective, GlIcon } from '@gitlab/ui'; import { confidentialityInfoText } from '../constants'; export default { components: { GlBadge, + GlIcon, }, directives: { GlTooltip: GlTooltipDirective, @@ -18,17 +19,31 @@ export default { type: String, required: true, }, + hideTextInSmallScreens: { + type: Boolean, + required: false, + default: false, + }, }, computed: { confidentialTooltip() { return confidentialityInfoText(this.workspaceType, this.issuableType); }, + confidentialTextClass() { + return { + 'gl-display-none gl-sm-display-block': this.hideTextInSmallScreens, + 'gl-ml-2': true, + }; + }, }, }; </script> <template> - <gl-badge v-gl-tooltip icon="eye-slash" :title="confidentialTooltip" variant="warning"> - {{ __('Confidential') }} + <gl-badge v-gl-tooltip :title="confidentialTooltip" variant="warning"> + <gl-icon name="eye-slash" :size="16" /> + <span data-testid="confidential-badge-text" :class="confidentialTextClass">{{ + __('Confidential') + }}</span> </gl-badge> </template> diff --git a/app/assets/javascripts/work_items/components/work_item_created_updated.vue b/app/assets/javascripts/work_items/components/work_item_created_updated.vue index 7008955efac..60a74b9cdeb 100644 --- a/app/assets/javascripts/work_items/components/work_item_created_updated.vue +++ b/app/assets/javascripts/work_items/components/work_item_created_updated.vue @@ -87,6 +87,7 @@ export default { class="gl-vertical-align-middle gl-display-inline-flex! gl-mr-2" :issuable-type="workItemType" :workspace-type="$options.WORKSPACE_PROJECT" + hide-text-in-small-screens /> <work-item-type-icon class="gl-vertical-align-middle gl-mr-0!" diff --git a/app/assets/javascripts/work_items/components/work_item_state_badge.vue b/app/assets/javascripts/work_items/components/work_item_state_badge.vue index 1d1bc7352b1..5c5b41b38e6 100644 --- a/app/assets/javascripts/work_items/components/work_item_state_badge.vue +++ b/app/assets/javascripts/work_items/components/work_item_state_badge.vue @@ -1,11 +1,12 @@ <script> -import { GlBadge } from '@gitlab/ui'; +import { GlBadge, GlIcon } from '@gitlab/ui'; import { __ } from '~/locale'; import { STATE_OPEN } from '../constants'; export default { components: { GlBadge, + GlIcon, }, props: { workItemState: { @@ -31,11 +32,8 @@ export default { </script> <template> - <gl-badge - :icon="workItemStateIcon" - :variant="workItemStateVariant" - class="gl-mr-2 gl-vertical-align-middle" - > - {{ stateText }} + <gl-badge :variant="workItemStateVariant" class="gl-mr-2 gl-vertical-align-middle"> + <gl-icon :name="workItemStateIcon" :size="16" /> + <span class="gl-display-none gl-sm-display-block gl-ml-2">{{ stateText }}</span> </gl-badge> </template> diff --git a/doc/ci/migration/examples/img/maven-freestyle-plugin.png b/doc/ci/migration/examples/img/maven-freestyle-plugin.png Binary files differnew file mode 100644 index 00000000000..ab3ece9bf5f --- /dev/null +++ b/doc/ci/migration/examples/img/maven-freestyle-plugin.png diff --git a/doc/ci/migration/examples/img/maven-freestyle-shell.png b/doc/ci/migration/examples/img/maven-freestyle-shell.png Binary files differnew file mode 100644 index 00000000000..f60d5320cb9 --- /dev/null +++ b/doc/ci/migration/examples/img/maven-freestyle-shell.png diff --git a/doc/ci/migration/examples/jenkins-maven.md b/doc/ci/migration/examples/jenkins-maven.md new file mode 100644 index 00000000000..a3590c41094 --- /dev/null +++ b/doc/ci/migration/examples/jenkins-maven.md @@ -0,0 +1,141 @@ +--- +stage: Verify +group: Pipeline Authoring +info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments +type: index, howto +--- + +# Migrating a Maven build from Jenkins to GitLab CI + +If you have a Maven build in Jenkins, you can use a [Java Spring](https://gitlab.com/gitlab-org/project-templates/spring) +project template to migrate to GitLab. The template uses Maven for its underlying dependency management. + +## Jenkins job options + +We are going to use three different options in Jenkins to test, build, and install the project: + +- Freestyle with shell execution +- Freestyle with the Maven task plugin +- A declarative pipeline using a Jenkinsfile + +All three of these options execute the same three commands: + +- `mvn test`: Run any tests found in the codebase +- `mvn package -DskipTests`: Compile the code into an executable type defined in the POM and skip running any tests since we did that already +- `mvn install -DskipTests`: Install the compiled executable into the local maven `.m2` repository and skip running any tests + +We are using a single, persistent Jenkins agent to handle running these three methods, which requires Maven to be pre-installed on the agent. +This method of execution is similar to a GitLab Shell runner. + +### Freestyle with shell execution + +This example utilizes Jenkins' built-in shell execution option so we can directly call `mvn` commands from the shell on the agent. + +![freestyle shell](img/maven-freestyle-shell.png) + +### Freestyle with Maven task plugin + +This example utilizes the Maven plugin in Jenkins to declare and execute any specific goals in the [Maven build lifecycle](https://maven.apache.org/guides/introduction/introduction-to-the-lifecycle.html) + +NOTE: +This plugin does not install Maven on the Jenkins agent, but rather provides a script wrapper around calling Maven commands. Maven must be installed before you use this method. + +![freestyle plugin](img/maven-freestyle-plugin.png) + +### Using a declarative pipeline + +Finally, we can run these Maven commands through a declarative pipeline stored either in the Jenkins pipeline configuration or directly in the Git repository. By default, this file is named `Jenksinfile`. + +This example uses shell execution commands instead of plugins. + +```groovy + pipeline { + agent any + stages { + stage('Test') { + steps { + sh "mvn test" + } + } + stage('Build') { + steps { + sh "mvn package -DskipTests" + } + } + stage('Install') { + steps { + sh "mvn install -DskipTests" + } + } + } + } +``` + +## Converting to GitLab CI + +Now that we have seen a few examples in Jenkins, let's refactor this process into GitLab CI. + +Prerequisites: + +- An available runner in GitLab +- Docker installed on the runner +- The runner is registered for this project and is using the Docker executor + +Instead of using a persistent machine for handling this build process, we are going to rely on an ephemeral Docker container to handle this execution. +This removes the need for maintaining a virtual machine and the Maven version installed on the virtual machine and allows us some more flexiblity with expanding and extending the functionality of our CI pipeline. + +Here is the final `.gitlab-ci.yml` file: + +```yaml +image: maven:3.6.3-openjdk-11 + +stages: + - test + - build + - install + +default: + cache: + key: $CI_COMMIT_REF_SLUG + paths: + - .m2/ + +variables: + MAVEN_OPTS: >- + -Dhttps.protocols=TLSv1.2 + -Dmaven.repo.local=$CI_PROJECT_DIR/.m2/repository + MAVEN_CLI_OPTS: >- + -DskipTests + +test code: + stage: test + script: + - mvn test + +build JAR: + stage: build + script: + - mvn $MAVEN_CLI_OPTS package + +install JAR: + stage: install + script: + - mvn $MAVEN_CLI_OPTS install +``` + +Let's break down the sections of this YAML file: + +- **Image**: The base Docker image where we execute all of our commands. In this case, it's an official Maven Docker image with everything we need already installed. +- **Stages**: Similar to the Jenkinsfile, we are defining three stages that run in order. Test jobs execute first, followed by build jobs, and finally install jobs. +- **Default**: Global default settings for the pipeline. + - **Cache**: Any data to be cached and reused between jobs. + - **Key**: The unique identifier for the specific cache archive. In this case, it's a shortened version of the Git commit ref. + - **Paths**: Any specific directories or files to include in the cache. In this case, we are caching the `.m2/` directory to avoid re-installing dependencies between jobs. +- **Variables**: We are setting a few environment variables to be used by each job. + - `MAVEN_OPTS`: These are Maven environment variables referenced whenever Maven is executed. + - `-Dhttps.protocols=TLSv1.2` sets our TLS protocol to version 1.2 for any HTTP requests we may make in the pipeline. + - `-Dmaven.repo.local=$CI_PROJECT_DIR/.m2/repository` sets the location of our local Maven repository to the GitLab project directory on the runner. This is to make sure we can access and modify the repository. + - `MAVEN_CLI_OPTS`: These are specific arguments to be injected into `mvn` commands. + - `-DskipTests` skips the 'test' stage in the Maven build lifecycle. + +- **Test code**, **Build JAR**, and **Install JAR**: These are the jobs to run in the pipeline. The top line of each section is a user-defined name for the job that shows up in the pipeline. The stage clause defines which stage the job runs in. (A pipeline contains one to many stages and a stage contains one to many jobs). The script section covers commands to run in that job. In this case, each job runs a single command. diff --git a/doc/development/activitypub/actor.md b/doc/development/activitypub/actor.md index 044dd730c2b..1d10e421df7 100644 --- a/doc/development/activitypub/actor.md +++ b/doc/development/activitypub/actor.md @@ -1,134 +1,11 @@ --- -stage: Create -group: Source Code -info: "To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments" +redirect_to: 'actors/index.md' +remove_date: '2023-12-08' --- -# Implement an ActivityPub actor **(EXPERIMENT)** +This document was moved to [another location](actors/index.md). -> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/127023) in GitLab 16.5 [with two flags](../../administration/feature_flags.md) named `activity_pub` and `activity_pub_project`. Disabled by default. This feature is an [Experiment](../../policy/experiment-beta-support.md). - -FLAG: -On self-managed GitLab, by default this feature is not available. To make it available, -an administrator can [enable the feature flags](../../administration/feature_flags.md) -named `activity_pub` and `activity_pub_project`. -On GitLab.com, this feature is not available. -The feature is not ready for production use. - -ActivityPub is based on three standard documents: - -- [ActivityPub](https://www.w3.org/TR/activitypub/) defines the HTTP - requests happening to implement federation. -- [ActivityStreams](https://www.w3.org/TR/activitystreams-core/) defines the - format of the JSON messages exchanged by the users of the protocol. -- [Activity Vocabulary](https://www.w3.org/TR/activitystreams-vocabulary/) - defines the various messages recognized by default. - -The first one is typically handled by controllers, while the two others are -related to what happen in serializers. - -To implement an ActivityPub actor, you must: - -- Implement the profile page of the resource. -- Implement the outbox page. -- Handle incoming requests on the inbox. - -All requests are made using -`application/ld+json; profile="https://www.w3.org/ns/activitystreams"` as `Accept` HTTP header. - -## Profile page - -Querying the profile page is used to retrieve: - -- General information about it, like name and description. -- URLs for the inbox and the outbox. - -To implement a profile page, create an ActivityStreams -serializer in `app/serializers/activity_pub/`, making your serializer -inherit from `ActivityStreamsSerializer`. See below in the serializers -section about the mandatory fields. - -To call your serializer in your controller: - -```ruby -opts = { - inbox: nil, - outbox: outbox_project_releases_url(project) -} - -render json: ActivityPub::ReleasesActorSerializer.new.represent(project, opts) -``` - -- `outbox` is the endpoint where to find the activities feed for this -actor. -- `inbox` is where to POST to subscribe to the feed. Not yet implemented, so pass `nil`. - -## Outbox page - -The outbox is the list of activities for the resource. It's a feed for the -resource, and it allows ActivityPub clients to show public activities for -this actor without having yet subscribed to it. - -To implement an outbox page, create an ActivityStreams -serializer in `app/serializers/activity_pub/`, making your serializer -inherit from `ActivityStreamsSerializer`. See below in the serializers -section about the mandatory fields. - -You call your serializer in your controller like this: - -```ruby -serializer = ActivityPub::ReleasesOutboxSerializer.new.with_pagination(request, response) -render json: serializer.represent(releases) -``` - -This converts the response to an `OrderedCollection` -ActivityPub type, with all the correct fields. - -## Inbox - -Not yet implemented. - -The inbox is where the ActivityPub compatible third-parties makes their -requests, to subscribe to the actor or send it messages. - -## ActivityStreams serializers - -The serializers implement half the core of ActivityPub support: they're all -about [ActivityStreams](https://www.w3.org/TR/activitystreams-core/), the -message format used by ActivityPub. - -To leverage the features doing most of the formatting for you, your -serializer should inherit from `ActivityPub::ActivityStreamsSerializer`. - -To use it, call the `#represent` method. It requires you to provide -`inbox` and `outbox` options (as mentioned above) if it -is an actor profile page. You don't need those if your serializer -represents an object that is just meant to be embedded as part of actors, -like the object representing the contact information for a user. - -Each resource serialized (included other objects embedded in your -actor) must provide an `id` and a `type` field. - -`id` is a URL. It's meant to be a unique identifier for the resource, and -it must point to an existing page: ideally, an actor. Otherwise, you can -just reference the closest actor and use an anchor, like this: - -```plaintext -https://gitlab.com/user/project/-/releases#release-1 -``` - -`type` should be taken from ActivityStreams core vocabulary: - -- [Activity types](https://www.w3.org/TR/activitystreams-vocabulary/#activity-types) -- [Actor types](https://www.w3.org/TR/activitystreams-vocabulary/#actor-types) -- [Object types](https://www.w3.org/TR/activitystreams-vocabulary/#object-types) - -The properties you can use are all documented in -[the ActivityStreams vocabulary document](https://www.w3.org/TR/activitystreams-vocabulary). -Given the type you have chosen for your resource, find the -`properties` list, telling you all available properties, direct or -inherited. - -It's worth noting that Mastodon adds one more property, `preferredName`. -Mastodon expects it to be set on any actor, or that actor is not recognized by -Mastodon. +<!-- This redirect file can be deleted after <2023-12-08>. --> +<!-- Redirects that point to other docs in the same project expire in three months. --> +<!-- Redirects that point to docs in a different project or site (for example, link is not relative and starts with `https:`) expire in one year. --> +<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/redirects.html --> diff --git a/doc/development/activitypub/actors/group.md b/doc/development/activitypub/actors/group.md new file mode 100644 index 00000000000..dad02298170 --- /dev/null +++ b/doc/development/activitypub/actors/group.md @@ -0,0 +1,205 @@ +--- +stage: Create +group: Source Code +info: "To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments" +--- + +# Activities for group actor **(EXPERIMENT)** + +> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/127023) in GitLab 16.5 [with two flags](../../../administration/feature_flags.md) named `activity_pub` and `activity_pub_project`. Disabled by default. This feature is an [Experiment](../../../policy/experiment-beta-support.md). + +FLAG: +On self-managed GitLab, by default this feature is not available. To make it available, +an administrator can [enable the feature flags](../../../administration/feature_flags.md) +named `activity_pub` and `activity_pub_project`. +On GitLab.com, this feature is not available. +The feature is not ready for production use. + +This feature requires two feature flags: + +- `activity_pub`: Enables or disables all ActivityPub-related features. +- `activity_pub_project`: Enables and disable ActivityPub features specific to + projects. Requires the `activity_pub` flag to also be enabled. + +## Profile + +```javascript +{ + "@context": "https://www.w3.org/ns/activitystreams", + "id": GROUP_URL, + "type": "Group", + "name": GROUP_NAME, + "summary": GROUP_DESCRIPTION, + "url": GROUP_URL, + "outbox": GROUP_OUTBOX_URL, + "inbox": null, +} +``` + +## Outbox + +The various activities for a group are: + +- [The group was created](#the-group-was-created). +- All project activities for projects in that group, and its subgroups. +- [A user joined the group](#a-user-joined-the-group). +- [A user left the group](#a-user-left-the-group). +- [The group was deleted](#the-group-was-deleted). +- [A subgroup was created](#a-subgroup-was-created). +- [A subgroup was deleted](#a-subgroup-was-deleted). + +### The group was created + +```javascript +{ + "id": GROUP_OUTBOX_URL#event_id, + "type": "Create", + "to": [ + "https://www.w3.org/ns/activitystreams#Public" + ], + "actor": { + "id": USER_PROFILE_URL, + "type": "Person", + "name": USER_NAME, + "url": USER_PROFILE_URL, + }, + "object": { + "id": GROUP_URL, + "type": "Group", + "name": GROUP_NAME, + "url": GROUP_URL, + } +} +``` + +### A user joined the group + +```javascript +{ + "id": GROUP_OUTBOX_URL#event_id, + "type": "Join", + "to": [ + "https://www.w3.org/ns/activitystreams#Public" + ], + "actor": { + "id": USER_PROFILE_URL, + "type": "Person", + "name": USER_NAME, + "url": USER_PROFILE_URL, + }, + "object": { + "id": GROUP_URL, + "type": "Group", + "name": GROUP_NAME, + "url": GROUP_URL, + }, +} +``` + +### A user left the group + +```javascript +{ + "id": GROUP_OUTBOX_URL#event_id, + "type": "Leave", + "to": [ + "https://www.w3.org/ns/activitystreams#Public" + ], + "actor": { + "id": USER_PROFILE_URL, + "type": "Person", + "name": USER_NAME, + "url": USER_PROFILE_URL, + }, + "object": { + "id": GROUP_URL, + "type": "Group", + "name": GROUP_NAME, + "url": GROUP_URL, + }, +} +``` + +### The group was deleted + +```javascript +{ + "id": GROUP_OUTBOX_URL#event_id, + "type": "Delete", + "to": [ + "https://www.w3.org/ns/activitystreams#Public" + ], + "actor": { + "id": USER_PROFILE_URL, + "type": "Person", + "name": USER_NAME, + "url": USER_PROFILE_URL, + }, + "object": { + "id": GROUP_URL, + "type": "Group", + "name": GROUP_NAME, + "url": GROUP_URL, + } +} +``` + +### A subgroup was created + +```javascript +{ + "id": GROUP_OUTBOX_URL#event_id, + "type": "Create", + "to": [ + "https://www.w3.org/ns/activitystreams#Public" + ], + "actor": { + "id": USER_PROFILE_URL, + "type": "Person", + "name": USER_NAME, + "url": USER_PROFILE_URL, + }, + "object": { + "id": GROUP_URL, + "type": "Group", + "name": GROUP_NAME, + "url": GROUP_URL, + "context": { + "id": PARENT_GROUP_URL, + "type": "Group", + "name": PARENT_GROUP_NAME, + "url": PARENT_GROUP_URL, + } + } +} +``` + +### A subgroup was deleted + +```javascript +{ + "id": GROUP_OUTBOX_URL#event_id, + "type": "Delete", + "to": [ + "https://www.w3.org/ns/activitystreams#Public" + ], + "actor": { + "id": USER_PROFILE_URL, + "type": "Person", + "name": USER_NAME, + "url": USER_PROFILE_URL, + }, + "object": { + "id": GROUP_URL, + "type": "Group", + "name": GROUP_NAME, + "url": GROUP_URL, + "context": { + "id": PARENT_GROUP_URL, + "type": "Group", + "name": PARENT_GROUP_NAME, + "url": PARENT_GROUP_URL, + } + } +} +``` diff --git a/doc/development/activitypub/actors/index.md b/doc/development/activitypub/actors/index.md new file mode 100644 index 00000000000..032cb26587a --- /dev/null +++ b/doc/development/activitypub/actors/index.md @@ -0,0 +1,148 @@ +--- +stage: Create +group: Source Code +info: "To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments" +--- + +# Implement an ActivityPub actor **(EXPERIMENT)** + +> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/127023) in GitLab 16.5 [with two flags](../../../administration/feature_flags.md) named `activity_pub` and `activity_pub_project`. Disabled by default. This feature is an [Experiment](../../../policy/experiment-beta-support.md). + +FLAG: +On self-managed GitLab, by default this feature is not available. To make it available, +an administrator can [enable the feature flags](../../../administration/feature_flags.md) +named `activity_pub` and `activity_pub_project`. +On GitLab.com, this feature is not available. +The feature is not ready for production use. + +This feature requires two feature flags: + +- `activity_pub`: Enables or disables all ActivityPub-related features. +- `activity_pub_project`: Enables and disable ActivityPub features specific to + projects. Requires the `activity_pub` flag to also be enabled. + +ActivityPub is based on three standard documents: + +- [ActivityPub](https://www.w3.org/TR/activitypub/) defines the HTTP + requests happening to implement federation. +- [ActivityStreams](https://www.w3.org/TR/activitystreams-core/) defines the + format of the JSON messages exchanged by the users of the protocol. +- [Activity Vocabulary](https://www.w3.org/TR/activitystreams-vocabulary/) + defines the various messages recognized by default. + +The first one is typically handled by controllers, while the two others are +related to what happen in serializers. + +To implement an ActivityPub actor, you must: + +- Implement the profile page of the resource. +- Implement the outbox page. +- Handle incoming requests on the inbox. + +All requests are made using +`application/ld+json; profile="https://www.w3.org/ns/activitystreams"` as `Accept` HTTP header. + +The actors we're implementing for the social features: + +- [Releases](releases.md) +- [Topics](topic.md) +- [Projects](project.md) +- [Groups](group.md) +- [Users](user.md) + +## Profile page + +Querying the profile page is used to retrieve: + +- General information about it, like name and description. +- URLs for the inbox and the outbox. + +To implement a profile page, create an ActivityStreams +serializer in `app/serializers/activity_pub/`, making your serializer +inherit from `ActivityStreamsSerializer`. See below in the serializers +section about the mandatory fields. + +To call your serializer in your controller: + +```ruby +opts = { + inbox: nil, + outbox: outbox_project_releases_url(project) +} + +render json: ActivityPub::ReleasesActorSerializer.new.represent(project, opts) +``` + +- `outbox` is the endpoint where to find the activities feed for this +actor. +- `inbox` is where to POST to subscribe to the feed. Not yet implemented, so pass `nil`. + +## Outbox page + +The outbox is the list of activities for the resource. It's a feed for the +resource, and it allows ActivityPub clients to show public activities for +this actor without having yet subscribed to it. + +To implement an outbox page, create an ActivityStreams +serializer in `app/serializers/activity_pub/`, making your serializer +inherit from `ActivityStreamsSerializer`. See below in the serializers +section about the mandatory fields. + +You call your serializer in your controller like this: + +```ruby +serializer = ActivityPub::ReleasesOutboxSerializer.new.with_pagination(request, response) +render json: serializer.represent(releases) +``` + +This converts the response to an `OrderedCollection` +ActivityPub type, with all the correct fields. + +## Inbox + +Not yet implemented. + +The inbox is where the ActivityPub compatible third-parties makes their +requests, to subscribe to the actor or send it messages. + +## ActivityStreams serializers + +The serializers implement half the core of ActivityPub support: they're all +about [ActivityStreams](https://www.w3.org/TR/activitystreams-core/), the +message format used by ActivityPub. + +To leverage the features doing most of the formatting for you, your +serializer should inherit from `ActivityPub::ActivityStreamsSerializer`. + +To use it, call the `#represent` method. It requires you to provide +`inbox` and `outbox` options (as mentioned above) if it +is an actor profile page. You don't need those if your serializer +represents an object that is just meant to be embedded as part of actors, +like the object representing the contact information for a user. + +Each resource serialized (included other objects embedded in your +actor) must provide an `id` and a `type` field. + +`id` is a URL. It's meant to be a unique identifier for the resource, and +it must point to an existing page: ideally, an actor. Otherwise, you can +just reference the closest actor and use an anchor, like this: + +```plaintext +https://gitlab.com/user/project/-/releases#release-1 +``` + +`type` should be taken from ActivityStreams core vocabulary: + +- [Activity types](https://www.w3.org/TR/activitystreams-vocabulary/#activity-types) +- [Actor types](https://www.w3.org/TR/activitystreams-vocabulary/#actor-types) +- [Object types](https://www.w3.org/TR/activitystreams-vocabulary/#object-types) + +The properties you can use are all documented in +[the ActivityStreams vocabulary document](https://www.w3.org/TR/activitystreams-vocabulary). +Given the type you have chosen for your resource, find the +`properties` list, telling you all available properties, direct or +inherited. + +It's worth noting that Mastodon adds one more property, `preferredName`. +Mastodon expects it to be set on any actor, or that actor is not recognized by +Mastodon. diff --git a/doc/development/activitypub/actors/project.md b/doc/development/activitypub/actors/project.md new file mode 100644 index 00000000000..4f876b9e3fa --- /dev/null +++ b/doc/development/activitypub/actors/project.md @@ -0,0 +1,640 @@ +--- +stage: Create +group: Source Code +info: "To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments" +--- + +# Activities for project actor **(EXPERIMENT)** + +> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/127023) in GitLab 16.5 [with two flags](../../../administration/feature_flags.md) named `activity_pub` and `activity_pub_project`. Disabled by default. This feature is an [Experiment](../../../policy/experiment-beta-support.md). + +FLAG: +On self-managed GitLab, by default this feature is not available. To make it available, +an administrator can [enable the feature flags](../../../administration/feature_flags.md) +named `activity_pub` and `activity_pub_project`. +On GitLab.com, this feature is not available. +The feature is not ready for production use. + +This feature requires two feature flags: + +- `activity_pub`: Enables or disables all ActivityPub-related features. +- `activity_pub_project`: Enables and disable ActivityPub features specific to + projects. Requires the `activity_pub` flag to also be enabled. + +## Profile + +```javascript +{ + "@context": "https://www.w3.org/ns/activitystreams", + "id": PROJECT_URL, + "type": "Application", + "name": PROJECT_NAME, + "summary": PROJECT_DESCRIPTION, + "url": PROJECT_URL, + "outbox": PROJECT_OUTBOX_URL, + "inbox": null, +} +``` + +## Outbox + +For a project, we can map the events happening on the project activity +timeline on GitLab, when a user: + +- [Creates the repository](#user-creates-the-repository). +- [Pushes commits](#user-pushes-commits). +- [Pushes a tag](#user-pushes-a-tag). +- [Opens a merge request](#user-opens-a-merge-request). +- [Accepts a merge request](#user-accepts-a-merge-request). +- [Closes a merge request](#user-closes-a-merge-request). +- [Opens an issue](#user-opens-an-issue). +- [Closes an issue](#user-closes-an-issue). +- [Reopens an issue](#user-reopens-an-issue). +- [Comments on a merge request](#user-comments-on-a-merge-request). +- [Comments on an issue](#user-comments-on-an-issue). +- [Creates a wiki page](#user-creates-a-wiki-page). +- [Updates a wiki page](#user-updates-a-wiki-page). +- [Destroys a wiki page](#user-destroys-a-wiki-page). +- [Joins the project](#user-joins-the-project). +- [Leaves the project](#user-leaves-the-project). +- [Deletes the repository](#user-deletes-the-repository). + +There's also a Design tab in the project activities, but it's just empty in +all projects I follow and I don't see anything related to it in my projects +sidebar. Maybe it's a premium feature? If so, it's of no concern to us for +public following through ActivityPub. + +### User creates the repository + +```javascript +{ + "id": PROJECT_OUTBOX_URL#event_id, + "type": "Create", + "to": [ + "https://www.w3.org/ns/activitystreams#Public" + ], + "actor": { + "id": USER_PROFILE_URL, + "type": "Person", + "name": USER_NAME, + "url": USER_PROFILE_URL, + }, + "object": { + "id": PROJECT_URL, + "type": "Application", + "name": PROJECT_NAME, + "summary": PROJECT_DESCRIPTION, + "url": PROJECT_URL, + } +} +``` + +### User pushes commits + +```javascript +{ + "id": PROJECT_OUTBOX_URL#event_id, + "type": "Update", + "actor": { + "id": USER_PROFILE_URL, + "type": "Person", + "name": USER_NAME, + "url": USER_PROFILE_URL, + }, + "object": { + "id": PROJECT_URL, + "type": "Application", + "name": PROJECT_NAME, + "summary": PROJECT_DESCRIPTION, + "url": PROJECT_URL, + }, + "result": COMMITS_DIFF_URL, +} +``` + +### User pushes a tag + +```javascript +{ + "id": PROJECT_OUTBOX_URL#event_id, + "type": "Update", + "actor": { + "id": USER_PROFILE_URL, + "type": "Person", + "name": USER_NAME, + "url": USER_PROFILE_URL, + }, + "object": { + "id": PROJECT_URL, + "type": "Application", + "name": PROJECT_NAME, + "summary": PROJECT_DESCRIPTION, + "url": PROJECT_URL, + }, + "name": TAG_NAME, + "result": COMMIT_URL, +} +``` + +### User opens a merge request + +```javascript +{ + "id": PROJECT_OUTBOX_URL#event_id, + "type": "Add", + "to": [ + "https://www.w3.org/ns/activitystreams#Public" + ], + "actor": { + "id": USER_PROFILE_URL, + "type": "Person", + "name": USER_NAME, + "url": USER_PROFILE_URL, + }, + "object": { + "id": MERGE_REQUEST_URL, + "type": "Application", + "name": MERGE_REQUEST_TITLE, + "url": MERGE_REQUEST_URL, + "context": { + "id": PROJECT_URL, + "type": "Application", + "name": PROJECT_NAME, + "summary": PROJECT_DESCRIPTION, + "url": PROJECT_URL, + }, + }, + "target": { + "id": PROJECT_URL, + "type": "Application", + "name": PROJECT_NAME, + "summary": PROJECT_DESCRIPTION, + "url": PROJECT_URL, + }, +} +``` + +### User accepts a merge request + +```javascript +{ + "id": PROJECT_OUTBOX_URL#event_id, + "type": "Accept", + "to": [ + "https://www.w3.org/ns/activitystreams#Public" + ], + "actor": { + "id": USER_PROFILE_URL, + "type": "Person", + "name": USER_NAME, + "url": USER_PROFILE_URL, + }, + "object": { + "id": MERGE_REQUEST_URL, + "type": "Application", + "name": MERGE_REQUEST_TITLE, + "url": MERGE_REQUEST_URL, + "context": { + "id": PROJECT_URL, + "type": "Application", + "name": PROJECT_NAME, + "summary": PROJECT_DESCRIPTION, + "url": PROJECT_URL, + }, + }, + "target": { + "id": PROJECT_URL, + "type": "Application", + "name": PROJECT_NAME, + "summary": PROJECT_DESCRIPTION, + "url": PROJECT_URL, + }, +} +``` + +### User closes a merge request + +```javascript +{ + "id": PROJECT_OUTBOX_URL#event_id, + "type": "Remove", + "to": [ + "https://www.w3.org/ns/activitystreams#Public" + ], + "actor": { + "id": USER_PROFILE_URL, + "type": "Person", + "name": USER_NAME, + "url": USER_PROFILE_URL, + }, + "object": { + "id": MERGE_REQUEST_URL, + "type": "Application", + "name": MERGE_REQUEST_TITLE, + "url": MERGE_REQUEST_URL, + "context": { + "id": PROJECT_URL, + "type": "Application", + "name": PROJECT_NAME, + "summary": PROJECT_DESCRIPTION, + "url": PROJECT_URL, + }, + }, + "origin": { + "id": PROJECT_URL, + "type": "Application", + "name": PROJECT_NAME, + "summary": PROJECT_DESCRIPTION, + "url": PROJECT_URL, + }, +} +``` + +### User opens an issue + +```javascript +{ + "id": PROJECT_OUTBOX_URL#event_id, + "type": "Add", + "to": [ + "https://www.w3.org/ns/activitystreams#Public" + ], + "actor": { + "id": USER_PROFILE_URL, + "type": "Person", + "name": USER_NAME, + "url": USER_PROFILE_URL, + }, + "object": { + "id": ISSUE_URL, + "type": "Page", + "name": ISSUE_TITLE, + "url": ISSUE_URL, + "context": { + "id": PROJECT_URL, + "type": "Application", + "name": PROJECT_NAME, + "summary": PROJECT_DESCRIPTION, + "url": PROJECT_URL, + } + }, + "target": { + "id": PROJECT_URL, + "type": "Application", + "name": PROJECT_NAME, + "summary": PROJECT_DESCRIPTION, + "url": PROJECT_URL, + } +} +``` + +Why to add the project both as `object.context` and `target`? For multiple +consistency reasons: + +- The **Add** activity is more commonly used with a `target`. +- The **Remove** activity used to close the issue is more + commonly used with an `origin`. +- The **Update** activity used to reopen an issue specifies that + `target` and `origin` have no specific meaning, making `context` better + suited for that. +- We could use `context` only with **Update**, but merge requests + must be taken into consideration. + +Merge requests are very similar to issues, so we want their activities to +be similar. While the best type for issues is `page`, the type chosen for +merge request is `application`, both to distinguish it from issues and because +they contain code. + +To distinguish merge requests from projects (which are also `application`), +merge requests are an `application` with another `application` (the project) +as context. Given the merge request will have a `context` even with the **Add** +and **Remove** activities, the same is done with issues for consistency. + +An alternative that was considered, but dismissed: instead of **Add** for issues, +use **Create**. That would have allowed us to always use `context`, but +it creates more problems that it solves. **Accept** and **Reject** could work quite +well for closing merge requests, but what would we use to close issues? +**Delete** is incorrect, as the issue is not deleted, just closed. +Reopening the issue later would require an **Update** after a +**Delete**. + +Using **Create** for opening issues and **Remove** for closing +issues would be asymmetrical: + +- **Create** is mirrored by **Delete**. +- **Add** is mirrored by **Remove**. + +To minimize pain for those who will build on top of those resources, it's best +to duplicate the project information as `context` and `target` / `origin`. + +### User closes an issue + +```javascript +{ + "id": PROJECT_OUTBOX_URL#event_id, + "type": "Remove", + "to": [ + "https://www.w3.org/ns/activitystreams#Public" + ], + "actor": { + "id": USER_PROFILE_URL, + "type": "Person", + "name": USER_NAME, + "url": USER_PROFILE_URL, + }, + "object": { + "id": ISSUE_URL, + "type": "Page", + "name": ISSUE_TITLE, + "url": ISSUE_URL, + "context": { + "id": PROJECT_URL, + "type": "Application", + "name": PROJECT_NAME, + "summary": PROJECT_DESCRIPTION, + "url": PROJECT_URL, + }, + }, + "origin": { + "id": PROJECT_URL, + "type": "Application", + "name": PROJECT_NAME, + "summary": PROJECT_DESCRIPTION, + "url": PROJECT_URL, + }, +} +``` + +### User reopens an issue + +```javascript +{ + "id": PROJECT_OUTBOX_URL#event_id, + "type": "Update", + "to": [ + "https://www.w3.org/ns/activitystreams#Public" + ], + "actor": { + "id": USER_PROFILE_URL, + "type": "Person", + "name": USER_NAME, + "url": USER_PROFILE_URL, + }, + "object": { + "id": ISSUE_URL, + "type": "Page", + "name": ISSUE_TITLE, + "url": ISSUE_URL, + "context": { + "id": PROJECT_URL, + "type": "Application", + "name": PROJECT_NAME, + "summary": PROJECT_DESCRIPTION, + "url": PROJECT_URL, + }, + }, +} +``` + +### User comments on a merge request + +```javascript +{ + "id": PROJECT_OUTBOX_URL#event_id, + "type": "Add", + "to": [ + "https://www.w3.org/ns/activitystreams#Public" + ], + "actor": { + "id": USER_PROFILE_URL, + "type": "Person", + "name": USER_NAME, + "url": USER_PROFILE_URL, + }, + "object": { + "id": NOTE_URL, + "type": "Note", + "content": NOTE_NOTE, + }, + "target": { + "id": MERGE_REQUEST_URL, + "type": "Application", + "name": MERGE_REQUEST_TITLE, + "url": MERGE_REQUEST_URL, + "context": { + "id": PROJECT_URL, + "type": "Application", + "name": PROJECT_NAME, + "summary": PROJECT_DESCRIPTION, + "url": PROJECT_URL, + }, + }, +} +``` + +### User comments on an issue + +```javascript +{ + "id": PROJECT_URL#event_id, + "type": "Add", + "to": [ + "https://www.w3.org/ns/activitystreams#Public" + ], + "actor": { + "id": USER_PROFILE_URL, + "type": "Person", + "name": USER_NAME, + "url": USER_PROFILE_URL, + }, + "object": { + "id": NOTE_URL, + "type": "Note", + "content": NOTE_NOTE, + }, + "target": { + "id": ISSUE_URL, + "type": "Page", + "name": ISSUE_TITLE, + "url": ISSUE_URL, + "context": { + "id": PROJECT_URL, + "type": "Application", + "name": PROJECT_NAME, + "summary": PROJECT_DESCRIPTION, + "url": PROJECT_URL, + }, + }, +} +``` + +### User creates a wiki page + +```javascript +{ + "id": PROJECT_OUTBOX_URL#event_id, + "type": "Create", + "to": [ + "https://www.w3.org/ns/activitystreams#Public" + ], + "actor": { + "id": USER_PROFILE_URL, + "type": "Person", + "name": USER_NAME, + "url": USER_PROFILE_URL, + }, + "object": { + "id": WIKI_PAGE_URL, + "type": "Page", + "name": WIKI_PAGE_HUMAN_TITLE, + "url": WIKI_PAGE_URL, + } +} +``` + +### User updates a wiki page + +```javascript +{ + "id": PROJECT_OUTBOX_URL#event_id, + "type": "Update", + "to": [ + "https://www.w3.org/ns/activitystreams#Public" + ], + "actor": { + "id": USER_PROFILE_URL, + "type": "Person", + "name": USER_NAME, + "url": USER_PROFILE_URL, + }, + "object": { + "id": WIKI_PAGE_URL, + "type": "Page", + "name": WIKI_PAGE_HUMAN_TITLE, + "url": WIKI_PAGE_URL, + } +} +``` + +### User destroys a wiki page + +```javascript +{ + "id": PROJECT_OUTBOX_URL#event_id, + "type": "Delete", + "to": [ + "https://www.w3.org/ns/activitystreams#Public" + ], + "actor": { + "id": USER_PROFILE_URL, + "type": "Person", + "name": USER_NAME, + "url": USER_PROFILE_URL, + }, + "object": { + "id": WIKI_PAGE_URL, + "type": "Page", + "name": WIKI_PAGE_HUMAN_TITLE, + "url": WIKI_PAGE_URL, + } +} +``` + +### User joins the project + +```javascript +{ + "id": PROJECT_OUTBOX_URL#event_id, + "type": "Add", + "to": [ + "https://www.w3.org/ns/activitystreams#Public" + ], + "actor": { + "id": USER_PROFILE_URL, + "type": "Person", + "name": USER_NAME, + "url": USER_PROFILE_URL, + }, + "object": { + "id": USER_PROFILE_URL, + "type": "Person", + "name": USER_NAME, + "url": USER_PROFILE_URL, + }, + "target": { + "id": PROJECT_URL, + "type": "Application", + "name": PROJECT_NAME, + "summary": PROJECT_DESCRIPTION, + "url": PROJECT_URL, + }, +} +``` + +The GitLab project timeline does not mention who added a member to the +project, so this does the same. However, the **Add** activity requires an Actor. +For that reason, we use the same person as actor and object. + +In the **Members** page of a project contains a `source` attribute. +While there is sometimes mention of who added the user, this is used mainly +to distinguish if the user is a member attached to the project directly, or +through a group. It would not be a good "actor" (that would rather be an +`origin` for the membership). + +### User leaves the project + +```javascript +{ + "id": PROJECT_OUTBOX_URL#event_id, + "type": "Remove", + "to": [ + "https://www.w3.org/ns/activitystreams#Public" + ], + "actor": { + "id": USER_PROFILE_URL, + "type": "Person", + "name": USER_NAME, + "url": USER_PROFILE_URL, + }, + "object": { + "id": USER_PROFILE_URL, + "type": "Person", + "name": USER_NAME, + "url": USER_PROFILE_URL, + }, + "target": { + "id": PROJECT_URL, + "type": "Application", + "name": PROJECT_NAME, + "summary": PROJECT_DESCRIPTION, + "url": PROJECT_URL, + }, +} +``` + +See [User joined the project](#user-joins-the-project). + +### User deletes the repository + +```javascript +{ + "id": PROJECT_OUTBOX_URL#event_id, + "type": "Delete", + "to": [ + "https://www.w3.org/ns/activitystreams#Public" + ], + "actor": { + "id": USER_PROFILE_URL, + "type": "Person", + "name": USER_NAME, + "url": USER_PROFILE_URL, + }, + "object": { + "id": PROJECT_URL, + "type": "Application", + "name": PROJECT_NAME, + "summary": PROJECT_DESCRIPTION, + "url": PROJECT_URL, + } +} +``` diff --git a/doc/development/activitypub/actors/releases.md b/doc/development/activitypub/actors/releases.md new file mode 100644 index 00000000000..009b98b6adf --- /dev/null +++ b/doc/development/activitypub/actors/releases.md @@ -0,0 +1,85 @@ +--- +stage: Create +group: Source Code +info: "To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments" +--- + +# Activities for following releases actor **(EXPERIMENT)** + +> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/127023) in GitLab 16.5 [with two flags](../../../administration/feature_flags.md) named `activity_pub` and `activity_pub_project`. Disabled by default. This feature is an [Experiment](../../../policy/experiment-beta-support.md). + +FLAG: +On self-managed GitLab, by default this feature is not available. To make it available, +an administrator can [enable the feature flags](../../../administration/feature_flags.md) +named `activity_pub` and `activity_pub_project`. +On GitLab.com, this feature is not available. +The feature is not ready for production use. + +This feature requires two feature flags: + +- `activity_pub`: Enables or disables all ActivityPub-related features. +- `activity_pub_project`: Enables and disable ActivityPub features specific to + projects. Requires the `activity_pub` flag to also be enabled. + +## Profile + +The profile is this actor is a bit different from other actors. We don't want to +show activities for a given release, but instead the releases for a given project. + +The profile endpoint is handled by `Projects::ReleasesController#index` +on the list of releases, and should reply with something like this: + +```javascript +{ + "@context": "https://www.w3.org/ns/activitystreams", + "id": PROJECT_RELEASES_URL, + "type": "Application", + "name": PROJECT_NAME + " releases", + "url": PROJECT_RELEASES_URL, + "content": PROJECT_DESCRIPTION, + "context": { + "id": PROJECT_URL, + "type": "Application", + "name": PROJECT_NAME, + "summary": PROJECT_DESCRIPTION, + "url": PROJECT_URL, + }, + "outbox": PROJECT_RELEASES_OUTBOX_URL, + "inbox": null, +} +``` + +## Outbox + +The release actor is relatively simple: the only activity happening is the +**Create release** event. + +```javascript +{ + "id": PROJECT_RELEASES_OUTBOX_URL#release_id, + "type": "Create", + "to": [ + "https://www.w3.org/ns/activitystreams#Public" + ], + "actor": { + "id": USER_PROFILE_URL, + "type": "Person", + "name": USER_NAME, + "url": USER_PROFILE_URL, + }, + "object": { + "id": RELEASE_URL, + "type": "Application", + "name": RELEASE_TITLE, + "url": RELEASE_URL, + "content": RELEASE_DESCRIPTION, + "context": { + "id": PROJECT_URL, + "type": "Application", + "name": PROJECT_NAME, + "summary": PROJECT_DESCRIPTION, + "url": PROJECT_URL, + }, + }, +} +``` diff --git a/doc/development/activitypub/actors/topic.md b/doc/development/activitypub/actors/topic.md new file mode 100644 index 00000000000..f99a6e0569a --- /dev/null +++ b/doc/development/activitypub/actors/topic.md @@ -0,0 +1,91 @@ +--- +stage: Create +group: Source Code +info: "To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments" +--- + +# Activities for topic actor **(EXPERIMENT)** + +> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/127023) in GitLab 16.5 [with two flags](../../../administration/feature_flags.md) named `activity_pub` and `activity_pub_project`. Disabled by default. This feature is an [Experiment](../../../policy/experiment-beta-support.md). + +FLAG: +On self-managed GitLab, by default this feature is not available. To make it available, +an administrator can [enable the feature flags](../../../administration/feature_flags.md) +named `activity_pub` and `activity_pub_project`. +On GitLab.com, this feature is not available. +The feature is not ready for production use. + +This feature requires two feature flags: + +- `activity_pub`: Enables or disables all ActivityPub-related features. +- `activity_pub_project`: Enables and disable ActivityPub features specific to + projects. Requires the `activity_pub` flag to also be enabled. + +## Profile + +```javascript +{ + "@context": "https://www.w3.org/ns/activitystreams", + "id": TOPIC_URL, + "type": "Group", + "name": TOPIC_NAME, + "url": TOPIC_URL, + "summary": TOPIC_DESCRIPTION, + "outbox": TOPIC_OUTBOX_URL, + "inbox": null, +} +``` + +## Outbox + +Like the release actor, the topic specification is not complex. It generates an +activity only when a new project has been added to the given topic. + +```javascript +{ + "id": TOPIC_OUTBOX_URL#event_id, + "type": "Add", + "to": [ + "https://www.w3.org/ns/activitystreams#Public" + ], + "actor": { + "id": PROJECT_URL, + "type": "Application", + "name": PROJECT_NAME, + "url": PROJECT_URL, + }, + "object": { + "id": TOPIC_URL, + "type": "Group", + "name": TOPIC_NAME, + "url": TOPIC_URL, + }, + }, +} +``` + +## Possible difficulties + +There is hidden complexity here. + +The simpler way to build this endpoint is to take the projects associated +to a topic, and sort them by descending creation date. However, +if we do that, discrepancies will occur when implementing the +activity push part of the standard. + +Adding the project to a topic is not made at project creation time. It's +made when a project's topics are _edited_. That action can happen a very long time +after the project creation date. In that case, a push activity is +created and sent to federated instances when adding the topic to the +project. However, the list in the outbox endpoint that sorts projects by descending +creation date doesn't show the project, because it was created long ago. + +No special logic happens when a topic is added to a project, except: + +- Cleaning up the topic list. +- Creating the topic in database, if it doesn't exist yet. + +No event is generated. We should add such an event so the activity +push create an event, ideally in `Projects::UpdateService`. Then, the outbox endpoint +can list those events to be sure to match what was sent. When doing that, we should +verify that it doesn't affect other pages or endpoints dealing with events. diff --git a/doc/development/activitypub/actors/user.md b/doc/development/activitypub/actors/user.md new file mode 100644 index 00000000000..9fe4f8ec88e --- /dev/null +++ b/doc/development/activitypub/actors/user.md @@ -0,0 +1,47 @@ +--- +stage: Create +group: Source Code +info: "To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments" +--- + +# Activities for following user actor **(EXPERIMENT)** + +> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/127023) in GitLab 16.5 [with two flags](../../../administration/feature_flags.md) named `activity_pub` and `activity_pub_project`. Disabled by default. This feature is an [Experiment](../../../policy/experiment-beta-support.md). + +FLAG: +On self-managed GitLab, by default this feature is not available. To make it available, +an administrator can [enable the feature flags](../../../administration/feature_flags.md) +named `activity_pub` and `activity_pub_project`. +On GitLab.com, this feature is not available. +The feature is not ready for production use. + +This feature requires two feature flags: + +- `activity_pub`: Enables or disables all ActivityPub-related features. +- `activity_pub_project`: Enables and disable ActivityPub features specific to + projects. Requires the `activity_pub` flag to also be enabled. + +## Profile + +This activity is the first resource ActivityPub has in mind: + +```javascript +{ + "@context": "https://www.w3.org/ns/activitystreams", + "id": USER_PROFILE_URL, + "type": "Person", + "name": USER_NAME, + "url": USER_PROFILE_URL, + "outbox": USER_OUTBOX_URL, + "inbox": null, +} +``` + +## Outbox + +The user actor is special because it can be linked to all events happening on the platform. +It's a join of events on other resources: + +- All release activities. +- All project activities. +- All group activities. diff --git a/doc/development/activitypub/index.md b/doc/development/activitypub/index.md index 82249ed3856..d89f18080f0 100644 --- a/doc/development/activitypub/index.md +++ b/doc/development/activitypub/index.md @@ -15,6 +15,9 @@ named `activity_pub` and `activity_pub_project`. On GitLab.com, this feature is not available. The feature is not ready for production use. +Usage of ActivityPub in GitLab is governed by the +[GitLab Testing Agreement](https://about.gitlab.com/handbook/legal/testing-agreement/). + The goal of those documents is to provide an implementation path for adding Fediverse capabilities to GitLab. @@ -22,6 +25,12 @@ This page describes the conceptual and high level point of view, while sub-pages discuss implementation in more technical depth (as in, how to implement this in the actual rails codebase of GitLab). +This feature requires two feature flags: + +- `activity_pub`: Enables or disables all ActivityPub-related features. +- `activity_pub_project`: Enables and disable ActivityPub features specific to + projects. Requires the `activity_pub` flag to also be enabled. + ## What Feel free to jump to [the Why section](#why) if you already know what @@ -204,4 +213,4 @@ brings something immediately. 1. **Implement ActivityPub to submit cross-instance merge requests** to enable submitting merge requests to other instances. -For now, see [how to implement an ActivityPub actor](actor.md). +For now, see [how to implement an ActivityPub actor](actors/index.md). diff --git a/doc/development/work_items.md b/doc/development/work_items.md index aee903c291d..2b28b2cd4f2 100644 --- a/doc/development/work_items.md +++ b/doc/development/work_items.md @@ -85,9 +85,10 @@ end We already use the concept of WITs within `issues` table through `issue_type` column. There are `issue`, `incident`, and `test_case` issue types. To extend this -so that in future we can allow users to define custom WITs, we will move the -`issue_type` to a separate table: `work_item_types`. The migration process of `issue_type` -to `work_item_types` will involve creating the set of WITs for all root-level groups. +so that in future we can allow users to define custom WITs, we will +move the `issue_type` to a separate table: `work_item_types`. The migration process of `issue_type` +to `work_item_types` will involve creating the set of WITs for all root-level groups as described in +[this epic](https://gitlab.com/groups/gitlab-org/-/epics/6536). NOTE: At first, defining a WIT will only be possible at the root-level group, which would then be inherited by subgroups. @@ -100,7 +101,7 @@ assume the following base types: `issue: 0`, `incident: 1`, `test_case: 2`. The respective `work_item_types` records: -| `group_id` | `base_type` | `title` | +| `namespace_id` | `base_type` | `title` | | -------------- | ----------- | --------- | | 11 | 0 | Issue | | 11 | 1 | Incident | @@ -192,6 +193,164 @@ Until the architecture of WIT widgets is finalized, we are holding off on the cr types. If a new work item type is absolutely necessary, please reach out to a member of the [Project Management Engineering Team](https://gitlab.com/gitlab-org/gitlab/-/issues/370599). +### Creating a new work item type in the database + +We have completed the removal of the `issue_type` column from the issues table, in favor of using the new +`work_item_types` table as described in [this epic](https://gitlab.com/groups/gitlab-org/-/epics/6536)). + +After the introduction of the `work_item_types` table, we added more `work_item_types`, and we want to make it +easier for other teams to do so. To introduce a new `work_item_type`, you must: + +1. Write a database migration to create a new record in the `work_item_types` table. +1. Update `Gitlab::DatabaseImporters::WorkItems::BaseTypeImporter`. + +The following MRs demonstrate how to introduce new `work_item_types`: + +- [MR example 1](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/127482) +- [MR example 2](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/127917) + +#### Write a database migration + +First, write a database migration that creates the new record in the `work_item_types` table. + +Keep the following in mind when you write your migration: + +- **Important:** Exclude new type from existing APIs. + - We probably want to exclude newly created work items of this type from showing + up in existing features (like issue lists) until we fully release a feature. For this reason, + we have to add a new type to + [this exclude list](https://gitlab.com/gitlab-org/gitlab/-/blob/a0a52dd05b5d3c6ca820b672f9c0626840d2429b/app/models/work_items/type.rb#L84), + unless it is expected that users can create new issues and work items with the new type as soon as the migration + is executed. +- Use a regular migration, not a post-deploy. + - We believe it would be beneficial to use + [regular migrations](migration_style_guide.md#choose-an-appropriate-migration-type) + to add new work item types instead of a + [post deploy migration](database/post_deployment_migrations.md). + This way, follow-up MRs that depend on the type being created can assume it exists right away, + instead of having to wait for the next release. +- Migrations should avoid failures. + - We expect data related to `work_item_types` to be in a certain state when running the migration that will create a new + type. At the moment, we write migrations that check the data and don't fail in the event we find + it in an inconsistent state. There's a discussion about how much we can rely on the state of data based on seeds and + migrations in [this issue](https://gitlab.com/gitlab-org/gitlab/-/issues/423483). We can only + have a successful pipeline if we write the migration so it doesn't fail if data exists in an inconsistent + state. We probably need to update some of the database jobs in order to change this. +- Add widget definitions for the new type. + - The migration adds the new work item type as well as the widget definitions that are required for each work item. + The widgets you choose depend on the feature the new work item supports, but there are some that probably + all new work items need, like `Description`. +- Optional. Create hierarchy restrictions. + - In one of the example MRs we also insert records in the `work_item_hierarchy_restrictions` table. This is only + necessary if the new work item type is going to use the `Hierarchy` widget. In this table, you must add what + work item type can have children and of what type. Also, you should specify the hierarchy depth for work items of the same + type. + +##### Example of adding a ticket work item + +The `Ticket` work item type already exists in the database, but we'll use it as an example migration. +Note that for a new type you need to use a new name and ENUM value. + +```ruby +class AddTicketWorkItemType < Gitlab::Database::Migration[2.1] + disable_ddl_transaction! + restrict_gitlab_migration gitlab_schema: :gitlab_main + + ISSUE_ENUM_VALUE = 0 + # Enum value comes from the model where the enum is defined in + # https://gitlab.com/gitlab-org/gitlab/-/blob/1253f12abddb69cd1418c9e13e289d828b489f36/app/models/work_items/type.rb#L30. + # A new work item type should simply pick the next integer value. + TICKET_ENUM_VALUE = 8 + TICKET_NAME = 'Ticket' + # Widget definitions also have an enum defined in + # https://gitlab.com/gitlab-org/gitlab/-/blob/1253f12abddb69cd1418c9e13e289d828b489f36/app/models/work_items/widget_definition.rb#L17. + # We need to provide both the enum and name as we plan to support custom widget names in the future. + TICKET_WIDGETS = { + 'Assignees' => 0, + 'Description' => 1, + 'Hierarchy' => 2, + 'Labels' => 3, + 'Milestone' => 4, + 'Notes' => 5, + 'Start and due date' => 6, + 'Health status' => 7, + 'Weight' => 8, + 'Iteration' => 9, + 'Notifications' => 14, + 'Current user todos' => 15, + 'Award emoji' => 16 + }.freeze + + class MigrationWorkItemType < MigrationRecord + self.table_name = 'work_item_types' + end + + class MigrationWidgetDefinition < MigrationRecord + self.table_name = 'work_item_widget_definitions' + end + + class MigrationHierarchyRestriction < MigrationRecord + self.table_name = 'work_item_hierarchy_restrictions' + end + + def up + existing_ticket_work_item_type = MigrationWorkItemType.find_by(base_type: TICKET_ENUM_VALUE, namespace_id: nil) + + return say('Ticket work item type record exists, skipping creation') if existing_ticket_work_item_type + + new_ticket_work_item_type = MigrationWorkItemType.create( + name: TICKET_NAME, + namespace_id: nil, + base_type: TICKET_ENUM_VALUE, + icon_name: 'issue-type-issue' + ) + + return say('Ticket work item type create record failed, skipping creation') if new_ticket_work_item_type.new_record? + + widgets = TICKET_WIDGETS.map do |widget_name, widget_enum_value| + { + work_item_type_id: new_ticket_work_item_type.id, + name: widget_name, + widget_type: widget_enum_value + } + end + + MigrationWidgetDefinition.upsert_all( + widgets, + unique_by: :index_work_item_widget_definitions_on_default_witype_and_name + ) + + issue_type = MigrationWorkItemType.find_by(base_type: ISSUE_ENUM_VALUE, namespace_id: nil) + return say('Issue work item type not found, skipping hierarchy restrictions creation') unless issue_type + + # This part of the migration is only necessary if the new type uses the `Hierarchy` widget. + restrictions = [ + { parent_type_id: new_ticket_work_item_type.id, child_type_id: new_ticket_work_item_type.id, maximum_depth: 1 }, + { parent_type_id: new_ticket_work_item_type.id, child_type_id: issue_type.id, maximum_depth: 1 } + ] + + MigrationHierarchyRestriction.upsert_all( + restrictions, + unique_by: :index_work_item_hierarchy_restrictions_on_parent_and_child + ) + end + + def down + # There's the remote possibility that issues could already be + # using this issue type, with a tight foreign constraint. + # Therefore we will not attempt to remove any data. + end +end +``` + +<!-- markdownlint-disable-next-line MD044 --> +#### Update Gitlab::DatabaseImporters::WorkItems::BaseTypeImporter + +The [BaseTypeImporter](https://gitlab.com/gitlab-org/gitlab/-/blob/f816a369d7d6bbd1d8d53d6c0bca4ca3389fdba7/lib/gitlab/database_importers/work_items/base_type_importer.rb) +is where we can clearly visualize the structure of the types we have and what widgets are associated with each of them. +`BaseTypeImporter` is the single source of truth for fresh GitLab installs and also our test suite. This should always +reflect what we change with migrations. + ### Custom work item types With the WIT widget metadata and the workflow around mapping WIT to specific diff --git a/spec/frontend/vue_shared/components/confidentiality_badge_spec.js b/spec/frontend/vue_shared/components/confidentiality_badge_spec.js index 92cd7597637..7f6d97e8e68 100644 --- a/spec/frontend/vue_shared/components/confidentiality_badge_spec.js +++ b/spec/frontend/vue_shared/components/confidentiality_badge_spec.js @@ -1,15 +1,20 @@ -import { GlBadge } from '@gitlab/ui'; +import { GlBadge, GlIcon } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; import { TYPE_ISSUE, TYPE_EPIC, WORKSPACE_GROUP, WORKSPACE_PROJECT } from '~/issues/constants'; import ConfidentialityBadge from '~/vue_shared/components/confidentiality_badge.vue'; -const createComponent = ({ workspaceType = WORKSPACE_PROJECT, issuableType = TYPE_ISSUE } = {}) => +const createComponent = ({ + workspaceType = WORKSPACE_PROJECT, + issuableType = TYPE_ISSUE, + hideTextInSmallScreens = false, +} = {}) => shallowMount(ConfidentialityBadge, { propsData: { workspaceType, issuableType, + hideTextInSmallScreens, }, }); @@ -20,6 +25,11 @@ describe('ConfidentialityBadge', () => { wrapper = createComponent(); }); + const findConfidentialityBadgeText = () => + wrapper.find('[data-testid="confidential-badge-text"]'); + const findBadge = () => wrapper.findComponent(GlBadge); + const findBadgeIcon = () => wrapper.findComponent(GlIcon); + it.each` workspaceType | issuableType | expectedTooltip ${WORKSPACE_PROJECT} | ${TYPE_ISSUE} | ${'Only project members with at least the Reporter role, the author, and assignees can view or be notified about this issue.'} @@ -32,14 +42,30 @@ describe('ConfidentialityBadge', () => { issuableType, }); - const badgeEl = wrapper.findComponent(GlBadge); - - expect(badgeEl.props()).toMatchObject({ - icon: 'eye-slash', + expect(findBadgeIcon().props('name')).toBe('eye-slash'); + expect(findBadge().props()).toMatchObject({ variant: 'warning', }); - expect(badgeEl.attributes('title')).toBe(expectedTooltip); - expect(badgeEl.text()).toBe('Confidential'); + expect(findBadge().attributes('title')).toBe(expectedTooltip); + expect(findBadge().text()).toBe('Confidential'); }, ); + + it('does not have `gl-sm-display-block` and `gl-display-none` when `hideTextInSmallScreens` is false', () => { + wrapper = createComponent({ hideTextInSmallScreens: false }); + + expect(findConfidentialityBadgeText().classes()).not.toContain( + 'gl-display-none', + 'gl-sm-display-block', + ); + }); + + it('has `gl-sm-display-block` and `gl-display-none` when `hideTextInSmallScreens` is true', () => { + wrapper = createComponent({ hideTextInSmallScreens: true }); + + expect(findConfidentialityBadgeText().classes()).toContain( + 'gl-display-none', + 'gl-sm-display-block', + ); + }); }); diff --git a/spec/frontend/vue_shared/issuable/show/components/issuable_header_spec.js b/spec/frontend/vue_shared/issuable/show/components/issuable_header_spec.js index 4d08ad54e58..a3d7b244685 100644 --- a/spec/frontend/vue_shared/issuable/show/components/issuable_header_spec.js +++ b/spec/frontend/vue_shared/issuable/show/components/issuable_header_spec.js @@ -107,6 +107,7 @@ describe('IssuableHeader component', () => { expect(findConfidentialityBadge().props()).toEqual({ issuableType: 'issue', workspaceType: 'project', + hideTextInSmallScreens: false, }); }); diff --git a/spec/frontend/work_items/components/work_item_state_badge_spec.js b/spec/frontend/work_items/components/work_item_state_badge_spec.js index 888d712cc5a..248f16a4081 100644 --- a/spec/frontend/work_items/components/work_item_state_badge_spec.js +++ b/spec/frontend/work_items/components/work_item_state_badge_spec.js @@ -1,4 +1,4 @@ -import { GlBadge } from '@gitlab/ui'; +import { GlBadge, GlIcon } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; import { STATE_OPEN, STATE_CLOSED } from '~/work_items/constants'; import WorkItemStateBadge from '~/work_items/components/work_item_state_badge.vue'; @@ -14,6 +14,7 @@ describe('WorkItemStateBadge', () => { }); }; const findStatusBadge = () => wrapper.findComponent(GlBadge); + const findStatusBadgeIcon = () => wrapper.findComponent(GlIcon); it.each` state | icon | stateText | variant @@ -24,7 +25,7 @@ describe('WorkItemStateBadge', () => { ({ state, icon, stateText, variant }) => { createComponent({ workItemState: state }); - expect(findStatusBadge().props('icon')).toBe(icon); + expect(findStatusBadgeIcon().props('name')).toBe(icon); expect(findStatusBadge().props('variant')).toBe(variant); expect(findStatusBadge().text()).toBe(stateText); }, |