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
3 changes: 3 additions & 0 deletions .github/workflows/TagBot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,5 +34,8 @@ jobs:
# The app's token, whose tags also start Self-Tag.yml by their push; GITHUB_TOKEN otherwise, whose tags Self-Tag.yml follows when TagBot's run completes.
token: ${{ steps.app.outputs.token || secrets.GITHUB_TOKEN }}
subdir: ReleaseFlow
# The release branch, whose commits TagBot is to tag: the back-merge into develop shares a release's tree, and without the branch TagBot may pick it.
# TagBot reads this first once it searches the release branch's history first, which its README documents, and only sets the release's target until then.
branch: master
# vX.Y.Z, at which the workflows call the action, rather than ReleaseFlow-vX.Y.Z, as for a package in a subdirectory.
tag_prefix: NO_PREFIX
31 changes: 26 additions & 5 deletions .github/workflows/tag.yml
Original file line number Diff line number Diff line change
Expand Up @@ -79,13 +79,30 @@ jobs:
REPOSITORY: ${{ github.repository }}
run: |
set -euo pipefail
# Each outcome is a notice on the run, and a line of the job's summary.
# Each outcome is a notice on the run, and a line of the job's summary; a failure is an error, and fails the step once every major tag is handled.
summary="${GITHUB_STEP_SUMMARY:-/dev/null}"
echo "## Major tags" >> "$summary"
report() {
echo "::notice title=Tag::$1"
echo "- $1" >> "$summary"
}
failed=false
fail() {
echo "::error title=Tag::$1"
echo "- $1" >> "$summary"
failed=true
}
# Run `gh`, and report its own message where it fails.
call() {
local what="$1"
shift
local message
if message=$(gh "$@" 2>&1 > /dev/null); then
return 0
fi
fail "Couldn't $what: ${message//$'\n'/ }"
return 1
}
# 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
Expand All @@ -100,11 +117,15 @@ jobs:
current=$(git rev-parse --verify --quiet "refs/tags/$major^{commit}" || true)
if [ "$current" = "$target" ]; then
report "$major is at $newest, $short, already."
elif [ -n "$current" ] && ! git merge-base --is-ancestor "$current" "$target"; then
# A major tag only moves forwards: a newest release that isn't descended from it, such as once a newer version's tag is deleted, leaves it where it is.
report "$major is at $(git rev-parse --short "$current"), which $newest, $short, doesn't descend from, so it stays."
elif [ -n "$current" ]; then
gh api --method PATCH "repos/$REPOSITORY/git/refs/tags/$major" -f sha="$target" -F force=true > /dev/null
report "Moved $major to $newest, $short."
call "move $major to $newest, $short" api --method PATCH "repos/$REPOSITORY/git/refs/tags/$major" -f sha="$target" -F force=true && report "Moved $major to $newest, $short."
else
gh api "repos/$REPOSITORY/git/refs" -f ref="refs/tags/$major" -f sha="$target" > /dev/null
report "Made $major at $newest, $short."
call "make $major at $newest, $short" api "repos/$REPOSITORY/git/refs" -f ref="refs/tags/$major" -f sha="$target" && report "Made $major at $newest, $short."
fi
done
if [ "$failed" = true ]; then
exit 1
fi
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,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.
It only moves a major tag forwards, to a release that descends from where the tag is, so deleting a version's tag doesn't take the major tag back to an older release; and where GitHub refuses a move, it reports GitHub's message as an error, and fails once every major tag is handled.
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`.

Expand Down
3 changes: 2 additions & 1 deletion ReleaseFlow/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,9 @@ 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, 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.
- 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 only moves a major tag forwards, to a release that descends from where it is, 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, or, where GitHub refuses a move, an error with GitHub's message.
- 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.
- This repository's TagBot names `master` as its release branch, so that its tags land on `master`'s merge commits rather than on the back-merges into `develop`, which share their trees, once TagBot searches that branch first.
- The reusable workflow `dependents.yml`, which runs the tests of the packages that depend on this one, each checked out at its default branch, with the pull request's branch in its place, as the checks `dependents / <repository>`.
- The back-merge's inputs `from` and `into`, which bring any branch into any other, such as a maintenance branch into the main branch, from a branch `back-merge/<from>-<commit>`; by default, the main branch into develop, as before.
- `prepare`, which prepares a release in a checkout of the branch it starts from: the next version after the last release, by `RELEASEFLOW_STEP`, where `auto` reads the unreleased section's `### Breaking` and `### Added`, set in `Project.toml`; the unreleased entries moved into its dated section, with the changelog's links; and the pins of `RELEASEFLOW_PIN_ACTION` set to it.
Expand Down
23 changes: 19 additions & 4 deletions ReleaseFlow/test/tag.jl
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
# Moving the major tags
# ---------------------

# The tag workflow's step, run in a repository with version tags, with a stand-in for `gh` that logs its calls.
# The tag workflow's step, run in a repository with version tags, with a stand-in for `gh` that logs its calls, and refuses them with `GH_REFUSE`'s message where that's set.
@testset "Tag" begin
directory = mktempdir()
bin = mkpath(joinpath(directory, "bin"))
write(joinpath(bin, "gh"), "#!/bin/bash\necho \"gh \$*\" >> \"\$LOG\"\n")
write(joinpath(bin, "gh"), "#!/bin/bash\necho \"gh \$*\" >> \"\$LOG\"\nif [ -n \"\$GH_REFUSE\" ]; then echo \"\$GH_REFUSE\" >&2; exit 1; fi\n")
chmod(joinpath(bin, "gh"), 0o755)
log = joinpath(directory, "log")
script = joinpath(directory, "step.sh")
Expand All @@ -14,10 +14,10 @@
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()
function step(; refuse = "")
write(log, "")
write(summary, "")
environment = Dict("PATH" => "$(bin):$(ENV["PATH"])", "LOG" => log, "REPOSITORY" => "Example/Toy.jl", "GITHUB_STEP_SUMMARY" => summary)
environment = Dict("PATH" => "$(bin):$(ENV["PATH"])", "LOG" => log, "REPOSITORY" => "Example/Toy.jl", "GITHUB_STEP_SUMMARY" => summary, "GH_REFUSE" => refuse)
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), summary = read(summary, String))
Expand Down Expand Up @@ -65,4 +65,19 @@
# An annotated tag counts at its commit.
git(work, "tag", "-a", "v2.2.0", "-m", "Release 2.2.0", commits[6])
@test step().log == moved("v2", commits[6]) * "\n"
# A major tag only moves forwards: once the newest version's tag is deleted, the newest left is behind the major tag, which stays.
tag!("v2", commits[6])
git(work, "tag", "-d", "v2.2.0")
result = step()
@test result.code == 0 && !occursin("v2", result.log)
@test occursin("::notice title=Tag::v2 is at $(short(commits[6])), which v2.0.0, $(short(commits[5])), doesn't descend from, so it stays.", result.output)
# A refused move is an error with GitHub's message, the other major tags are still handled, and the step fails.
tag!("v1", commits[1])
tag!("v2", commits[1])
result = step(refuse = "HTTP 403: Resource not accessible by integration\nsecond line")
@test result.code == 1
@test occursin(moved("v1", commits[3]), result.log) && occursin(moved("v2", commits[5]), result.log)
@test occursin("::error title=Tag::Couldn't move v1 to v1.10.0, $(short(commits[3])): HTTP 403: Resource not accessible by integration second line", result.output)
@test occursin("- Couldn't move v2 to v2.0.0, $(short(commits[5])): HTTP 403", result.summary)
@test !occursin("Moved", result.summary)
end
Loading