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,73 @@
1
+ require 'net/http'
2
+
3
+ module Bookbinder
4
+ class PdfGenerator
5
+ class MissingSource < StandardError
6
+ def initialize(required_file)
7
+ super "Could not find file #{required_file}"
8
+ end
9
+ end
10
+
11
+ def initialize(logger)
12
+ @logger = logger
13
+ end
14
+
15
+ def generate(sources, target, header, left_footer=nil)
16
+ sources.each { |s| check_destination_exists s }
17
+ check_destination_exists header
18
+
19
+ left_footer ||= " © Copyright 2013-#{Time.now.year}, Pivotal"
20
+
21
+ toc_xslt_path = File.expand_path('../../../toc.xslt', __FILE__)
22
+
23
+ command = <<-CMD
24
+ wkhtmltopdf \
25
+ --disable-external-links \
26
+ --disable-javascript \
27
+ --load-error-handling ignore \
28
+ --margin-top 26mm \
29
+ --margin-bottom 13mm \
30
+ --header-spacing 10 \
31
+ --header-html #{header} \
32
+ --footer-spacing 5 \
33
+ --footer-font-size 10 \
34
+ --footer-left '#{left_footer}' \
35
+ --footer-center '[page] of [toPage]' \
36
+ --print-media-type \
37
+ toc --xsl-style-sheet #{toc_xslt_path} \
38
+ #{sources.join(' ')} \
39
+ #{target}
40
+ CMD
41
+
42
+ `#{command}`
43
+
44
+ raise "'wkhtmltopdf' appears to have failed" unless $?.success? && File.exist?(target)
45
+
46
+ @logger.log "\nYour PDF file was generated to #{target.green}"
47
+
48
+ end
49
+
50
+ def check_file_exists(required_file)
51
+ unless File.exist? required_file
52
+ @logger.error "\nPDF Generation failed (could not find file)!"
53
+ raise MissingSource, required_file
54
+ end
55
+ end
56
+
57
+ def check_destination_exists(url)
58
+ uri = URI(url)
59
+ if uri.class == URI::Generic
60
+ check_file_exists url
61
+ elsif uri.class == URI::HTTP
62
+ check_url_exists url
63
+ else
64
+ @logger.error "Malformed destination provided for PDF generation source: #{url}"
65
+ end
66
+ end
67
+
68
+ def check_url_exists(url)
69
+ res = Net::HTTP.get_response(URI(url))
70
+ raise MissingSource, url if res.code == '404'
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,125 @@
1
+ require 'middleman-syntax'
2
+ require_relative 'bookbinder_logger'
3
+ require_relative 'directory_helpers'
4
+ require_relative 'section'
5
+ require_relative 'server_director'
6
+
7
+ module Bookbinder
8
+ class Publisher
9
+ include DirectoryHelperMethods
10
+
11
+ def initialize(logger, spider, static_site_generator)
12
+ @gem_root = File.expand_path('../../../', __FILE__)
13
+ @logger = logger
14
+ @spider = spider
15
+ @static_site_generator = static_site_generator
16
+ end
17
+
18
+ def publish(cli_options, output_paths, publish_config, git_accessor)
19
+ intermediate_directory = output_paths.fetch(:output_dir)
20
+ final_app_dir = output_paths.fetch(:final_app_dir)
21
+ master_middleman_dir = output_paths.fetch(:master_middleman_dir)
22
+ master_dir = File.join intermediate_directory, 'master_middleman'
23
+ workspace_dir = File.join master_dir, 'source'
24
+ build_directory = File.join master_dir, 'build/.'
25
+ public_directory = File.join final_app_dir, 'public'
26
+
27
+ @versions = publish_config.fetch(:versions, [])
28
+ @book_repo = publish_config[:book_repo]
29
+ prepare_directories final_app_dir, intermediate_directory, workspace_dir, master_middleman_dir, master_dir, git_accessor
30
+ FileUtils.cp 'redirects.rb', final_app_dir if File.exists?('redirects.rb')
31
+
32
+ target_tag = cli_options[:target_tag]
33
+ sections = gather_sections(workspace_dir, publish_config, output_paths, target_tag, git_accessor)
34
+ book = Book.new(logger: @logger,
35
+ full_name: @book_repo,
36
+ sections: publish_config.fetch(:sections))
37
+ host_for_sitemap = publish_config.fetch(:host_for_sitemap)
38
+
39
+ generate_site(cli_options, output_paths, publish_config, master_dir, book, sections, build_directory, public_directory, git_accessor)
40
+ generate_sitemap(final_app_dir, host_for_sitemap, @spider)
41
+
42
+ @logger.log "Bookbinder bound your book into #{final_app_dir.to_s.green}"
43
+
44
+ !@spider.has_broken_links?
45
+ end
46
+
47
+ private
48
+
49
+ def generate_sitemap(final_app_dir, host_for_sitemap, spider)
50
+ server_director = ServerDirector.new(@logger, directory: final_app_dir)
51
+ raise "Your public host must be a single String." unless host_for_sitemap.is_a?(String)
52
+
53
+ server_director.use_server { |port| spider.generate_sitemap host_for_sitemap, port }
54
+ end
55
+
56
+ def generate_site(cli_options, output_paths, publish_config, middleman_dir, book, sections, build_dir, public_dir, git_accessor)
57
+ @static_site_generator.run(middleman_dir,
58
+ publish_config.fetch(:template_variables, {}),
59
+ output_paths[:local_repo_dir],
60
+ cli_options[:verbose],
61
+ book,
62
+ sections,
63
+ publish_config[:host_for_sitemap],
64
+ publish_config[:archive_menu],
65
+ git_accessor
66
+ )
67
+ FileUtils.cp_r build_dir, public_dir
68
+ end
69
+
70
+ def gather_sections(workspace, publish_config, output_paths, target_tag, git_accessor)
71
+ section_data = publish_config.fetch(:sections)
72
+ section_data.map do |attributes|
73
+ section = Section.get_instance(@logger,
74
+ section_hash: attributes,
75
+ destination_dir: workspace,
76
+ local_repo_dir: output_paths[:local_repo_dir],
77
+ target_tag: target_tag,
78
+ git_accessor: git_accessor)
79
+ section
80
+ end
81
+ end
82
+
83
+ def prepare_directories(final_app, middleman_scratch_space, middleman_source, master_middleman_dir, middleman_dir, git_accessor)
84
+ forget_sections(middleman_scratch_space)
85
+ FileUtils.rm_rf File.join final_app, '.'
86
+ FileUtils.mkdir_p middleman_scratch_space
87
+ FileUtils.mkdir_p File.join final_app, 'public'
88
+ FileUtils.mkdir_p middleman_source
89
+
90
+ copy_directory_from_gem 'template_app', final_app
91
+ copy_directory_from_gem 'master_middleman', middleman_dir
92
+ FileUtils.cp_r File.join(master_middleman_dir, '.'), middleman_dir
93
+
94
+ copy_version_master_middleman(middleman_source, git_accessor)
95
+ end
96
+
97
+ # Copy the index file from each version into the version's directory. Because version
98
+ # subdirectories are sections, this is the only way they get content from their master
99
+ # middleman directory.
100
+ def copy_version_master_middleman(dest_dir, git_accessor)
101
+ @versions.each do |version|
102
+ Dir.mktmpdir(version) do |tmpdir|
103
+ book = Book.from_remote(logger: @logger, full_name: @book_repo,
104
+ destination_dir: tmpdir, ref: version, git_accessor: git_accessor)
105
+ index_source_dir = File.join(tmpdir, book.directory, 'master_middleman', source_dir_name)
106
+ index_dest_dir = File.join(dest_dir, version)
107
+ FileUtils.mkdir_p(index_dest_dir)
108
+
109
+ Dir.glob(File.join(index_source_dir, 'index.*')) do |f|
110
+ FileUtils.cp(File.expand_path(f), index_dest_dir)
111
+ end
112
+ end
113
+ end
114
+ end
115
+
116
+ def forget_sections(middleman_scratch)
117
+ Section.store.clear
118
+ FileUtils.rm_rf File.join middleman_scratch, '.'
119
+ end
120
+
121
+ def copy_directory_from_gem(dir, output_dir)
122
+ FileUtils.cp_r File.join(@gem_root, "#{dir}/."), output_dir
123
+ end
124
+ end
125
+ end
@@ -0,0 +1,34 @@
1
+ require_relative 'app_fetcher'
2
+
3
+ module Bookbinder
4
+ class Pusher
5
+ def initialize(cf_cli, app_fetcher)
6
+ @cf_cli = cf_cli
7
+ @app_fetcher = app_fetcher
8
+ end
9
+
10
+ def push(app_dir)
11
+ Dir.chdir(app_dir) do
12
+ cf_cli.login
13
+
14
+ old_app = app_fetcher.fetch_current_app
15
+
16
+ if old_app
17
+ new_app = old_app.with_flipped_name
18
+ cf_cli.start(new_app)
19
+ cf_cli.push(new_app)
20
+ cf_cli.map_routes(new_app)
21
+ cf_cli.takedown_old_target_app(old_app)
22
+ else
23
+ new_app = cf_cli.new_app
24
+ cf_cli.push(new_app)
25
+ cf_cli.map_routes(new_app)
26
+ end
27
+ end
28
+ end
29
+
30
+ private
31
+
32
+ attr_reader :cf_cli, :app_fetcher
33
+ end
34
+ end
@@ -0,0 +1,21 @@
1
+ require 'yaml'
2
+ require 'tempfile'
3
+
4
+ module Bookbinder
5
+ class RemoteYamlCredentialProvider
6
+ def initialize(logger, repository, git_accessor = Git)
7
+ @logger = logger
8
+ @repository = repository
9
+ @git_accessor = git_accessor
10
+ end
11
+
12
+ def credentials
13
+ @logger.log "Processing #{@repository.full_name.cyan}"
14
+ Dir.mktmpdir do |destination_dir|
15
+ @repository.copy_from_remote(destination_dir, @git_accessor)
16
+ cred_file_yaml = File.join(destination_dir, @repository.short_name, 'credentials.yml')
17
+ YAML.load_file(cred_file_yaml)
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,78 @@
1
+ require 'bookbinder/directory_helpers'
2
+
3
+ module Bookbinder
4
+ class Section
5
+
6
+ def self.store
7
+ @@store ||= {}
8
+ end
9
+
10
+ def self.get_instance(logger,
11
+ section_hash: {},
12
+ local_repo_dir: nil,
13
+ destination_dir: Dir.mktmpdir,
14
+ target_tag: nil,
15
+ git_accessor: Git)
16
+ @git_accessor = git_accessor
17
+ store.fetch([section_hash, local_repo_dir]) { acquire(logger, section_hash, local_repo_dir, destination_dir, target_tag, git_accessor) }
18
+ end
19
+
20
+ def initialize(logger, repository, subnav_template)
21
+ @logger = logger
22
+ @subnav_template = subnav_template
23
+ @repository = repository
24
+ @git_accessor = Git
25
+ end
26
+
27
+ def subnav_template
28
+ @subnav_template.gsub(/^_/, '').gsub(/\.erb$/, '') if @subnav_template
29
+ end
30
+
31
+ def directory
32
+ @repository.directory
33
+ end
34
+
35
+ def full_name
36
+ @repository.full_name
37
+ end
38
+
39
+ def copied?
40
+ @repository.copied?
41
+ end
42
+
43
+ def get_modification_date_for(file: nil, full_path: nil)
44
+ unless @repository.has_git_object?
45
+ begin
46
+ git_base_object = @git_accessor.open(@repository.path_to_local_repo)
47
+ rescue => e
48
+ raise "Invalid git repository! Cannot get modification date for section: #{@repository.path_to_local_repo}."
49
+ end
50
+ end
51
+ @repository.get_modification_date_for(file: file, git: git_base_object)
52
+ end
53
+
54
+ private
55
+
56
+ def self.acquire(logger, section_hash, local_repo_dir, destination, target_tag, git_accessor)
57
+ repository = section_hash['repository']
58
+ raise "section repository '#{repository}' is not a hash" unless repository.is_a?(Hash)
59
+ raise "section repository '#{repository}' missing name key" unless repository['name']
60
+ logger.log "Gathering #{repository['name'].cyan}"
61
+
62
+ repository = build_repository(logger, destination, local_repo_dir, section_hash, target_tag, git_accessor)
63
+ section = new(logger, repository, section_hash['subnav_template'])
64
+
65
+ store[[section_hash, local_repo_dir]] = section
66
+ end
67
+ private_class_method :acquire
68
+
69
+ def self.build_repository(logger, destination, local_repo_dir, repo_hash, target_tag, git_accessor)
70
+ if local_repo_dir
71
+ GitHubRepository.build_from_local(logger, repo_hash, local_repo_dir, destination)
72
+ else
73
+ GitHubRepository.build_from_remote(logger, repo_hash, destination, target_tag, git_accessor)
74
+ end
75
+ end
76
+ private_class_method :build_repository
77
+ end
78
+ end
@@ -0,0 +1,53 @@
1
+ require 'popen4'
2
+
3
+ module Bookbinder
4
+ class ServerDirector
5
+ def initialize(logger, directory: nil, port: 41722)
6
+ @logger = logger
7
+ @directory = directory
8
+ @port = port
9
+ end
10
+
11
+ def use_server
12
+ Dir.chdir(@directory) do
13
+ POpen4::popen4("puma -p #{@port}") do |stdout, stderr, stdin, pid|
14
+ begin
15
+ wait_for_server(stdout)
16
+ consume_stream_in_separate_thread(stdout)
17
+ consume_stream_in_separate_thread(stderr)
18
+ yield @port
19
+ ensure
20
+ stop_server(pid)
21
+ end
22
+ end
23
+ end
24
+ end
25
+
26
+ private
27
+
28
+ def wait_for_server(io)
29
+ Kernel.sleep(1)
30
+ begin
31
+ line = io.gets
32
+ raise 'Puma could not start' if line.nil?
33
+
34
+ @logger.log "Vienna says, #{line}"
35
+ end until line.include?('Listening on')
36
+
37
+ @logger.log 'Vienna is lovely this time of year.'
38
+ end
39
+
40
+ def stop_server(pid)
41
+ Process.kill 'KILL', pid
42
+ end
43
+
44
+ # avoids deadlocks by ensuring rack doesn't hang waiting to write to stderr
45
+ def consume_stream_in_separate_thread(stream)
46
+ Thread.new do
47
+ s = nil
48
+ while stream.read(1024, s)
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,19 @@
1
+ require 'open3'
2
+
3
+ module Bookbinder
4
+ module ShellOut
5
+ def shell_out(command, failure_okay = false)
6
+ Open3.popen3(command) do |input, stdout, stderr, wait_thr|
7
+ command_failed = (wait_thr.value != 0)
8
+ announce_failure(failure_okay, stderr, stdout) if command_failed
9
+ stdout.read
10
+ end
11
+ end
12
+
13
+ def announce_failure(failure_okay, stderr, stdout)
14
+ contents_of_stderr = stderr.read
15
+ error_message = contents_of_stderr.empty? ? stdout.read : contents_of_stderr
16
+ raise "\n#{error_message}" unless failure_okay
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,62 @@
1
+ module Bookbinder
2
+ class Sieve
3
+ def initialize(domain: ->(){ raise 'You must supply a domain parameter.' }.call)
4
+ @unverified_fragments_by_url = {}
5
+ @domain = domain
6
+ end
7
+
8
+ def links_from(page, is_first_pass)
9
+ if page.not_found?
10
+ working = []
11
+ broken = [Spider.prepend_location(page.referer, page.url)]
12
+ else
13
+ working = [page.url.to_s]
14
+ broken = broken_fragments_targeting(page, is_first_pass)
15
+ store_unverified_fragments_from(page) if is_first_pass
16
+ end
17
+
18
+ return broken, working
19
+ end
20
+
21
+ private
22
+
23
+ def store_unverified_fragments_from(page)
24
+ @unverified_fragments_by_url.merge! fragments_targeting_other_pages_from page
25
+ end
26
+
27
+ def broken_fragments_targeting(page, first_pass)
28
+ first_pass ? local_fragments_missing_from(page) : remote_fragments_missing_from(page)
29
+ end
30
+
31
+ def local_fragments_missing_from(page)
32
+ local_fragments = page.fragment_identifiers targeting_locally: true
33
+ local_fragments.reject { |uri| page.has_target_for?(uri) }.map { |uri| Spider.prepend_location(page.url, uri) }
34
+ end
35
+
36
+ def remote_fragments_missing_from(page)
37
+ @unverified_fragments_by_url.fetch(page.url, []).reject { |localized_identifier| page.has_target_for? URI(strip_location(localized_identifier)) }
38
+ end
39
+
40
+ def fragments_targeting_other_pages_from(page)
41
+ uris_with_fragments = page.fragment_identifiers(targeting_locally: false)
42
+ uris_with_fragments.reduce({}) { |dict, uri| merge_uris_under_targets(dict, page, uri) }
43
+ end
44
+
45
+ def merge_uris_under_targets(dict, page, uri)
46
+ target_url = URI::join @domain, uri.path
47
+ localized_identifier = Spider.prepend_location(page.url, "##{uri.fragment}")
48
+
49
+ if dict.has_key? target_url
50
+ dict[target_url] << localized_identifier
51
+ else
52
+ dict[target_url] = [localized_identifier]
53
+ end
54
+
55
+ dict
56
+ end
57
+
58
+ def strip_location(id)
59
+ id.split('=> ').last
60
+ end
61
+ end
62
+ end