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,35 @@
1
+ require_relative '../distributor'
2
+ require_relative 'bookbinder_command'
3
+ require_relative 'naming'
4
+
5
+ module Bookbinder
6
+ module Commands
7
+ class PushLocalToStaging < BookbinderCommand
8
+ extend Commands::Naming
9
+
10
+ def self.usage
11
+ "push_local_to_staging \t \t \t Push the contents of final_app to the staging host specified in credentials.yml"
12
+ end
13
+
14
+ def run(_)
15
+ Distributor.build(@logger, options).distribute
16
+ 0
17
+ end
18
+
19
+ private
20
+
21
+ def options
22
+ {
23
+ app_dir: './final_app',
24
+ build_number: ENV['BUILD_NUMBER'],
25
+
26
+ aws_credentials: config.aws_credentials,
27
+ cf_credentials: config.cf_staging_credentials,
28
+
29
+ book_repo: config.book_repo,
30
+ production: false
31
+ }
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,35 @@
1
+ require_relative '../distributor'
2
+ require_relative 'bookbinder_command'
3
+ require_relative 'naming'
4
+
5
+ module Bookbinder
6
+ module Commands
7
+ class PushToProd < BookbinderCommand
8
+ extend Commands::Naming
9
+
10
+ def self.usage
11
+ "push_to_prod [build_#] \t \t \t Push latest or <build_#> from your S3 bucket to the production host specified in credentials.yml"
12
+ end
13
+
14
+ def run(arguments)
15
+ Distributor.build(@logger, options(arguments)).distribute
16
+ 0
17
+ end
18
+
19
+ private
20
+
21
+ def options(arguments)
22
+ {
23
+ app_dir: Dir.mktmpdir,
24
+ build_number: arguments[0],
25
+
26
+ aws_credentials: config.aws_credentials,
27
+ cf_credentials: config.cf_production_credentials,
28
+
29
+ book_repo: config.book_repo,
30
+ production: true
31
+ }
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,42 @@
1
+ require_relative 'bookbinder_command'
2
+ require_relative 'naming'
3
+ require_relative 'publish'
4
+ require_relative 'push_local_to_staging'
5
+ require_relative 'build_and_push_tarball'
6
+
7
+ module Bookbinder
8
+ module Commands
9
+ class RunPublishCI < BookbinderCommand
10
+ extend Commands::Naming
11
+
12
+ def self.usage
13
+ "run_publish_ci \t \t \t \t Run publish, push_local_to_staging, and build_and_push_tarball for CI purposes"
14
+ end
15
+
16
+ def run(cli_args)
17
+ check_params
18
+ all_successfully_ran = publish(cli_args) == 0 && push_to_staging == 0 && push_tarball == 0
19
+ all_successfully_ran ? 0 : 1
20
+ end
21
+
22
+ private
23
+
24
+ def check_params
25
+ raise BuildAndPushTarball::MissingBuildNumber unless ENV['BUILD_NUMBER']
26
+ config.book_repo
27
+ end
28
+
29
+ def publish(cli_args)
30
+ Publish.new(@logger, @configuration_fetcher).run(['github'] + cli_args)
31
+ end
32
+
33
+ def push_to_staging
34
+ PushLocalToStaging.new(@logger, @configuration_fetcher).run []
35
+ end
36
+
37
+ def push_tarball
38
+ BuildAndPushTarball.new(@logger, @configuration_fetcher).run []
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,31 @@
1
+ require_relative '../book'
2
+ require_relative '../bookbinder_logger'
3
+ require_relative '../cli_error'
4
+
5
+ require_relative 'bookbinder_command'
6
+ require_relative 'naming'
7
+
8
+ module Bookbinder
9
+ module Commands
10
+ class Tag < BookbinderCommand
11
+ extend Commands::Naming
12
+
13
+ def self.usage
14
+ "tag <git tag> \t \t \t \t Apply the specified <git tag> to your book and all sections of your book"
15
+ end
16
+
17
+ def run(params)
18
+ tag = params.first
19
+ raise CliError::InvalidArguments unless tag
20
+
21
+ book = Book.new(logger: @logger, full_name: config.book_repo, sections: config.sections)
22
+
23
+ book.tag_self_and_sections_with tag
24
+
25
+ @logger.log 'Success!'.green
26
+ @logger.log " #{book.full_name.yellow} and its sections were tagged with #{tag.blue}"
27
+ 0
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,27 @@
1
+ require_relative 'bookbinder_command'
2
+ require_relative 'naming'
3
+
4
+ module Bookbinder
5
+ module Commands
6
+ class UpdateLocalDocRepos < BookbinderCommand
7
+ extend Commands::Naming
8
+
9
+ def self.usage
10
+ "update_local_doc_repos \t \t \t Run `git pull` on all sections that exist at the same directory level as your book directory"
11
+ end
12
+
13
+ def run(_)
14
+ config.sections.map { |conf| repo_for(conf) }.each(&:update_local_copy)
15
+ 0
16
+ end
17
+
18
+ private
19
+
20
+ def repo_for(section_config)
21
+ local_repo_dir = File.absolute_path('../')
22
+ GitHubRepository.new(logger: @logger, full_name: section_config['repository']['name'],
23
+ local_repo_dir: local_repo_dir)
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,25 @@
1
+ require_relative 'bookbinder_command'
2
+
3
+ module Bookbinder
4
+ module Commands
5
+ class Version < BookbinderCommand
6
+ def self.to_s
7
+ 'version'
8
+ end
9
+
10
+ def self.command_name
11
+ '--version'
12
+ end
13
+
14
+ def self.usage
15
+ "--version \t \t \t \t Print the version of bookbinder"
16
+ end
17
+
18
+ def run(*)
19
+ @logger.log "bookbinder #{Gem::Specification::load(File.join GEM_ROOT, "bookbinder.gemspec").version}"
20
+ 0
21
+ end
22
+
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,163 @@
1
+ require_relative 'remote_yaml_credential_provider'
2
+ require_relative 'git_hub_repository'
3
+
4
+ module Bookbinder
5
+ class Configuration
6
+
7
+ CURRENT_SCHEMA_VERSION = '1.0.0'
8
+ STARTING_SCHEMA_VERSION = '1.0.0'
9
+
10
+ class CredentialKeyError < StandardError;
11
+ end
12
+
13
+ class ConfigSchemaUnsupportedError < StandardError;
14
+ end
15
+
16
+ class AwsCredentials
17
+ REQUIRED_KEYS = %w(access_key secret_key green_builds_bucket).freeze
18
+
19
+ def initialize(cred_hash)
20
+ @creds = cred_hash
21
+ end
22
+
23
+ REQUIRED_KEYS.each do |method_name|
24
+ define_method(method_name) do
25
+ begin
26
+ creds.fetch(method_name)
27
+ rescue KeyError => e
28
+ raise CredentialKeyError, e
29
+ end
30
+ end
31
+ end
32
+
33
+ private
34
+
35
+ attr_reader :creds
36
+ end
37
+
38
+ class CfCredentials
39
+ REQUIRED_KEYS = %w(api_endpoint organization app_name).freeze
40
+ OPTIONAL_KEYS = %w(username password production_space production_host staging_space staging_host).freeze
41
+
42
+ def initialize(cred_hash, is_production)
43
+ @creds = cred_hash
44
+ @is_production = is_production
45
+ end
46
+
47
+ REQUIRED_KEYS.each do |method_name|
48
+ define_method(method_name) do
49
+ fetch(method_name)
50
+ end
51
+ end
52
+
53
+ OPTIONAL_KEYS.each do |method_name|
54
+ define_method(method_name) do
55
+ creds.fetch(method_name, nil)
56
+ end
57
+ end
58
+
59
+ def routes
60
+ key = is_production ? 'production_host' : 'staging_host'
61
+ fetch(key) if correctly_formatted_domain_and_routes?(key)
62
+ end
63
+
64
+ def flat_routes
65
+ routes.reduce([]) do |all_routes, domain_apps|
66
+ domain, apps = domain_apps
67
+ all_routes + apps.map { |app| [domain, app] }
68
+ end
69
+ end
70
+
71
+ def space
72
+ key = is_production ? 'production_space' : 'staging_space'
73
+ fetch(key)
74
+ end
75
+
76
+ private
77
+
78
+ attr_reader :creds, :is_production
79
+
80
+ def fetch(key)
81
+ creds.fetch(key)
82
+ rescue KeyError => e
83
+ raise CredentialKeyError, e
84
+ end
85
+
86
+ def correctly_formatted_domain_and_routes?(deploy_environment)
87
+ routes_hash = fetch(deploy_environment)
88
+ domains = routes_hash.keys
89
+ domains.each { |domain| correctly_formatted_domain?(domain, routes_hash) }
90
+ end
91
+
92
+ def correctly_formatted_domain?(domain, routes_hash)
93
+ raise 'Each domain in credentials must be a single string.' unless domain.is_a? String
94
+ raise "Domain #{domain} in credentials must contain a web extension, e.g. '.com'." unless domain.include?('.')
95
+ raise "Did you mean to add a list of hosts for domain #{domain}? Check your credentials.yml." unless routes_hash[domain]
96
+ raise "Hosts in credentials must be nested as an array under the desired domain #{domain}." unless routes_hash[domain].is_a? Array
97
+ raise "Did you mean to provide a hostname for the domain #{domain}? Check your credentials.yml." if routes_hash[domain].any?(&:nil?)
98
+ end
99
+ end
100
+
101
+ attr_reader :schema_version, :schema_major_version, :schema_minor_version, :schema_patch_version
102
+
103
+ def initialize(logger, config_hash)
104
+ @logger = logger
105
+ @config = config_hash
106
+ end
107
+
108
+ CONFIG_REQUIRED_KEYS = %w(book_repo layout_repo cred_repo sections public_host pdf pdf_index versions)
109
+ CONFIG_OPTIONAL_KEYS = %w(archive_menu)
110
+
111
+ CONFIG_REQUIRED_KEYS.each do |method_name|
112
+ define_method(method_name) do
113
+ config.fetch(method_name)
114
+ end
115
+ end
116
+
117
+ CONFIG_OPTIONAL_KEYS.each do |method_name|
118
+ define_method(method_name) do
119
+ config[method_name]
120
+ end
121
+ end
122
+
123
+ def has_option?(key)
124
+ @config.has_key?(key)
125
+ end
126
+
127
+ def template_variables
128
+ config.fetch('template_variables', {})
129
+ end
130
+
131
+ def aws_credentials
132
+ @aws_creds ||= AwsCredentials.new(credentials.fetch('aws'))
133
+ end
134
+
135
+ def cf_staging_credentials
136
+ @cf_staging_creds ||= CfCredentials.new(credentials.fetch('cloud_foundry'), false)
137
+ end
138
+
139
+ def cf_production_credentials
140
+ @cf_prod_creds ||= CfCredentials.new(credentials.fetch('cloud_foundry'), true)
141
+ end
142
+
143
+ def ==(o)
144
+ (o.class == self.class) && (o.config == self.config)
145
+ end
146
+
147
+ alias_method :eql?, :==
148
+
149
+ protected
150
+
151
+ attr_reader :config
152
+
153
+ private
154
+
155
+ def credentials
156
+ @credentials ||= RemoteYamlCredentialProvider.new(@logger, credentials_repository).credentials
157
+ end
158
+
159
+ def credentials_repository
160
+ @credentials_repository ||= GitHubRepository.new(logger: @logger, full_name: cred_repo)
161
+ end
162
+ end
163
+ end
@@ -0,0 +1,55 @@
1
+ require_relative 'yaml_loader'
2
+
3
+ module Bookbinder
4
+
5
+ class ConfigurationFetcher
6
+ attr_reader :logger,
7
+ :configuration_validator,
8
+ :config,
9
+ :config_file_path
10
+
11
+ def initialize(logger, configuration_validator, loader)
12
+ @loader = loader
13
+ @logger = logger
14
+ @configuration_validator = configuration_validator
15
+ end
16
+
17
+ def fetch_config
18
+ @config ||= validate(read_config_file)
19
+ end
20
+
21
+ def set_config_file_path config_file_path
22
+ @config_file_path = config_file_path
23
+ end
24
+
25
+
26
+ private
27
+
28
+ attr_reader :loader
29
+
30
+ def read_config_file
31
+
32
+ begin
33
+ config_hash = loader.load(config_file_path)
34
+ rescue FileNotFoundError => e
35
+ raise "The configuration file specified does not exist. Please create a config #{e} file at #{config_file_path} and try again."
36
+ rescue InvalidSyntaxError => e
37
+ raise "There is a syntax error in your config file: \n #{e}"
38
+ end
39
+
40
+ if config_hash
41
+ if File.exists?('./pdf_index.yml')
42
+ config_hash['pdf_index'] = loader.load(config_file_path)
43
+ else
44
+ config_hash['pdf_index'] = nil
45
+ end
46
+ end
47
+
48
+ config_hash
49
+ end
50
+
51
+ def validate(config_hash)
52
+ Configuration.new(logger, config_hash) if configuration_validator.valid?(config_hash, Configuration::CURRENT_SCHEMA_VERSION, Configuration::STARTING_SCHEMA_VERSION)
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,162 @@
1
+ module Bookbinder
2
+
3
+ class DuplicateSectionNameChecker
4
+ def check(config)
5
+ if duplicate_section_names?(config)
6
+ ConfigurationValidator::DuplicateSectionNameError
7
+ end
8
+ end
9
+
10
+ private
11
+
12
+ def duplicate_section_names?(config)
13
+ directory_names = config['sections'].map {|section| section['directory']}
14
+ directory_names.length != directory_names.uniq.length
15
+ end
16
+
17
+ end
18
+
19
+ class ArchiveMenuChecker
20
+ def initialize(file_system_accessor)
21
+ @file_system_accessor = file_system_accessor
22
+ end
23
+
24
+ def check(config)
25
+ partial_location = './master_middleman/source/archive_menus/_default.erb'
26
+ if config.has_key?("archive_menu") && config["archive_menu"].nil?
27
+ ConfigurationValidator::ArchiveMenuNotDefinedError.new 'Did you mean to provide an archive menu value to display? If you use the archive_menu key, you must provide at least one value.'
28
+ elsif archive_items(config).include?(nil)
29
+ ConfigurationValidator::EmptyArchiveItemsError.new 'Did you forget to add a value to the archive_menu?'
30
+ elsif config.has_key?("archive_menu") && !@file_system_accessor.file_exist?(partial_location)
31
+ ConfigurationValidator::MissingArchiveMenuPartialError.new "You must provide a template partial named at #{partial_location}"
32
+ end
33
+ end
34
+
35
+ private
36
+
37
+ def archive_items(config)
38
+ config.fetch('archive_menu', [])
39
+ end
40
+
41
+ end
42
+
43
+ Version = Struct.new(:major, :minor, :patch) do
44
+ class << self
45
+ def parse(raw_version)
46
+ if raw_version
47
+ new(*raw_version.split('.'))
48
+ else
49
+ new(nil, nil, nil)
50
+ end
51
+ end
52
+ end
53
+
54
+ def valid?
55
+ [major, minor, patch].all?(&:present?)
56
+ end
57
+
58
+ def to_s
59
+ [major, minor, patch].compact.join('.')
60
+ end
61
+ end
62
+
63
+ class VersionCheckerMessages
64
+ def initialize(user_schema_version, bookbinder_schema_version)
65
+ @user_schema_version = user_schema_version
66
+ @bookbinder_schema_version = bookbinder_schema_version
67
+ end
68
+
69
+ def schema_now_required_message
70
+ "[ERROR] Bookbinder now requires a certain schema. Please see README " +
71
+ "and provide a schema version."
72
+ end
73
+
74
+ def incompatible_schema_message
75
+ "[ERROR] Your config.yml format, schema version #{user_schema_version}, " +
76
+ "is older than this version of Bookbinder can support. Please update " +
77
+ "your config.yml keys and format to version #{bookbinder_schema_version} " +
78
+ "and try again."
79
+ end
80
+
81
+ def unrecognized_schema_version_message
82
+ "[ERROR] The config schema version #{user_schema_version} is " +
83
+ "unrecognized by this version of Bookbinder. The latest schema version " +
84
+ "is #{bookbinder_schema_version}."
85
+ end
86
+
87
+ private
88
+
89
+ attr_reader :user_schema_version, :bookbinder_schema_version
90
+ end
91
+
92
+ class ConfigVersionChecker
93
+ def initialize(bookbinder_schema_version, starting_schema_version, messages, logger)
94
+ @bookbinder_schema_version = bookbinder_schema_version
95
+ @starting_schema_version = starting_schema_version
96
+ @messages = messages
97
+ @logger = logger
98
+ end
99
+
100
+ def check(config)
101
+ user_schema_version = Version.parse(config['schema_version'])
102
+ if user_schema_version.valid?
103
+ if user_schema_version.major > bookbinder_schema_version.major
104
+ raise Configuration::ConfigSchemaUnsupportedError.new messages.unrecognized_schema_version_message
105
+ elsif user_schema_version.minor > bookbinder_schema_version.minor
106
+ raise Configuration::ConfigSchemaUnsupportedError.new messages.unrecognized_schema_version_message
107
+ elsif user_schema_version.patch > bookbinder_schema_version.patch
108
+ raise Configuration::ConfigSchemaUnsupportedError.new messages.unrecognized_schema_version_message
109
+ elsif user_schema_version.major < bookbinder_schema_version.major
110
+ raise Configuration::ConfigSchemaUnsupportedError.new messages.incompatible_schema_message
111
+ elsif user_schema_version.minor < bookbinder_schema_version.minor
112
+ @logger.warn nonbreaking_schema_message_for("minor")
113
+ elsif user_schema_version.patch < bookbinder_schema_version.patch
114
+ @logger.warn nonbreaking_schema_message_for("patch")
115
+ end
116
+ elsif bookbinder_schema_version != starting_schema_version
117
+ raise Configuration::ConfigSchemaUnsupportedError.new messages.schema_now_required_message
118
+ end
119
+ end
120
+
121
+ private
122
+
123
+ attr_reader :bookbinder_schema_version, :starting_schema_version, :messages
124
+
125
+ def nonbreaking_schema_message_for(version_level)
126
+ "[WARNING] Your schema is valid, but there exists a new #{version_level} version. Consider updating your config.yml."
127
+ end
128
+ end
129
+
130
+ class ConfigurationValidator
131
+ DuplicateSectionNameError = Class.new(RuntimeError)
132
+ MissingArchiveMenuPartialError = Class.new(RuntimeError)
133
+ EmptyArchiveItemsError = Class.new(RuntimeError)
134
+ ArchiveMenuNotDefinedError = Class.new(RuntimeError)
135
+
136
+ def initialize(logger, file_system_accessor)
137
+ @logger = logger
138
+ @file_system_accessor = file_system_accessor
139
+ end
140
+
141
+ def valid?(config_hash, bookbinder_schema_version, starting_schema_version)
142
+ raise 'Your config.yml appears to be empty. Please check and try again.' unless config_hash
143
+
144
+ user_config_schema_version = config_hash['schema_version']
145
+ exceptions = [
146
+ ConfigVersionChecker.new(Version.parse(bookbinder_schema_version),
147
+ Version.parse(starting_schema_version),
148
+ VersionCheckerMessages.new(Version.parse(user_config_schema_version),
149
+ bookbinder_schema_version),
150
+ @logger),
151
+ DuplicateSectionNameChecker.new,
152
+ ArchiveMenuChecker.new(@file_system_accessor)
153
+ ].map do |checker|
154
+ checker.check(config_hash)
155
+ end
156
+ exception = exceptions.compact.first
157
+ raise exception if exception
158
+
159
+ true
160
+ end
161
+ end
162
+ end