diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 46c1b56eb..34814f171 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,6 +10,17 @@ on: - master jobs: + validate: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v1 + - uses: ok-nick/setup-aftman@v0.4.2 + + - name: Validate Crate Versions + run: lune run validate-crate-versions + build: runs-on: ubuntu-latest diff --git a/.lune/semver.luau b/.lune/semver.luau new file mode 100644 index 000000000..73ceda378 --- /dev/null +++ b/.lune/semver.luau @@ -0,0 +1,81 @@ +--!strict +local SemVer = {} +SemVer.__index = SemVer + +export type SemVer = { + major: number, + minor: number?, + patch: number?, +} + +local function compare(a: number, b: number): number + if a > b then + return 1 + elseif a < b then + return -1 + end + + return 0 +end + +function SemVer.__tostring(self: SemVer): string + return string.format("%i.%i.%i", self.major, self.minor or 0, self.patch or 0) +end + +--[[ + Constructs a new SemVer from the provided parts. +]] +function SemVer.new(major: number, minor: number?, patch: number?): SemVer + return (setmetatable({ + major = major, + minor = minor, + patch = patch, + }, SemVer) :: any) :: SemVer +end + +--[[ + Parses `version` into a SemVer. +]] +function SemVer.parse(version: string) + local major, minor, patch, _ = version:match("^(%d+)%.(%d+)%.(%d+)(.*)$") + local realVersion = { + major = tonumber(major), + minor = tonumber(minor), + patch = tonumber(patch), + } + if realVersion.major == nil then + error(`could not parse major version from '{version}'`, 2) + end + if minor and realVersion.minor == nil then + error(`could not parse minor version from '{version}'`, 2) + end + if patch and realVersion.patch == nil then + error(`could not parse patch version from '{version}'`, 2) + end + + return (setmetatable(realVersion, SemVer) :: any) :: SemVer +end + +--[[ + Compares two SemVers and returns their status compared to one another. + + If `1` is returned, a is 'newer' than b. + If `-1` is returned, a is 'older' than b. + If `0` is returned, they are identical. +]] +function SemVer.compare(a: SemVer, b: SemVer) + local major = compare(a.major, b.major) + local minor = compare(a.minor or 0, b.minor or 0) + + if major ~= 0 then + return major + end + + if minor ~= 0 then + return minor + end + + return compare(a.patch or 0, b.patch or 0) +end + +return SemVer diff --git a/.lune/validate-crate-versions.luau b/.lune/validate-crate-versions.luau new file mode 100644 index 000000000..45d9b7ca1 --- /dev/null +++ b/.lune/validate-crate-versions.luau @@ -0,0 +1,107 @@ +--!strict +--#selene: allow(incorrect_standard_library_use) + +local IGNORE_CRATE_LIST = { + "rbx_util", + "rbx_reflector", +} + +local fs = require("@lune/fs") +local serde = require("@lune/serde") +local stdio = require("@lune/stdio") +local process = require("@lune/process") + +local SemVer = require("semver") + +type WorkspaceCargo = { + workspace: { + members: { string }, + }, +} + +type CrateCargo = { + package: { + name: string, + version: string, + }, + dependencies: { [string]: Dependency }, + ["dev-dependencies"]: { [string]: Dependency }?, +} + +type Dependency = string | { version: string?, path: string?, features: { string }, optional: boolean? } + +local function warn(...) + stdio.write(`[{stdio.color("yellow")}WARN{stdio.color("reset")}] `) + print(...) +end + +local function processDependencies(dependency_list: { [string]: Dependency }, output: { [string]: Dependency }) + for name, dependency in dependency_list do + if typeof(dependency) == "string" then + continue + end + if dependency.path then + output[name] = dependency + end + end +end + +local workspace: WorkspaceCargo = serde.decode("toml", fs.readFile("Cargo.toml")) + +local crate_info = {} + +for _, crate_name in workspace.workspace.members do + if table.find(IGNORE_CRATE_LIST, crate_name) then + continue + end + local cargo: CrateCargo = serde.decode("toml", fs.readFile(`{crate_name}/Cargo.toml`)) + local dependencies = {} + local dev_dependencies = {} + processDependencies(cargo.dependencies, dependencies) + if cargo.package["dev-dependencies"] then + processDependencies(cargo["dev-dependencies"] :: any, dev_dependencies) + end + crate_info[crate_name] = { + version = SemVer.parse(cargo.package.version), + dependencies = dependencies, + dev_dependencies = dev_dependencies, + } +end + +table.freeze(crate_info) + +local all_ok = true + +for crate_name, cargo in crate_info do + for name, dependency in cargo.dependencies do + if typeof(dependency) == "string" then + error("invariant: string dependency made it into path list") + end + if not crate_info[name] then + warn(`Dependency {name} of crate {crate_name} has a path but is not local to this workspace.`) + all_ok = false + continue + end + if not dependency.version then + warn(`Dependency {name} of crate {crate_name} has a path but no version specified. Please fix this.`) + all_ok = false + continue + end + local dependency_version = SemVer.parse(dependency.version :: string) + local cmp = SemVer.compare(crate_info[name].version, dependency_version) + if cmp == 0 then + continue + else + all_ok = false + warn( + `Dependency {name} of crate {crate_name} has a version mismatch. Current version: {dependency_version}. Should be: {crate_info[name].version}` + ) + end + end +end + +if all_ok then + process.exit(0) +else + process.exit(1) +end diff --git a/aftman.toml b/aftman.toml index b1bd6be64..0364079bf 100644 --- a/aftman.toml +++ b/aftman.toml @@ -3,3 +3,4 @@ rojo = "rojo-rbx/rojo@7.3.0" run-in-roblox = "rojo-rbx/run-in-roblox@0.3.0" selene = "Kampfkarren/selene@0.25.0" stylua = "JohnnyMorganz/StyLua@0.18.2" +lune = "lune-org/lune@0.8.8"