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,64 @@
1
+ require 'css_parser'
2
+ require_relative 'spider'
3
+
4
+ module Bookbinder
5
+ class CssLinkChecker
6
+ def broken_links_in_all_stylesheets
7
+ localized_links_in_stylesheets.reject { |link| target_exists?(link) }
8
+ end
9
+
10
+ private
11
+
12
+ def localized_links_in_stylesheets
13
+ links_in_stylesheets = []
14
+ Dir.glob('**/*.css').each { |stylesheet| links_in_stylesheets.concat localized_links_from(stylesheet) }
15
+
16
+ links_in_stylesheets
17
+ end
18
+
19
+ def localized_links_from(stylesheet)
20
+ localized_uris = []
21
+ css = CssParser::Parser.new
22
+ css.load_uri! stylesheet
23
+
24
+ css.each_selector do |s, declaration, sp|
25
+ contents_of_url_value = /url\((.*?)\)/
26
+ file_url = declaration.match contents_of_url_value
27
+ localized_uris << Spider.prepend_location(stylesheet, file_url[1]) if file_url
28
+ end
29
+
30
+ localized_uris
31
+ end
32
+
33
+ def target_exists?(localized_identifier)
34
+ dirs, link = strip_location(localized_identifier)
35
+ link.gsub!(/'|"/, '')
36
+
37
+ data_uri = /^['"]?data:image\//
38
+ remote_uri = /^['"]?https?:\/\//
39
+ absolute_uri = /^['"]?\//
40
+ relative_uri = //
41
+
42
+ case link
43
+ when data_uri then true
44
+ when remote_uri then http_reachable?(link)
45
+ when absolute_uri then File.exists?(File.join('.', 'public', link))
46
+ when relative_uri then
47
+ dirs = dirs.split('/')[0...-1]
48
+ computed_uri = File.expand_path(File.join dirs, link)
49
+ File.exists?(computed_uri)
50
+ end
51
+ end
52
+
53
+ def strip_location(id)
54
+ dirs, filepath = id.split('=> ')
55
+ [dirs, filepath]
56
+ end
57
+
58
+ def http_reachable?(link)
59
+ Net::HTTP.get_response(URI(link)).code == '200'
60
+ rescue SocketError => e
61
+ return false
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,15 @@
1
+ module Bookbinder
2
+ module DirectoryHelperMethods
3
+
4
+ private
5
+
6
+ def output_dir_name
7
+ 'output'
8
+ end
9
+
10
+ def source_dir_name
11
+ 'source'
12
+ end
13
+
14
+ end
15
+ end
@@ -0,0 +1,69 @@
1
+ require_relative 'app_fetcher'
2
+
3
+ module Bookbinder
4
+ class Distributor
5
+ EXPIRATION_HOURS = 2
6
+
7
+ def self.build(logger, options)
8
+ namespace = GitHubRepository.new(logger: logger, full_name: options[:book_repo]).short_name
9
+ namer = ArtifactNamer.new(namespace, options[:build_number], 'log', '/tmp')
10
+
11
+ archive = Archive.new(logger: logger, key: options[:aws_credentials].access_key, secret: options[:aws_credentials].secret_key)
12
+ cf_command_runner = CfCommandRunner.new(logger, options[:cf_credentials], namer.full_path)
13
+ cf_app_fetcher = AppFetcher.new(options[:cf_credentials].flat_routes, cf_command_runner)
14
+
15
+ pusher = Pusher.new(cf_command_runner, cf_app_fetcher)
16
+ new(logger, archive, pusher, namespace, namer, options)
17
+ end
18
+
19
+ def initialize(logger, archive, pusher, namespace, namer, options)
20
+ @logger = logger
21
+ @archive = archive
22
+ @pusher = pusher
23
+ @namespace = namespace
24
+ @namer = namer
25
+ @options = options
26
+ end
27
+
28
+ def distribute
29
+ begin
30
+ download if options[:production]
31
+ push_app
32
+ nil
33
+ rescue => e
34
+ cf_credentials = options[:cf_credentials]
35
+ cf_space = options[:production] ? cf_credentials.production_space : cf_credentials.staging_space
36
+ cf_routes = options[:production] ? cf_credentials.production_host : cf_credentials.staging_host
37
+ @logger.error "[ERROR] #{e.message}\n[DEBUG INFO]\nCF organization: #{cf_credentials.organization}\nCF space: #{cf_space}\nCF account: #{cf_credentials.username}\nroutes: #{cf_routes}"
38
+ ensure
39
+ upload_trace
40
+ end
41
+ end
42
+
43
+ private
44
+
45
+ attr_reader :options, :archive, :namer, :namespace, :pusher
46
+
47
+ def download
48
+ archive.download(download_dir: options[:app_dir], bucket: options[:aws_credentials].green_builds_bucket, build_number: options[:build_number],
49
+ namespace: namespace)
50
+ end
51
+
52
+ def push_app
53
+ warn if options[:production]
54
+ pusher.push(options[:app_dir])
55
+ end
56
+
57
+ def upload_trace
58
+ uploaded_file = archive.upload_file(options[:aws_credentials].green_builds_bucket, namer.filename, namer.full_path)
59
+ @logger.log("Your cf trace file is available at: #{uploaded_file.url(Time.now.to_i + EXPIRATION_HOURS*60*60).green}")
60
+ @logger.log("This URL will expire in #{EXPIRATION_HOURS} hours, so if you need to share it, make sure to save a copy now.")
61
+ rescue Errno::ENOENT
62
+ @logger.error "Could not find CF trace file: #{namer.full_path}"
63
+ end
64
+
65
+ def warn
66
+ @logger.warn 'Warning: You are pushing to CF Docs production. Be careful.'
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,63 @@
1
+ require 'octokit'
2
+
3
+ module Bookbinder
4
+ class GitClient < Octokit::Client
5
+ class GitClient::TokenException < StandardError;
6
+ end
7
+
8
+ def initialize(logger, *args)
9
+ @logger = logger
10
+ super(*args)
11
+ end
12
+
13
+ def head_sha(full_name)
14
+ commits(full_name).first.sha
15
+ end
16
+
17
+ def create_tag!(full_name, tagname, ref)
18
+ @logger.log 'Tagging ' + full_name.cyan
19
+ tag_result = create_tag(full_name, "tags/#{tagname}", 'Tagged by Bookbinder', ref, 'commit', 'Bookbinder', 'bookbinder@cloudfoundry.org', Time.now.iso8601)
20
+ create_ref(full_name, "tags/#{tagname}", tag_result.sha)
21
+ rescue Octokit::Unauthorized, Octokit::NotFound
22
+ raise_error_with_context
23
+ end
24
+
25
+ def archive_link(*args)
26
+ super
27
+ rescue Octokit::Unauthorized, Octokit::NotFound
28
+ raise_error_with_context
29
+ end
30
+
31
+ def tags(*args)
32
+ super
33
+ rescue Octokit::Unauthorized, Octokit::NotFound
34
+ raise_error_with_context
35
+ end
36
+
37
+ def refs(*args)
38
+ super
39
+ rescue Octokit::Unauthorized, Octokit::NotFound
40
+ raise_error_with_context
41
+ end
42
+
43
+ def last_modified_date_of(full_name, target_ref, file)
44
+ commits = commits(full_name, target_ref, path: file)
45
+ commits.first[:commit][:author][:date]
46
+ end
47
+
48
+ private
49
+
50
+ def commits(*args)
51
+ super
52
+ rescue Octokit::Unauthorized, Octokit::NotFound
53
+ raise_error_with_context
54
+ end
55
+
56
+ def raise_error_with_context
57
+ absent_token_message = ' Cannot access repository! You must set $GITHUB_API_TOKEN.'
58
+ invalid_token_message = ' Cannot access repository! Does your $GITHUB_API_TOKEN have access to this repository? Does it exist?'
59
+
60
+ ENV['GITHUB_API_TOKEN'] ? raise(TokenException.new(invalid_token_message)) : raise(TokenException.new(absent_token_message))
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,151 @@
1
+ require 'ruby-progressbar'
2
+ require 'bookbinder/shell_out'
3
+ require 'git'
4
+ require_relative 'git_client'
5
+ require_relative 'bookbinder_logger'
6
+
7
+ module Bookbinder
8
+ class GitHubRepository
9
+ class RepositoryCloneError < StandardError
10
+ def initialize(msg=nil)
11
+ super
12
+ end
13
+ end
14
+
15
+ include Bookbinder::ShellOut
16
+
17
+ attr_reader :full_name, :copied_to
18
+
19
+ def self.build_from_remote(logger, section_hash, destination_dir, target_ref, git_accessor)
20
+ full_name = section_hash.fetch('repository', {}).fetch('name')
21
+ target_ref = target_ref || section_hash.fetch('repository', {})['ref']
22
+ directory = section_hash['directory']
23
+ repository = new(logger: logger, full_name: full_name, target_ref: target_ref, github_token: ENV['GITHUB_API_TOKEN'], directory: directory)
24
+ repository.copy_from_remote(destination_dir, git_accessor) if destination_dir
25
+ repository
26
+ end
27
+
28
+ def self.build_from_local(logger, section_hash, local_repo_dir, destination_dir)
29
+ full_name = section_hash.fetch('repository').fetch('name')
30
+ directory = section_hash['directory']
31
+
32
+ repository = new(logger: logger, full_name: full_name, directory: directory, local_repo_dir: local_repo_dir)
33
+ repository.copy_from_local(destination_dir) if destination_dir
34
+
35
+ repository
36
+ end
37
+
38
+ def initialize(logger: nil, full_name: nil, target_ref: nil, github_token: nil, directory: nil, local_repo_dir: nil)
39
+ @logger = logger
40
+ #TODO better error message
41
+ raise 'No full_name provided ' unless full_name
42
+ @full_name = full_name
43
+ @github = GitClient.new(logger, access_token: github_token || ENV['GITHUB_API_TOKEN'])
44
+ @target_ref = target_ref
45
+ @directory = directory
46
+ @local_repo_dir = local_repo_dir
47
+ end
48
+
49
+ def tag_with(tagname)
50
+ @github.create_tag! full_name, tagname, head_sha
51
+ end
52
+
53
+ def short_name
54
+ full_name.split('/')[1]
55
+ end
56
+
57
+ def head_sha
58
+ @head_sha ||= @github.head_sha(full_name)
59
+ end
60
+
61
+ def directory
62
+ @directory || short_name
63
+ end
64
+
65
+ def copy_from_remote(destination_dir, git_accessor = Git)
66
+ begin
67
+ @git = git_accessor.clone("git@github.com:#{full_name}", directory, path: destination_dir)
68
+ rescue => e
69
+ if e.message.include? "Permission denied (publickey)"
70
+ raise RepositoryCloneError.new "Unable to access repository #{full_name}. You do not have the correct access rights. Please either add the key to your SSH agent, or set the GIT_SSH environment variable to override default SSH key usage. For more information run: `man git`."
71
+ elsif
72
+ e.message.include? "Repository not found."
73
+ raise RepositoryCloneError.new "Could not read from repository. Please make sure you have the correct access rights and the repository #{full_name} exists."
74
+ else
75
+ raise e
76
+ end
77
+ end
78
+ @git.checkout(target_ref) unless target_ref == 'master'
79
+ @copied_to = destination_dir
80
+ end
81
+
82
+ def copy_from_local(destination_dir)
83
+ if File.exist?(path_to_local_repo)
84
+ @logger.log ' copying '.yellow + path_to_local_repo
85
+ destination = File.join(destination_dir, directory)
86
+
87
+ unless destination.include?(path_to_local_repo)
88
+ FileUtils.mkdir_p(destination)
89
+ FileUtils.cp_r(File.join(path_to_local_repo, '.'), destination)
90
+ end
91
+
92
+ @copied_to = File.join(destination_dir, directory)
93
+ else
94
+ announce_skip
95
+ end
96
+ end
97
+
98
+ def copied?
99
+ !@copied_to.nil?
100
+ end
101
+
102
+ def has_tag?(tagname)
103
+ tags.any? { |tag| tag.name == tagname }
104
+ end
105
+
106
+ def update_local_copy
107
+ if File.exist?(path_to_local_repo)
108
+ @logger.log 'Updating ' + path_to_local_repo.cyan
109
+ Kernel.system("cd #{path_to_local_repo} && git pull")
110
+ else
111
+ announce_skip
112
+ end
113
+ end
114
+
115
+ def announce_skip
116
+ @logger.log ' skipping (not found) '.magenta + path_to_local_repo
117
+ end
118
+
119
+ def get_modification_date_for(file: nil, git: nil)
120
+ @git ||= git
121
+ raise "Unexpected Error: Git accessor unavailable." if @git.nil?
122
+
123
+ irrelevant_path_component = directory+'/'
124
+ repo_path = file.gsub(irrelevant_path_component, '')
125
+
126
+ begin
127
+ @git.log(1).object(repo_path).first.date
128
+ rescue Git::GitExecuteError => e
129
+ raise "This file does not exist or is not tracked by git! Cannot get last modified date for #{repo_path}."
130
+ end
131
+ end
132
+
133
+ def path_to_local_repo
134
+ File.join(@local_repo_dir, short_name)
135
+ end
136
+
137
+ def has_git_object?
138
+ !!@git
139
+ end
140
+
141
+ private
142
+
143
+ def target_ref
144
+ @target_ref ||= 'master'
145
+ end
146
+
147
+ def tags
148
+ @github.tags @full_name
149
+ end
150
+ end
151
+ end
@@ -0,0 +1,9 @@
1
+ module Bookbinder
2
+
3
+ class LocalFileSystemAccessor
4
+ def file_exist?(path)
5
+ File.exist?(path)
6
+ end
7
+ end
8
+
9
+ end
@@ -0,0 +1,86 @@
1
+ require 'middleman-core'
2
+ require 'middleman-core/cli'
3
+ require 'middleman-core/profiling'
4
+ require_relative 'code_example'
5
+
6
+ class Middleman::Cli::BuildAction
7
+ def handle_error(file_name, response, e=Thor::Error.new(response))
8
+ our_errors = [Bookbinder::GitClient::TokenException,
9
+ Bookbinder::CodeExample::InvalidSnippet,
10
+ QuicklinksRenderer::BadHeadingLevelError,
11
+ Git::GitExecuteError]
12
+ raise e if our_errors.include?(e.class)
13
+
14
+ original_handle_error(e, file_name, response)
15
+ end
16
+
17
+ private
18
+
19
+ def original_handle_error(e, file_name, response)
20
+ base.had_errors = true
21
+
22
+ base.say_status :error, file_name, :red
23
+ if base.debugging
24
+ raise e
25
+ exit(1)
26
+ elsif base.options["verbose"]
27
+ base.shell.say response, :red
28
+ end
29
+ end
30
+ end
31
+
32
+ module Bookbinder
33
+ class MiddlemanRunner
34
+ def initialize(logger)
35
+ @logger = logger
36
+ end
37
+
38
+ def run(middleman_dir, template_variables, local_repo_dir, verbose = false, book = nil, sections = [], production_host=nil, archive_menu=nil, git_accessor=Git)
39
+ @logger.log "\nRunning middleman...\n\n"
40
+
41
+ within(middleman_dir) do
42
+ invoke_against_current_dir(local_repo_dir, production_host, book, sections, template_variables, archive_menu, verbose, git_accessor)
43
+ end
44
+ end
45
+
46
+ private
47
+
48
+ def within(temp_root, &block)
49
+ Middleman::Cli::Build.instance_variable_set(:@_shared_instance, nil)
50
+ original_mm_root = ENV['MM_ROOT']
51
+ ENV['MM_ROOT'] = temp_root
52
+
53
+ Dir.chdir(temp_root) { block.call }
54
+
55
+ ENV['MM_ROOT'] = original_mm_root
56
+ end
57
+
58
+ def invoke_against_current_dir(local_repo_dir, production_host, book, sections, template_variables, archive_menu, verbose, git_accessor)
59
+ builder = Middleman::Cli::Build.shared_instance(verbose)
60
+
61
+ config = {
62
+ local_repo_dir: local_repo_dir,
63
+ production_host: production_host,
64
+ git_accessor: git_accessor,
65
+ sections: sections,
66
+ book: book,
67
+ template_variables: template_variables,
68
+ relative_links: false,
69
+ subnav_templates: subnavs_by_dir_name(sections),
70
+ archive_menu: archive_menu
71
+ }
72
+
73
+ config.each { |k, v| builder.config[k] = v }
74
+ Middleman::Cli::Build.new([], {quiet: !verbose}, {}).invoke :build, [], {verbose: verbose}
75
+ end
76
+
77
+ def subnavs_by_dir_name(sections)
78
+ sections.reduce({}) do |final_map, section|
79
+ namespace = section.directory.gsub('/', '_')
80
+ template = section.subnav_template || 'default'
81
+
82
+ final_map.merge(namespace => template)
83
+ end
84
+ end
85
+ end
86
+ end