Skip to content

Rolling Release

Rolling Release #1163

---
name: Rolling Release
on:
workflow_run:
workflows: ["Format Check", "Lint", "Tests", "Build"]
types: [completed]
permissions:
contents: write
concurrency:
# Cancel earlier rolling runs for the same source branch
group: ${{ github.workflow }}-${{ github.event.workflow_run.head_branch || github.ref_name || github.ref }}
cancel-in-progress: true
jobs:
build-test-release:
outputs:
NEXT_TAG: ${{ steps.rolling_tag.outputs.NEXT_TAG }}
if: ${{ github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.head_branch == 'main' }}
strategy:
fail-fast: false
matrix:
include:
- name: ubuntu-x64
runner: ubuntu-latest
shell: bash
cmake_arch: x86_64
- name: ubuntu-arm64
runner: ubuntu-24.04-arm
shell: bash
cmake_arch: aarch64
- name: macos-x64
runner: macos-13
shell: bash
cmake_arch: x86_64
- name: macos-arm64
runner: macos-14
shell: bash
cmake_arch: arm64
- name: windows-x64
runner: windows-latest
shell: powershell
cmake_arch: x64
- name: windows-arm64
runner: windows-11-arm
shell: powershell
cmake_arch: arm64
runs-on: ${{ matrix.runner }}
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: "Gate: require format/lint/tests/build success"
id: gate
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
HEAD_SHA: ${{ github.event.workflow_run.head_sha }}
BRANCH: ${{ github.event.workflow_run.head_branch }}
REQUIRED: '["Format Check","Lint","Tests","Build"]'
shell: bash
run: |
set -euo pipefail
repo='${{ github.repository }}'
total=$(echo "$REQUIRED" | jq 'length')
ok=0
for name in $(echo "$REQUIRED" | jq -r '.[]'); do
wid=$(curl -s -H "Authorization: token $GITHUB_TOKEN" -H "Accept: application/vnd.github+json" \
"https://api.github.com/repos/$repo/actions/workflows" | jq -r --arg n "$name" '.workflows[] | select(.name==$n) | .id' | head -n1)
if [ -z "$wid" ] || [ "$wid" = "null" ]; then
echo "workflow '$name' not found" >&2
continue
fi
conclusion=$(curl -s -H "Authorization: token $GITHUB_TOKEN" -H "Accept: application/vnd.github+json" \
"https://api.github.com/repos/$repo/actions/workflows/$wid/runs?per_page=50&branch=$BRANCH" | \
jq -r --arg sha "$HEAD_SHA" '.workflow_runs[] | select(.head_sha==$sha) | .conclusion' | head -n1)
if [ "$conclusion" = "success" ]; then
ok=$((ok+1))
else
echo "workflow '$name' not successful yet (conclusion=${conclusion:-none})"
fi
done
ready=false
if [ "$ok" -eq "$total" ]; then ready=true; fi
echo "ready=$ready" >> "$GITHUB_OUTPUT"
- name: Check recent releases
id: check_releases
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
shell: bash
run: |
SINCE="$(python3 -c 'from datetime import datetime, timedelta; print((datetime.utcnow() - timedelta(hours=24)).strftime("%Y-%m-%dT%H:%M:%SZ"))')"
COUNT=$(curl -s -H "Authorization: token $GITHUB_TOKEN" \
"https://api.github.com/repos/${{ github.repository }}/releases?per_page=100" \
| jq "[.[] | select(.created_at >= \"$SINCE\")] | length")
echo "releases_last24h=$COUNT" >> "$GITHUB_OUTPUT"
if [ "$COUNT" -ge 2 ]; then
echo "skip_build=true" >> "$GITHUB_OUTPUT"
else
echo "skip_build=false" >> "$GITHUB_OUTPUT"
fi
- name: Skip build when release limit reached
if: steps.check_releases.outputs.skip_build == 'true'
run: |
echo 'Skipping build: daily release threshold reached.'
- name: Set up C++ build environment
if: steps.check_releases.outputs.skip_build != 'true' && steps.gate.outputs.ready == 'true'
uses: aminya/setup-cpp@v1
- name: Enable ccache
if: steps.check_releases.outputs.skip_build != 'true' && steps.gate.outputs.ready == 'true' && runner.os != 'Windows'
uses: hendrikmuhs/ccache-action@v1.2.19
with:
key: ${{ runner.os }}-${{ matrix.name }}-rolling-ccache-${{ github.event.workflow_run.head_sha }}
restore-keys: |
${{ runner.os }}-${{ matrix.name }}-rolling-ccache-
- name: Cache CMake dependencies
if: steps.check_releases.outputs.skip_build != 'true' && steps.gate.outputs.ready == 'true'
uses: actions/cache@v4
with:
path: build/**/_deps
key: ${{ runner.os }}-cmake-${{ hashFiles('CMakeLists.txt', 'cmake/**', '**/CMakeLists.txt') }}
restore-keys: |
${{ runner.os }}-cmake-
- name: Configure (Linux/macOS)
if: steps.check_releases.outputs.skip_build != 'true' && steps.gate.outputs.ready == 'true' && runner.os != 'Windows'
shell: bash
env:
CMAKE_CXX_COMPILER_LAUNCHER: ccache
run: |
set -euo pipefail
if [[ "${RUNNER_OS}" == "Linux" ]]; then
export CC=gcc CXX=g++
else
export CC=clang CXX=clang++
fi
unset CFLAGS CXXFLAGS LDFLAGS
cmake --preset rolling -DCODEX_FORCE_NO_NSEC=ON
- name: Resolve binary directory (Linux/macOS)
if: steps.check_releases.outputs.skip_build != 'true' && steps.gate.outputs.ready == 'true' && runner.os != 'Windows'
shell: bash
run: |
set -euo pipefail
BIN_ROOT="build/rolling"
if [ ! -d "$BIN_ROOT" ]; then
echo "Binary root not found: $BIN_ROOT" >&2
exit 1
fi
BIN_DIR=$(find "$BIN_ROOT" -mindepth 1 -maxdepth 1 -type d | sort | head -n 1)
if [ -z "$BIN_DIR" ]; then
echo "No configure directory found under $BIN_ROOT" >&2
exit 1
fi
echo "Resolved binary directory: $BIN_DIR"
echo "ROLLING_BIN_DIR=$BIN_DIR" >> "$GITHUB_ENV"
- name: Build (Linux/macOS)
if: steps.check_releases.outputs.skip_build != 'true' && steps.gate.outputs.ready == 'true' && runner.os != 'Windows'
shell: bash
run: cmake --build --preset rolling --parallel
- name: Run tests (Linux/macOS)
if: steps.check_releases.outputs.skip_build != 'true' && steps.gate.outputs.ready == 'true' && runner.os != 'Windows'
shell: bash
run: |
set -euo pipefail
: "${ROLLING_BIN_DIR:?ROLLING_BIN_DIR must be set by the resolve step}"
ctest --test-dir "$ROLLING_BIN_DIR" -C RelWithDebInfo --output-on-failure --parallel
- name: Configure (Windows)
if: steps.check_releases.outputs.skip_build != 'true' && steps.gate.outputs.ready == 'true' && runner.os == 'Windows'
shell: powershell
run: |
$arch = '${{ matrix.cmake_arch }}'
$generatorArgs = if ($arch -eq 'arm64') { '-A ARM64' } else { '-A x64' }
cmake -S . -B build -G "Visual Studio 17 2022" $generatorArgs -DCMAKE_BUILD_TYPE=RelWithDebInfo -DCODEX_FORCE_NO_NSEC=ON -DCMAKE_INTERPROCEDURAL_OPTIMIZATION=ON -DCMAKE_INSTALL_RPATH_USE_LINK_PATH=ON -DCMAKE_POSITION_INDEPENDENT_CODE=ON
- name: Build (Windows)
if: steps.check_releases.outputs.skip_build != 'true' && steps.gate.outputs.ready == 'true' && runner.os == 'Windows'
shell: powershell
run: cmake --build build --config RelWithDebInfo --parallel
- name: Run tests (Windows)
if: steps.check_releases.outputs.skip_build != 'true' && steps.gate.outputs.ready == 'true' && runner.os == 'Windows'
shell: powershell
continue-on-error: true
run: ctest --test-dir build -C RelWithDebInfo --output-on-failure
- name: Stage artifact (Linux/macOS)
if: steps.check_releases.outputs.skip_build != 'true' && steps.gate.outputs.ready == 'true' && runner.os != 'Windows'
shell: bash
run: |
set -euo pipefail
: "${ROLLING_BIN_DIR:?ROLLING_BIN_DIR must be set by the resolve step}"
DEST="dist/${{ matrix.name }}"
mkdir -p "$DEST"
install -m 755 "$ROLLING_BIN_DIR/autogitpull" "$DEST/autogitpull-${{ matrix.name }}"
install -m 644 readme.md "$DEST/README.md"
install -m 644 license.md "$DEST/LICENSE.md"
- name: Stage artifact (Windows)
if: steps.check_releases.outputs.skip_build != 'true' && steps.gate.outputs.ready == 'true' && runner.os == 'Windows'
shell: powershell
run: |
$dest = "dist/${{ matrix.name }}"
New-Item -ItemType Directory -Force -Path $dest | Out-Null
Copy-Item -Path "build/RelWithDebInfo/autogitpull.exe" -Destination (Join-Path $dest 'autogitpull-${{ matrix.name }}.exe') -Force
Copy-Item -Path readme.md -Destination (Join-Path $dest 'README.md') -Force
Copy-Item -Path license.md -Destination (Join-Path $dest 'LICENSE.md') -Force
- name: Upload artifact
if: steps.check_releases.outputs.skip_build != 'true' && steps.gate.outputs.ready == 'true'
uses: actions/upload-artifact@v4
with:
name: autogitpull-${{ matrix.name }}
path: dist/${{ matrix.name }}/*
- name: Get next rolling tag (*nix)
if: steps.check_releases.outputs.skip_build != 'true' && steps.gate.outputs.ready == 'true' && runner.os != 'Windows'
id: rolling_tag
shell: bash
run: |
set -euo pipefail
TODAY=$(date +'%y.%m.%d')
NEXT="rolling-v${TODAY}"
echo "NEXT_TAG=$NEXT" >> "$GITHUB_ENV"
echo "NEXT_TAG=$NEXT" >> "$GITHUB_OUTPUT"
echo "Rolling tag: $NEXT"
- name: Get next rolling tag (Windows)
if: steps.check_releases.outputs.skip_build != 'true' && steps.gate.outputs.ready == 'true' && runner.os == 'Windows'
id: rolling_tag_win
shell: powershell
run: |
$today = Get-Date -Format "yy.MM.dd"
$next = "rolling-v$today"
"NEXT_TAG=$next" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8
"NEXT_TAG=$next" | Out-File -FilePath $env:GITHUB_OUTPUT -Append -Encoding utf8
Write-Host "Rolling tag: $next"
- name: Tag commit (*nix)
if: steps.check_releases.outputs.skip_build != 'true' && steps.gate.outputs.ready == 'true' && runner.os != 'Windows'
shell: bash
run: |
git config user.name github-actions
git config user.email github-actions@github.com
git tag "$NEXT_TAG"
git push origin "$NEXT_TAG"
- name: Tag commit (Windows)
if: steps.check_releases.outputs.skip_build != 'true' && steps.gate.outputs.ready == 'true' && runner.os == 'Windows'
shell: powershell
run: |
git config user.name github-actions
git config user.email github-actions@github.com
git tag $env:NEXT_TAG
git push origin $env:NEXT_TAG
publish:
if: ${{ github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.head_branch == 'main' && needs.build-test-release.outputs.NEXT_TAG != '' }}
needs: build-test-release
runs-on: ubuntu-latest
steps:
- name: Download artifacts
uses: actions/download-artifact@v4
with:
path: dist
- name: Create GitHub Release
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ needs.build-test-release.outputs.NEXT_TAG }}
name: ${{ needs.build-test-release.outputs.NEXT_TAG }}
body: Automated rolling release ${{ needs.build-test-release.outputs.NEXT_TAG }}
draft: false
prerelease: true
files: |
dist/**/autogitpull-*
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}