Created page with "--- Module:GitHubVersion --- Fetch the version from extension.json at the latest tag of a GitHub repo. --- Usage (wikitext): --- {{#invoke:GitHubVersion|extensionVersion|owner=wikimedia|repo=mediawiki-extensions-AdhocTranslation}} --- --- Optional named params: --- path - path to JSON file at repo root (default: "extension.json") --- fallback - what to show if version cannot be determined (default: empty) --- showTag - "yes" to return the tag name if "..." |
(No difference)
|
Latest revision as of 10:02, 19 March 2026
Documentation for this module may be created at Module:GitHubVersion/doc
--- Module:GitHubVersion
--- Fetch the version from extension.json at the latest tag of a GitHub repo.
--- Usage (wikitext):
--- {{#invoke:GitHubVersion|extensionVersion|owner=wikimedia|repo=mediawiki-extensions-AdhocTranslation}}
---
--- Optional named params:
--- path - path to JSON file at repo root (default: "extension.json")
--- fallback - what to show if version cannot be determined (default: empty)
--- showTag - "yes" to return the tag name if "version" is missing
--- timeout - HTTP timeout seconds (default: 5)
--- ua - extra User-Agent suffix (default: none)
---
--- Notes:
--- - This module uses GitHub /tags API (works even without releases).
--- - It then fetches https://raw.githubusercontent.com/<owner>/<repo>/<tag>/<path>
--- - The result is cached by the page cache; within a single parse we memoize.
local p = {}
local JSON = mw.text.jsonDecode
-- Basic in-process memoization for a single parse
local cache = {}
-- Utility: build a GitHub-friendly User-Agent
local function userAgent(extra)
-- GitHub requires a UA; include wiki/site name for politeness and traceability
local site = (mw.site and mw.site.server or "unknown-site")
local ua = string.format("MediaWiki-Scribunto/1 (%s)", site)
if extra and extra ~= "" then
ua = ua .. " " .. tostring(extra)
end
return ua
end
-- Perform an HTTP GET and return (ok, status, body) with normalized error handling
local function httpGet(url, timeout, ua, accept)
local opts = {
url = url,
method = "GET",
headers = {
["User-Agent"] = userAgent(ua),
["Accept"] = accept or "application/json",
},
timeout = tonumber(timeout) or 5,
}
local r = mw.http.request(opts)
if not r then
return false, 0, "no response"
end
-- Treat 200 as success; 3xx/4xx/5xx are failures we surface
if r.status ~= 200 then
return false, r.status, r.body or ""
end
return true, r.status, r.body or ""
end
-- Try to get the newest tag via /tags?per_page=1 (tags are listed newest-first by default)
local function fetchLatestTag(owner, repo, timeout, ua)
local url = string.format(
"https://api.github.com/repos/%s/%s/tags?per_page=1",
mw.uri.encode(owner), mw.uri.encode(repo)
)
local ok, status, body = httpGet(url, timeout, ua, "application/vnd.github+json")
if not ok then return nil, string.format("GitHub tags API error (%d)", status) end
local data = JSON(body)
if type(data) ~= "table" or #data == 0 or type(data[1].name) ~= "string" then
return nil, "No tags found"
end
return data[1].name, nil
end
-- Fetch and parse JSON at the raw URL of the tag
local function fetchJsonAtTag(owner, repo, tag, path, timeout, ua)
path = path or "extension.json"
local url = string.format(
"https://raw.githubusercontent.com/%s/%s/%s/%s",
mw.uri.encode(owner), mw.uri.encode(repo), mw.uri.encode(tag), path
)
local ok, status, body = httpGet(url, timeout, ua, "application/json")
if not ok then
return nil, string.format("raw.githubusercontent.com error (%d)", status)
end
local data = JSON(body)
if type(data) ~= "table" then
return nil, "Invalid JSON"
end
return data, nil
end
-- Public: get the "version" from extension.json at latest tag
function p.extensionVersion(frame)
local args = frame.args or {}
-- Allow Template invocation passthrough
if frame:getParent() then
for k, v in pairs(frame:getParent().args) do
if args[k] == nil or args[k] == "" then
args[k] = v
end
end
end
local owner = (args.owner or ""):gsub("^%s+", ""):gsub("%s+$", "")
local repo = (args.repo or ""):gsub("^%s+", ""):gsub("%s+$", "")
local path = (args.path or "extension.json")
local fallback = args.fallback or ""
local showTag = (args.showTag or ""):lower() == "yes"
local timeout = tonumber(args.timeout) or 5
local ua = args.ua or ""
if owner == "" or repo == "" then
return "⛔ owner and repo are required"
end
-- Cache key for memoization
local key = table.concat({ owner, repo, path, tostring(showTag) }, "|")
if cache[key] then
return cache[key]
end
-- 1) Get the latest tag
local tag, err = fetchLatestTag(owner, repo, timeout, ua)
if not tag then
-- As a fallback, try releases/latest (some repos use releases; others don't)
local url = string.format("https://api.github.com/repos/%s/%s/releases/latest",
mw.uri.encode(owner), mw.uri.encode(repo))
local ok, status, body = httpGet(url, timeout, ua, "application/vnd.github+json")
if ok then
local rel = JSON(body)
if type(rel) == "table" and type(rel.tag_name) == "string" then
tag = rel.tag_name
end
end
end
if not tag then
cache[key] = fallback
return fallback
end
-- 2) Fetch JSON file at tag
local data, jerr = fetchJsonAtTag(owner, repo, tag, path, timeout, ua)
if not data then
if showTag then
cache[key] = tag
return tag
end
cache[key] = fallback
return fallback
end
-- 3) Extract version (common field in extension.json)
local version = data.version
if type(version) == "string" and version ~= "" then
cache[key] = version
return version
end
-- If version is missing, optionally fall back to tag name
if showTag then
cache[key] = tag
return tag
end
cache[key] = fallback
return fallback
end
return p