Skip to content

ci: make the release path safe to tag from a branch (v0.6.3-beta.14) #10

ci: make the release path safe to tag from a branch (v0.6.3-beta.14)

ci: make the release path safe to tag from a branch (v0.6.3-beta.14) #10

name: Build and Release
on:
push:
tags:
- 'v*'
workflow_dispatch:
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: write
strategy:
matrix:
env: [wt32-sc01-plus]
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Cache PlatformIO
uses: actions/cache@v4
with:
path: |
~/.cache/pip
~/.platformio/.cache
key: ${{ runner.os }}-pio-${{ hashFiles('**/platformio.ini') }}
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: '3.12'
- name: Install PlatformIO
run: pip install --upgrade platformio
- name: Build ${{ matrix.env }}
run: pio run -e ${{ matrix.env }}
- name: Read version from app_config.h
id: version
run: |
# FW_VERSION moved out of main.cpp into app_config.h during the
# refactor. The old grep silently returned an empty string, which
# would have produced a release named "SpoolmanScale " with an empty
# version in manifest.json instead of failing.
VERSION=$(grep '#define FW_VERSION' src/app_config.h | sed 's/.*"\(.*\)"/\1/')
if [ -z "$VERSION" ]; then
echo "::error::FW_VERSION not found in src/app_config.h"
exit 1
fi
# Only on a tag push. A manual workflow_dispatch run has a branch
# name in GITHUB_REF_NAME and must not be held to this.
if [ "$GITHUB_REF_TYPE" = "tag" ] && [ "$VERSION" != "$GITHUB_REF_NAME" ]; then
echo "::error::Tag $GITHUB_REF_NAME does not match FW_VERSION $VERSION"
exit 1
fi
echo "Building $VERSION"
echo "version=$VERSION" >> $GITHUB_OUTPUT
- name: Refuse a tag that is not newer than what is published
if: github.ref_type == 'tag'
run: |
# The check above only compares the tag against FW_VERSION, so two
# matching but too low numbers pass it. Since the firmware now
# compares itself against version.json, a tag ranking below the
# published one would put a red dot on every device and offer them a
# downgrade. The ranking must stay identical to
# src/services/version_compare.cpp.
python3 - "${{ steps.version.outputs.version }}" <<'PY'
import json, re, sys
def rank(v):
if not v:
return -1
m = re.search(r"(\d+)\.(\d+)\.(\d+)", v)
if not m:
return -1
major, minor, patch = (int(g) for g in m.groups())
beta = 999 # no "beta" in the tag: final release
b = v.find("beta")
if b != -1:
beta = 0 # "-beta": the public release
m2 = re.match(r"\.(\d+)", v[b + 4:])
if m2:
beta = int(m2.group(1))
return major * 10**9 + minor * 10**6 + patch * 10**3 + beta
tag = sys.argv[1]
try:
with open("version.json") as f:
data = json.load(f)
except FileNotFoundError:
print("no version.json yet, nothing to compare against")
sys.exit(0)
published = max((data.get("stable") or "", data.get("prerelease") or ""),
key=rank)
if rank(tag) <= rank(published):
print(f"::error::Tag {tag} does not rank above the published "
f"{published}. Every device would be offered a downgrade.")
sys.exit(1)
print(f"{tag} ranks above {published}, ok")
PY
- name: Prepare release assets
run: |
# Always, for both kinds of tag. The device downloads
# releases/download/<tag>/SpoolmanScale.bin, so every release needs an
# asset under that exact name - and it has to be the binary just
# built, not whatever webflasher/ happens to hold from the last public
# release.
VERSION=${{ steps.version.outputs.version }}
cp .pio/build/${{ matrix.env }}/firmware.bin SpoolmanScale.bin
cp .pio/build/${{ matrix.env }}/firmware.bin SpoolmanScale_${VERSION}.bin
- name: Update the public web flasher
# Public tags only. An internal beta.N build would otherwise switch the
# flasher on the docs page to a test version that nobody outside the
# test should be offered.
if: "!contains(github.ref_name, '-beta.')"
run: |
VERSION=${{ steps.version.outputs.version }}
cp .pio/build/${{ matrix.env }}/bootloader.bin webflasher/bootloader.bin
cp .pio/build/${{ matrix.env }}/partitions.bin webflasher/partitions.bin
cp .pio/build/${{ matrix.env }}/firmware.bin webflasher/SpoolmanScale.bin
sed -i "s/\"version\": \".*\"/\"version\": \"$VERSION\"/" manifest.json
# The whole suffix is optional, in both directions. Without the
# counter group a second beta.N in a row matched only the prefix and
# produced "v0.6.3-beta.14.13"; without making "-beta" itself optional
# the line would stop matching for good after the first suffix-free
# release and index.html would freeze at that version.
sed -i -E "s/Firmware v[0-9]+\.[0-9]+\.[0-9]+(-beta(\.[0-9]+)?)?/Firmware $VERSION/" index.html
- name: Update version.json
run: |
# Read by the firmware's background update check. Deliberately a
# static file on Pages rather than api.github.com: that API allows
# 60 requests per hour and IP, which users behind carrier grade NAT
# share, and its release list runs to over a hundred kilobytes.
#
# A tag carrying a beta counter (v0.6.0-beta.3) is an internal build
# and only ever lands in "prerelease". A public tag (v0.6.0-beta)
# updates both, so devices with pre-releases switched off see it too.
VERSION=${{ steps.version.outputs.version }}
python3 - "$VERSION" <<'PY'
import json, re, sys
version = sys.argv[1]
data = {
"stable": "",
"prerelease": "",
"url": "https://github.com/Niko11111/SpoolmanScale/releases/latest",
}
try:
with open("version.json") as f:
data.update(json.load(f))
except FileNotFoundError:
pass
data["prerelease"] = version
if not re.search(r"-beta\.\d+$", version):
data["stable"] = version
with open("version.json", "w") as f:
json.dump(data, f, indent=2)
f.write("\n")
print("version.json:", data)
PY
- name: Publish the changed files to main
run: |
# GitHub Pages serves version.json, index.html, manifest.json and
# webflasher/ from main, so the files have to land there - but only
# the files.
#
# This used to be "git pull origin main --rebase" followed by
# "git push origin HEAD:main". The checkout sits on the tagged commit,
# so tagging any branch other than main pushed that branch's entire
# history onto main as a fast-forward. Tagging dev would have merged
# dev into main as a side effect of cutting a release.
#
# Instead: carry the generated files aside, check out main, put them
# back, and commit there. One commit, only the intended files, no
# foreign history.
VERSION=${{ steps.version.outputs.version }}
mkdir -p /tmp/ci-out/webflasher
cp version.json /tmp/ci-out/
if [ -z "$(echo "$VERSION" | grep -E -- '-beta\.[0-9]+$')" ]; then
cp manifest.json index.html /tmp/ci-out/
cp webflasher/bootloader.bin webflasher/partitions.bin webflasher/SpoolmanScale.bin /tmp/ci-out/webflasher/
fi
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git fetch origin main
git checkout -B ci-main origin/main
cp /tmp/ci-out/version.json version.json
git add version.json
if [ -f /tmp/ci-out/manifest.json ]; then
cp /tmp/ci-out/manifest.json manifest.json
cp /tmp/ci-out/index.html index.html
cp /tmp/ci-out/webflasher/*.bin webflasher/
git add manifest.json index.html webflasher/bootloader.bin webflasher/partitions.bin webflasher/SpoolmanScale.bin
fi
git diff --cached --quiet || git commit -m "CI: publish $VERSION"
git push origin ci-main:main
- name: Create GitHub Release
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ github.ref_name }}
name: SpoolmanScale ${{ steps.version.outputs.version }}
# Only internal builds carrying a beta counter are pre-releases.
# A public tag has to become the real "latest", because the update
# button downloads from releases/latest/download/ and that path skips
# pre-releases. Marked wrong, the device would announce the new
# version and then install the previous binary.
prerelease: ${{ contains(github.ref_name, '-beta.') }}
files: |
SpoolmanScale.bin
SpoolmanScale_${{ steps.version.outputs.version }}.bin
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}