Skip to content

Commit ef4e611

Browse files
authored
Automate repository docs inclusion (#1762)
So far every prometheus/alertmanater/... release branch had to be manually configured in the nanoc.yaml config file. With this change the most recent release branches will be checked out automatically if a corresponding semver tag exists. As the Prometheus git repository includes several hundreds of megabytes of vendored assets, the repository is cloned bare and all blobs are filtered by default. Each version is then checked out in an individual working tree and git's spare-chekcout feature is used to reduce the checkout to the `docs/` folder. The git data is cached in `tmp/repo_docs/` and will be recreated automatically if removed. Signed-off-by: Tobias Schmidt <[email protected]>
1 parent f1d7a5a commit ef4e611

File tree

12 files changed

+199
-377
lines changed

12 files changed

+199
-377
lines changed

.gitignore

+1-2
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,7 @@ output/
55

66
# Temporary file directory
77
tmp/
8-
/downloads/
9-
/repositories/
8+
downloads/
109

1110
# Crash Log
1211
crash.log

Makefile

+1-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ bundle:
99
bundle install --path vendor
1010

1111
clean:
12-
rm -rf output downloads repositories
12+
rm -rf output downloads
1313

1414
compile:
1515
$(NANOC)

Rules

+3-17
Original file line numberDiff line numberDiff line change
@@ -14,21 +14,8 @@
1414
# because “*” matches zero or more characters.
1515

1616
passthrough '/assets/**'
17-
18-
compile '/_redirects/' do
19-
end
20-
21-
route '/_redirects/' do
22-
'/_redirects'
23-
end
24-
25-
# TODO(ts): Remove these hacks once the nanoc4 upgrade is done.
26-
compile '*/images/*' do
27-
end
28-
29-
route '*/images/*' do
30-
item.identifier.chop + '.' + item[:extension]
31-
end
17+
passthrough '/_redirects'
18+
passthrough '*/images/*'
3219

3320
# RSS Feed
3421
compile '/blog/feed/' do
@@ -45,8 +32,7 @@ compile '*' do
4532
if item[:extension] == 'md'
4633
filter :redcarpet, options: {filter_html: true, autolink: true, no_intraemphasis: true, fenced_code_blocks: true, gh_blockcode: true, tables: true}, renderer_options: {with_toc_data: true}
4734
filter :normalize_links, item[:repo_docs] if item[:repo_docs]
48-
filter :outdated_content, item[:repo_docs] if item[:repo_docs] && item[:repo_docs][:outdated]
49-
filter :prerelease_content, item[:repo_docs] if item[:repo_docs] && item[:repo_docs][:prerelease]
35+
filter :version_warning, item[:repo_docs] if item[:repo_docs]
5036
filter :add_anchors
5137
filter :bootstrappify
5238
filter :admonition

layouts/header.html

+1-1
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
<link rel="icon" type="image/png" href="/assets/favicons/favicon-96x96.png" sizes="96x96">
2626
<link rel="icon" type="image/png" href="/assets/favicons/favicon-16x16.png" sizes="16x16">
2727
<link rel="manifest" href="/assets/favicons/android-chrome-manifest.json">
28-
<% if (c = @item[:repo_docs]) && c[:canonical] %><link rel="canonical" href="<%= @item.path.sub(c[:items_root], c[:canonical]) %>" /><% end %>
28+
<% if (c = @item[:repo_docs]) && c[:canonical_root] %><link rel="canonical" href="<%= @item.path.sub(c[:items_root], c[:canonical_root]) %>" /><% end %>
2929
<!-- Meta tag for indexing that enables faceted search in Algolia,
3030
see https://docsearch.algolia.com/docs/required-configuration/#introduce-global-information-as-meta-tags -->
3131
<meta name="docsearch:prometheus-version" content="<%= @item[:repo_docs] && @item[:repo_docs][:name] || 'none' %>" />

lib/data_sources/repo_docs.rb

+138-40
Original file line numberDiff line numberDiff line change
@@ -1,60 +1,158 @@
1-
# TODO(ts): Rewrite data source and use one single instance to combine all
2-
# different versions for a given path.
3-
class RepoDocsDataSource < ::Nanoc::DataSources::Filesystem
1+
require 'uri'
2+
3+
# The RepoDocs data source provides items sourced from other Git repositories.
4+
# For a given repository_url, all git version tags are fetched and for the most
5+
# recent (in order to save compilation time) tags the `docs/` folder in the
6+
# respective `release-<version>` is checked out and its content mounted under
7+
# the given `items_root`.
8+
#
9+
# As the Prometheus git repository includes several hundreds of megabytes of
10+
# vendored assets, the repository is cloned bare and all blobs are filtered by
11+
# default. Each version is then checked out in an individual working tree and
12+
# git's spare-checkout feature is used to reduce the checkout to the `docs/`
13+
# folder. The git data is cached in `tmp/repo_docs/`.
14+
class RepoDocsDataSource < ::Nanoc::DataSource
415
identifier :repo_docs
516

6-
PATH = "repositories"
17+
DOCS_DIRECTORY = 'docs'.freeze
18+
BRANCH_PATTERN = 'release-*'.freeze
19+
VERSION_REGEXP = /\Av\d+\.\d+\.\d+(?:-[a-z0-9.]+)?\z/.freeze
20+
TMPDIR = 'tmp/repo_docs/'.freeze
721

822
def up
9-
c = config[:config]
10-
11-
%x(
12-
scripts/checkout.sh \
13-
-d "#{docs_root}" \
14-
-t "#{repo_path}" \
15-
"#{c[:repository]}" "#{c[:refspec]}"
16-
)
17-
if $?.exitstatus != 0
18-
raise "Couldn't checkout repository #{c.inspect}"
19-
end
20-
21-
super
23+
validate
24+
sync_repository
2225
end
2326

2427
def items
25-
c = config.fetch(:config)
26-
super.map do |item|
27-
attrs = item.attributes.dup
28-
attrs[:repo_docs] = c
29-
attrs[:repo_docs][:items_root] = config.fetch(:items_root)
30-
# TODO(ts): Remove assumptions about the path layout, rewrite datasource.
31-
attrs[:repo_docs][:version_root] = config.fetch(:items_root).sub(%r{(.+/)[^/]+/\Z}, '\\1')
32-
# TODO(ts): Document that repo doc index.md will be ignored.
33-
if item.identifier == '/'
34-
attrs[:nav] = { strip: true }
28+
items_root = config.fetch(:items_root, '/')
29+
latest = latest_version
30+
31+
versions.inject([]) do |list, version|
32+
branch = "release-#{version}"
33+
dir = git_checkout(branch, DOCS_DIRECTORY)
34+
fs_config = { content_dir: dir, encoding: 'utf-8', identifier_type: 'legacy' }
35+
fs = ::Nanoc::DataSources::Filesystem.new(@site_config, '/', '/', fs_config)
36+
37+
fs.items.each do |item|
38+
attrs = item.attributes.dup
39+
attrs[:nav] = { strip: true } if item.identifier == '/'
40+
attrs[:repo_docs] = {
41+
name: version,
42+
refspec: branch,
43+
version: version,
44+
latest: latest,
45+
items_root: items_root,
46+
version_root: File.join(items_root, version, '/'),
47+
canonical_root: File.join(items_root, 'latest', '/'),
48+
repository_url: git_remote,
49+
entrypoint: config[:config][:entrypoint],
50+
}
51+
52+
if version == latest
53+
lattrs = attrs.dup
54+
lattrs[:repo_docs] = attrs[:repo_docs].dup
55+
lattrs[:repo_docs][:name] = "latest (#{version})"
56+
lattrs[:repo_docs][:version_root] = lattrs[:repo_docs][:canonical_root]
57+
list << new_item(item.content, lattrs, item.identifier.prefix('/latest'))
58+
end
59+
60+
list << new_item(item.content, attrs, item.identifier.prefix('/' + version))
3561
end
36-
new_item(item.content, attrs, item.identifier)
62+
63+
list
3764
end
3865
end
3966

40-
def content_dir_name
41-
File.join(repo_path, docs_root)
67+
private
68+
69+
def validate
70+
if !config[:config].has_key?(:entrypoint)
71+
fail ArgumentError, 'entrypoint config option must be set'
72+
end
73+
if !config[:config].has_key?(:repository_url)
74+
fail ArgumentError, 'repository config option must be set'
75+
end
76+
URI(config[:config][:repository_url]) # raises an exception if invalid
4277
end
4378

44-
def layouts_dir_name
45-
'unsupported'
79+
def git_remote
80+
config[:config][:repository_url]
4681
end
4782

48-
private
83+
def git_dir
84+
basename = File.basename(git_remote)
85+
basename += '.git' unless basename.end_with?('.git')
86+
File.join(TMPDIR, basename)
87+
end
88+
89+
def git_branches
90+
output = `cd #{git_dir} && git branch --format='%(refname:short)' --list '#{BRANCH_PATTERN}'`
91+
fail "Could not list git branches" if $?.exitstatus != 0
92+
output.split("\n")
93+
end
94+
95+
def git_tags
96+
output = `cd #{git_dir} && git tag`
97+
fail "Could not list git tags" if $?.exitstatus != 0
98+
output.split("\n")
99+
end
100+
101+
# git_checkout checks out the directory in the specified branch using git's
102+
# sparse checkout and returns the path to the location in the workingtree.
103+
def git_checkout(branch, directory)
104+
worktree = File.absolute_path(File.join(git_dir.delete_suffix('.git'), branch))
105+
if !File.exist?(File.join(worktree, '.git'))
106+
run_command("cd #{git_dir} && git worktree add --no-checkout #{worktree} #{branch}")
107+
end
108+
109+
worktree_info = File.join(git_dir, 'worktrees', branch, 'info')
110+
Dir.mkdir(worktree_info) if !Dir.exist?(worktree_info)
111+
File.write(File.join(worktree_info, 'sparse-checkout'), "/#{directory}\n")
112+
113+
run_command("cd #{worktree} && git reset --hard --quiet && git clean --force")
114+
File.join(worktree, directory)
115+
end
116+
117+
# sync_repository clones or updates a bare git repository and enables the
118+
# sparse checkout feature.
119+
def sync_repository
120+
if !Dir.exist?(git_dir)
121+
run_command("git clone --bare --filter=blob:none #{git_remote} #{git_dir}")
122+
run_command("cd #{git_dir} && git config core.sparseCheckout true")
123+
else
124+
run_command("cd #{git_dir} && git fetch --quiet")
125+
end
126+
end
127+
128+
# versions returns an ordered list of major.minor version names for which
129+
# documentation should be published. Only the most recent versions for which a
130+
# corresponding release-* branch exists are returned.
131+
def versions
132+
branches = git_branches
133+
all = git_tags
134+
.select { |v| v.match(VERSION_REGEXP) }
135+
.map { |v| v.delete_prefix('v').split('.')[0, 2].join('.') }
136+
.uniq
137+
.select { |v| branches.include?('release-' + v) }
138+
.sort_by { |v| v.split('.').map(&:to_i) }
139+
.reverse
140+
141+
# Number of versions is reduced to speed up site compilation time.
142+
grouped = all.group_by { |v| v.split('.').first }
143+
grouped.inject([]) do |list, (major, versions)|
144+
size = major == grouped.keys.first ? 10 : 1
145+
list += versions[0, size]
146+
end
147+
end
49148

50-
def docs_root
51-
c = config.fetch(:config)
52-
c.fetch(:root, 'docs/')
149+
# latest_version returns the latest released version.
150+
def latest_version
151+
tags = git_tags
152+
versions.find { |v| tags.any? { |t| t.start_with?('v' + v) && !t.include?('-') } }
53153
end
54154

55-
def repo_path
56-
c = config.fetch(:config)
57-
base = c.fetch(:repo_base, 'repositories')
58-
File.join(base, File.basename(c[:repository]), c[:name])
155+
def run_command(cmd)
156+
fail "Running command '#{cmd}' failed" if !system(cmd)
59157
end
60158
end

lib/filters/normalize_links.rb

+1-4
Original file line numberDiff line numberDiff line change
@@ -35,10 +35,7 @@ def run(content, config = {})
3535
end
3636

3737
def github_link_to(file, config)
38-
base = config[:repository]
39-
if base.end_with?('.git')
40-
base = base[0..-5]
41-
end
38+
base = config[:repository_url].delete_suffix('.git')
4239
File.join(base, 'blob', config[:refspec], file)
4340
end
4441

lib/filters/outdated_content.rb

-23
This file was deleted.

lib/filters/prerelease_content.rb

-23
This file was deleted.

lib/filters/version_warning.rb

+44
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
# encoding: utf-8
2+
3+
require 'nokogiri'
4+
5+
# VersionWarning adds a warning to the top of pre-release or outdated versioned
6+
# documentation pages.
7+
class VersionWarning < ::Nanoc::Filter
8+
identifier :version_warning
9+
10+
def run(content, params = {})
11+
case version_compare(params[:version], params[:latest])
12+
when 1
13+
type = 'a pre-release version'
14+
when 0
15+
return content
16+
when -1
17+
type = 'an old version'
18+
end
19+
20+
href = File.join(params[:canonical_root], params[:entrypoint])
21+
repo = File.basename(params[:repository_url], '.git').capitalize
22+
warning = %(<p>CAUTION: This page documents #{type} of #{repo}.
23+
Check out the <a href="#{href}">latest stable version</a>.</p>)
24+
25+
prepend_warning(content, warning)
26+
end
27+
28+
private
29+
30+
def prepend_warning(content, warning)
31+
doc = Nokogiri::HTML(content)
32+
body = doc.css('body')
33+
if first = body.children.first
34+
first.add_previous_sibling(warning)
35+
else
36+
body << Nokogiri::HTML::DocumentFragment.parse(warning)
37+
end
38+
doc.to_s
39+
end
40+
41+
def version_compare(a, b)
42+
a.split('.').map(&:to_i) <=> b.split('.').map(&:to_i)
43+
end
44+
end

lib/helpers/nav.rb

+4-9
Original file line numberDiff line numberDiff line change
@@ -90,21 +90,16 @@ def self.versioned?(item)
9090
!item[:repo_docs].nil?
9191
end
9292

93-
# latest? returns true if the item is part of the version group "latest".
94-
def self.latest?(opts)
95-
opts[:name].include?('latest')
96-
end
97-
9893
# current? returns true if the item is part of the selected version group. If
9994
# no group is selected (e.g. when a page outside of the versioned docs is
10095
# viewed), the latest version will be shown.
10196
def self.current?(opts, page)
10297
return false if opts.nil? || !page.respond_to?(:path)
10398

104-
if page.path.start_with?(opts[:version_root])
105-
page.path.start_with?(opts[:items_root])
99+
if page.path.start_with?(opts[:items_root])
100+
page.path.start_with?(opts[:version_root])
106101
else
107-
latest?(opts)
102+
opts[:version_root] == opts[:canonical_root]
108103
end
109104
end
110105

@@ -115,7 +110,7 @@ def self.picker(items, page, active)
115110
selected = current?(v, page) ? 'selected="selected"' : ''
116111
# TODO(ts): Refactor and think about linking directly to the page of the same version.
117112
first = items
118-
.find { |i| i.path.start_with?(v[:items_root]) }
113+
.find { |i| i.path.start_with?(v[:version_root]) }
119114
.children.sort_by { |c| c[:sort_rank] || 0 }.first
120115
%(<option value="#{first.path}" #{selected}>#{v[:name]}</option>)
121116
end

0 commit comments

Comments
 (0)