diff --git a/.github/workflows/reusable-publish-release.yml b/.github/workflows/reusable-publish-release.yml index f59ac4b..aabef27 100644 --- a/.github/workflows/reusable-publish-release.yml +++ b/.github/workflows/reusable-publish-release.yml @@ -47,6 +47,10 @@ on: description: "Branch that serves the feed URL" type: string default: publish + tag-prefix: + description: "Prefix for the version tags this creates; must match the feed's file-URL tag scheme" + type: string + default: v stable-channel: description: "Publishing this channel also records the release date on the dev branch" type: string @@ -77,6 +81,8 @@ jobs: runs-on: ubuntu-latest permissions: contents: write + env: + TAG_PREFIX: ${{ inputs.tag-prefix }} steps: - name: Generate a token from the release App id: app-token @@ -146,19 +152,40 @@ jobs: git push origin "HEAD:${{ inputs.dev-branch }}" fi - # Each published channel resolves files from a tag v. Derive them - # from the source channel's versions and create the missing ones at the released commit. + # Define the tag-naming scheme once for all + # the three steps that build tag names (create tags, publish guard, GitHub release). + - name: Define shared release helpers + run: | + set -euo pipefail + cat > "$RUNNER_TEMP/release-lib.sh" <<'SH' + # tag_name : the git tag a package version resolves to on a + # channel, as . + tag_name() { + local suffix + suffix=$(jq -r --arg c "$4" '(.vars.tagSuffix // {})[$c] // ""' "$3") + printf '%s%s%s\n' "$1" "$2" "$suffix" + } + # emit_tags : one tag per distinct version the + # feed advertises on version-channel, each suffixed for suffix-channel. The two channels differ + # when tagging a source channel's versions for a destination channel that will carry them. + emit_tags() { + local v + while IFS= read -r v; do + [ -n "$v" ] || continue + tag_name "$1" "$v" "$2" "$4" + done < <(lua _depctrl/depctrl.lua get-current-versions -f "$2" -c "$3") + } + SH + + # The versions come from the source channel; the suffix from each destination channel. + # Create the missing ones at the released commit. - name: Create the version tags run: | set -euo pipefail - jq -r --arg channels "${{ inputs.channels }}" --arg src "${{ inputs.source-channel }}" ' - ($channels | split(" ")) as $to - | (.vars.tagSuffix // {}) as $suf - | [ (.macros // {}), (.modules // {}) - | to_entries[] - | (.value.channels[$src].version // empty) as $v - | $to[] | "v" + ($v | tostring) + ($suf[.] // "") ] - | unique[]' DependencyControl.json > "$RUNNER_TEMP/tags.txt" + source "$RUNNER_TEMP/release-lib.sh" + for chan in ${{ inputs.channels }}; do + emit_tags "$TAG_PREFIX" DependencyControl.json "${{ inputs.source-channel }}" "$chan" + done | sort -u > "$RUNNER_TEMP/tags.txt" echo "Tags:"; sed 's/^/ /' "$RUNNER_TEMP/tags.txt" released_sha=$(git rev-parse HEAD) while IFS= read -r tag; do @@ -197,12 +224,11 @@ jobs: --default-channel "${{ inputs.default-channel }}" --released "$RELEASE_DATE" git fetch --tags --force --quiet - jq -r ' - (.vars.tagSuffix // {}) as $suf - | [ (.macros // {}), (.modules // {}) - | to_entries[] | (.value.channels // {}) | to_entries[] - | "v" + (.value.version | tostring) + ($suf[.key] // "") ] - | unique[]' _publish/DependencyControl.json > "$RUNNER_TEMP/publish-tags.txt" + source "$RUNNER_TEMP/release-lib.sh" + channels=$(jq -r '[ (.macros // {}), (.modules // {}) | .[] | select(type == "object") | .channels // {} | keys[] ] | unique[]' _publish/DependencyControl.json) + for chan in $channels; do + emit_tags "$TAG_PREFIX" _publish/DependencyControl.json "$chan" "$chan" + done | sort -u > "$RUNNER_TEMP/publish-tags.txt" while IFS= read -r tag; do [ -n "$tag" ] || continue git rev-parse -q --verify "refs/tags/$tag^{commit}" >/dev/null \ @@ -218,6 +244,21 @@ jobs: git push origin "HEAD:refs/heads/${{ inputs.publish-branch }}" fi + # The release version is the highest version any package reaches this run. Lockstep versioning + # bumps only the packages that changed, so the namesake module can sit below it (a release that + # only touches a bundled helper). The bundle asset and the GitHub release(s) both key off this one + # value so they can't disagree, and every released channel carries it (the publish step copied the + # source channel onto them). + - name: Resolve the release version + if: ${{ inputs.release-channels != '' }} + run: | + set -euo pipefail + ver=$(lua _depctrl/depctrl.lua get-version -f DependencyControl.json -c "${{ inputs.source-channel }}") + echo "Release version: v$ver" + echo "RELEASE_VERSION=$ver" >> "$GITHUB_ENV" + + # The bundler names the archive after the same release version (highest on the default channel), + # so the asset matches the release with no post-rename. - name: Build the release bundle id: bundle if: ${{ inputs.release-channels != '' }} @@ -243,18 +284,14 @@ jobs: BUNDLE_ZIP: ${{ steps.bundle.outputs.zip }} run: | set -euo pipefail + source "$RUNNER_TEMP/release-lib.sh" notes="$RUNNER_TEMP/release-notes.md" lua _depctrl/depctrl.lua release-notes --channel "${{ inputs.source-channel }}" --output "$notes" \ || echo "(no changelog entries for this release)" > "$notes" [ -s "$notes" ] || echo "(no changelog entries for this release)" > "$notes" for chan in ${{ inputs.channels }}; do case " ${{ inputs.release-channels }} " in *" $chan "*) ;; *) continue ;; esac - ver=$(jq -r --arg c "$chan" \ - '[ (.macros // {}), (.modules // {}) | to_entries[] | .value.channels[$c].version // empty ] | unique | .[0] // empty' \ - _publish/DependencyControl.json) - [ -n "$ver" ] || { echo "· no version on channel '$chan'; skipping its release"; continue; } - suffix=$(jq -r --arg c "$chan" '(.vars.tagSuffix // {})[$c] // ""' _publish/DependencyControl.json) - tag="v${ver}${suffix}" + tag=$(tag_name "$TAG_PREFIX" "$RELEASE_VERSION" _publish/DependencyControl.json "$chan") prerelease=""; [ "$chan" = "${{ inputs.stable-channel }}" ] || prerelease="--prerelease" if gh release view "$tag" >/dev/null 2>&1; then echo "· release $tag already exists — leaving it" diff --git a/DependencyControl.json b/DependencyControl.json index a235030..c4134ed 100644 --- a/DependencyControl.json +++ b/DependencyControl.json @@ -117,14 +117,14 @@ "url": "@{baseUrl}#@{namespace}", "channels": { "main": { - "version": "0.7.0", - "released": "2026-07-24", + "version": "0.8.0", + "released": null, "default": true, "files": [ { "name": ".moon", "url": "@{fileBaseUrl}", - "sha1": "FC202CDDF83FC14CE784836E9C8D93CC6BC227DF" + "sha1": "EA116FC2F7C3EDA9DC16E603C25669E3ABBA07EA" }, { "name": "/Constants.moon", @@ -275,7 +275,7 @@ { "name": "/UpdateFeed.moon", "url": "@{fileBaseUrl}", - "sha1": "3F565A4F1A9B530909BE6F0DB59561857842E689" + "sha1": "521AD8915BF09A02167ACDF73CE4C39E928F504A" }, { "name": "/UpdateTask.moon", @@ -544,7 +544,7 @@ { "name": "/UpdateFeed.moon", "url": "@{fileBaseUrl}", - "sha1": "6B566A8CE16D0292FA07303506A2D254C4CCC4DC", + "sha1": "3094DFD9B29DB2F238D8E9986DDB6DEC64BC8D5F", "type": "test" }, { @@ -769,6 +769,9 @@ "refactor: Reinstalling a managed module that exposes a plain version string instead of its DependencyControl record no longer demotes it to unmanaged, so it keeps receiving scheduled update checks.", "feat: New `resolve-feed` CLI command expands a feed's templates into a static, template-free feed compatible with the legacy v0.3.0 feed format.", "fix: Reloading a freshly updated module also evicts its cached submodules, so a multi-file module no longer reloads into a mix of new and stale code." + ], + "0.8.0": [ + "feat: New `get-version` and `get-current-versions` CLI commands report a feed channel's highest version or its distinct package versions." ] } }, @@ -779,14 +782,14 @@ "url": "http://dkolf.de/dkjson-lua/", "channels": { "main": { - "version": "0.7.1", - "released": "2026-07-24", + "version": "0.8.0", + "released": null, "default": true, "files": [ { "name": ".moon", "url": "@{fileBaseUrl}", - "sha1": "20D35A98AFCE11D67576CB8CE58040C663F23308" + "sha1": "3FACCD40F487E8205300E576B78F10D5AFFDDEE4" }, { "name": "/vendor/dkjson.lua", @@ -814,7 +817,7 @@ "0.7.0": [ "feat: Vendored dkjson v2.10 with a DependencyControl version record and json/dkjson self-registration." ], - "0.7.1": [ + "0.8.0": [ "fix: Upgrading from a DependencyControl v0.6.x install no longer fails when dkjson is pulled in as a new dependency; dkjson now skips a self-registration step that breaks pre-0.7.0 loaders." ] } diff --git a/depctrl.lua b/depctrl.lua index 4df7fbc..9b4ea4d 100644 --- a/depctrl.lua +++ b/depctrl.lua @@ -132,6 +132,17 @@ notesCmd:option("--version", "Version whose changelog to render (overrides --cha notesCmd:option("--title", "Optional top-level heading to prepend"):argname("") notesCmd:option("-o --output", "Write the notes to this file instead of stdout"):argname("") +local getVersionCmd = parser:command("get-version", + "Print a channel's highest version, or one package's version on it") +getVersionCmd:option("-f --feed", "Feed JSON path"):default("DependencyControl.json") +getVersionCmd:option("-c --channel", "Channel to read (default: the channel marked default: true)"):argname("") +getVersionCmd:option("-p --package", "Print this package's version instead of the channel's highest"):argname("") + +local getVersionsCmd = parser:command("get-current-versions", + "Print the distinct versions a channel advertises, one per line, lowest first") +getVersionsCmd:option("-f --feed", "Feed JSON path"):default("DependencyControl.json") +getVersionsCmd:option("-c --channel", "Channel to read (default: the channel marked default: true)"):argname("") + local typesCmd = parser:command("generate-types", "Extract LuaCATS annotations from module sources into LuaLS .d.lua type-definition files") typesCmd:option("-f --feed", "Feed JSON path"):default("DependencyControl.json") @@ -422,16 +433,7 @@ elseif args.command == "bundle" then local fileCount, errCount = feed:deployFiles(distDir, filter, false) - -- Name the archive after the feed's headline module (DepCtrl's own feed) where present, - -- otherwise fall back to the first module version so other feeds still bundle. - local mainVersion = feed:getModuleVersion("l0.DependencyControl") - if not mainVersion then - for ns in pairs(feed.data.modules or {}) do - mainVersion = feed:getModuleVersion(ns) - if mainVersion then break end - end - mainVersion = mainVersion or "0.0.0" - end + local mainVersion = feed:getHighestVersionOnChannel() or "0.0.0" local suffix = GitRepository(feed.feedDir):getVersionSuffix() local zipPath = outputDir .. pathSep .. (feed.data.name .. "-v%s%s.zip"):format(mainVersion, suffix) @@ -744,6 +746,33 @@ elseif args.command == "release-notes" then end os.exit(0) +-- ─── get-version ──────────────────────────────────────────────────────────────── +elseif args.command == "get-version" then + setupDepCtrl("get-version") + local feed = loadFeed(resolveAbsPath(args.feed)) + local version + if args.package then + version = feed:getPackageVersionOnChannel(args.package, args.channel) + else + version = feed:getHighestVersionOnChannel(args.channel) + end + if not version then + io.stderr:write("get-version: no version found for the requested channel" + .. (args.package and (" and package '" .. args.package .. "'") or "") .. ".\n") + os.exit(1) + end + io.stdout:write(version .. "\n") + os.exit(0) + +-- ─── get-current-versions ───────────────────────────────────────────────────────── +elseif args.command == "get-current-versions" then + setupDepCtrl("get-current-versions") + local feed = loadFeed(resolveAbsPath(args.feed)) + for _, version in ipairs(feed:getVersionsOnChannel(args.channel)) do + io.stdout:write(version .. "\n") + end + os.exit(0) + elseif args.command == "validate-schema" then if args.type ~= "config" and args.type ~= "feed" then io.stderr:write("--type must be 'config' or 'feed'.\n"); os.exit(2) diff --git a/modules/l0/DependencyControl.moon b/modules/l0/DependencyControl.moon index f5c6ec2..7c3fbec 100644 --- a/modules/l0/DependencyControl.moon +++ b/modules/l0/DependencyControl.moon @@ -108,7 +108,7 @@ Accessors.install DependencyControl rec = DependencyControl{ name: "DependencyControl", - version: "0.7.0", -- @{l0.DependencyControl:version} + version: "0.8.0", -- @{l0.DependencyControl:version} description: "Provides script management and auto-updating for Aegisub macros and modules.", author: "line0", url: "http://github.com/TypesettingTools/DependencyControl", diff --git a/modules/l0/DependencyControl/UpdateFeed.moon b/modules/l0/DependencyControl/UpdateFeed.moon index c0087a3..0998cec 100644 --- a/modules/l0/DependencyControl/UpdateFeed.moon +++ b/modules/l0/DependencyControl/UpdateFeed.moon @@ -1175,7 +1175,7 @@ class UpdateFeed unless version channel = opts.channel or @__defaultChannelName! return nil, msgs.formatReleaseNotes.noChannel unless channel - version = @__versionOnChannel channel + version = @getHighestVersionOnChannel channel return nil, msgs.formatReleaseNotes.channelEmpty\format channel unless version feedName = @rawFeedData.name @@ -1197,19 +1197,45 @@ class UpdateFeed a.name < b.name ReleaseNotes.renderMarkdown packages, {title: opts.title} - ---Highest version any package advertises on the given channel. - ---@param channel string Channel name to inspect. - ---@return string? version The version string, or nil when no package uses the channel. - ---@private - __versionOnChannel: (channel) => - best = nil + ---Distinct versions any feed advertises on the given channel, ascending by semantic version. + ---@param channel? string Channel to inspect; defaults to the channel marked default: true. + ---@return string[] versions Distinct version strings, lowest first; empty when the channel is absent or unused. + getVersionsOnChannel: (channel) => + channel or= @__defaultChannelName! + versions, seen = {}, {} + return versions unless channel for section in *{"macros", "modules"} for _, pkg in pairs @rawFeedData[section] or {} continue unless type(pkg) == "table" and pkg.channels ch = pkg.channels[channel] - continue unless ch and ch.version - best = ch.version if not best or SemanticVersion\toPacked(ch.version) > SemanticVersion\toPacked(best) - best + continue unless ch and ch.version and not seen[ch.version] + seen[ch.version] = true + versions[#versions + 1] = ch.version + table.sort versions, (a, b) -> SemanticVersion\toPacked(a) < SemanticVersion\toPacked(b) + versions + + ---Highest version on the given channel across the feed's packages. Under monorepo-style lockstep + ---versioning, where each release advances only the packages that changed, this is the repo's + ---release version. + ---@param channel? string Channel to inspect; defaults to the channel marked default: true. + ---@return string? version The highest version string, or nil when the channel is absent or unused. + getHighestVersionOnChannel: (channel) => + versions = @getVersionsOnChannel channel + versions[#versions] + + ---Version a single package advertises on the given channel. + ---@param namespace string Package namespace, found in either the macros or modules section. + ---@param channel? string Channel to inspect; defaults to the channel marked default: true. + ---@return string? version The version string, or nil when the package or channel is absent. + getPackageVersionOnChannel: (namespace, channel) => + channel or= @__defaultChannelName! + return nil unless channel + for section in *{"macros", "modules"} + pkg = @rawFeedData[section] and @rawFeedData[section][namespace] + if type(pkg) == "table" and pkg.channels + ch = pkg.channels[channel] + return ch.version if ch and ch.version + nil ---Name of the channel marked default: true, taken from the first package that declares one. ---@return string? channel The default channel name, or nil when no package marks a default. diff --git a/modules/l0/DependencyControl/test/UpdateFeed.moon b/modules/l0/DependencyControl/test/UpdateFeed.moon index 8749125..7b43d07 100644 --- a/modules/l0/DependencyControl/test/UpdateFeed.moon +++ b/modules/l0/DependencyControl/test/UpdateFeed.moon @@ -138,6 +138,55 @@ feed = {data: {modules: {}}, __class: UpdateFeed} ut\assertNil UpdateFeed.getModuleVersion feed, "no.Such.NS" + -- getVersionsOnChannel / getHighestVersionOnChannel / getPackageVersionOnChannel: channel version queries + + getVersionsOnChannel_distinctAscendingBySemver: (ut) -> + feed = stubSelf UpdateFeed, {__class: UpdateFeed, rawFeedData: { + macros: {"a.M": {channels: {stable: {version: "0.7.0"}}}} + modules: { + "l0.A": {channels: {stable: {version: "0.7.0"}}} -- duplicate of the macro's version + "l0.B": {channels: {stable: {version: "0.7.10"}}} + "l0.C": {channels: {stable: {version: "0.7.9"}}} + } + }} + versions = feed\getVersionsOnChannel "stable" + ut\assertItemsEqual versions, {"0.7.0", "0.7.9", "0.7.10"} + -- ordered by semantic version, so 0.7.10 sorts after 0.7.9 rather than lexically before it + ut\assertEquals versions[#versions], "0.7.10" + + getVersionsOnChannel_absentChannelIsEmpty: (ut) -> + feed = stubSelf UpdateFeed, {__class: UpdateFeed, rawFeedData: { + modules: {"l0.A": {channels: {stable: {version: "1.0.0"}}}} + }} + ut\assertEquals #feed\getVersionsOnChannel("nightly"), 0 + + getHighestVersionOnChannel_returnsHighest: (ut) -> + feed = stubSelf UpdateFeed, {__class: UpdateFeed, rawFeedData: {modules: { + "l0.A": {channels: {stable: {version: "0.7.0"}}} + "l0.B": {channels: {stable: {version: "0.7.1"}}} + }}} + ut\assertEquals feed\getHighestVersionOnChannel("stable"), "0.7.1" + + getHighestVersionOnChannel_defaultsToDefaultChannel: (ut) -> + feed = stubSelf UpdateFeed, {__class: UpdateFeed, rawFeedData: {modules: { + "l0.A": {channels: {main: {default: true, version: "0.7.0"}}} + "l0.B": {channels: {main: {default: true, version: "0.7.1"}}} + }}} + ut\assertEquals feed\getHighestVersionOnChannel!, "0.7.1" + + getPackageVersionOnChannel_found: (ut) -> + feed = stubSelf UpdateFeed, {__class: UpdateFeed, rawFeedData: {modules: { + "l0.A": {channels: {stable: {version: "0.7.0"}, alpha: {version: "0.8.0"}}} + }}} + ut\assertEquals feed\getPackageVersionOnChannel("l0.A", "alpha"), "0.8.0" + + getPackageVersionOnChannel_missingPackageOrChannel: (ut) -> + feed = stubSelf UpdateFeed, {__class: UpdateFeed, rawFeedData: {modules: { + "l0.A": {channels: {stable: {version: "0.7.0"}}} + }}} + ut\assertNil feed\getPackageVersionOnChannel("l0.Nope", "stable") + ut\assertNil feed\getPackageVersionOnChannel("l0.A", "nightly") + -- getProviders: virtual-package resolution lookup over the feed's module section getProviders_findsByBareAlias: (ut) -> diff --git a/modules/l0/dkjson.moon b/modules/l0/dkjson.moon index b889d21..29c586f 100644 --- a/modules/l0/dkjson.moon +++ b/modules/l0/dkjson.moon @@ -175,7 +175,7 @@ wrapper.encode = (value, state) -> wrapper.__depCtrlInit = (DependencyControl) -> wrapper.version = DependencyControl { name: "dkjson" - version: "0.7.1" -- @{l0.dkjson:version} + version: "0.8.0" -- @{l0.dkjson:version} description: "David Kolf's JSON module for Lua." author: "David Kolf" moduleName: "l0.dkjson"