ci: make the release path safe to tag from a branch (v0.6.3-beta.14) #10
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 }} |