diff options
Diffstat (limited to 'scripts/lib')
-rw-r--r-- | scripts/lib/glfm/constants.rb | 24 | ||||
-rw-r--r-- | scripts/lib/glfm/render_static_html.rb | 16 | ||||
-rw-r--r-- | scripts/lib/glfm/update_example_snapshots.rb | 57 | ||||
-rw-r--r-- | scripts/lib/glfm/update_specification.rb | 140 | ||||
-rw-r--r-- | scripts/lib/glfm/verify_all_generated_files_are_up_to_date.rb | 48 |
5 files changed, 231 insertions, 54 deletions
diff --git a/scripts/lib/glfm/constants.rb b/scripts/lib/glfm/constants.rb index e5790bbdd88..d020d2fec5c 100644 --- a/scripts/lib/glfm/constants.rb +++ b/scripts/lib/glfm/constants.rb @@ -10,25 +10,29 @@ module Glfm # GitHub Flavored Markdown specification file GHFM_SPEC_TXT_URI = 'https://raw.githubusercontent.com/github/cmark-gfm/master/test/spec.txt' GHFM_SPEC_VERSION = '0.29' - GHFM_SPEC_TXT_FILENAME = "ghfm_spec_v_#{GHFM_SPEC_VERSION}.txt" - GHFM_SPEC_TXT_PATH = specification_path.join('input/github_flavored_markdown', GHFM_SPEC_TXT_FILENAME) + GHFM_SPEC_MD_FILENAME = "ghfm_spec_v_#{GHFM_SPEC_VERSION}.md" + GHFM_SPEC_MD_PATH = specification_path.join('input/github_flavored_markdown', GHFM_SPEC_MD_FILENAME) # GitLab Flavored Markdown specification files specification_input_glfm_path = specification_path.join('input/gitlab_flavored_markdown') - GLFM_INTRO_TXT_PATH = specification_input_glfm_path.join('glfm_intro.txt') - GLFM_EXAMPLES_TXT_PATH = specification_input_glfm_path.join('glfm_canonical_examples.txt') + GLFM_INTRO_MD_PATH = specification_input_glfm_path.join('glfm_intro.md') + GLFM_OFFICIAL_SPECIFICATION_EXAMPLES_MD_PATH = + specification_input_glfm_path.join('glfm_official_specification_examples.md') + GLFM_INTERNAL_EXTENSION_EXAMPLES_MD_PATH = specification_input_glfm_path.join('glfm_internal_extension_examples.md') GLFM_EXAMPLE_STATUS_YML_PATH = specification_input_glfm_path.join('glfm_example_status.yml') GLFM_EXAMPLE_METADATA_YML_PATH = specification_input_glfm_path.join('glfm_example_metadata.yml') GLFM_EXAMPLE_NORMALIZATIONS_YML_PATH = specification_input_glfm_path.join('glfm_example_normalizations.yml') - GLFM_SPEC_TXT_PATH = specification_path.join('output/spec.txt') + GLFM_SPEC_OUTPUT_PATH = specification_path.join('output') + GLFM_SPEC_TXT_PATH = GLFM_SPEC_OUTPUT_PATH.join('spec.txt') + GLFM_SPEC_HTML_PATH = GLFM_SPEC_OUTPUT_PATH.join('spec.html') # Example Snapshot (ES) files - es_fixtures_path = File.expand_path("../../../glfm_specification/example_snapshots", __dir__) - ES_EXAMPLES_INDEX_YML_PATH = File.join(es_fixtures_path, 'examples_index.yml') - ES_MARKDOWN_YML_PATH = File.join(es_fixtures_path, 'markdown.yml') - ES_HTML_YML_PATH = File.join(es_fixtures_path, 'html.yml') - ES_PROSEMIRROR_JSON_YML_PATH = File.join(es_fixtures_path, 'prosemirror_json.yml') + EXAMPLE_SNAPSHOTS_PATH = File.expand_path("../../../glfm_specification/example_snapshots", __dir__) + ES_EXAMPLES_INDEX_YML_PATH = File.join(EXAMPLE_SNAPSHOTS_PATH, 'examples_index.yml') + ES_MARKDOWN_YML_PATH = File.join(EXAMPLE_SNAPSHOTS_PATH, 'markdown.yml') + ES_HTML_YML_PATH = File.join(EXAMPLE_SNAPSHOTS_PATH, 'html.yml') + ES_PROSEMIRROR_JSON_YML_PATH = File.join(EXAMPLE_SNAPSHOTS_PATH, 'prosemirror_json.yml') # Other constants used for processing files GLFM_SPEC_TXT_HEADER = <<~MARKDOWN diff --git a/scripts/lib/glfm/render_static_html.rb b/scripts/lib/glfm/render_static_html.rb index 8d72aec7c3b..6af73cd845d 100644 --- a/scripts/lib/glfm/render_static_html.rb +++ b/scripts/lib/glfm/render_static_html.rb @@ -20,21 +20,26 @@ require_relative 'shared' # Factorybot factory methods to create persisted model objects with stable # and consistent data values, to ensure consistent example snapshot HTML # across various machines and environments. RSpec also makes it easy to invoke -# the API # and obtain the response. +# the API and obtain the response. # # It is intended to be invoked as a helper subprocess from the `update_example_snapshots.rb` # script class. It's not intended to be run or used directly. This usage is also reinforced # by not naming the file with a `_spec.rb` ending. -RSpec.describe 'Render Static HTML', :api, type: :request do # rubocop:disable RSpec/TopLevelDescribePath +RSpec.describe 'Render Static HTML', :api, type: :request do include Glfm::Constants include Glfm::Shared # noinspection RailsParamDefResolve (RubyMine can't find the shared context from this file location) include_context 'with GLFM example snapshot fixtures' - it 'can create a project dependency graph using factories' do + it do markdown_hash = YAML.safe_load(File.open(ENV.fetch('INPUT_MARKDOWN_YML_PATH')), symbolize_names: true) - metadata_hash = YAML.safe_load(File.open(ENV.fetch('INPUT_METADATA_YML_PATH')), symbolize_names: true) + metadata_hash = + if input_metadata_yml_path = ENV['INPUT_METADATA_YML_PATH'] + YAML.safe_load(File.open(input_metadata_yml_path), symbolize_names: true) || {} + else + {} + end # NOTE: We cannot parallelize this loop like the Javascript WYSIWYG example generation does, # because the rspec `post` API cannot be parallized (it is not thread-safe, it can't find @@ -66,8 +71,7 @@ RSpec.describe 'Render Static HTML', :api, type: :request do # rubocop:disable R private def write_output_file(static_html_hash) - tmpfile = File.open(ENV.fetch('OUTPUT_STATIC_HTML_TEMPFILE_PATH'), 'w') yaml_string = dump_yaml_with_formatting(static_html_hash) - write_file(tmpfile, yaml_string) + write_file(ENV.fetch('OUTPUT_STATIC_HTML_TEMPFILE_PATH'), yaml_string) end end diff --git a/scripts/lib/glfm/update_example_snapshots.rb b/scripts/lib/glfm/update_example_snapshots.rb index 7dc0d0f7c4b..9075260e748 100644 --- a/scripts/lib/glfm/update_example_snapshots.rb +++ b/scripts/lib/glfm/update_example_snapshots.rb @@ -5,11 +5,12 @@ require 'yaml' require 'psych' require 'tempfile' require 'open3' +require 'active_support/core_ext/enumerable' require_relative 'constants' require_relative 'shared' require_relative 'parse_examples' -# IMPORTANT NOTE: See https://docs.gitlab.com/ee/development/gitlab_flavored_markdown/specification_guide/ +# IMPORTANT NOTE: See https://docs.gitlab.com/ee/development/gitlab_flavored_markdown/specification_guide/#update-example-snapshotsrb-script # for details on the implementation and usage of this script. This developers guide # contains diagrams and documentation of this script, # including explanations and examples of all files it reads and writes. @@ -29,8 +30,6 @@ module Glfm def process(skip_static_and_wysiwyg: false) output('Updating example snapshots...') - output('(Skipping static HTML generation)') if skip_static_and_wysiwyg - output("Reading #{GLFM_SPEC_TXT_PATH}...") glfm_spec_txt_lines = File.open(GLFM_SPEC_TXT_PATH).readlines @@ -115,11 +114,13 @@ module Glfm def write_snapshot_example_files(all_examples, skip_static_and_wysiwyg:) output("Reading #{GLFM_EXAMPLE_STATUS_YML_PATH}...") - glfm_examples_statuses = YAML.safe_load(File.open(GLFM_EXAMPLE_STATUS_YML_PATH), symbolize_names: true) + glfm_examples_statuses = YAML.safe_load(File.open(GLFM_EXAMPLE_STATUS_YML_PATH), symbolize_names: true) || {} validate_glfm_example_status_yml(glfm_examples_statuses) write_examples_index_yml(all_examples) + validate_glfm_config_file_example_names(all_examples) + write_markdown_yml(all_examples) if skip_static_and_wysiwyg @@ -151,6 +152,50 @@ module Glfm end end + def validate_glfm_config_file_example_names(all_examples) + valid_example_names = all_examples.pluck(:name).map(&:to_sym) # rubocop:disable CodeReuse/ActiveRecord + + # We are re-reading GLFM_EXAMPLE_STATUS_YML_PATH here, but that's OK, it's a small file, and rereading it + # allows us to handle it in the same loop as the other manually-curated config files. + [ + GLFM_EXAMPLE_STATUS_YML_PATH, + GLFM_EXAMPLE_METADATA_YML_PATH, + GLFM_EXAMPLE_NORMALIZATIONS_YML_PATH + ].each do |path| + output("Reading #{path}...") + io = File.open(path) + config_file_examples = YAML.safe_load(io, symbolize_names: true, aliases: true) + + # Skip validation if the config file is empty + next unless config_file_examples + + config_file_example_names = config_file_examples.keys + + # Validate that all example names exist in the config file refer to an existing example in `examples_index.yml`, + # unless it starts with the special prefix `00_`, which is preserved for usage as YAML anchors. + invalid_name = config_file_example_names.detect do |name| + !name.start_with?('00_') && valid_example_names.exclude?(name) + end + next unless invalid_name + + # NOTE: The extra spaces before punctuation in the error message allows for easier copy/pasting of the paths. + err_msg = + <<~TXT + + Error in input specification config file #{path} : + + Config file entry named #{invalid_name} + does not have a corresponding example entry in + #{ES_EXAMPLES_INDEX_YML_PATH} . + + Please delete or rename this config file entry. + + If this entry is being used as a YAML anchor, please rename it to start with '00_'. + TXT + raise err_msg + end + end + def write_examples_index_yml(all_examples) generate_and_write_for_all_examples( all_examples, ES_EXAMPLES_INDEX_YML_PATH, literal_scalars: false @@ -219,7 +264,7 @@ module Glfm # NOTE 2: We run this as an RSpec process, for the same reasons we run via Jest process below: # because that's the easiest way to ensure a reliable, fully-configured environment in which - # to execute the markdown-generation logic. Also, in the static/backend case, Rspec + # to execute the markdown-processing logic. Also, in the static/backend case, Rspec # provides the easiest and most reliable way to generate example data via Factorybot # creation of stable model records. This ensures consistent snapshot values across # machines/environments. @@ -244,7 +289,7 @@ module Glfm wysiwyg_html_and_json_tempfile_path = Dir::Tmpname.create(WYSIWYG_HTML_AND_JSON_TEMPFILE_BASENAME) {} ENV['OUTPUT_WYSIWYG_HTML_AND_JSON_TEMPFILE_PATH'] = wysiwyg_html_and_json_tempfile_path - cmd = %(yarn jest --testMatch '**/render_wysiwyg_html_and_json.js' #{__dir__}/render_wysiwyg_html_and_json.js) + cmd = "yarn jest --testMatch '**/render_wysiwyg_html_and_json.js' #{__dir__}/render_wysiwyg_html_and_json.js" run_external_cmd(cmd) output("Reading generated WYSIWYG HTML and prosemirror JSON from tempfile " \ diff --git a/scripts/lib/glfm/update_specification.rb b/scripts/lib/glfm/update_specification.rb index 73c23d40de5..c7264547e44 100644 --- a/scripts/lib/glfm/update_specification.rb +++ b/scripts/lib/glfm/update_specification.rb @@ -2,26 +2,45 @@ require 'fileutils' require 'open-uri' require 'pathname' +require 'tempfile' +require 'yaml' require_relative 'constants' require_relative 'shared' +# IMPORTANT NOTE: See https://docs.gitlab.com/ee/development/gitlab_flavored_markdown/specification_guide/#update-specificationrb-script +# for details on the implementation and usage of this script. This developers guide +# contains diagrams and documentation of this script, +# including explanations and examples of all files it reads and writes. +# +# Also note that this script is intentionally written in a pure-functional (not OO) style, +# with no dependencies on Rails or the GitLab libraries. These choices are intended to make +# it faster and easier to test and debug. module Glfm class UpdateSpecification include Constants include Shared - def process + def process(skip_spec_html_generation: false) output('Updating specification...') - ghfm_spec_txt_lines = load_ghfm_spec_txt - glfm_spec_txt_string = build_glfm_spec_txt(ghfm_spec_txt_lines) + + ghfm_spec_lines = load_ghfm_spec + glfm_spec_txt_string = build_glfm_spec_txt(ghfm_spec_lines) write_glfm_spec_txt(glfm_spec_txt_string) + + if skip_spec_html_generation + output("Skipping GLFM spec.html generation...") + return + end + + glfm_spec_html_string = generate_glfm_spec_html(glfm_spec_txt_string) + write_glfm_spec_html(glfm_spec_html_string) end private - def load_ghfm_spec_txt + def load_ghfm_spec # We only re-download the GitHub Flavored Markdown specification if the - # UPDATE_GHFM_SPEC_TXT environment variable is set to true, which should only + # UPDATE_GHFM_SPEC_MD environment variable is set to true, which should only # ever be done manually and locally, never in CI. This provides some security # protection against a possible injection attack vector, if the GitHub-hosted # version of the spec is ever temporarily compromised with an injection attack. @@ -29,40 +48,44 @@ module Glfm # This also avoids doing external network access to download the file # in CI jobs, which can avoid potentially flaky builds if the GitHub-hosted # version of the file is temporarily unavailable. - if ENV['UPDATE_GHFM_SPEC_TXT'] == 'true' - download_and_write_ghfm_spec_txt + if ENV['UPDATE_GHFM_SPEC_MD'] == 'true' + update_ghfm_spec_md else - read_existing_ghfm_spec_txt + read_existing_ghfm_spec_md end end - def read_existing_ghfm_spec_txt - output("Reading existing #{GHFM_SPEC_TXT_PATH}...") - File.open(GHFM_SPEC_TXT_PATH).readlines + def read_existing_ghfm_spec_md + output("Reading existing #{GHFM_SPEC_MD_PATH}...") + File.open(GHFM_SPEC_MD_PATH).readlines end - def download_and_write_ghfm_spec_txt + def update_ghfm_spec_md output("Downloading #{GHFM_SPEC_TXT_URI}...") - ghfm_spec_txt_uri_io = URI.open(GHFM_SPEC_TXT_URI) + # NOTE: We use `URI.parse` to avoid RuboCop warning "Security/Open", + # even though we are using a trusted URI from a string literal constant. + # See https://gitlab.com/gitlab-org/gitlab/-/merge_requests/98656#note_1138595002 for details. + ghfm_spec_txt_uri_parsed = URI.parse(GHFM_SPEC_TXT_URI) + ghfm_spec_txt_uri_io = ghfm_spec_txt_uri_parsed.open # Read IO stream into an array of lines for easy processing later - ghfm_spec_txt_lines = ghfm_spec_txt_uri_io.readlines - raise "Unable to read lines from #{GHFM_SPEC_TXT_URI}" if ghfm_spec_txt_lines.empty? + ghfm_spec_lines = ghfm_spec_txt_uri_io.readlines + raise "Unable to read lines from #{GHFM_SPEC_TXT_URI}" if ghfm_spec_lines.empty? # Make sure the GHFM spec version has not changed - validate_expected_spec_version!(ghfm_spec_txt_lines[2]) + validate_expected_spec_version!(ghfm_spec_lines[2]) # Reset IO stream and re-read into a single string for easy writing # noinspection RubyNilAnalysis ghfm_spec_txt_uri_io.seek(0) - ghfm_spec_txt_string = ghfm_spec_txt_uri_io.read - raise "Unable to read string from #{GHFM_SPEC_TXT_URI}" unless ghfm_spec_txt_string + ghfm_spec_string = ghfm_spec_txt_uri_io.read + raise "Unable to read string from #{GHFM_SPEC_TXT_URI}" unless ghfm_spec_string - output("Writing #{GHFM_SPEC_TXT_PATH}...") - GHFM_SPEC_TXT_PATH.dirname.mkpath - write_file(GHFM_SPEC_TXT_PATH, ghfm_spec_txt_string) + output("Writing #{GHFM_SPEC_MD_PATH}...") + GHFM_SPEC_MD_PATH.dirname.mkpath + write_file(GHFM_SPEC_MD_PATH, ghfm_spec_string) - ghfm_spec_txt_lines + ghfm_spec_lines end def validate_expected_spec_version!(version_line) @@ -76,7 +99,7 @@ module Glfm glfm_spec_txt_lines = ghfm_spec_txt_lines.dup replace_header(glfm_spec_txt_lines) replace_intro_section(glfm_spec_txt_lines) - insert_examples_txt(glfm_spec_txt_lines) + insert_examples(glfm_spec_txt_lines) glfm_spec_txt_lines.join('') end @@ -85,13 +108,13 @@ module Glfm end def replace_intro_section(spec_txt_lines) - glfm_intro_txt_lines = File.open(GLFM_INTRO_TXT_PATH).readlines - raise "Unable to read lines from #{GLFM_INTRO_TXT_PATH}" if glfm_intro_txt_lines.empty? + glfm_intro_md_lines = File.open(GLFM_INTRO_MD_PATH).readlines + raise "Unable to read lines from #{GLFM_INTRO_MD_PATH}" if glfm_intro_md_lines.empty? ghfm_intro_header_begin_index = spec_txt_lines.index do |line| line =~ INTRODUCTION_HEADER_LINE_TEXT end - raise "Unable to locate introduction header line in #{GHFM_SPEC_TXT_PATH}" if ghfm_intro_header_begin_index.nil? + raise "Unable to locate introduction header line in #{GHFM_SPEC_MD_PATH}" if ghfm_intro_header_begin_index.nil? # Find the index of the next header after the introduction header, starting from the index # of the introduction header this is the length of the intro section @@ -100,20 +123,29 @@ module Glfm end # Replace the intro section with the GitLab flavored Markdown intro section - spec_txt_lines[ghfm_intro_header_begin_index, ghfm_intro_section_length] = glfm_intro_txt_lines + spec_txt_lines[ghfm_intro_header_begin_index, ghfm_intro_section_length] = glfm_intro_md_lines end - def insert_examples_txt(spec_txt_lines) - glfm_examples_txt_lines = File.open(GLFM_EXAMPLES_TXT_PATH).readlines - raise "Unable to read lines from #{GLFM_EXAMPLES_TXT_PATH}" if glfm_examples_txt_lines.empty? + def insert_examples(spec_txt_lines) + official_spec_lines = File.open(GLFM_OFFICIAL_SPECIFICATION_EXAMPLES_MD_PATH).readlines + raise "Unable to read lines from #{GLFM_OFFICIAL_SPECIFICATION_EXAMPLES_MD_PATH}" if official_spec_lines.empty? + + internal_extension_lines = File.open(GLFM_INTERNAL_EXTENSION_EXAMPLES_MD_PATH).readlines + raise "Unable to read lines from #{GLFM_INTERNAL_EXTENSION_EXAMPLES_MD_PATH}" if internal_extension_lines.empty? ghfm_end_tests_comment_index = spec_txt_lines.index do |line| line =~ END_TESTS_COMMENT_LINE_TEXT end - raise "Unable to locate 'END TESTS' comment line in #{GHFM_SPEC_TXT_PATH}" if ghfm_end_tests_comment_index.nil? + raise "Unable to locate 'END TESTS' comment line in #{GHFM_SPEC_MD_PATH}" if ghfm_end_tests_comment_index.nil? # Insert the GLFM examples before the 'END TESTS' comment line - spec_txt_lines[ghfm_end_tests_comment_index - 1] = ["\n", glfm_examples_txt_lines, "\n"].flatten + spec_txt_lines[ghfm_end_tests_comment_index - 1] = [ + "\n", + official_spec_lines, + "\n", + internal_extension_lines, + "\n" + ].flatten spec_txt_lines end @@ -123,5 +155,49 @@ module Glfm FileUtils.mkdir_p(Pathname.new(GLFM_SPEC_TXT_PATH).dirname) write_file(GLFM_SPEC_TXT_PATH, glfm_spec_txt_string) end + + def generate_glfm_spec_html(glfm_spec_txt_string) + output("Generating spec.html from spec.txt markdown...") + + input_markdown_yml_string = <<~MARKDOWN + --- + spec_txt: | + #{glfm_spec_txt_string.gsub(/^/, ' ')} + MARKDOWN + + # NOTE: We must copy the input YAML file used by the `render_static_html.rb` + # to a separate temporary file in order for the script to read them, because it is run in + # a separate subprocess, and during unit testing we are unable to substitute the mock + # StringIO when reading the input files in the subprocess. + ENV['INPUT_MARKDOWN_YML_PATH'] = Dir::Tmpname.create(MARKDOWN_TEMPFILE_BASENAME) do |path| + write_file(path, input_markdown_yml_string) + end + + # NOTE 1: We shell out to perform the conversion of markdown to static HTML by invoking a + # separate subprocess. This allows us to avoid using the Rails API or environment in this + # script, which makes developing and running the unit tests for this script much faster, + # because they can use 'fast_spec_helper' which does not require the entire Rails environment. + + # NOTE 2: We run this as an RSpec process, for the same reasons we run via Jest process below: + # because that's the easiest way to ensure a reliable, fully-configured environment in which + # to execute the markdown-processing logic. Also, in the static/backend case. + + # Dir::Tmpname.create requires a block, but we are using the non-block form to get the path + # via the return value, so we pass an empty block to avoid an error. + static_html_tempfile_path = Dir::Tmpname.create(STATIC_HTML_TEMPFILE_BASENAME) {} + ENV['OUTPUT_STATIC_HTML_TEMPFILE_PATH'] = static_html_tempfile_path + + cmd = %(bin/rspec #{__dir__}/render_static_html.rb) + run_external_cmd(cmd) + + output("Reading generated spec.html from tempfile #{static_html_tempfile_path}...") + YAML.safe_load(File.open(static_html_tempfile_path), symbolize_names: true).fetch(:spec_txt) + end + + def write_glfm_spec_html(glfm_spec_html_string) + output("Writing #{GLFM_SPEC_TXT_PATH}...") + FileUtils.mkdir_p(Pathname.new(GLFM_SPEC_HTML_PATH).dirname) + write_file(GLFM_SPEC_HTML_PATH, "#{glfm_spec_html_string}\n") + end end end diff --git a/scripts/lib/glfm/verify_all_generated_files_are_up_to_date.rb b/scripts/lib/glfm/verify_all_generated_files_are_up_to_date.rb new file mode 100644 index 00000000000..0b824fc589d --- /dev/null +++ b/scripts/lib/glfm/verify_all_generated_files_are_up_to_date.rb @@ -0,0 +1,48 @@ +# frozen_string_literal: true +require_relative 'constants' +require_relative 'shared' + +# IMPORTANT NOTE: See https://docs.gitlab.com/ee/development/gitlab_flavored_markdown/specification_guide/#verify-all-generated-files-are-up-to-daterb-script +# for details on the implementation and usage of this script. This developers guide +# contains diagrams and documentation of this script, +# including explanations and examples of all files it reads and writes. +module Glfm + class VerifyAllGeneratedFilesAreUpToDate + include Constants + include Shared + + def process + verify_cmd = "git status --porcelain #{GLFM_SPEC_OUTPUT_PATH} #{EXAMPLE_SNAPSHOTS_PATH}" + verify_cmd_output = run_external_cmd(verify_cmd) + unless verify_cmd_output.empty? + msg = "ERROR: Cannot run `#{__FILE__}` because `#{verify_cmd}` shows the following uncommitted changes:\n" \ + "#{verify_cmd_output}" + raise(msg) + end + + output('Verifying all generated files are up to date after running GLFM scripts...') + + output("Running `yarn install --frozen-lockfile` to ensure `yarn check-dependencies` doesn't fail...") + run_external_cmd('yarn install --frozen-lockfile') + + # noinspection RubyMismatchedArgumentType + update_specification_script = File.expand_path('../../glfm/update-specification.rb', __dir__) + # noinspection RubyMismatchedArgumentType + update_example_snapshots_script = File.expand_path('../../glfm/update-example-snapshots.rb', __dir__) + + output("Running `#{update_specification_script}`...") + run_external_cmd(update_specification_script) + + output("Running `#{update_example_snapshots_script}`...") + run_external_cmd(update_example_snapshots_script) + + output("Running `#{verify_cmd}` to check that no modifications to generated files have occurred...") + verify_cmd_output = run_external_cmd(verify_cmd) + + return if verify_cmd_output.empty? + + raise "The following files were modified by running GLFM scripts. Please review, verify, and commit " \ + "the changes:\n#{verify_cmd_output}" + end + end +end |