Skip to content

Commit 4413dfc

Browse files
authored
Merge pull request #986 from jmerle/update-checker
Automatically check for documentation updates
2 parents 1fd1ed9 + 6e91700 commit 4413dfc

File tree

155 files changed

+1182
-3
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

155 files changed

+1182
-3
lines changed

.github/CONTRIBUTING.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ In addition to the [guidelines for contributing code](#contributing-code-and-fea
6161

6262
Please don't submit a pull request updating the version number of a documentation, unless a change is required in the scraper and you've verified that it works.
6363

64-
To ask that an existing documentation be updated, please use the [Trello board](https://trello.com/c/2B0hmW7M/52-request-updates-here).
64+
To ask that an existing documentation be updated, first check the last two [Documentation versions reports](https://github.com/freeCodeCamp/devdocs/issues?utf8=%E2%9C%93&q=Documentation+versions+report+is%3Aissue+author%3Adevdocs-bot+sort%3Aupdated-desc). Only create an issue if the documentation has been wrongly marked as up-to-date.
6565

6666
## Coding conventions
6767

.travis.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@ before_script:
66
- gem update --system
77
- gem install bundler
88

9+
script:
10+
- if [ "$TRAVIS_EVENT_TYPE" != "cron" ]; then bundle exec rake; fi
11+
- if [ "$TRAVIS_EVENT_TYPE" = "cron" ]; then bundle exec thor updates:check --github-token $GH_TOKEN --upload; fi
12+
913
deploy:
1014
provider: heroku
1115
app: devdocs

Gemfile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ group :docs do
4242
gem 'unix_utils', require: false
4343
gem 'tty-pager', require: false
4444
gem 'net-sftp', '>= 2.1.3.rc2', require: false
45+
gem 'terminal-table', require: false
4546
end
4647

4748
group :test do

Gemfile.lock

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,8 @@ GEM
104104
unicode-display_width (~> 1.4.0)
105105
unicode_utils (~> 1.4.0)
106106
strings-ansi (0.1.0)
107+
terminal-table (1.8.0)
108+
unicode-display_width (~> 1.1, >= 1.1.1)
107109
thin (1.7.2)
108110
daemons (~> 1.0, >= 1.0.9)
109111
eventmachine (~> 1.0, >= 1.0.4)
@@ -157,6 +159,7 @@ DEPENDENCIES
157159
sinatra-contrib
158160
sprockets
159161
sprockets-helpers
162+
terminal-table
160163
sprockets-sass
161164
thin
162165
thor

docs/adding-docs.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ Adding a documentation may look like a daunting task but once you get the hang o
1717
10. To add syntax highlighting or execute custom JavaScript on the pages, create a file in the `assets/javascripts/views/pages/` directory (take a look at the other files to see how it works).
1818
11. Add the documentation's icon in the `public/icons/docs/[my_doc]/` directory, in both 16x16 and 32x32-pixels formats. It'll be added to the icon spritesheet after your pull request is merged.
1919
12. Add the documentation's copyright details to the list in `assets/javascripts/templates/pages/about_tmpl.coffee`. This is the data shown in the table on the [about](https://devdocs.io/about) page, and is ordered alphabetically. Simply copying an existing item, placing it in the right slot and updating the values to match the new scraper will do the job.
20+
13. Ensure `thor updates:check [my_doc]` shows the correct latest version.
2021

2122
If the documentation includes more than a few hundreds pages and is available for download, try to scrape it locally (e.g. using `FileScraper`). It'll make the development process much faster and avoids putting too much load on the source site. (It's not a problem if your scraper is coupled to your local setup, just explain how it works in your pull request.)
2223

docs/scraper-reference.md

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,3 +184,44 @@ More information about how filters work is available on the [Filter Reference](.
184184
Overrides the `:title` option for the root page only.
185185

186186
_Note: this filter is disabled by default._
187+
188+
## Keeping scrapers up-to-date
189+
190+
In order to keep scrapers up-to-date the `get_latest_version(opts)` method should be overridden. If `self.release` is defined, this should return the latest version of the documentation. If `self.release` is not defined, it should return the Epoch time when the documentation was last modified. If the documentation will never change, simply return `1.0.0`. The result of this method is periodically reported in a "Documentation versions report" issue which helps maintainers keep track of outdated documentations.
191+
192+
To make life easier, there are a few utility methods that you can use in `get_latest_version`:
193+
* `fetch(url, opts)`
194+
195+
Makes a GET request to the url and returns the response body.
196+
197+
Example: [lib/docs/scrapers/bash.rb](../lib/docs/scrapers/bash.rb)
198+
* `fetch_doc(url, opts)`
199+
200+
Makes a GET request to the url and returns the HTML body converted to a Nokogiri document.
201+
202+
Example: [lib/docs/scrapers/git.rb](../lib/docs/scrapers/git.rb)
203+
* `fetch_json(url, opts)`
204+
205+
Makes a GET request to the url and returns the JSON body converted to a dictionary.
206+
207+
Example: [lib/docs/scrapers/mdn/mdn.rb](../lib/docs/scrapers/mdn/mdn.rb)
208+
* `get_npm_version(package, opts)`
209+
210+
Returns the latest version of the given npm package.
211+
212+
Example: [lib/docs/scrapers/bower.rb](../lib/docs/scrapers/bower.rb)
213+
* `get_latest_github_release(owner, repo, opts)`
214+
215+
Returns the tag name of the latest GitHub release of the given repository. If the tag name is preceded by a "v", the "v" will be removed.
216+
217+
Example: [lib/docs/scrapers/jsdoc.rb](../lib/docs/scrapers/jsdoc.rb)
218+
* `get_github_tags(owner, repo, opts)`
219+
220+
Returns the list of tags on the given repository ([format](https://developer.github.com/v3/repos/#list-tags)).
221+
222+
Example: [lib/docs/scrapers/liquid.rb](../lib/docs/scrapers/liquid.rb)
223+
* `get_github_file_contents(owner, repo, path, opts)`
224+
225+
Returns the contents of the requested file in the default branch of the given repository.
226+
227+
Example: [lib/docs/scrapers/minitest.rb](../lib/docs/scrapers/minitest.rb)

lib/docs/core/doc.rb

Lines changed: 99 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,6 @@ def store_meta(store)
152152
end
153153
end
154154

155-
156155
def initialize
157156
raise NotImplementedError, "#{self.class} is an abstract class and cannot be instantiated." if self.class.abstract
158157
end
@@ -164,5 +163,104 @@ def build_page(id, &block)
164163
def build_pages(&block)
165164
raise NotImplementedError
166165
end
166+
167+
def get_scraper_version(opts)
168+
if self.class.method_defined?(:options) and !options[:release].nil?
169+
options[:release]
170+
else
171+
# If options[:release] does not exist, we return the Epoch timestamp of when the doc was last modified in DevDocs production
172+
json = fetch_json('https://devdocs.io/docs.json', opts)
173+
items = json.select {|item| item['name'] == self.class.name}
174+
items = items.map {|item| item['mtime']}
175+
items.max
176+
end
177+
end
178+
179+
# Should return the latest version of this documentation
180+
# If options[:release] is defined, it should be in the same format
181+
# If options[:release] is not defined, it should return the Epoch timestamp of when the documentation was last updated
182+
# If the docs will never change, simply return '1.0.0'
183+
def get_latest_version(opts)
184+
raise NotImplementedError
185+
end
186+
187+
# Returns whether or not this scraper is outdated.
188+
#
189+
# The default implementation assumes the documentation uses a semver(-like) approach when it comes to versions.
190+
# Patch updates are ignored because there are usually little to no documentation changes in bug-fix-only releases.
191+
#
192+
# Scrapers of documentations that do not use this versioning approach should override this method.
193+
#
194+
# Examples of the default implementation:
195+
# 1 -> 2 = outdated
196+
# 1.1 -> 1.2 = outdated
197+
# 1.1.1 -> 1.1.2 = not outdated
198+
def is_outdated(scraper_version, latest_version)
199+
scraper_parts = scraper_version.to_s.split(/\./).map(&:to_i)
200+
latest_parts = latest_version.to_s.split(/\./).map(&:to_i)
201+
202+
# Only check the first two parts, the third part is for patch updates
203+
[0, 1].each do |i|
204+
break if i >= scraper_parts.length or i >= latest_parts.length
205+
return true if latest_parts[i] > scraper_parts[i]
206+
return false if latest_parts[i] < scraper_parts[i]
207+
end
208+
209+
false
210+
end
211+
212+
private
213+
214+
#
215+
# Utility methods for get_latest_version
216+
#
217+
218+
def fetch(url, opts)
219+
headers = {}
220+
221+
if opts.key?(:github_token) and url.start_with?('https://api.github.com/')
222+
headers['Authorization'] = "token #{opts[:github_token]}"
223+
end
224+
225+
opts[:logger].debug("Fetching #{url}")
226+
response = Request.run(url, { connecttimeout: 15, headers: headers })
227+
228+
if response.success?
229+
response.body
230+
else
231+
reason = response.timed_out? ? "Timed out while connecting to #{url}" : "Couldn't fetch #{url} (response code #{response.code})"
232+
opts[:logger].error(reason)
233+
raise reason
234+
end
235+
end
236+
237+
def fetch_doc(url, opts)
238+
body = fetch(url, opts)
239+
Nokogiri::HTML.parse(body, nil, 'UTF-8')
240+
end
241+
242+
def fetch_json(url, opts)
243+
JSON.parse fetch(url, opts)
244+
end
245+
246+
def get_npm_version(package, opts)
247+
json = fetch_json("https://registry.npmjs.com/#{package}", opts)
248+
json['dist-tags']['latest']
249+
end
250+
251+
def get_latest_github_release(owner, repo, opts)
252+
release = fetch_json("https://api.github.com/repos/#{owner}/#{repo}/releases/latest", opts)
253+
tag_name = release['tag_name']
254+
tag_name.start_with?('v') ? tag_name[1..-1] : tag_name
255+
end
256+
257+
def get_github_tags(owner, repo, opts)
258+
fetch_json("https://api.github.com/repos/#{owner}/#{repo}/tags", opts)
259+
end
260+
261+
def get_github_file_contents(owner, repo, path, opts)
262+
json = fetch_json("https://api.github.com/repos/#{owner}/#{repo}/contents/#{path}", opts)
263+
Base64.decode64(json['content'])
264+
end
167265
end
168266
end

lib/docs/scrapers/angular.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,10 @@ def handle_response(response)
155155
end
156156
end
157157

158+
def get_latest_version(opts)
159+
get_npm_version('@angular/core', opts)
160+
end
161+
158162
private
159163

160164
def parse(response)

lib/docs/scrapers/angularjs.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,5 +69,9 @@ class Angularjs < UrlScraper
6969
self.release = '1.2.32'
7070
self.base_url = "https://code.angularjs.org/#{release}/docs/partials/"
7171
end
72+
73+
def get_latest_version(opts)
74+
get_npm_version('angular', opts)
75+
end
7276
end
7377
end

lib/docs/scrapers/ansible.rb

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,5 +87,10 @@ class Ansible < UrlScraper
8787
quickstart.html
8888
list_of_all_modules.html)
8989
end
90+
91+
def get_latest_version(opts)
92+
doc = fetch_doc('https://docs.ansible.com/ansible/latest/index.html', opts)
93+
doc.at_css('.DocSiteProduct-CurrentVersion').content.strip
94+
end
9095
end
9196
end

0 commit comments

Comments
 (0)