Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 59 additions & 22 deletions .github/workflows/reusable-publish-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -146,19 +152,40 @@ jobs:
git push origin "HEAD:${{ inputs.dev-branch }}"
fi

# Each published channel resolves files from a tag v<version><tagSuffix[channel]>. Derive them
# from the source channel's versions and create the missing ones at the released commit.
# Define the tag-naming scheme <prefix><version><tagSuffix[channel]> 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 <prefix> <version> <feed> <channel>: the git tag a package version resolves to on a
# channel, as <prefix><version><tagSuffix[channel]>.
tag_name() {
local suffix
suffix=$(jq -r --arg c "$4" '(.vars.tagSuffix // {})[$c] // ""' "$3")
printf '%s%s%s\n' "$1" "$2" "$suffix"
}
# emit_tags <prefix> <feed> <version-channel> <suffix-channel>: 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
Expand Down Expand Up @@ -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 \
Expand All @@ -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 != '' }}
Expand All @@ -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"
Expand Down
21 changes: 12 additions & 9 deletions DependencyControl.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -275,7 +275,7 @@
{
"name": "/UpdateFeed.moon",
"url": "@{fileBaseUrl}",
"sha1": "3F565A4F1A9B530909BE6F0DB59561857842E689"
"sha1": "521AD8915BF09A02167ACDF73CE4C39E928F504A"
},
{
"name": "/UpdateTask.moon",
Expand Down Expand Up @@ -544,7 +544,7 @@
{
"name": "/UpdateFeed.moon",
"url": "@{fileBaseUrl}",
"sha1": "6B566A8CE16D0292FA07303506A2D254C4CCC4DC",
"sha1": "3094DFD9B29DB2F238D8E9986DDB6DEC64BC8D5F",
"type": "test"
},
{
Expand Down Expand Up @@ -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."
]
}
},
Expand All @@ -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",
Expand Down Expand Up @@ -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."
]
}
Expand Down
49 changes: 39 additions & 10 deletions depctrl.lua
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,17 @@ notesCmd:option("--version", "Version whose changelog to render (overrides --cha
notesCmd:option("--title", "Optional top-level heading to prepend"):argname("<text>")
notesCmd:option("-o --output", "Write the notes to this file instead of stdout"):argname("<path>")

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("<name>")
getVersionCmd:option("-p --package", "Print this package's version instead of the channel's highest"):argname("<namespace>")

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("<name>")

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")
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion modules/l0/DependencyControl.moon
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
46 changes: 36 additions & 10 deletions modules/l0/DependencyControl/UpdateFeed.moon
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.
Expand Down
Loading
Loading