fix: dont use oldField as regex to replace to newField

This commit is contained in:
ernisto 2025-04-25 15:03:08 -03:00
parent 9c83137753
commit 07a8d04523

View file

@ -1,221 +1,224 @@
--> Updates tooling bin versions and READMEs --> Updates tooling bin versions and READMEs
local serde = require("@lune/serde") local serde = require("@lune/serde")
local stdio = require("@lune/stdio") local stdio = require("@lune/stdio")
local process = require("@lune/process") local process = require("@lune/process")
local DateTime = require("@lune/datetime") local DateTime = require("@lune/datetime")
local pathfs = require("../lune_packages/pathfs") local pathfs = require("../lune_packages/pathfs")
local base64 = require("../lune_packages/base64") local base64 = require("../lune_packages/base64")
local manifestTypes = require("../toolchainlib/src/manifest") local manifestTypes = require("../toolchainlib/src/manifest")
local Result = require("../lune_packages/result") local Result = require("../lune_packages/result")
local Option = require("../lune_packages/option") local Option = require("../lune_packages/option")
type Result<T, E> = Result.Result<T, E> type Result<T, E> = Result.Result<T, E>
type Option<T> = Option.Option<T> type Option<T> = Option.Option<T>
local Github = require("../toolchainlib/src/github") local Github = require("../toolchainlib/src/github")
type GithubContents = { type GithubContents = {
name: string, name: string,
path: string, path: string,
sha: string, sha: string,
size: number, size: number,
url: string, url: string,
html_url: string, html_url: string,
git_url: string, git_url: string,
download_url: string, download_url: string,
type: "file" | "dir" | "symlink", type: "file" | "dir" | "symlink",
content: string, content: string,
encoding: "base64", encoding: "base64",
_links: { _links: {
self: string, self: string,
git: string, git: string,
html: string, html: string,
}, },
} }
local function isoDateToTimestamp(isoDate: string): number local function isoDateToTimestamp(isoDate: string): number
return DateTime.fromIsoDate(isoDate).unixTimestamp return DateTime.fromIsoDate(isoDate).unixTimestamp
end end
local function stripLeadingVersion(version: string): string local function stripLeadingVersion(version: string): string
local stripped = string.gsub(version, "^v", "") local stripped = string.gsub(version, "^v", "")
return stripped return stripped
end end
local function confirmAndClear(msg: string, default: boolean?): boolean local function confirmAndClear(msg: string, default: boolean?): boolean
local yes = stdio.prompt("confirm", msg, default) local yes = stdio.prompt("confirm", msg, default)
stdio.write( stdio.write(
-- Move to the previous line, clear it, move cursor to start of line, -- Move to the previous line, clear it, move cursor to start of line,
-- and show cursor (if hidden) -- and show cursor (if hidden)
"\x1b[A\x1b[K\x1b[0G\x1b[?25h" "\x1b[A\x1b[K\x1b[0G\x1b[?25h"
) )
return yes return yes
end end
local INFO_PREFIX = `{stdio.color("green")}{stdio.style("bold")}info{stdio.color("reset")}:` local INFO_PREFIX = `{stdio.color("green")}{stdio.style("bold")}info{stdio.color("reset")}:`
local WARN_PREFIX = `{stdio.color("yellow")}{stdio.style("bold")}warn{stdio.color("reset")}:` local WARN_PREFIX = `{stdio.color("yellow")}{stdio.style("bold")}warn{stdio.color("reset")}:`
local ERROR_PREFIX = `{stdio.color("red")}{stdio.style("bold")}error{stdio.color("reset")}:` local ERROR_PREFIX = `{stdio.color("red")}{stdio.style("bold")}error{stdio.color("reset")}:`
local function warn(...) local function warn(...)
stdio.ewrite(`{WARN_PREFIX} {stdio.format(...)}\n`) stdio.ewrite(`{WARN_PREFIX} {stdio.format(...)}\n`)
end end
local function error(msg: string): never local function error(msg: string): never
stdio.ewrite(`{ERROR_PREFIX} {msg}\n`) stdio.ewrite(`{ERROR_PREFIX} {msg}\n`)
return process.exit(1) return process.exit(1)
end end
local function assert(expr: boolean, msg: string) local function assert(expr: boolean, msg: string)
if not expr then if not expr then
error(msg) error(msg)
end end
end end
local START_TIME = os.clock() local START_TIME = os.clock()
local BINS_SRC_DIR = pathfs.getAbsolutePathOf(pathfs.Path.from("bins")) local BINS_SRC_DIR = pathfs.getAbsolutePathOf(pathfs.Path.from("bins"))
for _, binSrc in pathfs.readDir(BINS_SRC_DIR) do for _, binSrc in pathfs.readDir(BINS_SRC_DIR) do
local absPath = BINS_SRC_DIR:join(binSrc) local absPath = BINS_SRC_DIR:join(binSrc)
local binEntrypoint = absPath:join("init.luau") local binEntrypoint = absPath:join("init.luau")
local manifestPath = absPath:join("pesde.toml") local manifestPath = absPath:join("pesde.toml")
local readmePath = absPath:join("README.md") local readmePath = absPath:join("README.md")
-- Make sure our constructed entrypoint and manifest paths exist -- Make sure our constructed entrypoint and manifest paths exist
assert( assert(
pathfs.isFile(binEntrypoint) and pathfs.isFile(manifestPath), pathfs.isFile(binEntrypoint) and pathfs.isFile(manifestPath),
"Either binary entrypoint or manifest not found" "Either binary entrypoint or manifest not found"
) )
local manifestContents = pathfs.readFile(manifestPath) local manifestContents = pathfs.readFile(manifestPath)
local manifest: manifestTypes.PesdeManifest = serde.decode("toml", manifestContents) local manifest: manifestTypes.PesdeManifest = serde.decode("toml", manifestContents)
local entrypointContents = pathfs.readFile(binEntrypoint) local entrypointContents = pathfs.readFile(binEntrypoint)
local repoName = string.match(entrypointContents, 'require%("./lune_packages/core"%)%("([^"]+)"') local repoName = string.match(entrypointContents, 'require%("./lune_packages/core"%)%("([^"]+)"')
local version = manifest.version local version = manifest.version
-- Make sure we have a repo name and version -- Make sure we have a repo name and version
assert(repoName ~= nil, `Failed to get repo name for tool {binSrc}`) assert(repoName ~= nil, `Failed to get repo name for tool {binSrc}`)
local gh = Github.new( local gh = Github.new(
repoName :: string, repoName :: string,
Option.Some({ Option.Some({
authToken = Option.from(process.env.GITHUB_TOKEN) :: Option<string>, authToken = Option.from(process.env.GITHUB_TOKEN) :: Option<string>,
retries = Option.None :: Option<number>, retries = Option.None :: Option<number>,
} :: Github.Config) :: Option<Github.Config> } :: Github.Config) :: Option<Github.Config>
) )
local transactions = gh:queueTransactions({ "FetchReleases" }) local transactions = gh:queueTransactions({ "FetchReleases" })
-- Fetch releases, and order them by published date -- Fetch releases, and order them by published date
local releases = transactions[1]:unwrap() :: Github.GithubReleases local releases = transactions[1]:unwrap() :: Github.GithubReleases
table.sort(releases, function(a, b) table.sort(releases, function(a, b)
return isoDateToTimestamp(a.published_at) < isoDateToTimestamp(b.published_at) return isoDateToTimestamp(a.published_at) < isoDateToTimestamp(b.published_at)
end) end)
-- Filter for only versions which are after the current version -- Filter for only versions which are after the current version
local releasesAfter = {} local releasesAfter = {}
local versionIdx = math.huge local versionIdx = math.huge
for idx, release in releases do for idx, release in releases do
if stripLeadingVersion(release.tag_name) == version then if stripLeadingVersion(release.tag_name) == version then
versionIdx = idx versionIdx = idx
continue continue
end end
if idx > versionIdx then if idx > versionIdx then
releasesAfter[release.tag_name] = release releasesAfter[release.tag_name] = release
end end
end end
for newVersion, newRelease in releasesAfter do for newVersion, newRelease in releasesAfter do
print( print(
`{INFO_PREFIX} Found new tool release {stdio.style("bold")}{binSrc}{stdio.style("reset")}@{stdio.style( `{INFO_PREFIX} Found new tool release {stdio.style("bold")}{binSrc}{stdio.style("reset")}@{stdio.style(
"dim" "dim"
)}{newVersion}{stdio.style("reset")}` )}{newVersion}{stdio.style("reset")}`
) )
-- HACK: To prevent messing with our existing toml ordering and formatting -- HACK: To prevent messing with our existing toml ordering and formatting
-- we just replace the old version field string with the new version field -- we just replace the old version field string with the new version field
-- string -- string
-- the string returned by serde.encode end with newlines, so remove them to maintain compatibility with different line endings -- the string returned by serde.encode end with newlines, so remove them to maintain compatibility with different line endings
-- Old version field string: -- Old version field string:
local oldField = string.gsub(serde.encode("toml", { version = manifest.version }), "%s+$", "") local oldField = string.gsub(serde.encode("toml", { version = manifest.version }), "%s+$", "")
-- New version field string: -- New version field string:
local newField = string.gsub(serde.encode("toml", { version = stripLeadingVersion(newVersion) }), "%s+$", "") local newField = string.gsub(serde.encode("toml", { version = stripLeadingVersion(newVersion) }), "%s+$", "")
local updatedManifest = string.gsub( local updatedManifest, replaces = string.gsub(
manifestContents, manifestContents,
oldField, string.gsub(oldField, "[%.%+%-]", "%%%0"),
newField, newField,
-- Only replace the first occurrence to be safe -- Only replace the first occurrence to be safe
1 1
) )
if replaces == 0 then
local toWrite = table.find(process.args, "--yes") error("failed to replace version field in manifest")
or table.find(process.args, "-y") end
or confirmAndClear(`Update manifest for {binSrc}?`, false)
local toWrite = table.find(process.args, "--yes")
if toWrite then or table.find(process.args, "-y")
print( or confirmAndClear(`Update manifest for {binSrc}?`, false)
`{INFO_PREFIX} Updated manifest {stdio.style("dim")}{manifestPath:stripPrefix(pathfs.cwd)}{stdio.style(
"reset" if toWrite then
)}` print(
) `{INFO_PREFIX} Updated manifest {stdio.style("dim")}{manifestPath:stripPrefix(pathfs.cwd)}{stdio.style(
"reset"
pathfs.writeFile(manifestPath, updatedManifest) )}`
end )
end
pathfs.writeFile(manifestPath, updatedManifest)
-- Fetch README for the tool repo, if present end
transactions = gh:queueTransactions({ end
{
type = "Custom", -- Fetch README for the tool repo, if present
payload = { transactions = gh:queueTransactions({
method = "GET", {
url = `https://api.github.com/repos/{repoName}/readme`, type = "Custom",
}, payload = {
}, method = "GET",
}) url = `https://api.github.com/repos/{repoName}/readme`,
},
local contentsResp = transactions[1] :: Result<GithubContents, string> },
})
local readmeRes = contentsResp:andThen(function(resp: GithubContents)
-- If there was not an error, and the contents are base64 encoded, local contentsResp = transactions[1] :: Result<GithubContents, string>
-- we decode the contents and return the decoded buffer
if resp.encoding == "base64" then local readmeRes = contentsResp:andThen(function(resp: GithubContents)
-- NOTE: Github uses a special format for encoding the contents, where it -- If there was not an error, and the contents are base64 encoded,
-- separates the entire file into multiple lines, and encodes each line in -- we decode the contents and return the decoded buffer
-- base64 if resp.encoding == "base64" then
-- NOTE: Github uses a special format for encoding the contents, where it
-- This should be done by the base64 library, but oh well -- separates the entire file into multiple lines, and encodes each line in
local content = string.gsub(resp.content, "%s+", "") -- base64
local decoded = base64.decode(buffer.fromstring(content))
return Result.Ok(decoded) -- This should be done by the base64 library, but oh well
end local content = string.gsub(resp.content, "%s+", "")
local decoded = base64.decode(buffer.fromstring(content))
return Result.Err(`Unsupported encoding: {resp.encoding}`) return Result.Ok(decoded)
end) end
-- NOTE: Ideally this block should be a match, but I cannot make use of return Result.Err(`Unsupported encoding: {resp.encoding}`)
-- control flow expressions from within a function end)
if readmeRes:isErr() then
warn(`Failed to fetch README from tool repo {repoName}:`, readmeRes:unwrapErr()) -- NOTE: Ideally this block should be a match, but I cannot make use of
continue -- control flow expressions from within a function
end if readmeRes:isErr() then
warn(`Failed to fetch README from tool repo {repoName}:`, readmeRes:unwrapErr())
-- Write the updated README to the tool's directory continue
local readmeContents = readmeRes:unwrap() end
-- There used to be some issues with encoding if not deleted and recreated
pathfs.removeFile(readmePath) -- Write the updated README to the tool's directory
pathfs.writeFile(readmePath, readmeContents) local readmeContents = readmeRes:unwrap()
-- There used to be some issues with encoding if not deleted and recreated
print( pathfs.removeFile(readmePath)
`{INFO_PREFIX} Wrote README to {stdio.style("dim")}{readmePath:stripPrefix(pathfs.cwd)}{stdio.style("reset")}` pathfs.writeFile(readmePath, readmeContents)
)
end print(
`{INFO_PREFIX} Wrote README to {stdio.style("dim")}{readmePath:stripPrefix(pathfs.cwd)}{stdio.style("reset")}`
local timeElapsed = string.format("%.2fs", os.clock() - START_TIME) )
print(`{INFO_PREFIX} Finished checking for tool updates in {stdio.style("dim")}{timeElapsed}{stdio.style("reset")}!`) end
local timeElapsed = string.format("%.2fs", os.clock() - START_TIME)
print(`{INFO_PREFIX} Finished checking for tool updates in {stdio.style("dim")}{timeElapsed}{stdio.style("reset")}!`)