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
133 changes: 133 additions & 0 deletions .github/workflows/package.yml
Original file line number Diff line number Diff line change
Expand Up @@ -118,3 +118,136 @@ jobs:
with:
name: page_print-darwin-arm64-gem
path: pkg/page_print-*-arm64-darwin.gem

publish:
name: Publish
needs: [source, linux, darwin-arm64]
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v')
runs-on: ubuntu-latest
environment: release
permissions:
id-token: write
contents: write

steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
persist-credentials: false

- uses: ruby/setup-ruby@v1
with:
ruby-version: "3.4.7"

- uses: actions/download-artifact@v4
with:
path: gems
pattern: page_print-*-gem
merge-multiple: true

- name: Expect exact gem artifacts for tag version
run: |
set -euo pipefail
version="${GITHUB_REF_NAME#v}"
expected=(
"page_print-${version}.gem"
"page_print-${version}-x86_64-linux.gem"
"page_print-${version}-arm64-darwin.gem"
)

# download-artifact may nest files; flatten expected gems to gems/
mkdir -p gems
for name in "${expected[@]}"; do
if [[ ! -f "gems/${name}" ]]; then
match="$(find gems -type f -name "${name}" | head -n 1 || true)"
[[ -n "${match}" ]] || { echo "Missing expected gem artifact: ${name}" >&2; exit 1; }
mv "${match}" "gems/${name}"
fi
done

mapfile -t found < <(find gems -type f -name 'page_print-*.gem' -printf '%f\n' | sort)
printf 'Found gems:\n'
printf ' %s\n' "${found[@]}"

missing=()
for name in "${expected[@]}"; do
[[ -f "gems/${name}" ]] || missing+=("${name}")
done
unexpected=()
for base in "${found[@]}"; do
keep=false
for name in "${expected[@]}"; do
[[ "${base}" == "${name}" ]] && keep=true && break
done
[[ "${keep}" == true ]] || unexpected+=("${base}")
done

if ((${#missing[@]})); then
echo "Missing expected gem artifacts: ${missing[*]}" >&2
exit 1
fi
if ((${#unexpected[@]})); then
echo "Unexpected gem artifacts: ${unexpected[*]}" >&2
exit 1
fi

- uses: rubygems/configure-rubygems-credentials@v2.1.0

- name: Push gems to RubyGems
run: |
set -euo pipefail
version="${GITHUB_REF_NAME#v}"
already_published() {
printf '%s' "$1" | grep -Eqi 'repushing of gem versions is not allowed|already (exists|been pushed|published)'
}

for name in \
"page_print-${version}.gem" \
"page_print-${version}-x86_64-linux.gem" \
"page_print-${version}-arm64-darwin.gem"
do
echo "Pushing ${name}"
set +e
output="$(gem push "gems/${name}" 2>&1)"
status=$?
set -e
printf '%s\n' "${output}"
if [[ "${status}" -eq 0 ]]; then
continue
fi
if already_published "${output}"; then
echo "${name} is already published, skipping"
continue
fi
echo "Failed to publish ${name}" >&2
exit 1
done

- name: Create GitHub Release
env:
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
version="${GITHUB_REF_NAME#v}"
tag="${GITHUB_REF_NAME}"

prev="$(git tag -l 'v*' --sort=-v:refname | awk -v current="${tag}" '$0 != current { print; exit }')"
if [[ -n "${prev}" ]]; then
notes="$(git log "${prev}..${tag}" --oneline)"
[[ -n "${notes}" ]] || notes="No commits since ${prev}."
else
notes="Initial release."
fi

assets=(
"gems/page_print-${version}.gem"
"gems/page_print-${version}-x86_64-linux.gem"
"gems/page_print-${version}-arm64-darwin.gem"
)

if gh release view "${tag}" >/dev/null 2>&1; then
echo "Release ${tag} already exists; refreshing assets"
gh release upload "${tag}" "${assets[@]}" --clobber
else
gh release create "${tag}" --title "${tag}" --notes "${notes}" "${assets[@]}"
fi
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,18 @@ gem build page_print.gemspec
gem install ./page_print-*.gem
```

## Releasing

Publish a new version with:

```sh
bin/bump 0.1.6
```

That updates `lib/page_print/version.rb`, commits `Release 0.1.6`, creates annotated tag `v0.1.6`, and pushes `main` plus the tag. Requires a clean worktree on `main`.

The Package workflow then builds the source and platform gems, pushes them to RubyGems (trusted publishing / OIDC, GitHub environment `release`), and creates a GitHub Release with the gem artifacts and commit notes since the previous `v*` tag.

## Building Native Gems

Build an `x86_64-linux` platform gem with a vendored PlutoBook library:
Expand Down
66 changes: 66 additions & 0 deletions bin/bump
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
#!/usr/bin/env ruby
# frozen_string_literal: true

require "open3"
require "shellwords"

ROOT = File.expand_path("..", __dir__)
VERSION_FILE = File.join(ROOT, "lib/page_print/version.rb")

def abort_with(message)
warn message
exit 1
end

def run(*command, capture: false)
if capture
output, status = Open3.capture2e(*command, chdir: ROOT)
abort_with("Command failed: #{command.shelljoin}\n#{output}") unless status.success?

output
else
system(*command, chdir: ROOT) || abort_with("Command failed: #{command.shelljoin}")
end
end

def usage
"Usage: bin/bump X.Y.Z\nExample: bin/bump 0.1.6"
end

def parse_version(version)
version.split(".").map(&:to_i)
end

abort_with(usage) unless ARGV.length == 1

version = ARGV.fetch(0)
abort_with("Version must be X.Y.Z (digits only)") unless version.match?(/\A\d+\.\d+\.\d+\z/)

branch = run("git", "branch", "--show-current", capture: true).strip
abort_with("Must be on main (currently #{branch.empty? ? "detached HEAD" : branch})") unless branch == "main"

status = run("git", "status", "--porcelain", capture: true)
abort_with("Worktree is dirty:\n#{status}") unless status.empty?

contents = File.read(VERSION_FILE)
match = contents.match(/VERSION\s*=\s*['"]([^'"]+)['"]/)
abort_with("Could not parse VERSION from lib/page_print/version.rb") unless match

current = match[1]
unless (parse_version(version) <=> parse_version(current)) == 1
abort_with("Version #{version} must be greater than current #{current}")
end

tag = "v#{version}"
abort_with("Tag #{tag} already exists locally") unless run("git", "tag", "-l", tag, capture: true).strip.empty?
abort_with("Tag #{tag} already exists on origin") unless run("git", "ls-remote", "--tags", "origin", "refs/tags/#{tag}", capture: true).strip.empty?

File.write(VERSION_FILE, contents.sub(/VERSION\s*=\s*['"][^'"]+['"]/, "VERSION = '#{version}'"))

run("git", "add", "lib/page_print/version.rb")
run("git", "commit", "-m", "Release #{version}")
run("git", "tag", "-a", tag, "-m", "Release #{version}")
run("git", "push", "origin", "main")
run("git", "push", "origin", tag)

puts "Pushed #{tag}. CI Package workflow will publish gems and create the GitHub Release."
165 changes: 0 additions & 165 deletions bin/release

This file was deleted.

Loading