Skip to content

List the deployments related to a release

Release notes

img

As GitLab offers an integrated platform for the whole DevSecOps domain, it was possible for a long time to create a release and register a deployment from a git tag. At the same time, most of these objects were not visually connected on the GitLab UI. Since GitLab 17.6 the deployment pages of tagged deployments show the related release's notes on the deployment within an environment. Recently, we added another integration point, showing the list of related deployments on the release pages. This allows release managers to see where a specific release was already deployed or is waiting to be deployed.

We would like to express our gratitude to Anton Kalmykov for contributing both features to GitLab.

Problem to solve

As a release manager, I want to know where the current release was already deployed to, and quickly reach related deployment approval information.

Proposal

Add a section similarly to Assets

Screenshot_2024-10-29_at_09.11.51

It might be better to redesign the page and move the Release notes, Assets and Deployments into a tab-based setup, instead of the accordions at the top.

The deployment list should show all the deployments related to the git tag of the release. Columns and information to share:

Environment Status Deployment ID Commit Triggerer Created Deployed
Environment name linked to the specific environment Status badge (no link) The ID of the deployment linked to the deployment details page Commit related informations: message, branch, sha User who triggered the related pipeline Created datetime of the deployment job Finished datetime of the deployment job (if available, shown for non-successful jobs too)

The deployment details should be visible only for Reporter role and up.

Design proposal

Screenshot_2024-11-12_at_14.24.34

Implementation guide

  1. Prepare the related deployments data in the release model:
def related_deployments
  Deployment
    .with(Gitlab::SQL::CTE.new(:available_environments, project.environments.available.select(:id)).to_arel)
    .where('environment_id IN (SELECT * FROM available_environments)')
    .where(ref: tag)
    .with_environment_page_associations
end
  1. Use the data in the release_helper.

    • create a helper method:

      def deployments_for_release
          project = @release.project
          commit = project.repository.commit(@release.tag)
      
          deployments = @release.related_deployments
      
          deployments.map do |deployment|
            user = deployment.deployable.user
            environment = deployment.environment
      
            {
              environment: {
                name: environment&.name,
                url: environment ? project_environment_url(project, environment) : nil
              },
              status: deployment.status,
              deployment: {
                id: deployment.id,
                url: project_environment_deployment_path(project, environment, deployment)
              },
              commit: {
                sha: commit.id,
                name: commit.author_name,
                commit_url: project_commit_url(deployment.project, commit),
                short_sha: commit.short_id,
                title: commit.title
              },
              triggerer: {
                name: user&.name,
                web_url: user ? user_url(user) : nil,
                avatar_url: user&.avatar_url
              },
              created_at: deployment.created_at,
              finished_at: deployment.finished_at
            }
          end
        end
    • provide this data to the frontend by updating data_for_show_page method:

      def data_for_show_page
          {
            project_id: @project.id,
            project_path: @project.full_path,
            tag_name: @release.tag,
            deployments: deployments_for_release.to_json
          }
      end
  2. Process the newly added data on the frontend by updating the mount_show file.

    diff --git a/app/assets/javascripts/releases/mount_show.js b/app/assets/javascripts/releases/mount_show.js
    index 5e0bce404bb0..87a795859f34 100644
    --- a/app/assets/javascripts/releases/mount_show.js
    +++ b/app/assets/javascripts/releases/mount_show.js
    @@ -1,6 +1,7 @@
     import Vue from 'vue';
     import VueApollo from 'vue-apollo';
     import createDefaultClient from '~/lib/graphql';
    +import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
     import ReleaseShowApp from './components/app_show.vue';
     
     Vue.use(VueApollo);
    @@ -14,7 +15,7 @@ export default () => {
     
       if (!el) return false;
     
    -  const { projectPath, tagName } = el.dataset;
    +  const { projectPath, tagName, deployments } = el.dataset;
     
       return new Vue({
         el,
    @@ -23,6 +24,11 @@ export default () => {
           projectPath,
           tagName,
         },
    -    render: (h) => h(ReleaseShowApp),
    +    render: (h) =>
    +      h(ReleaseShowApp, {
    +        props: {
    +          deployments: convertObjectPropsToCamelCase(JSON.parse(deployments), { deep: true }),
    +        },
    +      }),
       });
     };
    

    Within this change, we are reading the new deployments field. We need to process it from JSON and convert properties to camelCase to conform with other instances on the frontend.

  3. Create a new component to display the related deployments - release_block_deployments.vue

  4. Provide deployments prop to the newly added component and render the component only when the deployments list is present:

    diff --git a/app/assets/javascripts/releases/components/app_show.vue b/app/assets/javascripts/releases/components/app_show.vue
    index dd2d35431b7e..631c92fe2b70 100644
    --- a/app/assets/javascripts/releases/components/app_show.vue
    +++ b/app/assets/javascripts/releases/components/app_show.vue
    @@ -21,6 +21,13 @@ export default {
           default: '',
         },
       },
    +  props: {
    +    deployments: {
    +      type: Array,
    +      required: false,
    +      default: () => [],
    +    },
    +  },
       apollo: {
         // eslint-disable-next-line @gitlab/vue-no-undef-apollo-properties
         release: {
    @@ -71,6 +78,6 @@ export default {
       <div class="gl-mt-3">
         <release-skeleton-loader v-if="$apollo.queries.release.loading" />
     
    -    <release-block v-else-if="release" :release="release" />
    +    <release-block v-else-if="release" :release="release" :deployments="deployments" />
       </div>
     </template>
    diff --git a/app/assets/javascripts/releases/components/release_block.vue b/app/assets/javascripts/releases/components/release_block.vue
    index 3183434aa17b..20700a1f5e3f 100644
    --- a/app/assets/javascripts/releases/components/release_block.vue
    +++ b/app/assets/javascripts/releases/components/release_block.vue
    @@ -15,6 +15,7 @@ import ReleaseBlockAssets from './release_block_assets.vue';
     import ReleaseBlockFooter from './release_block_footer.vue';
     import ReleaseBlockTitle from './release_block_title.vue';
     import ReleaseBlockMilestoneInfo from './release_block_milestone_info.vue';
    +import ReleaseBlockDeployments from './release_block_deployments.vue';
     
     export default {
       name: 'ReleaseBlock',
    @@ -26,6 +27,7 @@ export default {
         ReleaseBlockFooter,
         ReleaseBlockTitle,
         ReleaseBlockMilestoneInfo,
    +    ReleaseBlockDeployments,
       },
       directives: {
         SafeHtml,
    @@ -42,6 +44,11 @@ export default {
           required: false,
           default: CREATED_ASC,
         },
    +    deployments: {
    +      type: Array,
    +      required: false,
    +      default: () => [],
    +    },
       },
       data() {
         return {
    @@ -136,7 +143,7 @@ export default {
           </gl-button>
         </template>
     
    -    <div class="gl-flex gl-flex-col gl-gap-5">
    +    <div class="gl-mx-5 gl-my-4 gl-flex gl-flex-col gl-gap-5">
           <div
             v-if="shouldRenderMilestoneInfo"
             class="gl-border-b-1 gl-border-gray-100 gl-border-b-solid"
    @@ -152,6 +159,15 @@ export default {
             />
           </div>
     
    +      <release-block-deployments
    +        v-if="deployments.length"
    +        :class="{
    +          'gl-border-b-1 gl-border-gray-100 gl-pb-5 gl-border-b-solid':
    +            shouldRenderAssets || hasEvidence || release.descriptionHtml,
    +        }"
    +        :deployments="deployments"
    +      />
    +
           <release-block-assets
             v-if="shouldRenderAssets"
             :assets="assets"
    
  5. Here's an example of how to display the data on the release page. It uses the GlTable similarly to how it's done on the environment page. Feel free to adjust the code as you see fit:

    <script>
    import { GlLink, GlButton, GlCollapse, GlIcon, GlBadge, GlTableLite } from '@gitlab/ui';
    import Commit from '~/vue_shared/components/commit.vue';
    import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
    import DeploymentStatusLink from '~/environments/components/deployment_status_link.vue';
    import DeploymentTriggerer from '~/environments/environment_details/components/deployment_triggerer.vue';
    import { __ } from '~/locale';
    
    export default {
      name: 'ReleaseBlockDeployments',
      components: {
        GlLink,
        GlButton,
        GlCollapse,
        GlIcon,
        GlBadge,
        GlTableLite,
        TimeAgoTooltip,
        Commit,
        DeploymentStatusLink,
        DeploymentTriggerer,
      },
      props: {
        deployments: {
          type: Array,
          required: true,
        },
      },
      data() {
        return {
          isDeploymentsExpanded: true,
        };
      },
      methods: {
        toggleDeploymentsExpansion() {
          this.isDeploymentsExpanded = !this.isDeploymentsExpanded;
        },
      },
      tableFields: [
        {
          key: 'environment',
          label: __('Environment'),
          tdClass: '!gl-align-middle',
          thClass: '!gl-border-t-0',
        },
        {
          key: 'status',
          label: __('Status'),
          tdClass: '!gl-align-middle',
          thClass: '!gl-border-t-0',
        },
        {
          key: 'deploymentId',
          label: __('Deployment ID'),
          tdClass: '!gl-align-middle',
          thClass: '!gl-border-t-0',
        },
        {
          key: 'commit',
          label: __('Commit'),
          tdClass: '!gl-align-middle',
          thClass: '!gl-border-t-0',
        },
        {
          key: 'triggerer',
          label: __('Triggerer'),
          tdClass: '!gl-align-middle',
          thClass: '!gl-border-t-0',
        },
        {
          key: 'created',
          label: __('Created'),
          tdClass: '!gl-align-middle gl-whitespace-nowrap',
          thClass: '!gl-border-t-0',
        },
        {
          key: 'finished',
          label: __('Finished'),
          tdClass: '!gl-align-middle gl-whitespace-nowrap',
          thClass: '!gl-border-t-0',
        },
      ],
    };
    </script>
    
    <template>
      <div>
        <gl-button
          data-testid="accordion-button"
          variant="link"
          class="!gl-text-default"
          button-text-classes="gl-heading-5"
          @click="toggleDeploymentsExpansion"
        >
          <gl-icon
            name="chevron-right"
            class="gl-transition-all"
            :class="{ 'gl-rotate-90': isDeploymentsExpanded }"
          />
          {{ __('Deployments') }}
          <gl-badge variant="neutral" class="gl-inline-block">{{ deployments.length }}</gl-badge>
        </gl-button>
        <gl-collapse v-model="isDeploymentsExpanded">
          <div class="gl-pl-6 gl-pt-3">
            <gl-table-lite :items="deployments" :fields="$options.tableFields" stacked="lg">
              <template #cell(environment)="{ item }">
                <gl-link :href="item.environment.url"> {{ item.environment.name }} </gl-link>
              </template>
              <template #cell(status)="{ item }">
                <deployment-status-link :deployment="item" :status="item.status" />
              </template>
              <template #cell(deploymentId)="{ item }">
                <gl-link :href="item.deployment.url"> {{ item.deployment.id }} </gl-link>
              </template>
              <template #cell(triggerer)="{ item }">
                <deployment-triggerer :triggerer="item.triggerer" />
              </template>
              <template #cell(commit)="{ item }">
                <commit
                  :short-sha="item.commit.shortSha"
                  :commit-url="item.commit.commitUrl"
                  :title="item.commit.title"
                  :author="item.commit.author"
                  :show-ref-info="false"
                />
              </template>
              <template #cell(created)="{ item }">
                <time-ago-tooltip
                  :time="item.createdAt"
                  enable-truncation
                  data-testid="deployment-created-at"
                />
              </template>
              <template #cell(finished)="{ item }">
                <time-ago-tooltip
                  v-if="item.finishedAt"
                  :time="item.finishedAt"
                  enable-truncation
                  data-testid="deployment-finished-at"
                />
              </template>
            </gl-table-lite>
          </div>
        </gl-collapse>
      </div>
    </template>
    
  6. As the new section is now the first one and should be expanded by default, we should update the release_block_assets section to be collapsed by default:

    diff --git a/app/assets/javascripts/releases/components/release_block_assets.vue b/app/assets/javascripts/releases/components/release_block_assets.vue
    index 3953a3990c29..8f95c77f16b1 100644
    --- a/app/assets/javascripts/releases/components/release_block_assets.vue
    +++ b/app/assets/javascripts/releases/components/release_block_assets.vue
    @@ -24,7 +24,7 @@ export default {
       },
       data() {
         return {
    -      isAssetsExpanded: true,
    +      isAssetsExpanded: false,
         };
       },
       computed: {
  7. Update the corresponding tests:

  8. Update the translations by running tooling/bin/gettext_extractor locale/gitlab.pot

Implementation guide for adding internal events tracking

As the feature-related changes should affect any files, I suggest adding events and metrics in a separate MR.

  1. Follow the documentation here
  2. Run scripts/internal_events/cli.rb and create 4 events using the following options:
      1. New Event -- track when a specific scenario occurs on gitlab instances ex) a user applies a label to an issue
Event description Event name Event identifiers Additional property Group Tiers
User opens the deployments section on the release page click_expand_deployments_on_release_page [namespace, project, user] None cd:deploy:environments free, premium, ultimate
User opens the assets section on the release page click_expand_assets_on_release_page [namespace, project, user] None cd:deploy:environments free, premium, ultimate
User clicks on the environment link click_environment_link_on_release_page [namespace, project, user] None cd:deploy:environments free, premium, ultimate
User clicks on the deployment link click_deployment_link_on_release_page [namespace, project, user] None cd:deploy:environments free, premium, ultimate
    1. Save & Create Metric
  • Monthly/Weekly count of unique users who triggered <event_name>
  • Use the event description to create a metric description
  1. To trigger this events follow this guide

Intended users

Feature Usage Metrics

  • MAU of user clicks on the tab/accordion (for every option)
  • MAU of user clicks on the environment links
  • MAU of user clicks on the deployment ID links

Does this feature require an audit event?

no

Edited by Viktor Nagy (GitLab)