diff --git a/app/assets/javascripts/jobs/components/job/empty_state.vue b/app/assets/javascripts/jobs/components/job/empty_state.vue
index 65b9600e6646b93e18179f97a945b31faf6da5d2..053d5a4e740bd18878874487eb34064ee93d222f 100644
--- a/app/assets/javascripts/jobs/components/job/empty_state.vue
+++ b/app/assets/javascripts/jobs/components/job/empty_state.vue
@@ -20,6 +20,14 @@ export default {
       type: String,
       required: true,
     },
+    isRetryable: {
+      type: Boolean,
+      required: false,
+    },
+    jobId: {
+      type: Number,
+      required: true,
+    },
     title: {
       type: String,
       required: true,
@@ -54,8 +62,8 @@ export default {
     },
   },
   computed: {
-    isGraphQL() {
-      return this.glFeatures?.graphqlJobApp;
+    showGraphQLManualVariablesForm() {
+      return this.glFeatures?.graphqlJobApp && this.isRetryable;
     },
     shouldRenderManualVariables() {
       return this.playable && !this.scheduled;
@@ -77,14 +85,18 @@ export default {
 
         <p v-if="content" data-testid="job-empty-state-content">{{ content }}</p>
       </div>
-      <template v-if="isGraphQL">
-        <manual-variables-form v-if="shouldRenderManualVariables" :action="action" />
+      <template v-if="showGraphQLManualVariablesForm">
+        <manual-variables-form
+          v-if="shouldRenderManualVariables"
+          :job-id="jobId"
+          @hideManualVariablesForm="$emit('hideManualVariablesForm')"
+        />
       </template>
       <template v-else>
         <legacy-manual-variables-form v-if="shouldRenderManualVariables" :action="action" />
       </template>
-      <div class="text-content">
-        <div v-if="action && !shouldRenderManualVariables" class="text-center">
+      <div v-if="action && !shouldRenderManualVariables" class="text-content">
+        <div class="text-center">
           <gl-link
             :href="action.path"
             :data-method="action.method"
diff --git a/app/assets/javascripts/jobs/components/job/graphql/mutations/job_retry_with_variables.mutation.graphql b/app/assets/javascripts/jobs/components/job/graphql/mutations/job_retry_with_variables.mutation.graphql
new file mode 100644
index 0000000000000000000000000000000000000000..2b79892a07251271bbefec7e5584ceba2c06677b
--- /dev/null
+++ b/app/assets/javascripts/jobs/components/job/graphql/mutations/job_retry_with_variables.mutation.graphql
@@ -0,0 +1,16 @@
+mutation retryJobWithVariables($id: CiBuildID!, $variables: [CiVariableInput!]) {
+  jobRetry(input: { id: $id, variables: $variables }) {
+    job {
+      id
+      manualVariables {
+        nodes {
+          id
+          key
+          value
+        }
+      }
+      webPath
+    }
+    errors
+  }
+}
diff --git a/app/assets/javascripts/jobs/components/job/graphql/queries/get_job.query.graphql b/app/assets/javascripts/jobs/components/job/graphql/queries/get_job.query.graphql
new file mode 100644
index 0000000000000000000000000000000000000000..aaf1dec8e0fd7c493c9afb5c4049a19251f74aa0
--- /dev/null
+++ b/app/assets/javascripts/jobs/components/job/graphql/queries/get_job.query.graphql
@@ -0,0 +1,17 @@
+query getJob($fullPath: ID!, $id: JobID!) {
+  project(fullPath: $fullPath) {
+    id
+    job(id: $id) {
+      id
+      manualJob
+      manualVariables {
+        nodes {
+          id
+          key
+          value
+        }
+      }
+      name
+    }
+  }
+}
diff --git a/app/assets/javascripts/jobs/components/job/job_app.vue b/app/assets/javascripts/jobs/components/job/job_app.vue
index e5fbf77be1e6aeee0819f11005ed8d9e16cac66e..c6d900ef13e265b750057b45b68f48dace26bfb8 100644
--- a/app/assets/javascripts/jobs/components/job/job_app.vue
+++ b/app/assets/javascripts/jobs/components/job/job_app.vue
@@ -72,6 +72,7 @@ export default {
   data() {
     return {
       searchResults: [],
+      showUpdateVariablesState: false,
     };
   },
   computed: {
@@ -122,6 +123,10 @@ export default {
       return this.shouldRenderCalloutMessage && !this.hasUnmetPrerequisitesFailure;
     },
 
+    isJobRetryable() {
+      return Boolean(this.job.retry_path);
+    },
+
     itemName() {
       return sprintf(__('Job %{jobName}'), { jobName: this.job.name });
     },
@@ -169,10 +174,16 @@ export default {
       'toggleScrollButtons',
       'toggleScrollAnimation',
     ]),
+    onHideManualVariablesForm() {
+      this.showUpdateVariablesState = false;
+    },
     onResize() {
       this.updateSidebar();
       this.updateScroll();
     },
+    onUpdateVariables() {
+      this.showUpdateVariablesState = true;
+    },
     updateSidebar() {
       const breakpoint = bp.getBreakpointSize();
       if (breakpoint === 'xs' || breakpoint === 'sm') {
@@ -272,14 +283,12 @@ export default {
         </div>
         <!-- job log -->
         <div
-          v-if="hasJobLog"
+          v-if="hasJobLog && !showUpdateVariablesState"
           class="build-log-container gl-relative"
           :class="{ 'gl-mt-3': !job.archived }"
         >
           <log-top-bar
             :class="{
-              'sidebar-expanded': isSidebarOpen,
-              'sidebar-collapsed': !isSidebarOpen,
               'has-archived-block': job.archived,
             }"
             :size="jobLogSize"
@@ -300,14 +309,17 @@ export default {
 
         <!-- empty state -->
         <empty-state
-          v-if="!hasJobLog"
+          v-if="!hasJobLog || showUpdateVariablesState"
           :illustration-path="emptyStateIllustration.image"
           :illustration-size-class="emptyStateIllustration.size"
+          :is-retryable="isJobRetryable"
+          :job-id="job.id"
           :title="emptyStateTitle"
           :content="emptyStateIllustration.content"
           :action="emptyStateAction"
           :playable="job.playable"
           :scheduled="job.scheduled"
+          @hideManualVariablesForm="onHideManualVariablesForm()"
         />
         <!-- EO empty state -->
 
@@ -321,9 +333,9 @@ export default {
         'right-sidebar-expanded': isSidebarOpen,
         'right-sidebar-collapsed': !isSidebarOpen,
       }"
-      :erase-path="job.erase_path"
       :artifact-help-url="artifactHelpUrl"
       data-testid="job-sidebar"
+      @updateVariables="onUpdateVariables()"
     />
   </div>
 </template>
diff --git a/app/assets/javascripts/jobs/components/job/legacy_manual_variables_form.vue b/app/assets/javascripts/jobs/components/job/legacy_manual_variables_form.vue
index 1898e02c94e705a3925f9054f88175fc5162af4c..2b6b6f8e59e5373fc7c147101c0ef83e6057c504 100644
--- a/app/assets/javascripts/jobs/components/job/legacy_manual_variables_form.vue
+++ b/app/assets/javascripts/jobs/components/job/legacy_manual_variables_form.vue
@@ -6,6 +6,7 @@ import {
   GlButton,
   GlLink,
   GlSprintf,
+  GlTooltipDirective,
 } from '@gitlab/ui';
 import { uniqueId } from 'lodash';
 import { mapActions } from 'vuex';
@@ -13,7 +14,7 @@ import { helpPagePath } from '~/helpers/help_page_helper';
 import { s__ } from '~/locale';
 
 export default {
-  name: 'ManualVariablesForm',
+  name: 'LegacyManualVariablesForm',
   components: {
     GlFormInputGroup,
     GlInputGroupText,
@@ -22,6 +23,9 @@ export default {
     GlLink,
     GlSprintf,
   },
+  directives: {
+    GlTooltip: GlTooltipDirective,
+  },
   props: {
     action: {
       type: Object,
@@ -42,6 +46,7 @@ export default {
     value: 'value',
   },
   i18n: {
+    clearInputs: s__('CiVariables|Clear inputs'),
     header: s__('CiVariables|Variables'),
     keyLabel: s__('CiVariables|Key'),
     valueLabel: s__('CiVariables|Value'),
@@ -152,11 +157,13 @@ export default {
 
         <gl-button
           v-if="canRemove(index)"
+          v-gl-tooltip
+          :aria-label="$options.i18n.clearInputs"
+          :title="$options.i18n.clearInputs"
           class="gl-flex-grow-0 gl-flex-basis-0"
           category="tertiary"
           variant="danger"
           icon="clear"
-          :aria-label="__('Delete variable')"
           data-testid="delete-variable-btn"
           @click="deleteVariable(variable.id)"
         />
diff --git a/app/assets/javascripts/jobs/components/job/manual_variables_form.vue b/app/assets/javascripts/jobs/components/job/manual_variables_form.vue
index 2f97301979ca507ab9033b57e87218cb4c9e5f16..e8edc7fc56f78d97e67622584dc1a1b5cb358e40 100644
--- a/app/assets/javascripts/jobs/components/job/manual_variables_form.vue
+++ b/app/assets/javascripts/jobs/components/job/manual_variables_form.vue
@@ -5,15 +5,23 @@ import {
   GlFormInput,
   GlButton,
   GlLink,
+  GlLoadingIcon,
   GlSprintf,
+  GlTooltipDirective,
 } from '@gitlab/ui';
-import { uniqueId } from 'lodash';
-import { mapActions } from 'vuex';
+import { cloneDeep, uniqueId } from 'lodash';
+import { fetchPolicies } from '~/lib/graphql';
+import { createAlert } from '~/flash';
+import { convertToGraphQLId } from '~/graphql_shared/utils';
+import { JOB_GRAPHQL_ERRORS, GRAPHQL_ID_TYPES } from '~/jobs/constants';
 import { helpPagePath } from '~/helpers/help_page_helper';
+import { redirectTo } from '~/lib/utils/url_utility';
 import { s__ } from '~/locale';
+import GetJob from './graphql/queries/get_job.query.graphql';
+import retryJobWithVariablesMutation from './graphql/mutations/job_retry_with_variables.mutation.graphql';
 
 // This component is a port of ~/jobs/components/job/legacy_manual_variables_form.vue
-// It is meant to fetch the job information via GraphQL instead of REST API.
+// It is meant to fetch/update the job information via GraphQL instead of REST API.
 
 export default {
   name: 'ManualVariablesForm',
@@ -23,63 +31,73 @@ export default {
     GlFormInput,
     GlButton,
     GlLink,
+    GlLoadingIcon,
     GlSprintf,
   },
-  props: {
-    action: {
-      type: Object,
-      required: false,
-      default: null,
-      validator(value) {
-        return (
-          value === null ||
-          (Object.prototype.hasOwnProperty.call(value, 'path') &&
-            Object.prototype.hasOwnProperty.call(value, 'method') &&
-            Object.prototype.hasOwnProperty.call(value, 'button_title'))
-        );
+  directives: {
+    GlTooltip: GlTooltipDirective,
+  },
+  inject: ['projectPath'],
+  apollo: {
+    variables: {
+      query: GetJob,
+      variables() {
+        return {
+          fullPath: this.projectPath,
+          id: convertToGraphQLId(GRAPHQL_ID_TYPES.commitStatus, this.jobId),
+        };
+      },
+      fetchPolicy: fetchPolicies.CACHE_AND_NETWORK,
+      update(data) {
+        const jobVariables = cloneDeep(data?.project?.job?.manualVariables?.nodes);
+        return [...jobVariables.reverse(), ...this.variables];
+      },
+      error() {
+        createAlert({ message: JOB_GRAPHQL_ERRORS.jobQueryErrorText });
       },
     },
   },
+  props: {
+    jobId: {
+      type: Number,
+      required: true,
+    },
+  },
   inputTypes: {
     key: 'key',
     value: 'value',
   },
   i18n: {
+    clearInputs: s__('CiVariables|Clear inputs'),
+    formHelpText: s__(
+      'CiVariables|Specify variable values to be used in this run. The values specified in %{linkStart}CI/CD settings%{linkEnd} will be used as default',
+    ),
     header: s__('CiVariables|Variables'),
     keyLabel: s__('CiVariables|Key'),
-    valueLabel: s__('CiVariables|Value'),
     keyPlaceholder: s__('CiVariables|Input variable key'),
+    runAgainButtonText: s__('CiVariables|Run job again'),
+    valueLabel: s__('CiVariables|Value'),
     valuePlaceholder: s__('CiVariables|Input variable value'),
-    formHelpText: s__(
-      'CiVariables|Specify variable values to be used in this run. The values specified in %{linkStart}CI/CD settings%{linkEnd} will be used as default',
-    ),
   },
   data() {
     return {
+      job: {},
       variables: [
         {
-          key: '',
-          secretValue: '',
           id: uniqueId(),
+          key: '',
+          value: '',
         },
       ],
-      triggerBtnDisabled: false,
+      runAgainBtnDisabled: false,
     };
   },
   computed: {
     variableSettings() {
       return helpPagePath('ci/variables/index', { anchor: 'add-a-cicd-variable-to-a-project' });
     },
-    preparedVariables() {
-      // we need to ensure no empty variables are passed to the API
-      // and secretValue should be snake_case when passed to the API
-      return this.variables
-        .filter((variable) => variable.key !== '')
-        .map(({ key, secretValue }) => ({ key, secret_value: secretValue }));
-    },
   },
   methods: {
-    ...mapActions(['triggerManualJob']),
     addEmptyVariable() {
       const lastVar = this.variables[this.variables.length - 1];
 
@@ -88,9 +106,9 @@ export default {
       }
 
       this.variables.push({
-        key: '',
-        secret_value: '',
         id: uniqueId(),
+        key: '',
+        value: '',
       });
     },
     canRemove(index) {
@@ -105,16 +123,45 @@ export default {
     inputRef(type, id) {
       return `${this.$options.inputTypes[type]}-${id}`;
     },
-    trigger() {
-      this.triggerBtnDisabled = true;
+    navigateToRetriedJob(retryPath) {
+      redirectTo(retryPath);
+    },
+    async retryJob() {
+      try {
+        // filtering out 'id' along with empty variables to send only key, value in the mutation.
+        // This will be removed in: https://gitlab.com/gitlab-org/gitlab/-/issues/377268
+        const preparedVariables = this.variables
+          .filter((variable) => variable.key !== '')
+          .map(({ key, value }) => ({ key, value }));
 
-      this.triggerManualJob(this.preparedVariables);
+        const { data } = await this.$apollo.mutate({
+          mutation: retryJobWithVariablesMutation,
+          variables: {
+            id: convertToGraphQLId(GRAPHQL_ID_TYPES.ciBuild, this.jobId),
+            // we need to ensure no empty variables are passed to the API
+            variables: preparedVariables,
+          },
+        });
+        if (data.jobRetry?.errors?.length) {
+          createAlert({ message: data.jobRetry.errors[0] });
+        } else {
+          this.navigateToRetriedJob(data.jobRetry?.job?.webPath);
+        }
+      } catch (error) {
+        createAlert({ message: JOB_GRAPHQL_ERRORS.retryMutationErrorText });
+      }
+    },
+    runAgain() {
+      this.runAgainBtnDisabled = true;
+
+      this.retryJob();
     },
   },
 };
 </script>
 <template>
-  <div class="row gl-justify-content-center">
+  <gl-loading-icon v-if="$apollo.queries.variables.loading" class="gl-mt-9" size="lg" />
+  <div v-else class="row gl-justify-content-center">
     <div class="col-10" data-testid="manual-vars-form">
       <label>{{ $options.i18n.header }}</label>
 
@@ -147,7 +194,7 @@ export default {
           </template>
           <gl-form-input
             :ref="inputRef('value', variable.id)"
-            v-model="variable.secretValue"
+            v-model="variable.value"
             :placeholder="$options.i18n.valuePlaceholder"
             data-testid="ci-variable-value"
           />
@@ -155,11 +202,13 @@ export default {
 
         <gl-button
           v-if="canRemove(index)"
+          v-gl-tooltip
+          :aria-label="$options.i18n.clearInputs"
+          :title="$options.i18n.clearInputs"
           class="gl-flex-grow-0 gl-flex-basis-0"
           category="tertiary"
           variant="danger"
           icon="clear"
-          :aria-label="__('Delete variable')"
           data-testid="delete-variable-btn"
           @click="deleteVariable(variable.id)"
         />
@@ -178,16 +227,23 @@ export default {
         </gl-sprintf>
       </div>
       <div class="gl-display-flex gl-justify-content-center gl-mt-5">
+        <gl-button
+          class="gl-mt-5"
+          :aria-label="__('Cancel')"
+          data-testid="cancel-btn"
+          @click="$emit('hideManualVariablesForm')"
+          >{{ __('Cancel') }}</gl-button
+        >
         <gl-button
           class="gl-mt-5"
           variant="confirm"
           category="primary"
-          :aria-label="__('Trigger manual job')"
-          :disabled="triggerBtnDisabled"
-          data-testid="trigger-manual-job-btn"
-          @click="trigger"
+          :aria-label="__('Run manual job again')"
+          :disabled="runAgainBtnDisabled"
+          data-testid="run-manual-job-btn"
+          @click="runAgain"
         >
-          {{ action.button_title }}
+          {{ $options.i18n.runAgainButtonText }}
         </gl-button>
       </div>
     </div>
diff --git a/app/assets/javascripts/jobs/components/job/sidebar/job_sidebar_retry_button.vue b/app/assets/javascripts/jobs/components/job/sidebar/job_sidebar_retry_button.vue
index dd620977f0c80d865e195b3b099cce1869d306f3..65175df555aadbacade53a560de4a86cff648414 100644
--- a/app/assets/javascripts/jobs/components/job/sidebar/job_sidebar_retry_button.vue
+++ b/app/assets/javascripts/jobs/components/job/sidebar/job_sidebar_retry_button.vue
@@ -1,19 +1,23 @@
 <script>
-import { GlButton, GlModalDirective } from '@gitlab/ui';
+import { GlButton, GlDropdown, GlDropdownItem, GlModalDirective } from '@gitlab/ui';
 import { mapGetters } from 'vuex';
+import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
 import { JOB_SIDEBAR_COPY } from '~/jobs/constants';
 
 export default {
   name: 'JobSidebarRetryButton',
   i18n: {
-    retryLabel: JOB_SIDEBAR_COPY.retry,
+    ...JOB_SIDEBAR_COPY,
   },
   components: {
     GlButton,
+    GlDropdown,
+    GlDropdownItem,
   },
   directives: {
     GlModal: GlModalDirective,
   },
+  mixins: [glFeatureFlagsMixin()],
   props: {
     modalId: {
       type: String,
@@ -23,9 +27,16 @@ export default {
       type: String,
       required: true,
     },
+    isManualJob: {
+      type: Boolean,
+      required: true,
+    },
   },
   computed: {
     ...mapGetters(['hasForwardDeploymentFailure']),
+    showRetryDropdown() {
+      return this.glFeatures?.graphqlJobApp && this.isManualJob;
+    },
   },
 };
 </script>
@@ -33,17 +44,30 @@ export default {
   <gl-button
     v-if="hasForwardDeploymentFailure"
     v-gl-modal="modalId"
-    :aria-label="$options.i18n.retryLabel"
+    :aria-label="$options.i18n.retryJobLabel"
     category="primary"
     variant="confirm"
     icon="retry"
     data-testid="retry-job-button"
   />
-
+  <gl-dropdown
+    v-else-if="showRetryDropdown"
+    icon="retry"
+    category="primary"
+    :right="true"
+    variant="confirm"
+  >
+    <gl-dropdown-item :href="href" data-method="post">
+      {{ $options.i18n.runAgainJobButtonLabel }}
+    </gl-dropdown-item>
+    <gl-dropdown-item @click="$emit('updateVariablesClicked')">
+      {{ $options.i18n.updateVariables }}
+    </gl-dropdown-item>
+  </gl-dropdown>
   <gl-button
     v-else
     :href="href"
-    :aria-label="$options.i18n.retryLabel"
+    :aria-label="$options.i18n.retryJobLabel"
     category="primary"
     variant="confirm"
     icon="retry"
diff --git a/app/assets/javascripts/jobs/components/job/sidebar/legacy_job_sidebar_retry_button.vue b/app/assets/javascripts/jobs/components/job/sidebar/legacy_job_sidebar_retry_button.vue
new file mode 100644
index 0000000000000000000000000000000000000000..8a821b69f8ce4432ca3309bc4627067e8477f818
--- /dev/null
+++ b/app/assets/javascripts/jobs/components/job/sidebar/legacy_job_sidebar_retry_button.vue
@@ -0,0 +1,53 @@
+<script>
+import { GlButton, GlModalDirective } from '@gitlab/ui';
+import { mapGetters } from 'vuex';
+import { JOB_SIDEBAR_COPY } from '~/jobs/constants';
+
+export default {
+  name: 'LegacyJobSidebarRetryButton',
+  i18n: {
+    retryLabel: JOB_SIDEBAR_COPY.retryJobLabel,
+  },
+  components: {
+    GlButton,
+  },
+  directives: {
+    GlModal: GlModalDirective,
+  },
+  props: {
+    modalId: {
+      type: String,
+      required: true,
+    },
+    href: {
+      type: String,
+      required: true,
+    },
+  },
+  computed: {
+    ...mapGetters(['hasForwardDeploymentFailure']),
+  },
+};
+</script>
+<template>
+  <gl-button
+    v-if="hasForwardDeploymentFailure"
+    v-gl-modal="modalId"
+    :aria-label="$options.i18n.retryLabel"
+    category="primary"
+    variant="confirm"
+    icon="retry"
+    data-testid="retry-job-button"
+  />
+
+  <gl-button
+    v-else
+    :href="href"
+    :aria-label="$options.i18n.retryLabel"
+    category="primary"
+    variant="confirm"
+    icon="retry"
+    data-method="post"
+    data-testid="retry-job-link"
+  />
+</template>
diff --git a/app/assets/javascripts/jobs/components/job/sidebar/legacy_sidebar_header.vue b/app/assets/javascripts/jobs/components/job/sidebar/legacy_sidebar_header.vue
index 64b497c3550ab35724ecaea31d0b6d7bd3bfdbda..5bbb831a293822ccbed34371e835c3066be4009f 100644
--- a/app/assets/javascripts/jobs/components/job/sidebar/legacy_sidebar_header.vue
+++ b/app/assets/javascripts/jobs/components/job/sidebar/legacy_sidebar_header.vue
@@ -2,7 +2,7 @@
 import { GlButton, GlTooltipDirective } from '@gitlab/ui';
 import { mapActions } from 'vuex';
 import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate/tooltip_on_truncate.vue';
-import { JOB_SIDEBAR_COPY, forwardDeploymentFailureModalId } from '~/jobs/constants';
+import { JOB_SIDEBAR_COPY, forwardDeploymentFailureModalId, PASSED_STATUS } from '~/jobs/constants';
 import JobSidebarRetryButton from './job_sidebar_retry_button.vue';
 
 export default {
@@ -25,20 +25,15 @@ export default {
       required: true,
       default: () => ({}),
     },
-    erasePath: {
-      type: String,
-      required: false,
-      default: null,
-    },
   },
   computed: {
     retryButtonCategory() {
       return this.job.status && this.job.recoverable ? 'primary' : 'secondary';
     },
     buttonTitle() {
-      return this.job.status && this.job.status.text === 'passed'
+      return this.job.status && this.job.status.text === PASSED_STATUS
         ? this.$options.i18n.runAgainJobButtonLabel
-        : this.$options.i18n.retryJobButtonLabel;
+        : this.$options.i18n.retryJobLabel;
     },
   },
   methods: {
@@ -50,17 +45,15 @@ export default {
 <template>
   <div class="gl-py-5 gl-display-flex gl-align-items-center">
     <tooltip-on-truncate :title="job.name" truncate-target="child"
-      ><h4 class="gl-my-0 gl-mr-3 gl-text-truncate">
-        {{ job.name }}
-      </h4>
+      ><h4 class="gl-my-0 gl-mr-3 gl-text-truncate">{{ job.name }}</h4>
     </tooltip-on-truncate>
     <div class="gl-flex-grow-1 gl-flex-shrink-0 gl-text-right">
       <gl-button
-        v-if="erasePath"
+        v-if="job.erase_path"
         v-gl-tooltip.left
         :title="$options.i18n.eraseLogButtonLabel"
         :aria-label="$options.i18n.eraseLogButtonLabel"
-        :href="erasePath"
+        :href="job.erase_path"
         :data-confirm="$options.i18n.eraseLogConfirmText"
         class="gl-mr-2"
         data-testid="job-log-erase-link"
@@ -76,6 +69,7 @@ export default {
         :category="retryButtonCategory"
         :href="job.retry_path"
         :modal-id="$options.forwardDeploymentFailureModalId"
+        :is-manual-job="false"
         variant="confirm"
         data-qa-selector="retry_button"
         data-testid="retry-button"
diff --git a/app/assets/javascripts/jobs/components/job/sidebar/sidebar.vue b/app/assets/javascripts/jobs/components/job/sidebar/sidebar.vue
index aac6a0ad6d392058e01864018f27fb00d70e1f81..02c3d60557ba23a78ed4b29e906f5757490da997 100644
--- a/app/assets/javascripts/jobs/components/job/sidebar/sidebar.vue
+++ b/app/assets/javascripts/jobs/components/job/sidebar/sidebar.vue
@@ -2,13 +2,13 @@
 import { GlButton, GlIcon } from '@gitlab/ui';
 import { isEmpty } from 'lodash';
 import { mapActions, mapGetters, mapState } from 'vuex';
-import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
 import { JOB_SIDEBAR_COPY, forwardDeploymentFailureModalId } from '~/jobs/constants';
+import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
+import ArtifactsBlock from './artifacts_block.vue';
 import CommitBlock from './commit_block.vue';
 import JobsContainer from './jobs_container.vue';
 import JobRetryForwardDeploymentModal from './job_retry_forward_deployment_modal.vue';
 import JobSidebarDetailsContainer from './sidebar_job_details_container.vue';
-import ArtifactsBlock from './artifacts_block.vue';
 import LegacySidebarHeader from './legacy_sidebar_header.vue';
 import SidebarHeader from './sidebar_header.vue';
 import StagesDropdown from './stages_dropdown.vue';
@@ -41,11 +41,6 @@ export default {
       required: false,
       default: '',
     },
-    erasePath: {
-      type: String,
-      required: false,
-      default: null,
-    },
   },
   computed: {
     ...mapGetters(['hasForwardDeploymentFailure']),
@@ -89,8 +84,13 @@ export default {
   <aside class="right-sidebar build-sidebar" data-offset-top="101" data-spy="affix">
     <div class="sidebar-container">
       <div class="blocks-container">
-        <sidebar-header v-if="isGraphQL" :erase-path="erasePath" :job="job" />
-        <legacy-sidebar-header v-else :erase-path="erasePath" :job="job" />
+        <sidebar-header
+          v-if="isGraphQL"
+          :rest-job="job"
+          :job-id="job.id"
+          @updateVariables="$emit('updateVariables')"
+        />
+        <legacy-sidebar-header v-else :job="job" />
         <div
           v-if="job.terminal_path || job.new_issue_path"
           class="gl-py-5"
diff --git a/app/assets/javascripts/jobs/components/job/sidebar/sidebar_header.vue b/app/assets/javascripts/jobs/components/job/sidebar/sidebar_header.vue
index 523710598bf058ccb9f9e7f4621512c923d00d58..c124f52ae794304b74264f7d4154dccc5f6468bb 100644
--- a/app/assets/javascripts/jobs/components/job/sidebar/sidebar_header.vue
+++ b/app/assets/javascripts/jobs/components/job/sidebar/sidebar_header.vue
@@ -1,8 +1,17 @@
 <script>
 import { GlButton, GlTooltipDirective } from '@gitlab/ui';
 import { mapActions } from 'vuex';
+import { createAlert } from '~/flash';
+import { convertToGraphQLId } from '~/graphql_shared/utils';
 import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate/tooltip_on_truncate.vue';
-import { JOB_SIDEBAR_COPY, forwardDeploymentFailureModalId } from '~/jobs/constants';
+import {
+  JOB_GRAPHQL_ERRORS,
+  GRAPHQL_ID_TYPES,
+  JOB_SIDEBAR_COPY,
+  forwardDeploymentFailureModalId,
+  PASSED_STATUS,
+} from '~/jobs/constants';
+import GetJob from '../graphql/queries/get_job.query.graphql';
 import JobSidebarRetryButton from './job_sidebar_retry_button.vue';
 
 // This component is a port of ~/jobs/components/job/sidebar/legacy_sidebar_header.vue
@@ -22,21 +31,58 @@ export default {
     JobSidebarRetryButton,
     TooltipOnTruncate,
   },
-  props: {
+  inject: ['projectPath'],
+  apollo: {
     job: {
+      query: GetJob,
+      variables() {
+        return {
+          fullPath: this.projectPath,
+          id: convertToGraphQLId(GRAPHQL_ID_TYPES.commitStatus, this.jobId),
+        };
+      },
+      update(data) {
+        const { name, manualJob } = data?.project?.job || {};
+        return {
+          name,
+          manualJob,
+        };
+      },
+      error() {
+        createAlert({ message: JOB_GRAPHQL_ERRORS.jobQueryErrorText });
+      },
+    },
+  },
+  props: {
+    jobId: {
+      type: Number,
+      required: true,
+    },
+    restJob: {
       type: Object,
       required: true,
       default: () => ({}),
     },
-    erasePath: {
-      type: String,
-      required: false,
-      default: null,
-    },
+  },
+  data() {
+    return {
+      job: {},
+    };
   },
   computed: {
+    buttonTitle() {
+      return this.restJob.status?.text === PASSED_STATUS
+        ? this.$options.i18n.runAgainJobButtonLabel
+        : this.$options.i18n.retryJobLabel;
+    },
+    canShowJobRetryButton() {
+      return this.restJob.retry_path && !this.$apollo.queries.job.loading;
+    },
+    isManualJob() {
+      return this.job?.manualJob;
+    },
     retryButtonCategory() {
-      return this.job.status && this.job.recoverable ? 'primary' : 'secondary';
+      return this.restJob.status && this.restJob.recoverable ? 'primary' : 'secondary';
     },
   },
   methods: {
@@ -48,17 +94,15 @@ export default {
 <template>
   <div class="gl-py-5 gl-display-flex gl-align-items-center">
     <tooltip-on-truncate :title="job.name" truncate-target="child"
-      ><h4 class="gl-my-0 gl-mr-3 gl-text-truncate">
-        {{ job.name }}
-      </h4>
+      ><h4 class="gl-my-0 gl-mr-3 gl-text-truncate" data-testid="job-name">{{ job.name }}</h4>
     </tooltip-on-truncate>
     <div class="gl-flex-grow-1 gl-flex-shrink-0 gl-text-right">
       <gl-button
-        v-if="erasePath"
+        v-if="restJob.erase_path"
         v-gl-tooltip.left
         :title="$options.i18n.eraseLogButtonLabel"
         :aria-label="$options.i18n.eraseLogButtonLabel"
-        :href="erasePath"
+        :href="restJob.erase_path"
         :data-confirm="$options.i18n.eraseLogConfirmText"
         class="gl-mr-2"
         data-testid="job-log-erase-link"
@@ -67,23 +111,25 @@ export default {
         icon="remove"
       />
       <job-sidebar-retry-button
-        v-if="job.retry_path"
+        v-if="canShowJobRetryButton"
         v-gl-tooltip.left
-        :title="$options.i18n.retryJobButtonLabel"
-        :aria-label="$options.i18n.retryJobButtonLabel"
+        :title="buttonTitle"
+        :aria-label="buttonTitle"
+        :is-manual-job="isManualJob"
         :category="retryButtonCategory"
-        :href="job.retry_path"
+        :href="restJob.retry_path"
         :modal-id="$options.forwardDeploymentFailureModalId"
         variant="confirm"
         data-qa-selector="retry_button"
         data-testid="retry-button"
+        @updateVariablesClicked="$emit('updateVariables')"
       />
       <gl-button
-        v-if="job.cancel_path"
+        v-if="restJob.cancel_path"
         v-gl-tooltip.left
         :title="$options.i18n.cancelJobButtonLabel"
         :aria-label="$options.i18n.cancelJobButtonLabel"
-        :href="job.cancel_path"
+        :href="restJob.cancel_path"
         variant="danger"
         icon="cancel"
         data-method="post"
diff --git a/app/assets/javascripts/jobs/constants.js b/app/assets/javascripts/jobs/constants.js
index e9475994e8bec22e02ea0f6f341c587f1c26b0f8..405aea111812ab01c80c478f1a143f96e460458f 100644
--- a/app/assets/javascripts/jobs/constants.js
+++ b/app/assets/javascripts/jobs/constants.js
@@ -5,6 +5,11 @@ const moreInfo = __('More information');
 
 export const forwardDeploymentFailureModalId = 'forward-deployment-failure';
 
+export const GRAPHQL_ID_TYPES = {
+  commitStatus: 'CommitStatus',
+  ciBuild: 'Ci::Build',
+};
+
 export const JOB_SIDEBAR_COPY = {
   cancel,
   cancelJobButtonLabel: s__('Job|Cancel'),
@@ -12,10 +17,15 @@ export const JOB_SIDEBAR_COPY = {
   eraseLogButtonLabel: s__('Job|Erase job log and artifacts'),
   eraseLogConfirmText: s__('Job|Are you sure you want to erase this job log and artifacts?'),
   newIssue: __('New issue'),
-  retry: __('Retry'),
-  retryJobButtonLabel: s__('Job|Retry'),
+  retryJobLabel: s__('Job|Retry'),
   toggleSidebar: __('Toggle Sidebar'),
   runAgainJobButtonLabel: s__('Job|Run again'),
+  updateVariables: s__('Job|Update CI/CD variables'),
+};
+
+export const JOB_GRAPHQL_ERRORS = {
+  retryMutationErrorText: __('There was an error running the job. Please try again.'),
+  jobQueryErrorText: __('There was an error fetching the job.'),
 };
 
 export const JOB_RETRY_FORWARD_DEPLOYMENT_MODAL = {
@@ -31,3 +41,4 @@ export const JOB_RETRY_FORWARD_DEPLOYMENT_MODAL = {
 };
 
 export const SUCCESS_STATUS = 'SUCCESS';
+export const PASSED_STATUS = 'passed';
diff --git a/app/assets/javascripts/jobs/index.js b/app/assets/javascripts/jobs/index.js
index 9dd47f4046c72a3d45e8a2d93232862b5eb93a87..44bb1ffb1bcdc658738f237211bc4eaaa5cbed92 100644
--- a/app/assets/javascripts/jobs/index.js
+++ b/app/assets/javascripts/jobs/index.js
@@ -1,10 +1,17 @@
 import { GlToast } from '@gitlab/ui';
 import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import createDefaultClient from '~/lib/graphql';
 import JobApp from './components/job/job_app.vue';
 import createStore from './store';
 
+Vue.use(VueApollo);
 Vue.use(GlToast);
 
+const apolloProvider = new VueApollo({
+  defaultClient: createDefaultClient(),
+});
+
 const initializeJobPage = (element) => {
   const store = createStore();
 
@@ -26,11 +33,13 @@ const initializeJobPage = (element) => {
 
   return new Vue({
     el: element,
+    apolloProvider,
     store,
     components: {
       JobApp,
     },
     provide: {
+      projectPath,
       retryOutdatedJobDocsUrl,
     },
     render(createElement) {
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 5f704c92047b4bc87faf0c5ea7bbda23a6703fa7..5d9bb3d40aaa3634970de9bb3ac483df89bca91f 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -8499,6 +8499,9 @@ msgstr ""
 msgid "CiVariables|Cannot use Masked Variable with current value"
 msgstr ""
 
+msgid "CiVariables|Clear inputs"
+msgstr ""
+
 msgid "CiVariables|Environments"
 msgstr ""
 
@@ -8526,6 +8529,9 @@ msgstr ""
 msgid "CiVariables|Remove variable row"
 msgstr ""
 
+msgid "CiVariables|Run job again"
+msgstr ""
+
 msgid "CiVariables|Scope"
 msgstr ""
 
@@ -23615,6 +23621,9 @@ msgstr ""
 msgid "Job|This job is stuck because you don't have any active runners that can run this job."
 msgstr ""
 
+msgid "Job|Update CI/CD variables"
+msgstr ""
+
 msgid "Job|Waiting for resource"
 msgstr ""
 
@@ -35098,6 +35107,9 @@ msgstr ""
 msgid "Run housekeeping tasks to automatically optimize Git repositories. Disabling this option will cause performance to degenerate over time."
 msgstr ""
 
+msgid "Run manual job again"
+msgstr ""
+
 msgid "Run manual or delayed jobs"
 msgstr ""
 
@@ -41458,6 +41470,9 @@ msgstr ""
 msgid "There was an error fetching the environments information."
 msgstr ""
 
+msgid "There was an error fetching the job."
+msgstr ""
+
 msgid "There was an error fetching the jobs for your project."
 msgstr ""
 
@@ -41497,6 +41512,9 @@ msgstr ""
 msgid "There was an error retrieving the Jira users."
 msgstr ""
 
+msgid "There was an error running the job. Please try again."
+msgstr ""
+
 msgid "There was an error saving your changes."
 msgstr ""
 
diff --git a/spec/features/projects/jobs_spec.rb b/spec/features/projects/jobs_spec.rb
index 96a8168e708e34c3b35e1d6584e81e59831b6529..4f7b7b5b98f92b909e55d40405401fcd19457303 100644
--- a/spec/features/projects/jobs_spec.rb
+++ b/spec/features/projects/jobs_spec.rb
@@ -215,10 +215,6 @@
         visit project_job_path(project, job)
       end
 
-      it 'shows retry button' do
-        expect(page).to have_link('Retry')
-      end
-
       context 'if job passed' do
         it 'does not show New issue button' do
           expect(page).not_to have_link('New issue')
diff --git a/spec/frontend/jobs/components/job/empty_state_spec.js b/spec/frontend/jobs/components/job/empty_state_spec.js
index 299b607ad7822b16c4620202b97203a4746882d4..e1b9aa743e0eb8b2f39424bb888faae9a1cb15b6 100644
--- a/spec/frontend/jobs/components/job/empty_state_spec.js
+++ b/spec/frontend/jobs/components/job/empty_state_spec.js
@@ -1,5 +1,6 @@
 import { mount } from '@vue/test-utils';
 import EmptyState from '~/jobs/components/job/empty_state.vue';
+import { mockId } from './mock_data';
 
 describe('Empty State', () => {
   let wrapper;
@@ -7,6 +8,7 @@ describe('Empty State', () => {
   const defaultProps = {
     illustrationPath: 'illustrations/pending_job_empty.svg',
     illustrationSizeClass: 'svg-430',
+    jobId: mockId,
     title: 'This job has not started yet',
     playable: false,
   };
diff --git a/spec/frontend/jobs/components/job/job_sidebar_retry_button_spec.js b/spec/frontend/jobs/components/job/job_sidebar_retry_button_spec.js
index 18d5f35bde47fd2c59ea293753e90035a3b917b2..b04a5e07ea5ae2d4f903bc08bb3526e6b00e2a01 100644
--- a/spec/frontend/jobs/components/job/job_sidebar_retry_button_spec.js
+++ b/spec/frontend/jobs/components/job/job_sidebar_retry_button_spec.js
@@ -16,6 +16,7 @@ describe('Job Sidebar Retry Button', () => {
     wrapper = shallowMountExtended(JobsSidebarRetryButton, {
       propsData: {
         href: job.retry_path,
+        isManualJob: true,
         modalId: 'modal-id',
         ...props,
       },
diff --git a/spec/frontend/jobs/components/job/legacy_sidebar_header_spec.js b/spec/frontend/jobs/components/job/legacy_sidebar_header_spec.js
index 95eb10118ee65c15ef8be589791c86e25fbdf4c9..8fbb418232b82be80d02a9e7a780c034a36b566e 100644
--- a/spec/frontend/jobs/components/job/legacy_sidebar_header_spec.js
+++ b/spec/frontend/jobs/components/job/legacy_sidebar_header_spec.js
@@ -32,12 +32,8 @@ describe('Legacy Sidebar Header', () => {
   });
 
   describe('when job log is erasable', () => {
-    const path = '/root/ci-project/-/jobs/1447/erase';
-
     beforeEach(() => {
-      createWrapper({
-        erasePath: path,
-      });
+      createWrapper();
     });
 
     it('renders erase job link', () => {
@@ -45,13 +41,13 @@ describe('Legacy Sidebar Header', () => {
     });
 
     it('erase job link has correct path', () => {
-      expect(findEraseLink().attributes('href')).toBe(path);
+      expect(findEraseLink().attributes('href')).toBe(job.erase_path);
     });
   });
 
   describe('when job log is not erasable', () => {
     beforeEach(() => {
-      createWrapper();
+      createWrapper({ job: { ...job, erase_path: null } });
     });
 
     it('does not render erase button', () => {
@@ -77,8 +73,7 @@ describe('Legacy Sidebar Header', () => {
 
   describe('when there is no retry path', () => {
     it('should not render a retry button', async () => {
-      const copy = { ...job, retry_path: null };
-      createWrapper({ job: copy });
+      createWrapper({ job: { ...job, retry_path: null } });
 
       expect(findRetryButton().exists()).toBe(false);
     });
@@ -100,9 +95,7 @@ describe('Legacy Sidebar Header', () => {
       it('should have a different label when the job status is failed', () => {
         createWrapper({ job: { ...job, status: failedJobStatus } });
 
-        expect(findRetryButton().attributes('title')).toBe(
-          LegacySidebarHeader.i18n.retryJobButtonLabel,
-        );
+        expect(findRetryButton().attributes('title')).toBe(LegacySidebarHeader.i18n.retryJobLabel);
       });
     });
   });
diff --git a/spec/frontend/jobs/components/job/manual_variables_form_spec.js b/spec/frontend/jobs/components/job/manual_variables_form_spec.js
index 5806f9f75f9c815eb5ed5d13a236dc0b355aede0..4384b2f4d7f77b0bebcfd1dfad0e0ddd504b9f2f 100644
--- a/spec/frontend/jobs/components/job/manual_variables_form_spec.js
+++ b/spec/frontend/jobs/components/job/manual_variables_form_spec.js
@@ -1,46 +1,70 @@
 import { GlSprintf, GlLink } from '@gitlab/ui';
-import { mount } from '@vue/test-utils';
-import Vue, { nextTick } from 'vue';
-import Vuex from 'vuex';
-import { extendedWrapper } from 'helpers/vue_test_utils_helper';
+import { createLocalVue } from '@vue/test-utils';
+import VueApollo from 'vue-apollo';
+import { nextTick } from 'vue';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import { convertToGraphQLId } from '~/graphql_shared/utils';
+import { GRAPHQL_ID_TYPES } from '~/jobs/constants';
+import waitForPromises from 'helpers/wait_for_promises';
 import ManualVariablesForm from '~/jobs/components/job/manual_variables_form.vue';
-
-Vue.use(Vuex);
+import getJobQuery from '~/jobs/components/job/graphql/queries/get_job.query.graphql';
+import retryJobMutation from '~/jobs/components/job/graphql/mutations/job_retry_with_variables.mutation.graphql';
+import {
+  mockFullPath,
+  mockId,
+  mockJobResponse,
+  mockJobWithVariablesResponse,
+  mockJobMutationData,
+} from './mock_data';
+
+const localVue = createLocalVue();
+localVue.use(VueApollo);
+
+const defaultProvide = {
+  projectPath: mockFullPath,
+};
 
 describe('Manual Variables Form', () => {
   let wrapper;
-  let store;
-
-  const requiredProps = {
-    action: {
-      path: '/play',
-      method: 'post',
-      button_title: 'Trigger this manual action',
-    },
+  let mockApollo;
+  let getJobQueryResponse;
+
+  const createComponent = ({ options = {}, props = {} } = {}) => {
+    wrapper = mountExtended(ManualVariablesForm, {
+      propsData: {
+        ...props,
+        jobId: mockId,
+      },
+      provide: {
+        ...defaultProvide,
+      },
+      ...options,
+    });
   };
 
-  const createComponent = (props = {}) => {
-    store = new Vuex.Store({
-      actions: {
-        triggerManualJob: jest.fn(),
-      },
+  const createComponentWithApollo = async ({ props = {} } = {}) => {
+    const requestHandlers = [[getJobQuery, getJobQueryResponse]];
+
+    mockApollo = createMockApollo(requestHandlers);
+
+    const options = {
+      localVue,
+      apolloProvider: mockApollo,
+    };
+
+    createComponent({
+      props,
+      options,
     });
 
-    wrapper = extendedWrapper(
-      mount(ManualVariablesForm, {
-        propsData: { ...requiredProps, ...props },
-        store,
-        stubs: {
-          GlSprintf,
-        },
-      }),
-    );
+    return waitForPromises();
   };
 
   const findHelpText = () => wrapper.findComponent(GlSprintf);
   const findHelpLink = () => wrapper.findComponent(GlLink);
-
-  const findTriggerBtn = () => wrapper.findByTestId('trigger-manual-job-btn');
+  const findCancelBtn = () => wrapper.findByTestId('cancel-btn');
+  const findRerunBtn = () => wrapper.findByTestId('run-manual-job-btn');
   const findDeleteVarBtn = () => wrapper.findByTestId('delete-variable-btn');
   const findAllDeleteVarBtns = () => wrapper.findAllByTestId('delete-variable-btn');
   const findDeleteVarBtnPlaceholder = () => wrapper.findByTestId('delete-variable-btn-placeholder');
@@ -62,95 +86,134 @@ describe('Manual Variables Form', () => {
   };
 
   beforeEach(() => {
-    createComponent();
+    getJobQueryResponse = jest.fn();
   });
 
   afterEach(() => {
     wrapper.destroy();
   });
 
-  it('creates a new variable when user enters a new key value', async () => {
-    expect(findAllVariables()).toHaveLength(1);
+  describe('when page renders', () => {
+    beforeEach(async () => {
+      getJobQueryResponse.mockResolvedValue(mockJobResponse);
+      await createComponentWithApollo();
+    });
+
+    it('renders help text with provided link', () => {
+      expect(findHelpText().exists()).toBe(true);
+      expect(findHelpLink().attributes('href')).toBe(
+        '/help/ci/variables/index#add-a-cicd-variable-to-a-project',
+      );
+    });
+
+    it('renders buttons', () => {
+      expect(findCancelBtn().exists()).toBe(true);
+      expect(findRerunBtn().exists()).toBe(true);
+    });
+  });
+
+  describe('when job has variables', () => {
+    beforeEach(async () => {
+      getJobQueryResponse.mockResolvedValue(mockJobWithVariablesResponse);
+      await createComponentWithApollo();
+    });
 
-    await setCiVariableKey();
+    it('sets manual job variables', () => {
+      const queryKey = mockJobWithVariablesResponse.data.project.job.manualVariables.nodes[0].key;
+      const queryValue =
+        mockJobWithVariablesResponse.data.project.job.manualVariables.nodes[0].value;
 
-    expect(findAllVariables()).toHaveLength(2);
+      expect(findCiVariableKey().element.value).toBe(queryKey);
+      expect(findCiVariableValue().element.value).toBe(queryValue);
+    });
   });
 
-  it('does not create extra empty variables', async () => {
-    expect(findAllVariables()).toHaveLength(1);
+  describe('when mutation fires', () => {
+    beforeEach(async () => {
+      await createComponentWithApollo();
+      jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue(mockJobMutationData);
+    });
 
-    await setCiVariableKey();
+    it('passes variables in correct format', async () => {
+      await setCiVariableKey();
 
-    expect(findAllVariables()).toHaveLength(2);
+      await findCiVariableValue().setValue('new value');
 
-    await setCiVariableKey();
+      await findRerunBtn().vm.$emit('click');
 
-    expect(findAllVariables()).toHaveLength(2);
+      expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledTimes(1);
+      expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith({
+        mutation: retryJobMutation,
+        variables: {
+          id: convertToGraphQLId(GRAPHQL_ID_TYPES.ciBuild, mockId),
+          variables: [
+            {
+              key: 'new key',
+              value: 'new value',
+            },
+          ],
+        },
+      });
+    });
   });
 
-  it('removes the correct variable row', async () => {
-    const variableKeyNameOne = 'key-one';
-    const variableKeyNameThree = 'key-three';
+  describe('updating variables in UI', () => {
+    beforeEach(async () => {
+      getJobQueryResponse.mockResolvedValue(mockJobResponse);
+      await createComponentWithApollo();
+    });
 
-    await setCiVariableKeyByPosition(0, variableKeyNameOne);
+    it('creates a new variable when user enters a new key value', async () => {
+      expect(findAllVariables()).toHaveLength(1);
 
-    await setCiVariableKeyByPosition(1, 'key-two');
+      await setCiVariableKey();
 
-    await setCiVariableKeyByPosition(2, variableKeyNameThree);
+      expect(findAllVariables()).toHaveLength(2);
+    });
 
-    expect(findAllVariables()).toHaveLength(4);
+    it('does not create extra empty variables', async () => {
+      expect(findAllVariables()).toHaveLength(1);
 
-    await findAllDeleteVarBtns().at(1).trigger('click');
+      await setCiVariableKey();
 
-    expect(findAllVariables()).toHaveLength(3);
+      expect(findAllVariables()).toHaveLength(2);
 
-    expect(findAllCiVariableKeys().at(0).element.value).toBe(variableKeyNameOne);
-    expect(findAllCiVariableKeys().at(1).element.value).toBe(variableKeyNameThree);
-    expect(findAllCiVariableKeys().at(2).element.value).toBe('');
-  });
+      await setCiVariableKey();
 
-  it('trigger button is disabled after trigger action', async () => {
-    expect(findTriggerBtn().props('disabled')).toBe(false);
+      expect(findAllVariables()).toHaveLength(2);
+    });
 
-    await findTriggerBtn().trigger('click');
+    it('removes the correct variable row', async () => {
+      const variableKeyNameOne = 'key-one';
+      const variableKeyNameThree = 'key-three';
 
-    expect(findTriggerBtn().props('disabled')).toBe(true);
-  });
+      await setCiVariableKeyByPosition(0, variableKeyNameOne);
 
-  it('delete variable button should only show when there is more than one variable', async () => {
-    expect(findDeleteVarBtn().exists()).toBe(false);
+      await setCiVariableKeyByPosition(1, 'key-two');
 
-    await setCiVariableKey();
+      await setCiVariableKeyByPosition(2, variableKeyNameThree);
 
-    expect(findDeleteVarBtn().exists()).toBe(true);
-  });
+      expect(findAllVariables()).toHaveLength(4);
 
-  it('delete variable button placeholder should only exist when a user cannot remove', async () => {
-    expect(findDeleteVarBtnPlaceholder().exists()).toBe(true);
-  });
+      await findAllDeleteVarBtns().at(1).trigger('click');
 
-  it('renders help text with provided link', () => {
-    expect(findHelpText().exists()).toBe(true);
-    expect(findHelpLink().attributes('href')).toBe(
-      '/help/ci/variables/index#add-a-cicd-variable-to-a-project',
-    );
-  });
+      expect(findAllVariables()).toHaveLength(3);
 
-  it('passes variables in correct format', async () => {
-    jest.spyOn(store, 'dispatch');
+      expect(findAllCiVariableKeys().at(0).element.value).toBe(variableKeyNameOne);
+      expect(findAllCiVariableKeys().at(1).element.value).toBe(variableKeyNameThree);
+      expect(findAllCiVariableKeys().at(2).element.value).toBe('');
+    });
 
-    await setCiVariableKey();
+    it('delete variable button should only show when there is more than one variable', async () => {
+      expect(findDeleteVarBtn().exists()).toBe(false);
 
-    await findCiVariableValue().setValue('new value');
+      await setCiVariableKey();
 
-    await findTriggerBtn().trigger('click');
+      expect(findDeleteVarBtn().exists()).toBe(true);
+    });
 
-    expect(store.dispatch).toHaveBeenCalledWith('triggerManualJob', [
-      {
-        key: 'new key',
-        secret_value: 'new value',
-      },
-    ]);
+    it('delete variable button placeholder should only exist when a user cannot remove', async () => {
+      expect(findDeleteVarBtnPlaceholder().exists()).toBe(true);
+    });
   });
 });
diff --git a/spec/frontend/jobs/components/job/mock_data.js b/spec/frontend/jobs/components/job/mock_data.js
new file mode 100644
index 0000000000000000000000000000000000000000..9596e859475415fe933143475652adabb14a5948
--- /dev/null
+++ b/spec/frontend/jobs/components/job/mock_data.js
@@ -0,0 +1,76 @@
+export const mockFullPath = 'Commit451/lab-coat';
+export const mockId = 401;
+
+export const mockJobResponse = {
+  data: {
+    project: {
+      id: 'gid://gitlab/Project/4',
+      job: {
+        id: 'gid://gitlab/Ci::Build/401',
+        manualJob: true,
+        manualVariables: {
+          nodes: [],
+          __typename: 'CiManualVariableConnection',
+        },
+        name: 'manual_job',
+        retryable: true,
+        status: 'SUCCESS',
+        __typename: 'CiJob',
+      },
+      __typename: 'Project',
+    },
+  },
+};
+
+export const mockJobWithVariablesResponse = {
+  data: {
+    project: {
+      id: 'gid://gitlab/Project/4',
+      job: {
+        id: 'gid://gitlab/Ci::Build/401',
+        manualJob: true,
+        manualVariables: {
+          nodes: [
+            {
+              id: 'gid://gitlab/Ci::JobVariable/150',
+              key: 'new key',
+              value: 'new value',
+              __typename: 'CiManualVariable',
+            },
+          ],
+          __typename: 'CiManualVariableConnection',
+        },
+        name: 'manual_job',
+        retryable: true,
+        status: 'SUCCESS',
+        __typename: 'CiJob',
+      },
+      __typename: 'Project',
+    },
+  },
+};
+
+export const mockJobMutationData = {
+  data: {
+    jobRetry: {
+      job: {
+        id: 'gid://gitlab/Ci::Build/401',
+        manualVariables: {
+          nodes: [
+            {
+              id: 'gid://gitlab/Ci::JobVariable/151',
+              key: 'new key',
+              value: 'new value',
+              __typename: 'CiManualVariable',
+            },
+          ],
+          __typename: 'CiManualVariableConnection',
+        },
+        webPath: '/Commit451/lab-coat/-/jobs/401',
+        __typename: 'CiJob',
+      },
+      errors: [],
+      __typename: 'JobRetryPayload',
+    },
+  },
+};
diff --git a/spec/frontend/jobs/components/job/sidebar_header_spec.js b/spec/frontend/jobs/components/job/sidebar_header_spec.js
index cb32ca9d3dcfc7b0b0a0e68be4e50f521999f710..422e2f6207cd2e95f138654f1e0153cda889c14a 100644
--- a/spec/frontend/jobs/components/job/sidebar_header_spec.js
+++ b/spec/frontend/jobs/components/job/sidebar_header_spec.js
@@ -1,91 +1,101 @@
-import { shallowMount } from '@vue/test-utils';
-import { extendedWrapper } from 'helpers/vue_test_utils_helper';
+import { createLocalVue } from '@vue/test-utils';
+import VueApollo from 'vue-apollo';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import SidebarHeader from '~/jobs/components/job/sidebar/sidebar_header.vue';
 import JobRetryButton from '~/jobs/components/job/sidebar/job_sidebar_retry_button.vue';
-import LegacySidebarHeader from '~/jobs/components/job/sidebar/legacy_sidebar_header.vue';
-import createStore from '~/jobs/store';
-import job from '../../mock_data';
+import getJobQuery from '~/jobs/components/job/graphql/queries/get_job.query.graphql';
+import { mockFullPath, mockId, mockJobResponse } from './mock_data';
 
-describe('Legacy Sidebar Header', () => {
-  let store;
-  let wrapper;
+const localVue = createLocalVue();
+localVue.use(VueApollo);
 
-  const findCancelButton = () => wrapper.findByTestId('cancel-button');
-  const findRetryButton = () => wrapper.findComponent(JobRetryButton);
-  const findEraseLink = () => wrapper.findByTestId('job-log-erase-link');
-
-  const createWrapper = (props) => {
-    store = createStore();
-
-    wrapper = extendedWrapper(
-      shallowMount(LegacySidebarHeader, {
-        propsData: {
-          job,
-          ...props,
-        },
-        store,
-      }),
-    );
+const defaultProvide = {
+  projectPath: mockFullPath,
+};
+
+describe('Sidebar Header', () => {
+  let wrapper;
+  let mockApollo;
+  let getJobQueryResponse;
+
+  const createComponent = ({ options = {}, props = {}, restJob = {} } = {}) => {
+    wrapper = shallowMountExtended(SidebarHeader, {
+      propsData: {
+        ...props,
+        jobId: mockId,
+        restJob,
+      },
+      provide: {
+        ...defaultProvide,
+      },
+      ...options,
+    });
   };
 
-  afterEach(() => {
-    wrapper.destroy();
-  });
+  const createComponentWithApollo = async ({ props = {}, restJob = {} } = {}) => {
+    const requestHandlers = [[getJobQuery, getJobQueryResponse]];
 
-  describe('when job log is erasable', () => {
-    const path = '/root/ci-project/-/jobs/1447/erase';
+    mockApollo = createMockApollo(requestHandlers);
 
-    beforeEach(() => {
-      createWrapper({
-        erasePath: path,
-      });
-    });
+    const options = {
+      localVue,
+      apolloProvider: mockApollo,
+    };
 
-    it('renders erase job link', () => {
-      expect(findEraseLink().exists()).toBe(true);
+    createComponent({
+      props,
+      restJob,
+      options,
     });
 
-    it('erase job link has correct path', () => {
-      expect(findEraseLink().attributes('href')).toBe(path);
-    });
-  });
+    return waitForPromises();
+  };
 
-  describe('when job log is not erasable', () => {
-    beforeEach(() => {
-      createWrapper();
-    });
+  const findCancelButton = () => wrapper.findByTestId('cancel-button');
+  const findEraseButton = () => wrapper.findByTestId('job-log-erase-link');
+  const findJobName = () => wrapper.findByTestId('job-name');
+  const findRetryButton = () => wrapper.findComponent(JobRetryButton);
 
-    it('does not render erase button', () => {
-      expect(findEraseLink().exists()).toBe(false);
-    });
+  beforeEach(async () => {
+    getJobQueryResponse = jest.fn();
   });
 
-  describe('when the job is retryable', () => {
-    beforeEach(() => {
-      createWrapper();
-    });
+  afterEach(() => {
+    wrapper.destroy();
+  });
 
-    it('should render the retry button', () => {
-      expect(findRetryButton().props('href')).toBe(job.retry_path);
+  describe('when rendering contents', () => {
+    beforeEach(async () => {
+      getJobQueryResponse.mockResolvedValue(mockJobResponse);
     });
-  });
 
-  describe('when there is no retry path', () => {
-    it('should not render a retry button', async () => {
-      const copy = { ...job, retry_path: null };
-      createWrapper({ job: copy });
+    it('renders the correct job name', async () => {
+      await createComponentWithApollo();
+      expect(findJobName().text()).toBe(mockJobResponse.data.project.job.name);
+    });
 
+    it('does not render buttons with no paths', async () => {
+      await createComponentWithApollo();
+      expect(findCancelButton().exists()).toBe(false);
+      expect(findEraseButton().exists()).toBe(false);
       expect(findRetryButton().exists()).toBe(false);
     });
-  });
 
-  describe('when the job is cancelable', () => {
-    beforeEach(() => {
-      createWrapper();
+    it('renders a retry button with a path', async () => {
+      await createComponentWithApollo({ restJob: { retry_path: 'retry/path' } });
+      expect(findRetryButton().exists()).toBe(true);
+    });
+
+    it('renders a cancel button with a path', async () => {
+      await createComponentWithApollo({ restJob: { cancel_path: 'cancel/path' } });
+      expect(findCancelButton().exists()).toBe(true);
     });
 
-    it('should render link to cancel job', () => {
-      expect(findCancelButton().props('icon')).toBe('cancel');
-      expect(findCancelButton().attributes('href')).toBe(job.cancel_path);
+    it('renders an erase button with a path', async () => {
+      await createComponentWithApollo({ restJob: { erase_path: 'erase/path' } });
+      expect(findEraseButton().exists()).toBe(true);
     });
   });
 });