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,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