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