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