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