bookbindery 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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