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
19 changes: 14 additions & 5 deletions .github/workflows/tag.yml
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,8 @@ permissions: {}

jobs:
tag:
name: Tag
# Shown under the caller's job, such as `tag / Major tags`.
name: Major tags
runs-on: ubuntu-latest
environment: ${{ inputs.environment }}
permissions:
Expand Down Expand Up @@ -78,24 +79,32 @@ jobs:
REPOSITORY: ${{ github.repository }}
run: |
set -euo pipefail
# Each outcome is a notice on the run, and a line of the job's summary.
summary="${GITHUB_STEP_SUMMARY:-/dev/null}"
echo "## Major tags" >> "$summary"
report() {
echo "::notice title=Tag::$1"
echo "- $1" >> "$summary"
}
# The version tags, vX.Y.Z only, oldest first.
versions=$(git tag --list 'v*' | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' | sort -V || true)
if [ -z "$versions" ]; then
echo "There's no version tag vX.Y.Z, so there's no major tag to move."
report "There's no version tag vX.Y.Z, so there's no major tag to move."
exit 0
fi
for major in $(echo "$versions" | sed -E 's/^(v[0-9]+)\..*$/\1/' | sort -u); do
newest=$(echo "$versions" | grep -E "^$major\." | tail -n 1)
# The commit of a tag, also of an annotated one.
target=$(git rev-parse "$newest^{commit}")
short=$(git rev-parse --short "$target")
current=$(git rev-parse --verify --quiet "refs/tags/$major^{commit}" || true)
if [ "$current" = "$target" ]; then
echo "$major is at $newest, $target, already."
report "$major is at $newest, $short, already."
elif [ -n "$current" ]; then
gh api --method PATCH "repos/$REPOSITORY/git/refs/tags/$major" -f sha="$target" -F force=true > /dev/null
echo "Moved $major to $newest, $target."
report "Moved $major to $newest, $short."
else
gh api "repos/$REPOSITORY/git/refs" -f ref="refs/tags/$major" -f sha="$target" > /dev/null
echo "Tagged $newest's commit, $target, $major."
report "Made $major at $newest, $short."
fi
done
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ jobs:
It works alongside [TagBot](https://github.com/JuliaRegistries/TagBot), which tags each release `vX.Y.Z` once the registry records it, at the commit that the registry records, with its GitHub release, but never moves a major tag; for a package in a subdirectory, TagBot's `tag_prefix: NO_PREFIX` makes its tags `vX.Y.Z` rather than `<Package>-vX.Y.Z`.
The tag workflow follows whatever version tags exist, whenever they appear, including those that a maintainer pushes by hand: a major tag moves only to its major version's newest release, by version, so an older version's tag doesn't move it, pre-releases such as `v2.0.0-rc1` are left out, and a rerun changes nothing.
So callers of the major tag get a release once it's registered and tagged, and never a release whose tag doesn't exist yet.
For each major tag, it reports what it did, made it, moved it or left it, as a notice on the run and a line of the job's summary, under the job `<caller's job> / Major tags`.

It moves the tags with the ReleaseFlow app's token, or else `RELEASEFLOW_TOKEN`, or else the job's own token; a ruleset on `v*` tags must let that token through, such as with the app on its bypass list.
Its triggers run on the default branch or on a tag, not on the main branch, so the `releaseflow` environment must allow them, as described in [The ReleaseFlow environment](#the-releaseflow-environment), or the caller passes `with: { environment: '' }` to run the job without an environment, with the job's own token.
Expand Down
2 changes: 1 addition & 1 deletion ReleaseFlow/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm

### Added

- The reusable workflow `tag.yml`, which moves each major tag, such as `v1`, to the newest release of its major version, `vX.Y.Z`, whichever made the tag, TagBot or a maintainer, and whenever, for a repository that callers follow by its major tag; it never moves a major tag backwards, and leaves pre-releases out.
- The reusable workflow `tag.yml`, which moves each major tag, such as `v1`, to the newest release of its major version, `vX.Y.Z`, whichever made the tag, TagBot or a maintainer, and whenever, for a repository that callers follow by its major tag; it never moves a major tag backwards, leaves pre-releases out, and reports what it did for each major tag as a notice on the run and a line of the job's summary.
- This repository moves `v1` with it, through `Self-Tag.yml`, once TagBot tags a release `vX.Y.Z`, with `tag_prefix: NO_PREFIX`, in place of `ReleaseFlow-vX.Y.Z`, and with the ReleaseFlow app's token, which the ruleset on `v*` tags lets through; so the workflows' callers get a release once ReleaseFlow's is registered.

## [1.0.1] - 2026-10-20
Expand Down
18 changes: 14 additions & 4 deletions ReleaseFlow/test/tag.jl
Original file line number Diff line number Diff line change
Expand Up @@ -13,40 +13,50 @@
work = repository()
git(work, "config", "tag.gpgsign", "false")
commits = [commit!(work, "Commit $(i)", "file.txt" => string(i)) for i in 1:6]
summary = joinpath(directory, "summary.md")
function step()
write(log, "")
environment = Dict("PATH" => "$(bin):$(ENV["PATH"])", "LOG" => log, "REPOSITORY" => "Example/Toy.jl")
write(summary, "")
environment = Dict("PATH" => "$(bin):$(ENV["PATH"])", "LOG" => log, "REPOSITORY" => "Example/Toy.jl", "GITHUB_STEP_SUMMARY" => summary)
output = IOBuffer()
process = run(pipeline(Cmd(addenv(`bash $(script)`, environment); dir = work, ignorestatus = true); stdout = output, stderr = output))
return (code = process.exitcode, output = String(take!(output)), log = read(log, String))
return (code = process.exitcode, output = String(take!(output)), log = read(log, String), summary = read(summary, String))
end
short(commit) = git(work, "rev-parse", "--short", commit)
tag!(name, commit) = git(work, "tag", "-f", name, commit)
created(major, commit) = "gh api repos/Example/Toy.jl/git/refs -f ref=refs/tags/$(major) -f sha=$(commit)"
moved(major, commit) = "gh api --method PATCH repos/Example/Toy.jl/git/refs/tags/$(major) -f sha=$(commit) -F force=true"

# Without version tags, there's nothing to move.
result = step()
@test result.code == 0 && occursin("There's no version tag vX.Y.Z", result.output) && isempty(result.log)
@test result.code == 0 && isempty(result.log)
@test occursin("::notice title=Tag::There's no version tag vX.Y.Z", result.output)
@test result.summary == "## Major tags\n- There's no version tag vX.Y.Z, so there's no major tag to move.\n"
# The newest release of a major version without its major tag: the major tag is made at its commit.
tag!("v1.0.0", commits[1])
tag!("v1.0.1", commits[2])
result = step()
@test result.code == 0 && result.log == created("v1", commits[2]) * "\n"
@test occursin("::notice title=Tag::Made v1 at v1.0.1, $(short(commits[2])).", result.output)
# A major tag at an older release moves to the newest.
tag!("v1", commits[1])
result = step()
@test result.log == moved("v1", commits[2]) * "\n"
@test occursin("::notice title=Tag::Moved v1 to v1.0.1, $(short(commits[2])).", result.output)
# A major tag at the newest release stays.
tag!("v1", commits[2])
result = step()
@test isempty(result.log) && occursin("v1 is at v1.0.1, $(commits[2]), already.", result.output)
@test isempty(result.log) && occursin("::notice title=Tag::v1 is at v1.0.1, $(short(commits[2])), already.", result.output)
@test result.summary == "## Major tags\n- v1 is at v1.0.1, $(short(commits[2])), already.\n"
# Each major version's tag follows its own newest release, by version rather than by date, and pre-releases are left out.
tag!("v1.10.0", commits[3])
tag!("v1.9.0", commits[4])
tag!("v2.0.0", commits[5])
tag!("v2.1.0-rc1", commits[6])
result = step()
@test result.log == moved("v1", commits[3]) * "\n" * created("v2", commits[5]) * "\n"
# One line for each major tag.
@test result.summary == "## Major tags\n- Moved v1 to v1.10.0, $(short(commits[3])).\n- Made v2 at v2.0.0, $(short(commits[5])).\n"
# A version tag pushed by hand, older than the newest, moves nothing.
tag!("v1", commits[3])
tag!("v2", commits[5])
Expand Down
Loading