diff --git a/app/controllers/projects/static_site_editor_controller.rb b/app/controllers/projects/static_site_editor_controller.rb
index 0d9a6f568a1c8e5a25c70881d85a5d6d88305aa4..fed6307514e85ebc48ffd3deab09359b518580ce 100644
--- a/app/controllers/projects/static_site_editor_controller.rb
+++ b/app/controllers/projects/static_site_editor_controller.rb
@@ -3,6 +3,7 @@
 class Projects::StaticSiteEditorController < Projects::ApplicationController
   include ExtractsPath
   include CreatesCommit
+  include BlobHelper
 
   layout 'fullscreen'
 
@@ -24,28 +25,7 @@ def index
   end
 
   def show
-    service_response = ::StaticSiteEditor::ConfigService.new(
-      container: project,
-      current_user: current_user,
-      params: {
-        ref: @ref,
-        path: @path,
-        return_url: params[:return_url]
-      }
-    ).execute
-
-    if service_response.success?
-      Gitlab::UsageDataCounters::StaticSiteEditorCounter.increment_views_count
-
-      @data = serialize_necessary_payload_values_to_json(service_response.payload)
-    else
-      # TODO: For now, if the service returns any error, the user is redirected
-      #       to the root project page with the error message displayed as an alert.
-      #       See https://gitlab.com/gitlab-org/gitlab/-/issues/213285#note_414808004
-      #       for discussion of plans to handle this via a page owned by the Static Site Editor.
-      flash[:alert] = service_response.message
-      redirect_to project_path(project)
-    end
+    redirect_to ide_edit_path(project, @ref, @path)
   end
 
   private
diff --git a/doc/administration/gitaly/configure_gitaly.md b/doc/administration/gitaly/configure_gitaly.md
index 0fb285c50d61bced07401806174673c30599ac86..88efd1885db460146b7e39e8350161013ebe089a 100644
--- a/doc/administration/gitaly/configure_gitaly.md
+++ b/doc/administration/gitaly/configure_gitaly.md
@@ -300,7 +300,7 @@ disable enforcement. For more information, see the documentation on configuring
    ```toml
    listen_addr = '0.0.0.0:8075'
 
-   runtime_dir = '/var/opt/gitlab/gitaly'
+   internal_socket_dir = '/var/opt/gitlab/gitaly'
 
    [logging]
    format = 'json'
@@ -308,9 +308,6 @@ disable enforcement. For more information, see the documentation on configuring
    dir = '/var/log/gitaly'
    ```
 
-    For GitLab 14.9 and earlier, set `internal_socket_dir = '/var/opt/gitlab/gitaly'` instead
-    of `runtime_dir`.
-
 1. Append the following to `/home/git/gitaly/config.toml` for each respective Gitaly server:
 
    On `gitaly1.internal`:
diff --git a/jest.config.base.js b/jest.config.base.js
index 0eab5caffb003673960d90703b93c53e829487bb..59c127fcb8a4d86f06f77680039d687392a687c1 100644
--- a/jest.config.base.js
+++ b/jest.config.base.js
@@ -109,7 +109,7 @@ module.exports = (path, options = {}) => {
   return {
     clearMocks: true,
     testMatch,
-    moduleFileExtensions: ['js', 'json', 'vue', 'gql', 'graphql'],
+    moduleFileExtensions: ['js', 'json', 'vue', 'gql', 'graphql', 'yaml'],
     moduleNameMapper,
     collectCoverageFrom,
     coverageDirectory: coverageDirectory(),
@@ -127,6 +127,7 @@ module.exports = (path, options = {}) => {
       '^.+_worker\\.js$': './spec/frontend/__helpers__/web_worker_transformer.js',
       '^.+\\.js$': 'babel-jest',
       '^.+\\.vue$': 'vue-jest',
+      '^.+\\.yml$': './spec/frontend/__helpers__/yaml_transformer.js',
       '^.+\\.(md|zip|png)$': 'jest-raw-loader',
     },
     transformIgnorePatterns: [
diff --git a/lib/gitlab/setup_helper.rb b/lib/gitlab/setup_helper.rb
index a498e329c3fdf804f4a02fc81cf894add9e4c226..1389939f28babda619a8fc7258c538892b077262 100644
--- a/lib/gitlab/setup_helper.rb
+++ b/lib/gitlab/setup_helper.rb
@@ -124,9 +124,9 @@ def configuration_toml(gitaly_dir, storage_paths, options, gitaly_ruby: true)
 
           config[:storage] = storages
 
-          runtime_dir = options[:runtime_dir] || File.join(gitaly_dir, 'run')
-          FileUtils.mkdir(runtime_dir) unless File.exist?(runtime_dir)
-          config[:runtime_dir] = runtime_dir
+          internal_socket_dir = options[:internal_socket_dir] || File.join(gitaly_dir, 'internal_sockets')
+          FileUtils.mkdir(internal_socket_dir) unless File.exist?(internal_socket_dir)
+          config[:internal_socket_dir] = internal_socket_dir
 
           config[:'gitaly-ruby'] = { dir: File.join(gitaly_dir, 'ruby') } if gitaly_ruby
           config[:'gitlab-shell'] = { dir: Gitlab.config.gitlab_shell.path }
diff --git a/package.json b/package.json
index d7908b33ec9a957eaba8c4bc34eecd3810febc5c..fc8d43d6a1f62531ebb073c477f315cfc34593cc 100644
--- a/package.json
+++ b/package.json
@@ -206,6 +206,8 @@
     "@types/jest": "^26.0.24",
     "@vue/test-utils": "1.3.0",
     "acorn": "^6.3.0",
+    "ajv": "^8.10.0",
+    "ajv-formats": "^2.1.1",
     "axios-mock-adapter": "^1.15.0",
     "babel-jest": "^26.5.2",
     "babel-plugin-dynamic-import-node": "^2.3.3",
diff --git a/qa/qa/tools/test_resources_handler.rb b/qa/qa/tools/test_resources_handler.rb
index 476f87fff6bf154456671bd0c5ec204a01dfb904..d90e11a168192170c1be3d4d37595477a3bee5f7 100644
--- a/qa/qa/tools/test_resources_handler.rb
+++ b/qa/qa/tools/test_resources_handler.rb
@@ -81,6 +81,7 @@ def download(ci_project_name)
 
         return puts "\nNothing to download!" if files_list.empty?
 
+        FileUtils.mkdir_p('tmp/')
         files_list.each do |file_name|
           local_path = "tmp/#{file_name.split('/').last}"
           Runtime::Logger.info("Downloading #{file_name} to #{local_path}")
diff --git a/spec/controllers/projects/static_site_editor_controller_spec.rb b/spec/controllers/projects/static_site_editor_controller_spec.rb
index 26161b5fb5cbe6db8a29a5491446c480341c8622..e1f25589eeb9f6643e5b4e822f7dc9000d627d81 100644
--- a/spec/controllers/projects/static_site_editor_controller_spec.rb
+++ b/spec/controllers/projects/static_site_editor_controller_spec.rb
@@ -76,12 +76,11 @@
           get :show, params: default_params
         end
 
-        it 'increases the views counter' do
-          expect(Gitlab::UsageDataCounters::StaticSiteEditorCounter).to have_received(:increment_views_count)
-        end
+        it 'redirects to the Web IDE' do
+          get :show, params: default_params
 
-        it 'renders the edit page' do
-          expect(response).to render_template(:show)
+          expected_path_regex = %r[-/ide/project/#{project.full_path}/edit/master/-/README.md]
+          expect(response).to redirect_to(expected_path_regex)
         end
 
         it 'assigns ref and path variables' do
@@ -96,62 +95,6 @@
             expect(response).to have_gitlab_http_status(:not_found)
           end
         end
-
-        context 'when invalid config file' do
-          let(:service_response) { ServiceResponse.error(message: 'invalid') }
-
-          it 'redirects to project page and flashes error message' do
-            expect(response).to redirect_to(project_path(project))
-            expect(controller).to set_flash[:alert].to('invalid')
-          end
-        end
-
-        context 'with a service response payload containing multiple data types' do
-          let(:data) do
-            {
-              a_string: 'string',
-              an_array: [
-                {
-                  foo: 'bar'
-                }
-              ],
-              an_integer: 123,
-              a_hash: {
-                a_deeper_hash: {
-                  foo: 'bar'
-                }
-              },
-              a_boolean: true,
-              a_nil: nil
-            }
-          end
-
-          let(:assigns_data) { assigns(:data) }
-
-          it 'leaves data values which are strings as strings' do
-            expect(assigns_data[:a_string]).to eq('string')
-          end
-
-          it 'leaves data values which are integers as integers' do
-            expect(assigns_data[:an_integer]).to eq(123)
-          end
-
-          it 'serializes data values which are booleans to JSON' do
-            expect(assigns_data[:a_boolean]).to eq('true')
-          end
-
-          it 'serializes data values which are arrays to JSON' do
-            expect(assigns_data[:an_array]).to eq('[{"foo":"bar"}]')
-          end
-
-          it 'serializes data values which are hashes to JSON' do
-            expect(assigns_data[:a_hash]).to eq('{"a_deeper_hash":{"foo":"bar"}}')
-          end
-
-          it 'serializes data values which are nil to an empty string' do
-            expect(assigns_data[:a_nil]).to eq('')
-          end
-        end
       end
     end
   end
diff --git a/spec/features/static_site_editor_spec.rb b/spec/features/static_site_editor_spec.rb
deleted file mode 100644
index 98313905a334ba003ce0c7d8430a138241f51a5d..0000000000000000000000000000000000000000
--- a/spec/features/static_site_editor_spec.rb
+++ /dev/null
@@ -1,113 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe 'Static Site Editor' do
-  include ContentSecurityPolicyHelpers
-
-  let_it_be(:user) { create(:user) }
-  let_it_be(:project) { create(:project, :public, :repository) }
-
-  let(:sse_path) { project_show_sse_path(project, 'master/README.md') }
-
-  before_all do
-    project.add_developer(user)
-  end
-
-  before do
-    sign_in(user)
-  end
-
-  context "when no config file is present" do
-    before do
-      visit sse_path
-    end
-
-    it 'renders SSE page with all generated config values and default config file values' do
-      node = page.find('#static-site-editor')
-
-      # assert generated config values are present
-      expect(node['data-base-url']).to eq("/#{project.full_path}/-/sse/master%2FREADME.md")
-      expect(node['data-branch']).to eq('master')
-      expect(node['data-commit-id']).to match(/\A[0-9a-f]{40}\z/)
-      expect(node['data-is-supported-content']).to eq('true')
-      expect(node['data-merge-requests-illustration-path'])
-        .to match(%r{/assets/illustrations/merge_requests-.*\.svg})
-      expect(node['data-namespace']).to eq(project.namespace.full_path)
-      expect(node['data-project']).to eq(project.path)
-      expect(node['data-project-id']).to eq(project.id.to_s)
-
-      # assert default config file values are present
-      expect(node['data-image-upload-path']).to eq('source/images')
-      expect(node['data-mounts']).to eq('[{"source":"source","target":""}]')
-      expect(node['data-static-site-generator']).to eq('middleman')
-    end
-  end
-
-  context "when a config file is present" do
-    let(:config_file_yml) do
-      <<~YAML
-        image_upload_path: custom-image-upload-path
-        mounts:
-          - source: source1
-            target: ""
-          - source: source2
-            target: target2
-        static_site_generator: middleman
-      YAML
-    end
-
-    before do
-      allow_next_instance_of(Repository) do |repository|
-        allow(repository).to receive(:blob_data_at).and_return(config_file_yml)
-      end
-
-      visit sse_path
-    end
-
-    it 'renders Static Site Editor page values read from config file' do
-      node = page.find('#static-site-editor')
-
-      # assert user-specified config file values are present
-      expected_mounts = '[{"source":"source1","target":""},{"source":"source2","target":"target2"}]'
-      expect(node['data-image-upload-path']).to eq('custom-image-upload-path')
-      expect(node['data-mounts']).to eq(expected_mounts)
-      expect(node['data-static-site-generator']).to eq('middleman')
-    end
-  end
-
-  describe 'Static Site Editor Content Security Policy' do
-    subject { response_headers['Content-Security-Policy'] }
-
-    context 'when no global CSP config exists' do
-      before do
-        setup_csp_for_controller(Projects::StaticSiteEditorController)
-      end
-
-      it 'does not add CSP directives' do
-        visit sse_path
-
-        is_expected.to be_blank
-      end
-    end
-
-    context 'when a global CSP config exists' do
-      let_it_be(:cdn_url) { 'https://some-cdn.test' }
-      let_it_be(:youtube_url) { 'https://www.youtube.com' }
-
-      before do
-        csp = ActionDispatch::ContentSecurityPolicy.new do |p|
-          p.frame_src :self, cdn_url
-        end
-
-        setup_existing_csp_for_controller(Projects::StaticSiteEditorController, csp)
-      end
-
-      it 'appends youtube to the CSP frame-src policy' do
-        visit sse_path
-
-        is_expected.to eql("frame-src 'self' #{cdn_url} #{youtube_url}")
-      end
-    end
-  end
-end
diff --git a/spec/frontend/__helpers__/matchers/index.js b/spec/frontend/__helpers__/matchers/index.js
index 76571bafb06ab65b2c09ae6760c9a98f2803eb1d..9b83ced10e1bb91c982c773ce960b0d7ee0f214d 100644
--- a/spec/frontend/__helpers__/matchers/index.js
+++ b/spec/frontend/__helpers__/matchers/index.js
@@ -1,3 +1,4 @@
 export * from './to_have_sprite_icon';
 export * from './to_have_tracking_attributes';
 export * from './to_match_interpolated_text';
+export * from './to_validate_json_schema';
diff --git a/spec/frontend/__helpers__/matchers/to_validate_json_schema.js b/spec/frontend/__helpers__/matchers/to_validate_json_schema.js
new file mode 100644
index 0000000000000000000000000000000000000000..ff391f08c55192d8cff5238057e814c03bc7b453
--- /dev/null
+++ b/spec/frontend/__helpers__/matchers/to_validate_json_schema.js
@@ -0,0 +1,34 @@
+// NOTE: Make sure to initialize ajv when using this helper
+
+const getAjvErrorMessage = ({ errors }) => {
+  return (errors || []).map((error) => {
+    return `Error with item ${error.instancePath}: ${error.message}`;
+  });
+};
+
+export function toValidateJsonSchema(testData, validator) {
+  if (!(validator instanceof Function && validator.schema)) {
+    return {
+      validator,
+      message: () =>
+        'Validator must be a validating function with property "schema", created with `ajv.compile`. See https://ajv.js.org/api.html#ajv-compile-schema-object-data-any-boolean-promise-any.',
+      pass: false,
+    };
+  }
+
+  const isValid = validator(testData);
+
+  return {
+    actual: testData,
+    message: () => {
+      if (isValid) {
+        // We can match, but still fail because we're in a `expect...not.` context
+        return 'Expected the given data not to pass the schema validation, but found that it was considered valid.';
+      }
+
+      const errorMessages = getAjvErrorMessage(validator).join('\n');
+      return `Expected the given data to pass the schema validation, but found that it was considered invalid. Errors:\n${errorMessages}`;
+    },
+    pass: isValid,
+  };
+}
diff --git a/spec/frontend/__helpers__/matchers/to_validate_json_schema_spec.js b/spec/frontend/__helpers__/matchers/to_validate_json_schema_spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..fd42c710c65993b7f1316d0dd1fda097c6f7b217
--- /dev/null
+++ b/spec/frontend/__helpers__/matchers/to_validate_json_schema_spec.js
@@ -0,0 +1,65 @@
+import Ajv from 'ajv';
+import AjvFormats from 'ajv-formats';
+
+const JSON_SCHEMA = {
+  type: 'object',
+  properties: {
+    fruit: {
+      type: 'string',
+      minLength: 3,
+    },
+  },
+};
+
+const ajv = new Ajv({
+  strictTypes: false,
+  strictTuples: false,
+  allowMatchingProperties: true,
+});
+
+AjvFormats(ajv);
+const schema = ajv.compile(JSON_SCHEMA);
+
+describe('custom matcher toValidateJsonSchema', () => {
+  it('throws error if validator is not compiled correctly', () => {
+    expect(() => {
+      expect({}).toValidateJsonSchema({});
+    }).toThrow(
+      'Validator must be a validating function with property "schema", created with `ajv.compile`. See https://ajv.js.org/api.html#ajv-compile-schema-object-data-any-boolean-promise-any.',
+    );
+  });
+
+  describe('positive assertions', () => {
+    it.each`
+      description      | input
+      ${'valid input'} | ${{ fruit: 'apple' }}
+    `('schema validation passes for $description', ({ input }) => {
+      expect(input).toValidateJsonSchema(schema);
+    });
+
+    it('throws if not matching', () => {
+      expect(() => expect(null).toValidateJsonSchema(schema)).toThrowError(
+        `Expected the given data to pass the schema validation, but found that it was considered invalid. Errors:
+Error with item : must be object`,
+      );
+    });
+  });
+
+  describe('negative assertions', () => {
+    it.each`
+      description                    | input
+      ${'no input'}                  | ${null}
+      ${'input with invalid type'}   | ${'banana'}
+      ${'input with invalid length'} | ${{ fruit: 'aa' }}
+      ${'input with invalid type'}   | ${{ fruit: 12345 }}
+    `('schema validation fails for $description', ({ input }) => {
+      expect(input).not.toValidateJsonSchema(schema);
+    });
+
+    it('throws if matching', () => {
+      expect(() => expect({ fruit: 'apple' }).not.toValidateJsonSchema(schema)).toThrowError(
+        'Expected the given data not to pass the schema validation, but found that it was considered valid.',
+      );
+    });
+  });
+});
diff --git a/spec/frontend/__helpers__/yaml_transformer.js b/spec/frontend/__helpers__/yaml_transformer.js
new file mode 100644
index 0000000000000000000000000000000000000000..a23f9b1f71549cc78b0798d6d83e591ba1319d6c
--- /dev/null
+++ b/spec/frontend/__helpers__/yaml_transformer.js
@@ -0,0 +1,11 @@
+/* eslint-disable import/no-commonjs */
+const JsYaml = require('js-yaml');
+
+// This will transform YAML files to JSON strings
+module.exports = {
+  process: (sourceContent) => {
+    const jsonContent = JsYaml.load(sourceContent);
+    const json = JSON.stringify(jsonContent);
+    return `module.exports = ${json}`;
+  },
+};
diff --git a/spec/frontend/editor/schema/ci/ci_schema_spec.js b/spec/frontend/editor/schema/ci/ci_schema_spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..628c34a27c178ce502d993a0f0f8c71eaeea9781
--- /dev/null
+++ b/spec/frontend/editor/schema/ci/ci_schema_spec.js
@@ -0,0 +1,90 @@
+import Ajv from 'ajv';
+import AjvFormats from 'ajv-formats';
+import CiSchema from '~/editor/schema/ci.json';
+
+// JSON POSITIVE TESTS
+import AllowFailureJson from './json_tests/positive_tests/allow_failure.json';
+import EnvironmentJson from './json_tests/positive_tests/environment.json';
+import GitlabCiDependenciesJson from './json_tests/positive_tests/gitlab-ci-dependencies.json';
+import GitlabCiJson from './json_tests/positive_tests/gitlab-ci.json';
+import InheritJson from './json_tests/positive_tests/inherit.json';
+import MultipleCachesJson from './json_tests/positive_tests/multiple-caches.json';
+import RetryJson from './json_tests/positive_tests/retry.json';
+import TerraformReportJson from './json_tests/positive_tests/terraform_report.json';
+import VariablesMixStringAndUserInputJson from './json_tests/positive_tests/variables_mix_string_and_user_input.json';
+import VariablesJson from './json_tests/positive_tests/variables.json';
+
+// JSON NEGATIVE TESTS
+import DefaultNoAdditionalPropertiesJson from './json_tests/negative_tests/default_no_additional_properties.json';
+import InheritDefaultNoAdditionalPropertiesJson from './json_tests/negative_tests/inherit_default_no_additional_properties.json';
+import JobVariablesMustNotContainObjectsJson from './json_tests/negative_tests/job_variables_must_not_contain_objects.json';
+import ReleaseAssetsLinksEmptyJson from './json_tests/negative_tests/release_assets_links_empty.json';
+import ReleaseAssetsLinksInvalidLinkTypeJson from './json_tests/negative_tests/release_assets_links_invalid_link_type.json';
+import ReleaseAssetsLinksMissingJson from './json_tests/negative_tests/release_assets_links_missing.json';
+import RetryUnknownWhenJson from './json_tests/negative_tests/retry_unknown_when.json';
+
+// YAML POSITIVE TEST
+import CacheYaml from './yaml_tests/positive_tests/cache.yml';
+import FilterYaml from './yaml_tests/positive_tests/filter.yml';
+import IncludeYaml from './yaml_tests/positive_tests/include.yml';
+import RulesYaml from './yaml_tests/positive_tests/rules.yml';
+
+// YAML NEGATIVE TEST
+import CacheNegativeYaml from './yaml_tests/negative_tests/cache.yml';
+import IncludeNegativeYaml from './yaml_tests/negative_tests/include.yml';
+
+const ajv = new Ajv({
+  strictTypes: false,
+  strictTuples: false,
+  allowMatchingProperties: true,
+});
+
+AjvFormats(ajv);
+const schema = ajv.compile(CiSchema);
+
+describe('positive tests', () => {
+  it.each(
+    Object.entries({
+      // JSON
+      AllowFailureJson,
+      EnvironmentJson,
+      GitlabCiDependenciesJson,
+      GitlabCiJson,
+      InheritJson,
+      MultipleCachesJson,
+      RetryJson,
+      TerraformReportJson,
+      VariablesMixStringAndUserInputJson,
+      VariablesJson,
+
+      // YAML
+      CacheYaml,
+      FilterYaml,
+      IncludeYaml,
+      RulesYaml,
+    }),
+  )('schema validates %s', (_, input) => {
+    expect(input).toValidateJsonSchema(schema);
+  });
+});
+
+describe('negative tests', () => {
+  it.each(
+    Object.entries({
+      // JSON
+      DefaultNoAdditionalPropertiesJson,
+      JobVariablesMustNotContainObjectsJson,
+      InheritDefaultNoAdditionalPropertiesJson,
+      ReleaseAssetsLinksEmptyJson,
+      ReleaseAssetsLinksInvalidLinkTypeJson,
+      ReleaseAssetsLinksMissingJson,
+      RetryUnknownWhenJson,
+
+      // YAML
+      CacheNegativeYaml,
+      IncludeNegativeYaml,
+    }),
+  )('schema validates %s', (_, input) => {
+    expect(input).not.toValidateJsonSchema(schema);
+  });
+});
diff --git a/spec/frontend/editor/schema/ci/json_tests/negative_tests/default_no_additional_properties.json b/spec/frontend/editor/schema/ci/json_tests/negative_tests/default_no_additional_properties.json
new file mode 100644
index 0000000000000000000000000000000000000000..955c19ef1ab9e20736e18850b79d04d8f7dcd09f
--- /dev/null
+++ b/spec/frontend/editor/schema/ci/json_tests/negative_tests/default_no_additional_properties.json
@@ -0,0 +1,12 @@
+{
+  "default": {
+    "secrets": {
+      "DATABASE_PASSWORD": {
+        "vault": "production/db/password"
+      }
+    },
+    "environment": {
+      "name": "test"
+    }
+  }
+}
diff --git a/spec/frontend/editor/schema/ci/json_tests/negative_tests/inherit_default_no_additional_properties.json b/spec/frontend/editor/schema/ci/json_tests/negative_tests/inherit_default_no_additional_properties.json
new file mode 100644
index 0000000000000000000000000000000000000000..7411e4c24342f657b07abd0e51207f4a04255a4a
--- /dev/null
+++ b/spec/frontend/editor/schema/ci/json_tests/negative_tests/inherit_default_no_additional_properties.json
@@ -0,0 +1,8 @@
+{
+  "karma": {
+    "inherit": {
+      "default": ["secrets"]
+    },
+    "script": "karma"
+  }
+}
diff --git a/spec/frontend/editor/schema/ci/json_tests/negative_tests/job_variables_must_not_contain_objects.json b/spec/frontend/editor/schema/ci/json_tests/negative_tests/job_variables_must_not_contain_objects.json
new file mode 100644
index 0000000000000000000000000000000000000000..bfdbf26ee70c3cd7baff5d6a5b3a8694013dc934
--- /dev/null
+++ b/spec/frontend/editor/schema/ci/json_tests/negative_tests/job_variables_must_not_contain_objects.json
@@ -0,0 +1,12 @@
+{
+  "gitlab-ci-variables-object": {
+    "stage": "test",
+    "script": ["true"],
+    "variables": {
+      "DEPLOY_ENVIRONMENT": {
+        "value": "staging",
+        "description": "The deployment target. Change this variable to 'canary' or 'production' if needed."
+      }
+    }
+  }
+}
diff --git a/spec/frontend/editor/schema/ci/json_tests/negative_tests/release_assets_links_empty.json b/spec/frontend/editor/schema/ci/json_tests/negative_tests/release_assets_links_empty.json
new file mode 100644
index 0000000000000000000000000000000000000000..84a1aa1469845e0395c3637ab71c461a0b91eedb
--- /dev/null
+++ b/spec/frontend/editor/schema/ci/json_tests/negative_tests/release_assets_links_empty.json
@@ -0,0 +1,13 @@
+{
+  "gitlab-ci-release-assets-links-empty": {
+    "script": "dostuff",
+    "stage": "deploy",
+    "release": {
+      "description": "Created using the release-cli $EXTRA_DESCRIPTION",
+      "tag_name": "$CI_COMMIT_TAG",
+      "assets": {
+        "links": []
+      }
+    }
+  }
+}
diff --git a/spec/frontend/editor/schema/ci/json_tests/negative_tests/release_assets_links_invalid_link_type.json b/spec/frontend/editor/schema/ci/json_tests/negative_tests/release_assets_links_invalid_link_type.json
new file mode 100644
index 0000000000000000000000000000000000000000..048911aefa3c34f73921ee3523a2e3a9b9284628
--- /dev/null
+++ b/spec/frontend/editor/schema/ci/json_tests/negative_tests/release_assets_links_invalid_link_type.json
@@ -0,0 +1,24 @@
+{
+  "gitlab-ci-release-assets-links-invalid-link-type": {
+    "script": "dostuff",
+    "stage": "deploy",
+    "release": {
+      "description": "Created using the release-cli $EXTRA_DESCRIPTION",
+      "tag_name": "$CI_COMMIT_TAG",
+      "assets": {
+        "links": [
+          {
+            "name": "asset1",
+            "url": "https://example.com/assets/1"
+          },
+          {
+            "name": "asset2",
+            "url": "https://example.com/assets/2",
+            "filepath": "/pretty/url/1",
+            "link_type": "invalid"
+          }
+        ]
+      }
+    }
+  }
+}
diff --git a/spec/frontend/editor/schema/ci/json_tests/negative_tests/release_assets_links_missing.json b/spec/frontend/editor/schema/ci/json_tests/negative_tests/release_assets_links_missing.json
new file mode 100644
index 0000000000000000000000000000000000000000..6f0b5a3bff8155d08b57ff4e5214e8309c54f53a
--- /dev/null
+++ b/spec/frontend/editor/schema/ci/json_tests/negative_tests/release_assets_links_missing.json
@@ -0,0 +1,11 @@
+{
+  "gitlab-ci-release-assets-links-missing": {
+    "script": "dostuff",
+    "stage": "deploy",
+    "release": {
+      "description": "Created using the release-cli $EXTRA_DESCRIPTION",
+      "tag_name": "$CI_COMMIT_TAG",
+      "assets": {}
+    }
+  }
+}
diff --git a/spec/frontend/editor/schema/ci/json_tests/negative_tests/retry_unknown_when.json b/spec/frontend/editor/schema/ci/json_tests/negative_tests/retry_unknown_when.json
new file mode 100644
index 0000000000000000000000000000000000000000..433504f52c64084a0f047432473c7b8ccf8b0e4e
--- /dev/null
+++ b/spec/frontend/editor/schema/ci/json_tests/negative_tests/retry_unknown_when.json
@@ -0,0 +1,9 @@
+{
+  "gitlab-ci-retry-object-unknown-when": {
+    "stage": "test",
+    "script": "rspec",
+    "retry": {
+      "when": "gitlab-ci-retry-object-unknown-when"
+    }
+  }
+}
diff --git a/spec/frontend/editor/schema/ci/json_tests/positive_tests/allow_failure.json b/spec/frontend/editor/schema/ci/json_tests/positive_tests/allow_failure.json
new file mode 100644
index 0000000000000000000000000000000000000000..44d42116c1ae79a7c1f975f6da9d2ee5ebe3afdb
--- /dev/null
+++ b/spec/frontend/editor/schema/ci/json_tests/positive_tests/allow_failure.json
@@ -0,0 +1,19 @@
+{
+  "job1": {
+    "stage": "test",
+    "script": ["execute_script_that_will_fail"],
+    "allow_failure": true
+  },
+  "job2": {
+    "script": ["exit 1"],
+    "allow_failure": {
+      "exit_codes": 137
+    }
+  },
+  "job3": {
+    "script": ["exit 137"],
+    "allow_failure": {
+      "exit_codes": [137, 255]
+    }
+  }
+}
diff --git a/spec/frontend/editor/schema/ci/json_tests/positive_tests/environment.json b/spec/frontend/editor/schema/ci/json_tests/positive_tests/environment.json
new file mode 100644
index 0000000000000000000000000000000000000000..0c6f7935063e5b64f8047b95a37941d2f3fa480d
--- /dev/null
+++ b/spec/frontend/editor/schema/ci/json_tests/positive_tests/environment.json
@@ -0,0 +1,75 @@
+{
+  "deploy to production 1": {
+    "stage": "deploy",
+    "script": "git push production HEAD: master",
+    "environment": "production"
+  },
+  "deploy to production 2": {
+    "stage": "deploy",
+    "script": "git push production HEAD:master",
+    "environment": {
+      "name": "production"
+    }
+  },
+  "deploy to production 3": {
+    "stage": "deploy",
+    "script": "git push production HEAD:master",
+    "environment": {
+      "name": "production",
+      "url": "https://prod.example.com"
+    }
+  },
+  "review_app 1": {
+    "stage": "deploy",
+    "script": "make deploy-app",
+    "environment": {
+      "name": "review/$CI_COMMIT_REF_NAME",
+      "url": "https://$CI_ENVIRONMENT_SLUG.example.com",
+      "on_stop": "stop_review_app"
+    }
+  },
+  "stop_review_app": {
+    "stage": "deploy",
+    "variables": {
+      "GIT_STRATEGY": "none"
+    },
+    "script": "make delete-app",
+    "when": "manual",
+    "environment": {
+      "name": "review/$CI_COMMIT_REF_NAME",
+      "action": "stop"
+    }
+  },
+  "review_app 2": {
+    "script": "deploy-review-app",
+    "environment": {
+      "name": "review/$CI_COMMIT_REF_NAME",
+      "auto_stop_in": "1 day"
+    }
+  },
+  "deploy 1": {
+    "stage": "deploy",
+    "script": "make deploy-app",
+    "environment": {
+      "name": "production",
+      "kubernetes": {
+        "namespace": "production"
+      }
+    }
+  },
+  "deploy 2": {
+    "script": "echo",
+    "environment": {
+      "name": "customer-portal",
+      "deployment_tier": "production"
+    }
+  },
+  "deploy as review app": {
+    "stage": "deploy",
+    "script": "make deploy",
+    "environment": {
+      "name": "review/$CI_COMMIT_REF_NAME",
+      "url": "https://$CI_ENVIRONMENT_SLUG.example.com/"
+    }
+  }
+}
diff --git a/spec/frontend/editor/schema/ci/json_tests/positive_tests/gitlab-ci-dependencies.json b/spec/frontend/editor/schema/ci/json_tests/positive_tests/gitlab-ci-dependencies.json
new file mode 100644
index 0000000000000000000000000000000000000000..5ffa7fa799ece925bb69458aa0dfad2cce3246d2
--- /dev/null
+++ b/spec/frontend/editor/schema/ci/json_tests/positive_tests/gitlab-ci-dependencies.json
@@ -0,0 +1,68 @@
+{
+  ".build_config": {
+    "before_script": ["echo test"]
+  },
+  ".build_script": "echo build script",
+  "default": {
+    "image": "ruby:2.5",
+    "services": ["docker:dind"],
+    "cache": {
+      "paths": ["vendor/"]
+    },
+    "before_script": ["bundle install --path vendor/"],
+    "after_script": ["rm -rf tmp/"]
+  },
+  "stages": ["install", "build", "test", "deploy"],
+  "image": "foo:latest",
+  "install task1": {
+    "image": "node:latest",
+    "stage": "install",
+    "script": "npm install",
+    "artifacts": {
+      "paths": ["node_modules/"]
+    }
+  },
+  "build dev": {
+    "image": "node:latest",
+    "stage": "build",
+    "needs": [
+      {
+        "job": "install task1"
+      }
+    ],
+    "script": "npm run build:dev"
+  },
+  "build prod": {
+    "image": "node:latest",
+    "stage": "build",
+    "needs": ["install task1"],
+    "script": "npm run build:prod"
+  },
+  "test": {
+    "image": "node:latest",
+    "stage": "build",
+    "needs": [
+      "install task1",
+      {
+        "job": "build dev",
+        "artifacts": true
+      }
+    ],
+    "script": "npm run test"
+  },
+  "deploy it": {
+    "image": "node:latest",
+    "stage": "deploy",
+    "needs": [
+      {
+        "job": "build dev",
+        "artifacts": false
+      },
+      {
+        "job": "build prod",
+        "artifacts": true
+      }
+    ],
+    "script": "npm run test"
+  }
+}
diff --git a/spec/frontend/editor/schema/ci/json_tests/positive_tests/gitlab-ci.json b/spec/frontend/editor/schema/ci/json_tests/positive_tests/gitlab-ci.json
new file mode 100644
index 0000000000000000000000000000000000000000..89420bbc35fd0f899bfd36303e25910619319e98
--- /dev/null
+++ b/spec/frontend/editor/schema/ci/json_tests/positive_tests/gitlab-ci.json
@@ -0,0 +1,350 @@
+{
+  ".build_config": {
+    "before_script": ["echo test"]
+  },
+  ".build_script": "echo build script",
+  ".example_variables": {
+    "foo": "hello",
+    "bar": 42
+  },
+  ".example_services": [
+    "docker:dind",
+    {
+      "name": "sql:latest",
+      "command": ["/usr/bin/super-sql", "run"]
+    }
+  ],
+  "default": {
+    "image": "ruby:2.5",
+    "services": ["docker:dind"],
+    "cache": {
+      "paths": ["vendor/"]
+    },
+    "before_script": ["bundle install --path vendor/"],
+    "after_script": ["rm -rf tmp/"],
+    "tags": ["ruby", "postgres"],
+    "artifacts": {
+      "name": "%CI_COMMIT_REF_NAME%",
+      "expose_as": "artifact 1",
+      "paths": ["path/to/file.txt", "target/*.war"],
+      "when": "on_failure"
+    },
+    "retry": 2,
+    "timeout": "3 hours 30 minutes",
+    "interruptible": true
+  },
+  "stages": ["build", "test", "deploy", "random"],
+  "image": "foo:latest",
+  "services": ["sql:latest"],
+  "before_script": ["echo test", "echo test2"],
+  "after_script": [],
+  "cache": {
+    "key": "asd",
+    "paths": ["dist/", ".foo"],
+    "untracked": false,
+    "policy": "pull"
+  },
+  "variables": {
+    "STAGE": "yep",
+    "PROD": "nope"
+  },
+  "include": [
+    "https://gitlab.com/awesome-project/raw/master/.before-script-template.yml",
+    "/templates/.after-script-template.yml",
+    { "template": "Auto-DevOps.gitlab-ci.yml" },
+    {
+      "project": "my-group/my-project",
+      "ref": "master",
+      "file": "/templates/.gitlab-ci-template.yml"
+    },
+    {
+      "project": "my-group/my-project",
+      "ref": "master",
+      "file": ["/templates/.gitlab-ci-template.yml", "/templates/another-template-to-include.yml"]
+    }
+  ],
+  "build": {
+    "image": {
+      "name": "node:latest"
+    },
+    "services": [],
+    "stage": "build",
+    "script": "npm run build",
+    "before_script": ["npm install"],
+    "rules": [
+      {
+        "if": "moo",
+        "changes": ["Moofile"],
+        "exists": ["main.cow"],
+        "when": "delayed",
+        "start_in": "3 hours"
+      }
+    ],
+    "retry": {
+      "max": 1,
+      "when": "stuck_or_timeout_failure"
+    },
+    "cache": {
+      "key": "$CI_COMMIT_REF_NAME",
+      "paths": ["node_modules/"],
+      "policy": "pull-push"
+    },
+    "artifacts": {
+      "paths": ["dist/"],
+      "expose_as": "link_name_in_merge_request",
+      "name": "bundles",
+      "when": "on_success",
+      "expire_in": "1 week",
+      "reports": {
+        "junit": "result.xml",
+        "cobertura": "cobertura-coverage.xml",
+        "codequality": "codequality.json",
+        "sast": "sast.json",
+        "dependency_scanning": "scan.json",
+        "container_scanning": "scan2.json",
+        "dast": "dast.json",
+        "license_management": "license.json",
+        "performance": "performance.json",
+        "metrics": "metrics.txt"
+      }
+    },
+    "variables": {
+      "FOO_BAR": "..."
+    },
+    "only": {
+      "kubernetes": "active",
+      "variables": ["$FOO_BAR == '...'"],
+      "changes": ["/path/to/file", "/another/file"]
+    },
+    "except": ["master", "tags"],
+    "tags": ["docker"],
+    "allow_failure": true,
+    "when": "manual"
+  },
+  "error-report": {
+    "when": "on_failure",
+    "script": "report error",
+    "stage": "test"
+  },
+  "test": {
+    "image": {
+      "name": "node:latest",
+      "entrypoint": [""]
+    },
+    "stage": "test",
+    "script": "npm test",
+    "parallel": 5,
+    "retry": {
+      "max": 2,
+      "when": [
+        "runner_system_failure",
+        "stuck_or_timeout_failure",
+        "script_failure",
+        "unknown_failure",
+        "always"
+      ]
+    },
+    "artifacts": {
+      "reports": {
+        "junit": ["result.xml"],
+        "cobertura": ["cobertura-coverage.xml"],
+        "codequality": ["codequality.json"],
+        "sast": ["sast.json"],
+        "dependency_scanning": ["scan.json"],
+        "container_scanning": ["scan2.json"],
+        "dast": ["dast.json"],
+        "license_management": ["license.json"],
+        "performance": ["performance.json"],
+        "metrics": ["metrics.txt"]
+      }
+    },
+    "coverage": "/Cycles: \\d+\\.\\d+$/",
+    "dependencies": []
+  },
+  "docker": {
+    "script": "docker build -t foo:latest",
+    "when": "delayed",
+    "start_in": "10 min",
+    "timeout": "1h",
+    "retry": 1,
+    "only": {
+      "changes": ["Dockerfile", "docker/scripts/*"]
+    }
+  },
+  "deploy": {
+    "services": [
+      {
+        "name": "sql:latest",
+        "entrypoint": [""],
+        "command": ["/usr/bin/super-sql", "run"],
+        "alias": "super-sql"
+      },
+      "sql:latest",
+      {
+        "name": "sql:latest",
+        "alias": "default-sql"
+      }
+    ],
+    "script": "dostuff",
+    "stage": "deploy",
+    "environment": {
+      "name": "prod",
+      "url": "http://example.com",
+      "on_stop": "stop-deploy"
+    },
+    "only": ["master"],
+    "release": {
+      "name": "Release $CI_COMMIT_TAG",
+      "description": "Created using the release-cli $EXTRA_DESCRIPTION",
+      "tag_name": "$CI_COMMIT_TAG",
+      "ref": "$CI_COMMIT_TAG",
+      "milestones": ["m1", "m2", "m3"],
+      "released_at": "2020-07-15T08:00:00Z",
+      "assets": {
+        "links": [
+          {
+            "name": "asset1",
+            "url": "https://example.com/assets/1"
+          },
+          {
+            "name": "asset2",
+            "url": "https://example.com/assets/2",
+            "filepath": "/pretty/url/1",
+            "link_type": "other"
+          },
+          {
+            "name": "asset3",
+            "url": "https://example.com/assets/3",
+            "link_type": "runbook"
+          },
+          {
+            "name": "asset4",
+            "url": "https://example.com/assets/4",
+            "link_type": "package"
+          },
+          {
+            "name": "asset5",
+            "url": "https://example.com/assets/5",
+            "link_type": "image"
+          }
+        ]
+      }
+    }
+  },
+  ".performance-tmpl": {
+    "after_script": ["echo after"],
+    "before_script": ["echo before"],
+    "variables": {
+      "SCRIPT_NOT_REQUIRED": "true"
+    }
+  },
+  "performance-a": {
+    "extends": ".performance-tmpl",
+    "script": "echo test"
+  },
+  "performance-b": {
+    "extends": ".performance-tmpl"
+  },
+  "workflow": {
+    "rules": [
+      {
+        "if": "$CI_COMMIT_REF_NAME =~ /-wip$/",
+        "when": "never"
+      },
+      {
+        "if": "$CI_COMMIT_TAG",
+        "when": "never"
+      },
+      {
+        "when": "always"
+      }
+    ]
+  },
+  "job": {
+    "script": "echo Hello, Rules!",
+    "rules": [
+      {
+        "if": "$CI_MERGE_REQUEST_TARGET_BRANCH_NAME == \"master\"",
+        "when": "manual",
+        "allow_failure": true
+      }
+    ]
+  },
+  "microservice_a": {
+    "trigger": {
+      "include": "path/to/microservice_a.yml"
+    }
+  },
+  "microservice_b": {
+    "trigger": {
+      "include": [{ "local": "path/to/microservice_b.yml" }, { "template": "SAST.gitlab-cy.yml" }],
+      "strategy": "depend"
+    }
+  },
+  "child-pipeline": {
+    "stage": "test",
+    "trigger": {
+      "include": [
+        {
+          "artifact": "generated-config.yml",
+          "job": "generate-config"
+        }
+      ]
+    }
+  },
+  "child-pipeline-simple": {
+    "stage": "test",
+    "trigger": {
+      "include": "other/file.yml"
+    }
+  },
+  "complex": {
+    "stage": "deploy",
+    "trigger": {
+      "project": "my/deployment",
+      "branch": "stable"
+    }
+  },
+  "parallel-integer": {
+    "stage": "test",
+    "script": ["echo ${CI_NODE_INDEX} ${CI_NODE_TOTAL}"],
+    "parallel": 5
+  },
+  "parallel-matrix-simple": {
+    "stage": "test",
+    "script": ["echo ${MY_VARIABLE}"],
+    "parallel": {
+      "matrix": [
+        {
+          "MY_VARIABLE": 0
+        },
+        {
+          "MY_VARIABLE": "sample"
+        },
+        {
+          "MY_VARIABLE": ["element0", 1, "element2"]
+        }
+      ]
+    }
+  },
+  "parallel-matrix-gitlab-docs": {
+    "stage": "deploy",
+    "script": ["bin/deploy"],
+    "parallel": {
+      "matrix": [
+        {
+          "PROVIDER": "aws",
+          "STACK": ["app1", "app2"]
+        },
+        {
+          "PROVIDER": "ovh",
+          "STACK": ["monitoring", "backup", "app"]
+        },
+        {
+          "PROVIDER": ["gcp", "vultr"],
+          "STACK": ["data", "processing"]
+        }
+      ]
+    }
+  }
+}
diff --git a/spec/frontend/editor/schema/ci/json_tests/positive_tests/inherit.json b/spec/frontend/editor/schema/ci/json_tests/positive_tests/inherit.json
new file mode 100644
index 0000000000000000000000000000000000000000..3f72afa6ceb93dff881363d96de7a00134fd86e9
--- /dev/null
+++ b/spec/frontend/editor/schema/ci/json_tests/positive_tests/inherit.json
@@ -0,0 +1,54 @@
+{
+  "default": {
+    "image": "ruby:2.4",
+    "before_script": ["echo Hello World"]
+  },
+  "variables": {
+    "DOMAIN": "example.com",
+    "WEBHOOK_URL": "https://my-webhook.example.com"
+  },
+  "rubocop": {
+    "inherit": {
+      "default": false,
+      "variables": false
+    },
+    "script": "bundle exec rubocop"
+  },
+  "rspec": {
+    "inherit": {
+      "default": ["image"],
+      "variables": ["WEBHOOK_URL"]
+    },
+    "script": "bundle exec rspec"
+  },
+  "capybara": {
+    "inherit": {
+      "variables": false
+    },
+    "script": "bundle exec capybara"
+  },
+  "karma": {
+    "inherit": {
+      "default": true,
+      "variables": ["DOMAIN"]
+    },
+    "script": "karma"
+  },
+  "inherit literally all": {
+    "inherit": {
+      "default": [
+        "after_script",
+        "artifacts",
+        "before_script",
+        "cache",
+        "image",
+        "interruptible",
+        "retry",
+        "services",
+        "tags",
+        "timeout"
+      ]
+    },
+    "script": "true"
+  }
+}
diff --git a/spec/frontend/editor/schema/ci/json_tests/positive_tests/multiple-caches.json b/spec/frontend/editor/schema/ci/json_tests/positive_tests/multiple-caches.json
new file mode 100644
index 0000000000000000000000000000000000000000..360938e5ce7433050e05b3e775407af9296ac6b5
--- /dev/null
+++ b/spec/frontend/editor/schema/ci/json_tests/positive_tests/multiple-caches.json
@@ -0,0 +1,24 @@
+{
+  "test-job": {
+    "stage": "build",
+    "cache": [
+      {
+        "key": {
+          "files": ["Gemfile.lock"]
+        },
+        "paths": ["vendor/ruby"]
+      },
+      {
+        "key": {
+          "files": ["yarn.lock"]
+        },
+        "paths": [".yarn-cache/"]
+      }
+    ],
+    "script": [
+      "bundle install --path=vendor",
+      "yarn install --cache-folder .yarn-cache",
+      "echo Run tests..."
+    ]
+  }
+}
diff --git a/spec/frontend/editor/schema/ci/json_tests/positive_tests/retry.json b/spec/frontend/editor/schema/ci/json_tests/positive_tests/retry.json
new file mode 100644
index 0000000000000000000000000000000000000000..1337e5e7bc84a2d1de908ded71996cdb996095b2
--- /dev/null
+++ b/spec/frontend/editor/schema/ci/json_tests/positive_tests/retry.json
@@ -0,0 +1,60 @@
+{
+  "gitlab-ci-retry-int": {
+    "stage": "test",
+    "script": "rspec",
+    "retry": 2
+  },
+  "gitlab-ci-retry-object-no-max": {
+    "stage": "test",
+    "script": "rspec",
+    "retry": {
+      "when": "runner_system_failure"
+    }
+  },
+  "gitlab-ci-retry-object-single-when": {
+    "stage": "test",
+    "script": "rspec",
+    "retry": {
+      "max": 2,
+      "when": "runner_system_failure"
+    }
+  },
+  "gitlab-ci-retry-object-multiple-when": {
+    "stage": "test",
+    "script": "rspec",
+    "retry": {
+      "max": 2,
+      "when": ["runner_system_failure", "stuck_or_timeout_failure"]
+    }
+  },
+  "gitlab-ci-retry-object-multiple-when-dupes": {
+    "stage": "test",
+    "script": "rspec",
+    "retry": {
+      "max": 2,
+      "when": ["runner_system_failure", "runner_system_failure"]
+    }
+  },
+  "gitlab-ci-retry-object-all-when": {
+    "stage": "test",
+    "script": "rspec",
+    "retry": {
+      "max": 2,
+      "when": [
+        "always",
+        "unknown_failure",
+        "script_failure",
+        "api_failure",
+        "stuck_or_timeout_failure",
+        "runner_system_failure",
+        "runner_unsupported",
+        "stale_schedule",
+        "job_execution_timeout",
+        "archived_failure",
+        "unmet_prerequisites",
+        "scheduler_failure",
+        "data_integrity_failure"
+      ]
+    }
+  }
+}
diff --git a/spec/frontend/editor/schema/ci/json_tests/positive_tests/terraform_report.json b/spec/frontend/editor/schema/ci/json_tests/positive_tests/terraform_report.json
new file mode 100644
index 0000000000000000000000000000000000000000..0e444a4ba620709822cdf0df7b0a153e39ec1eaa
--- /dev/null
+++ b/spec/frontend/editor/schema/ci/json_tests/positive_tests/terraform_report.json
@@ -0,0 +1,50 @@
+{
+  "image": {
+    "name": "registry.gitlab.com/gitlab-org/gitlab-build-images:terraform",
+    "entrypoint": [
+      "/usr/bin/env",
+      "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
+    ]
+  },
+  "variables": {
+    "PLAN": "plan.tfplan",
+    "JSON_PLAN_FILE": "tfplan.json"
+  },
+  "cache": {
+    "paths": [".terraform"]
+  },
+  "before_script": [
+    "alias convert_report=\"jq -r '([.resource_changes[]?.change.actions?]|flatten)|{\"create\":(map(select(.==\"create\"))|length),\"update\":(map(select(.==\"update\"))|length),\"delete\":(map(select(.==\"delete\"))|length)}'\"",
+    "terraform --version",
+    "terraform init"
+  ],
+  "stages": ["validate", "build", "test", "deploy"],
+  "validate": {
+    "stage": "validate",
+    "script": ["terraform validate"]
+  },
+  "plan": {
+    "stage": "build",
+    "script": [
+      "terraform plan -out=$PLAN",
+      "terraform show --json $PLAN | convert_report > $JSON_PLAN_FILE"
+    ],
+    "artifacts": {
+      "name": "plan",
+      "paths": ["$PLAN"],
+      "reports": {
+        "terraform": "$JSON_PLAN_FILE"
+      }
+    }
+  },
+  "apply": {
+    "stage": "deploy",
+    "environment": {
+      "name": "production"
+    },
+    "script": ["terraform apply -input=false $PLAN"],
+    "dependencies": ["plan"],
+    "when": "manual",
+    "only": ["master"]
+  }
+}
diff --git a/spec/frontend/editor/schema/ci/json_tests/positive_tests/variables.json b/spec/frontend/editor/schema/ci/json_tests/positive_tests/variables.json
new file mode 100644
index 0000000000000000000000000000000000000000..ce59b3fbbec63ecc038777001aa74447faceeb05
--- /dev/null
+++ b/spec/frontend/editor/schema/ci/json_tests/positive_tests/variables.json
@@ -0,0 +1,22 @@
+{
+  "variables": {
+    "DEPLOY_ENVIRONMENT": {
+      "value": "staging",
+      "description": "The deployment target. Change this variable to 'canary' or 'production' if needed."
+    }
+  },
+  "gitlab-ci-variables-string": {
+    "stage": "test",
+    "script": ["true"],
+    "variables": {
+      "TEST_VAR": "String variable"
+    }
+  },
+  "gitlab-ci-variables-integer": {
+    "stage": "test",
+    "script": ["true"],
+    "variables": {
+      "canonical": 685230
+    }
+  }
+}
diff --git a/spec/frontend/editor/schema/ci/json_tests/positive_tests/variables_mix_string_and_user_input.json b/spec/frontend/editor/schema/ci/json_tests/positive_tests/variables_mix_string_and_user_input.json
new file mode 100644
index 0000000000000000000000000000000000000000..87a9ec05b575f8fbd582d2b59e7378b54941cce6
--- /dev/null
+++ b/spec/frontend/editor/schema/ci/json_tests/positive_tests/variables_mix_string_and_user_input.json
@@ -0,0 +1,10 @@
+{
+  "variables": {
+    "SOME_STR": "--batch-mode --errors --fail-at-end --show-version",
+    "SOME_INT": 10,
+    "SOME_USER_INPUT_FLAG": {
+      "value": "flag value",
+      "description": "Some Flag!"
+    }
+  }
+}
diff --git a/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/cache.yml b/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/cache.yml
new file mode 100644
index 0000000000000000000000000000000000000000..ee533f54d3bcc1ce37d02f8bd8a0d29201f2f2a6
--- /dev/null
+++ b/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/cache.yml
@@ -0,0 +1,15 @@
+# Covers https://gitlab.com/gitlab-org/gitlab/-/merge_requests/70779
+stages:
+  - prepare
+
+# invalid cache:when value
+job1:
+  stage: prepare
+  cache:
+    when: 0
+
+# invalid cache:when value
+job2:
+  stage: prepare
+  cache:
+    when: 'never'
diff --git a/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/include.yml b/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/include.yml
new file mode 100644
index 0000000000000000000000000000000000000000..287150a765fb314e11a785e3cf71a0d9bba926a1
--- /dev/null
+++ b/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/include.yml
@@ -0,0 +1,17 @@
+# Covers https://gitlab.com/gitlab-org/gitlab/-/merge_requests/70779
+stages:
+  - prepare
+
+# missing file property
+childPipeline:
+  stage: prepare
+  trigger:
+    include:
+      - project: 'my-group/my-pipeline-library'
+
+# missing project property
+childPipeline2:
+  stage: prepare
+  trigger:
+    include:
+      - file: '.gitlab-ci.yml'
diff --git a/spec/frontend/editor/schema/ci/yaml_tests/positive_tests/cache.yml b/spec/frontend/editor/schema/ci/yaml_tests/positive_tests/cache.yml
new file mode 100644
index 0000000000000000000000000000000000000000..436c7d7269971a024a0163c7591d2b4e8c09186f
--- /dev/null
+++ b/spec/frontend/editor/schema/ci/yaml_tests/positive_tests/cache.yml
@@ -0,0 +1,25 @@
+# Covers https://gitlab.com/gitlab-org/gitlab/-/merge_requests/70779
+stages:
+  - prepare
+
+# test for cache:when values
+job1:
+  stage: prepare
+  script:
+    - echo 'running job'
+  cache:
+    when: 'on_success'
+
+job2:
+  stage: prepare
+  script:
+    - echo 'running job'
+  cache:
+    when: 'on_failure'
+
+job3:
+  stage: prepare
+  script:
+    - echo 'running job'
+  cache:
+    when: 'always'
diff --git a/spec/frontend/editor/schema/ci/yaml_tests/positive_tests/filter.yml b/spec/frontend/editor/schema/ci/yaml_tests/positive_tests/filter.yml
new file mode 100644
index 0000000000000000000000000000000000000000..2b29c24fa3ca823c8a9df5568f0b41e2985490aa
--- /dev/null
+++ b/spec/frontend/editor/schema/ci/yaml_tests/positive_tests/filter.yml
@@ -0,0 +1,18 @@
+# Covers https://gitlab.com/gitlab-org/gitlab/-/merge_requests/79335
+deploy-template:
+  script:
+    - echo "hello world"
+  only:
+    - foo
+  except:
+    - bar
+
+# null value allowed
+deploy-without-only:
+  extends: deploy-template
+  only:
+
+# null value allowed
+deploy-without-except:
+  extends: deploy-template
+  except:
diff --git a/spec/frontend/editor/schema/ci/yaml_tests/positive_tests/include.yml b/spec/frontend/editor/schema/ci/yaml_tests/positive_tests/include.yml
new file mode 100644
index 0000000000000000000000000000000000000000..3497be28058e442c59e306597d68a17f3c9e6782
--- /dev/null
+++ b/spec/frontend/editor/schema/ci/yaml_tests/positive_tests/include.yml
@@ -0,0 +1,32 @@
+# Covers https://gitlab.com/gitlab-org/gitlab/-/merge_requests/70779
+
+# test for include:rules
+include:
+  - local: builds.yml
+    rules:
+      - if: '$INCLUDE_BUILDS == "true"'
+        when: always
+
+stages:
+  - prepare
+
+# test for trigger:include
+childPipeline:
+  stage: prepare
+  script:
+    - echo 'creating pipeline...'
+  trigger:
+    include:
+      - project: 'my-group/my-pipeline-library'
+        file: '.gitlab-ci.yml'
+
+# accepts optional ref property
+childPipeline2:
+  stage: prepare
+  script:
+    - echo 'creating pipeline...'
+  trigger:
+    include:
+      - project: 'my-group/my-pipeline-library'
+        file: '.gitlab-ci.yml'
+        ref: 'main'
diff --git a/spec/frontend/editor/schema/ci/yaml_tests/positive_tests/rules.yml b/spec/frontend/editor/schema/ci/yaml_tests/positive_tests/rules.yml
new file mode 100644
index 0000000000000000000000000000000000000000..27a199cff1392177a2990cdbd691b94c4b21686e
--- /dev/null
+++ b/spec/frontend/editor/schema/ci/yaml_tests/positive_tests/rules.yml
@@ -0,0 +1,13 @@
+# Covers https://gitlab.com/gitlab-org/gitlab/-/merge_requests/74164
+
+# test for workflow:rules:changes and workflow:rules:exists
+workflow:
+  rules:
+    - if: '$CI_PIPELINE_SOURCE == "schedule"'
+      exists:
+        - Dockerfile
+      changes:
+        - Dockerfile
+      variables:
+        IS_A_FEATURE: 'true'
+      when: always
diff --git a/spec/support/helpers/gitaly_setup.rb b/spec/support/helpers/gitaly_setup.rb
index 0ad83bdeeb24571e2c6a30b2dafb1ae4f9825350..a4ee618457da3b5ae30f237bcaca81ad3a3d1b2b 100644
--- a/spec/support/helpers/gitaly_setup.rb
+++ b/spec/support/helpers/gitaly_setup.rb
@@ -267,7 +267,7 @@ def setup_gitaly
       { 'default' => repos_path },
       force: true,
       options: {
-        runtime_dir: File.join(gitaly_dir, "run2"),
+        internal_socket_dir: File.join(gitaly_dir, "internal_gitaly2"),
         gitaly_socket: "gitaly2.socket",
         config_filename: "gitaly2.config.toml"
       }
diff --git a/yarn.lock b/yarn.lock
index 9a1ed097f381626d2403683aa6446801c515d9c8..239cdab66924a7525be8ecfe7eb972983c14445f 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2516,10 +2516,10 @@ ajv@^6.1.0, ajv@^6.10.0, ajv@^6.10.2, ajv@^6.12.3, ajv@^6.12.4, ajv@^6.12.5:
     json-schema-traverse "^0.4.1"
     uri-js "^4.2.2"
 
-ajv@^8.0.0, ajv@^8.0.1, ajv@^8.8.0:
-  version "8.9.0"
-  resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.9.0.tgz#738019146638824dea25edcf299dcba1b0e7eb18"
-  integrity sha512-qOKJyNj/h+OWx7s5DePL6Zu1KeM9jPZhwBqs+7DzP6bGOvqzVCSf0xueYmVuaC/oQ/VtS2zLMLHdQFbkka+XDQ==
+ajv@^8.0.0, ajv@^8.0.1, ajv@^8.10.0, ajv@^8.8.0:
+  version "8.10.0"
+  resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.10.0.tgz#e573f719bd3af069017e3b66538ab968d040e54d"
+  integrity sha512-bzqAEZOjkrUMl2afH8dknrq5KEk2SrwdBROR+vH1EKVQTqaUbJVPdc/gEdggTMM0Se+s+Ja4ju4TlNcStKl2Hw==
   dependencies:
     fast-deep-equal "^3.1.1"
     json-schema-traverse "^1.0.0"