bookbindery 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. checksums.yaml +7 -0
  2. data/bin/bookbinder +6 -0
  3. data/lib/bookbinder.rb +59 -0
  4. data/lib/bookbinder/app_fetcher.rb +40 -0
  5. data/lib/bookbinder/archive.rb +95 -0
  6. data/lib/bookbinder/artifact_namer.rb +22 -0
  7. data/lib/bookbinder/blue_green_app.rb +27 -0
  8. data/lib/bookbinder/book.rb +54 -0
  9. data/lib/bookbinder/bookbinder_logger.rb +33 -0
  10. data/lib/bookbinder/cf_command_runner.rb +114 -0
  11. data/lib/bookbinder/cf_routes.rb +19 -0
  12. data/lib/bookbinder/cli.rb +68 -0
  13. data/lib/bookbinder/cli_error.rb +6 -0
  14. data/lib/bookbinder/code_example.rb +40 -0
  15. data/lib/bookbinder/command_runner.rb +32 -0
  16. data/lib/bookbinder/command_validator.rb +24 -0
  17. data/lib/bookbinder/commands/bookbinder_command.rb +18 -0
  18. data/lib/bookbinder/commands/build_and_push_tarball.rb +31 -0
  19. data/lib/bookbinder/commands/generate_pdf.rb +140 -0
  20. data/lib/bookbinder/commands/help.rb +31 -0
  21. data/lib/bookbinder/commands/naming.rb +9 -0
  22. data/lib/bookbinder/commands/publish.rb +138 -0
  23. data/lib/bookbinder/commands/push_local_to_staging.rb +35 -0
  24. data/lib/bookbinder/commands/push_to_prod.rb +35 -0
  25. data/lib/bookbinder/commands/run_publish_ci.rb +42 -0
  26. data/lib/bookbinder/commands/tag.rb +31 -0
  27. data/lib/bookbinder/commands/update_local_doc_repos.rb +27 -0
  28. data/lib/bookbinder/commands/version.rb +25 -0
  29. data/lib/bookbinder/configuration.rb +163 -0
  30. data/lib/bookbinder/configuration_fetcher.rb +55 -0
  31. data/lib/bookbinder/configuration_validator.rb +162 -0
  32. data/lib/bookbinder/css_link_checker.rb +64 -0
  33. data/lib/bookbinder/directory_helpers.rb +15 -0
  34. data/lib/bookbinder/distributor.rb +69 -0
  35. data/lib/bookbinder/git_client.rb +63 -0
  36. data/lib/bookbinder/git_hub_repository.rb +151 -0
  37. data/lib/bookbinder/local_file_system_accessor.rb +9 -0
  38. data/lib/bookbinder/middleman_runner.rb +86 -0
  39. data/lib/bookbinder/pdf_generator.rb +73 -0
  40. data/lib/bookbinder/publisher.rb +125 -0
  41. data/lib/bookbinder/pusher.rb +34 -0
  42. data/lib/bookbinder/remote_yaml_credential_provider.rb +21 -0
  43. data/lib/bookbinder/section.rb +78 -0
  44. data/lib/bookbinder/server_director.rb +53 -0
  45. data/lib/bookbinder/shell_out.rb +19 -0
  46. data/lib/bookbinder/sieve.rb +62 -0
  47. data/lib/bookbinder/sitemap_generator.rb +19 -0
  48. data/lib/bookbinder/spider.rb +91 -0
  49. data/lib/bookbinder/stabilimentum.rb +59 -0
  50. data/lib/bookbinder/usage_messenger.rb +33 -0
  51. data/lib/bookbinder/yaml_loader.rb +22 -0
  52. data/master_middleman/bookbinder_helpers.rb +133 -0
  53. data/master_middleman/config.rb +23 -0
  54. data/master_middleman/quicklinks_renderer.rb +78 -0
  55. data/master_middleman/submodule_aware_assets.rb +45 -0
  56. data/template_app/Gemfile +7 -0
  57. data/template_app/Gemfile.lock +20 -0
  58. data/template_app/app.rb +3 -0
  59. data/template_app/config.ru +9 -0
  60. data/template_app/lib/rack_static.rb +19 -0
  61. data/template_app/lib/vienna_application.rb +26 -0
  62. metadata +462 -0
@@ -0,0 +1,19 @@
1
+ module Bookbinder
2
+ class CfRoutes
3
+ def initialize(raw_routes)
4
+ @raw_routes = raw_routes
5
+ end
6
+
7
+ def apps_by_host_and_domain
8
+ @apps_by_host_and_domain ||= Hash[
9
+ raw_routes.lines[3..-1].
10
+ map { |line| line.split(/\s+/, 3) }.
11
+ map { |item| [item[0..1], item[2].split(',').map(&:strip)] }
12
+ ]
13
+ end
14
+
15
+ private
16
+
17
+ attr_reader :raw_routes
18
+ end
19
+ end
@@ -0,0 +1,68 @@
1
+ require_relative 'command_runner'
2
+ require_relative 'local_file_system_accessor'
3
+ require_relative 'command_validator'
4
+
5
+ require_relative 'commands/build_and_push_tarball'
6
+ require_relative 'commands/generate_pdf'
7
+ require_relative 'commands/publish'
8
+ require_relative 'commands/version'
9
+ require_relative 'commands/help'
10
+
11
+ module Bookbinder
12
+ class Cli
13
+ FLAGS = [
14
+ Commands::Version,
15
+ Commands::Help
16
+ ]
17
+
18
+ COMMANDS = [
19
+ Commands::BuildAndPushTarball,
20
+ Commands::GeneratePDF,
21
+ Commands::Publish,
22
+ Commands::PushLocalToStaging,
23
+ Commands::PushToProd,
24
+ Commands::RunPublishCI,
25
+ Commands::Tag,
26
+ Commands::UpdateLocalDocRepos,
27
+ ].freeze
28
+
29
+ def run(args)
30
+ command_name = args[0]
31
+ command_arguments = args[1..-1]
32
+
33
+ logger = BookbinderLogger.new
34
+ yaml_loader = YAMLLoader.new
35
+ local_file_system_accessor = LocalFileSystemAccessor.new
36
+ configuration_validator = ConfigurationValidator.new(logger, local_file_system_accessor)
37
+ configuration_fetcher = ConfigurationFetcher.new(logger, configuration_validator, yaml_loader)
38
+ configuration_fetcher.set_config_file_path './config.yml'
39
+ usage_messenger = UsageMessenger.new
40
+ usage_message = usage_messenger.construct_for(COMMANDS, FLAGS)
41
+ command_validator = CommandValidator.new usage_messenger, COMMANDS + FLAGS, usage_message
42
+
43
+ command_runner = CommandRunner.new(configuration_fetcher, usage_message, logger, COMMANDS + FLAGS)
44
+
45
+ begin
46
+ command_name ? command_validator.validate!(command_name) : command_name = '--help'
47
+
48
+ command_runner.run command_name, command_arguments
49
+
50
+ rescue Commands::Publish::VersionUnsupportedError => e
51
+ logger.error "config.yml at version '#{e.message}' has an unsupported API."
52
+ 1
53
+ rescue Configuration::CredentialKeyError => e
54
+ logger.error "#{e.message}, in credentials.yml"
55
+ 1
56
+ rescue KeyError => e
57
+ logger.error "#{e.message} from your configuration."
58
+ 1
59
+ rescue CliError::UnknownCommand => e
60
+ logger.log e.message
61
+ 1
62
+ rescue RuntimeError => e
63
+ logger.error e.message
64
+ 1
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,6 @@
1
+ module Bookbinder
2
+ module CliError
3
+ InvalidArguments = Class.new(StandardError)
4
+ UnknownCommand = Class.new(StandardError)
5
+ end
6
+ end
@@ -0,0 +1,40 @@
1
+ require_relative 'section'
2
+
3
+ module Bookbinder
4
+ class CodeExample < Section
5
+ class InvalidSnippet < StandardError
6
+ def initialize(repo, marker)
7
+ super "Error with marker #{marker.cyan} #{'in'.red} #{repo.cyan}#{'.'.red}"
8
+ end
9
+ end
10
+
11
+ def get_snippet_and_language_at(marker)
12
+ unless @repository.copied?
13
+ @repository.announce_skip
14
+ return ''
15
+ end
16
+
17
+ prepared_snippet_at(marker)
18
+ end
19
+
20
+ private
21
+
22
+ def prepared_snippet_at(marker)
23
+ snippet = ''
24
+ FileUtils.cd(@repository.copied_to) { snippet = scrape_for(marker) }
25
+
26
+ raise InvalidSnippet.new(full_name, marker) if snippet.empty?
27
+ lines = snippet.split("\n")
28
+ language_match = lines[0].match(/code_snippet #{Regexp.escape(marker)} start (\w+)/)
29
+ language = language_match[1] if language_match
30
+ [lines[1..-2].join("\n"), language]
31
+ end
32
+
33
+ def scrape_for(marker)
34
+ locale = 'LC_CTYPE=C LANG=C' # Quiets 'sed: RE error: illegal byte sequence'
35
+ result = `#{locale} find . -exec sed -ne '/code_snippet #{marker} start/,/code_snippet #{marker} end/ p' {} \\; 2> /dev/null`
36
+ result = "" unless result.lines.last && result.lines.last.match(/code_snippet #{marker} end/)
37
+ result
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,32 @@
1
+ require_relative 'cli_error'
2
+
3
+ module Bookbinder
4
+ class CommandRunner
5
+ def initialize(configuration_fetcher, usage_message, logger, commands)
6
+ @configuration_fetcher = configuration_fetcher
7
+ @usage_message = usage_message
8
+ @logger = logger
9
+ @commands = commands
10
+ end
11
+
12
+ def run(command_name, command_arguments)
13
+ command = commands.detect { |known_command| known_command.command_name == command_name }
14
+ begin
15
+ if command_name == '--help'
16
+ command.new(logger, usage_message).run command_arguments
17
+ else
18
+ command.new(logger, @configuration_fetcher).run command_arguments
19
+ end
20
+ rescue CliError::InvalidArguments
21
+ logger.log command.usage
22
+ 1
23
+ end
24
+ end
25
+
26
+ private
27
+
28
+ attr_reader :logger, :usage_message, :commands
29
+
30
+ end
31
+ end
32
+
@@ -0,0 +1,24 @@
1
+ require_relative 'cli_error'
2
+
3
+ module Bookbinder
4
+ class CommandValidator
5
+ def initialize(usage_messenger, commands, usage_text)
6
+ @usage_messenger = usage_messenger
7
+ @commands = commands
8
+ @usage_text = usage_text
9
+ end
10
+
11
+ def validate! command_name
12
+ known_command_names = commands.map(&:command_name)
13
+ command_type = "#{command_name}".match(/^--/) ? 'flag' : 'command'
14
+ if !known_command_names.include?(command_name)
15
+ raise CliError::UnknownCommand.new "Unrecognized #{command_type} '#{command_name}'\n" + usage_text
16
+ end
17
+ end
18
+
19
+ private
20
+
21
+ attr_reader :usage_messenger, :commands, :usage_text
22
+ end
23
+ end
24
+
@@ -0,0 +1,18 @@
1
+ module Bookbinder
2
+ module Commands
3
+ class BookbinderCommand
4
+ def initialize(logger, configuration_fetcher)
5
+ @logger = logger
6
+ @configuration_fetcher = configuration_fetcher
7
+ end
8
+
9
+ private
10
+
11
+ def config
12
+ @config ||= configuration_fetcher.fetch_config
13
+ end
14
+
15
+ attr_reader :configuration_fetcher
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,31 @@
1
+ require_relative 'naming'
2
+ require_relative 'bookbinder_command'
3
+ require_relative '../archive'
4
+
5
+ module Bookbinder
6
+ module Commands
7
+ class BuildAndPushTarball < BookbinderCommand
8
+ extend Commands::Naming
9
+
10
+ class MissingBuildNumber < StandardError
11
+ def initialize
12
+ super 'You must set $BUILD_NUMBER to push an identifiable build.'
13
+ end
14
+ end
15
+
16
+ def self.usage
17
+ "build_and_push_tarball \t \t \t Create a tarball from the final_app directory and push to the S3 bucket specified in your credentials.yml"
18
+ end
19
+
20
+ def run(_)
21
+ raise MissingBuildNumber unless ENV['BUILD_NUMBER']
22
+ config = configuration_fetcher.fetch_config
23
+ aws_credentials = config.aws_credentials
24
+ repository = Archive.new(logger: @logger, key: aws_credentials.access_key, secret: aws_credentials.secret_key)
25
+ repository.create_and_upload_tarball(build_number: ENV['BUILD_NUMBER'], bucket: aws_credentials.green_builds_bucket,
26
+ namespace: GitHubRepository.new(logger: @logger, full_name: config.book_repo).short_name)
27
+ 0
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,140 @@
1
+ require_relative '../pdf_generator'
2
+ require_relative '../server_director'
3
+ require_relative 'bookbinder_command'
4
+ require_relative 'naming'
5
+
6
+ module Bookbinder
7
+ module Commands
8
+ class GeneratePDF < BookbinderCommand
9
+ extend Commands::Naming
10
+
11
+ class AppNotPublished < StandardError
12
+ def initialize(msg='You must publish locally before you generate a PDF.')
13
+ super(msg)
14
+ end
15
+ end
16
+
17
+ class PDFConfigMissing < StandardError
18
+ def initialize(file)
19
+ super("PDF config file '#{file}' does not exist.")
20
+ end
21
+ end
22
+
23
+ class PDFOptionMissing < StandardError
24
+ def initialize(msg='No PDF options provided in config.yml')
25
+ super(msg)
26
+ end
27
+ end
28
+
29
+ class IncompletePDFConfig < StandardError
30
+ def initialize(file:nil, key:nil)
31
+ super("#{file} is missing required key '#{key}'")
32
+ end
33
+ end
34
+
35
+ def self.usage
36
+ "generate_pdf [<file_name>.yml] \t \t Generate a PDF from the files specified in <file_name.yml>"
37
+ end
38
+
39
+ def run(params)
40
+ raise AppNotPublished unless Dir.exists?('final_app')
41
+
42
+ @pdf_config_file = find_pdf_config_file(params)
43
+
44
+ ServerDirector.new(@logger, directory: 'final_app').use_server { |port| capture_pages_into_pdf_at(port) }
45
+ 0
46
+ end
47
+
48
+ def find_pdf_config_file(params)
49
+ if params.first
50
+ pdf_config_file = File.expand_path(params.first)
51
+ raise PDFConfigMissing, File.basename(pdf_config_file) unless File.exists?(pdf_config_file)
52
+ pdf_config_file
53
+ else
54
+ @logger.warn "Declaring PDF options in config.yml is deprecated.\nDeclare them in a PDF config file, instead, and target that file when you re-invoke bookbinder.\ne.g. bookbinder generate_pdf theGoodParts.yml"
55
+ end
56
+ end
57
+
58
+ private
59
+
60
+ def capture_pages_into_pdf_at(port)
61
+ local_host = "localhost:#{port}"
62
+ header = pdf_options.fetch('header') { raise IncompletePDFConfig, {file: @pdf_config_file, key: :header} }
63
+
64
+ output_filename = get_output_file_name
65
+ urls_to_capture = get_urls_to_capture(local_host)
66
+
67
+ PdfGenerator.new(@logger).generate(urls_to_capture, output_filename, "http://#{local_host}/#{header}", pdf_options['copyright_notice'])
68
+ end
69
+
70
+ def pdf_options
71
+ @pdf_options ||= @pdf_config_file ? YAML.load(File.read(@pdf_config_file)) : main_config_pdf_options
72
+ end
73
+
74
+ def main_config_pdf_options
75
+ raise PDFOptionMissing unless config.has_option?('pdf')
76
+ {
77
+ 'filename' => config.pdf.fetch('filename'),
78
+ 'header' => config.pdf.fetch('header')
79
+ }
80
+ end
81
+
82
+ def get_output_file_name
83
+ if @pdf_config_file
84
+ File.basename(@pdf_config_file).gsub(/yml/, 'pdf')
85
+ else
86
+ pdf_options.fetch('filename')
87
+ end
88
+ end
89
+
90
+ def get_urls_to_capture(local_host)
91
+ if @pdf_config_file
92
+ pages = pdf_options.fetch('pages')
93
+ return expand_urls(pages, local_host) if any_include_wildcard?(pages)
94
+ pages.map { |l| "http://#{local_host}/#{l}" }
95
+ else
96
+ sitemap_links(local_host)
97
+ end
98
+ end
99
+
100
+ def any_include_wildcard?(pages)
101
+ pages.each do |page|
102
+ return true if page.include?('*')
103
+ end
104
+ false
105
+ end
106
+
107
+ def expand_urls(pages, local_host)
108
+ final_pages = []
109
+ pages.each do |page|
110
+ if page.include?('*')
111
+ matching_pages = find_matching_files_in_directory(page, local_host)
112
+ final_pages += matching_pages
113
+ else
114
+ final_pages << "http://#{local_host}/#{page}"
115
+ end
116
+ end
117
+ final_pages
118
+ end
119
+
120
+ def find_matching_files_in_directory(wildcard_page, local_host)
121
+ wildcard_path = wildcard_page.gsub('*','')
122
+ possible_links = sitemap_links(local_host)
123
+ possible_links.select { |link| URI(link).path.match(/^\/#{Regexp.escape(wildcard_path)}/)}
124
+ end
125
+
126
+ def sitemap_links(local_host)
127
+ raw_links = Nokogiri::XML(sitemap).css('loc').map &:inner_html
128
+ deployed_host = URI(raw_links[0]).host
129
+
130
+ deployed_host ?
131
+ raw_links.map { |l| l.gsub(/#{Regexp.escape(deployed_host)}/, local_host) } :
132
+ raw_links.map { |l| "http://#{local_host}/#{l}" }
133
+ end
134
+
135
+ def sitemap
136
+ File.read File.join('public', 'sitemap.xml')
137
+ end
138
+ end
139
+ end
140
+ end
@@ -0,0 +1,31 @@
1
+ module Bookbinder
2
+ module Commands
3
+ class Help
4
+ def initialize(logger, usage_message)
5
+ @logger = logger
6
+ @usage_message = usage_message
7
+ end
8
+
9
+ def self.to_s
10
+ 'help'
11
+ end
12
+
13
+ def self.command_name
14
+ '--help'
15
+ end
16
+
17
+ def self.usage
18
+ "--help \t \t \t \t \t Print this message"
19
+ end
20
+
21
+ def run(*)
22
+ logger.log(usage_message)
23
+ 0
24
+ end
25
+
26
+ private
27
+
28
+ attr_reader :logger, :usage_message
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,9 @@
1
+ module Bookbinder
2
+ module Commands
3
+ module Naming
4
+ def command_name
5
+ name.demodulize.underscore
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,138 @@
1
+ require_relative '../book'
2
+ require_relative '../cli_error'
3
+ require_relative '../configuration'
4
+ require_relative '../directory_helpers'
5
+ require_relative '../middleman_runner'
6
+ require_relative '../publisher'
7
+ require_relative '../spider'
8
+ require_relative 'bookbinder_command'
9
+ require_relative 'naming'
10
+
11
+ module Bookbinder
12
+ module Commands
13
+ class Publish < BookbinderCommand
14
+ VersionUnsupportedError = Class.new(StandardError)
15
+
16
+ include Bookbinder::DirectoryHelperMethods
17
+ extend Commands::Naming
18
+
19
+ def self.usage
20
+ "publish <local|github> [--verbose] \t Bind the sections specified in config.yml from <local> or <github> into the final_app directory"
21
+ end
22
+
23
+ def run(cli_arguments, git_accessor=Git)
24
+ raise CliError::InvalidArguments unless arguments_are_valid?(cli_arguments)
25
+ @git_accessor = git_accessor
26
+
27
+ final_app_dir = File.absolute_path('final_app')
28
+ bind_book(cli_arguments, final_app_dir)
29
+ end
30
+
31
+ private
32
+
33
+ def bind_book(cli_arguments, final_app_dir)
34
+ generate_site_etc(cli_arguments, final_app_dir)
35
+ end
36
+
37
+ def generate_site_etc(cli_args, final_app_dir, target_tag=nil)
38
+ # TODO: general solution to turn all string keys to symbols
39
+ verbosity = cli_args.include?('--verbose')
40
+ location = cli_args[0]
41
+
42
+ cli_options = { verbose: verbosity, target_tag: target_tag }
43
+ output_paths = output_directory_paths(location, final_app_dir)
44
+ publish_config = publish_config(location)
45
+ spider = Spider.new(@logger, app_dir: final_app_dir)
46
+ static_site_generator = MiddlemanRunner.new(@logger)
47
+
48
+ success = Publisher.new(@logger, spider, static_site_generator).publish(cli_options, output_paths, publish_config, @git_accessor)
49
+ success ? 0 : 1
50
+ end
51
+
52
+ def output_directory_paths(location, final_app_dir)
53
+ local_repo_dir = (location == 'local') ? File.absolute_path('..') : nil
54
+
55
+ {
56
+ final_app_dir: final_app_dir,
57
+ local_repo_dir: local_repo_dir,
58
+ output_dir: File.absolute_path(output_dir_name),
59
+ master_middleman_dir: layout_repo_path(local_repo_dir)
60
+ }
61
+ end
62
+
63
+ def publish_config(location)
64
+ arguments = {
65
+ sections: config.sections,
66
+ book_repo: config.book_repo,
67
+ host_for_sitemap: config.public_host,
68
+ archive_menu: config.archive_menu
69
+ }
70
+
71
+ optional_arguments = {}
72
+ optional_arguments.merge!(template_variables: config.template_variables) if config.respond_to?(:template_variables)
73
+ if publishing_to_github? location
74
+ config.versions.each { |version| arguments[:sections].concat sections_from version, @git_accessor }
75
+ optional_arguments.merge!(versions: config.versions)
76
+ end
77
+
78
+ arguments.merge! optional_arguments
79
+ end
80
+
81
+ def sections_from(version, git_accessor)
82
+ config_file = File.join book_checkout(version, git_accessor), 'config.yml'
83
+ attrs = YAML.load(File.read(config_file))['sections']
84
+ raise VersionUnsupportedError.new(version) if attrs.nil?
85
+
86
+ attrs.map do |section_hash|
87
+ section_hash['repository']['ref'] = version
88
+ section_hash['directory'] = File.join(version, section_hash['directory'])
89
+ section_hash
90
+ end
91
+ end
92
+
93
+ def book_checkout(ref, git_accessor=Git)
94
+ temp_workspace = Dir.mktmpdir('book_checkout')
95
+ book = Book.from_remote(logger: @logger,
96
+ full_name: config.book_repo,
97
+ destination_dir: temp_workspace,
98
+ ref: ref,
99
+ git_accessor: git_accessor,
100
+ )
101
+
102
+ File.join temp_workspace, book.directory
103
+ end
104
+
105
+ def layout_repo_path(local_repo_dir)
106
+ if config.has_option?('layout_repo')
107
+ if local_repo_dir
108
+ File.join(local_repo_dir, config.layout_repo.split('/').last)
109
+ else
110
+ section = {'repository' => {'name' => config.layout_repo}}
111
+ destination_dir = Dir.mktmpdir
112
+ repository = GitHubRepository.build_from_remote(@logger, section, destination_dir, 'master', @git_accessor)
113
+ if repository
114
+ File.join(destination_dir, repository.directory)
115
+ else
116
+ raise 'failed to fetch repository'
117
+ end
118
+ end
119
+ else
120
+ File.absolute_path('master_middleman')
121
+ end
122
+ end
123
+
124
+ def arguments_are_valid?(arguments)
125
+ return false unless arguments.any?
126
+ verbose = arguments[1] && arguments[1..-1].include?('--verbose')
127
+ tag_provided = arguments[1] && (arguments[1..-1] - ['--verbose']).any?
128
+ nothing_special = arguments[1..-1].empty?
129
+
130
+ %w(local github).include?(arguments[0]) && (tag_provided || verbose || nothing_special)
131
+ end
132
+
133
+ def publishing_to_github?(publish_location)
134
+ config.has_option?('versions') && publish_location != 'local'
135
+ end
136
+ end
137
+ end
138
+ end