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.
- checksums.yaml +7 -0
- data/bin/bookbinder +6 -0
- data/lib/bookbinder.rb +59 -0
- data/lib/bookbinder/app_fetcher.rb +40 -0
- data/lib/bookbinder/archive.rb +95 -0
- data/lib/bookbinder/artifact_namer.rb +22 -0
- data/lib/bookbinder/blue_green_app.rb +27 -0
- data/lib/bookbinder/book.rb +54 -0
- data/lib/bookbinder/bookbinder_logger.rb +33 -0
- data/lib/bookbinder/cf_command_runner.rb +114 -0
- data/lib/bookbinder/cf_routes.rb +19 -0
- data/lib/bookbinder/cli.rb +68 -0
- data/lib/bookbinder/cli_error.rb +6 -0
- data/lib/bookbinder/code_example.rb +40 -0
- data/lib/bookbinder/command_runner.rb +32 -0
- data/lib/bookbinder/command_validator.rb +24 -0
- data/lib/bookbinder/commands/bookbinder_command.rb +18 -0
- data/lib/bookbinder/commands/build_and_push_tarball.rb +31 -0
- data/lib/bookbinder/commands/generate_pdf.rb +140 -0
- data/lib/bookbinder/commands/help.rb +31 -0
- data/lib/bookbinder/commands/naming.rb +9 -0
- data/lib/bookbinder/commands/publish.rb +138 -0
- data/lib/bookbinder/commands/push_local_to_staging.rb +35 -0
- data/lib/bookbinder/commands/push_to_prod.rb +35 -0
- data/lib/bookbinder/commands/run_publish_ci.rb +42 -0
- data/lib/bookbinder/commands/tag.rb +31 -0
- data/lib/bookbinder/commands/update_local_doc_repos.rb +27 -0
- data/lib/bookbinder/commands/version.rb +25 -0
- data/lib/bookbinder/configuration.rb +163 -0
- data/lib/bookbinder/configuration_fetcher.rb +55 -0
- data/lib/bookbinder/configuration_validator.rb +162 -0
- data/lib/bookbinder/css_link_checker.rb +64 -0
- data/lib/bookbinder/directory_helpers.rb +15 -0
- data/lib/bookbinder/distributor.rb +69 -0
- data/lib/bookbinder/git_client.rb +63 -0
- data/lib/bookbinder/git_hub_repository.rb +151 -0
- data/lib/bookbinder/local_file_system_accessor.rb +9 -0
- data/lib/bookbinder/middleman_runner.rb +86 -0
- data/lib/bookbinder/pdf_generator.rb +73 -0
- data/lib/bookbinder/publisher.rb +125 -0
- data/lib/bookbinder/pusher.rb +34 -0
- data/lib/bookbinder/remote_yaml_credential_provider.rb +21 -0
- data/lib/bookbinder/section.rb +78 -0
- data/lib/bookbinder/server_director.rb +53 -0
- data/lib/bookbinder/shell_out.rb +19 -0
- data/lib/bookbinder/sieve.rb +62 -0
- data/lib/bookbinder/sitemap_generator.rb +19 -0
- data/lib/bookbinder/spider.rb +91 -0
- data/lib/bookbinder/stabilimentum.rb +59 -0
- data/lib/bookbinder/usage_messenger.rb +33 -0
- data/lib/bookbinder/yaml_loader.rb +22 -0
- data/master_middleman/bookbinder_helpers.rb +133 -0
- data/master_middleman/config.rb +23 -0
- data/master_middleman/quicklinks_renderer.rb +78 -0
- data/master_middleman/submodule_aware_assets.rb +45 -0
- data/template_app/Gemfile +7 -0
- data/template_app/Gemfile.lock +20 -0
- data/template_app/app.rb +3 -0
- data/template_app/config.ru +9 -0
- data/template_app/lib/rack_static.rb +19 -0
- data/template_app/lib/vienna_application.rb +26 -0
- 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,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,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
|