Stable Release #44
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: Stable Release | |
| on: | |
| schedule: | |
| - cron: "0 0 * * *" # Daily at 00:00 UTC | |
| workflow_dispatch: | |
| push: | |
| branches: | |
| - main | |
| env: | |
| cache_nonce: 0 | |
| jobs: | |
| # Run tests | |
| test: | |
| name: Run tests | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 30 | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v6 | |
| - name: Install mise | |
| uses: jdx/mise-action@v3 | |
| with: | |
| enable: true | |
| cache: true | |
| - name: Create virtual environment | |
| run: | | |
| python -m venv venv | |
| - name: Install test dependencies | |
| run: | | |
| source venv/bin/activate | |
| pip install -r requirements-test.txt | |
| - name: Run tests with coverage | |
| run: | | |
| source venv/bin/activate | |
| pytest tests/ -v --cov=src --cov-report=xml --cov-report=term | |
| - name: Upload coverage reports | |
| if: github.event_name != 'workflow_dispatch' | |
| uses: codecov/codecov-action@v5 | |
| continue-on-error: true | |
| with: | |
| token: ${{ secrets.CODECOV_TOKEN }} | |
| files: ./coverage.xml | |
| flags: unittests | |
| name: codecov-umbrella | |
| fail_ci_if_error: false | |
| # Check for new upstream tags | |
| check-updates: | |
| runs-on: ubuntu-latest | |
| needs: test | |
| outputs: | |
| should_build: ${{ steps.check.outputs.should_build }} | |
| tag_name: ${{ steps.check.outputs.tag_name }} | |
| tag_date: ${{ steps.check.outputs.tag_date }} | |
| tag_commit: ${{ steps.check.outputs.tag_commit }} | |
| steps: | |
| - name: Check for new upstream tags | |
| id: check | |
| run: | | |
| # Get latest stable release from upstream (exclude pre-releases) | |
| # Use GitHub API to properly identify pre-releases | |
| API_URL="https://api.github.com/repos/logos-storage/logos-storage-nim/releases" | |
| LATEST_RELEASE=$(curl -s "$API_URL" | python3 -c "import sys, json; releases = json.load(sys.stdin); stable_releases = [r for r in releases if not r.get('prerelease', False)]; print(f\"{stable_releases[0]['tag_name']}|{stable_releases[0]['target_commitish']}\")" 2>/dev/null) | |
| if [ -z "$LATEST_RELEASE" ]; then | |
| echo "No stable releases found in upstream" | |
| exit 1 | |
| fi | |
| LATEST_TAG=$(echo "$LATEST_RELEASE" | cut -d'|' -f1) | |
| LATEST_COMMIT=$(echo "$LATEST_RELEASE" | cut -d'|' -f2) | |
| # Get tag date using GitHub releases API | |
| API_URL="https://api.github.com/repos/logos-storage/logos-storage-nim/releases/tags/$LATEST_TAG" | |
| TAG_DATE=$(curl -s "$API_URL" | python3 -c "import sys, json; data=json.load(sys.stdin); print(data.get('published_at', 'Unknown'))" 2>/dev/null || echo "Unknown") | |
| # Get latest stable release from this repository (exclude pre-releases) | |
| OUR_LATEST_STABLE=$(curl -s "https://api.github.com/repos/${{ github.repository }}/releases?per_page=10" | python3 -c "import sys, json; data=json.load(sys.stdin); stable=[r for r in data if not r.get('prerelease')]; print(stable[0]['tag_name'] if stable else '')" 2>/dev/null || echo "") | |
| # If no stable release exists, we should build | |
| if [ -z "$OUR_LATEST_STABLE" ]; then | |
| echo "should_build=true" >> $GITHUB_OUTPUT | |
| echo "tag_name=$LATEST_TAG" >> $GITHUB_OUTPUT | |
| echo "tag_date=$TAG_DATE" >> $GITHUB_OUTPUT | |
| echo "tag_commit=$LATEST_COMMIT" >> $GITHUB_OUTPUT | |
| echo "No previous stable release found, building for tag: $LATEST_TAG" | |
| exit 0 | |
| fi | |
| # Compare tag names directly | |
| if [ "$LATEST_TAG" == "$OUR_LATEST_STABLE" ]; then | |
| echo "should_build=false" >> $GITHUB_OUTPUT | |
| echo "Latest tag $LATEST_TAG is already released" | |
| else | |
| echo "should_build=true" >> $GITHUB_OUTPUT | |
| echo "tag_name=$LATEST_TAG" >> $GITHUB_OUTPUT | |
| echo "tag_date=$TAG_DATE" >> $GITHUB_OUTPUT | |
| echo "tag_commit=$LATEST_COMMIT" >> $GITHUB_OUTPUT | |
| echo "New tag detected: $LATEST_TAG (our latest: $OUR_LATEST_STABLE)" | |
| fi | |
| # Matrix build | |
| build: | |
| needs: [test, check-updates] | |
| if: needs.check-updates.outputs.should_build == 'true' | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - os: linux | |
| cpu: amd64 | |
| builder: ubuntu-latest | |
| - os: linux | |
| cpu: arm64 | |
| builder: ubuntu-22.04-arm | |
| - os: macos | |
| cpu: arm64 | |
| builder: macos-latest | |
| - os: windows | |
| cpu: amd64 | |
| builder: windows-latest | |
| name: ${{ matrix.os }}-${{ matrix.cpu }} | |
| runs-on: ${{ matrix.builder }} | |
| timeout-minutes: 120 | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v6 | |
| - name: Install mise | |
| uses: jdx/mise-action@v3 | |
| with: | |
| enable: true | |
| cache: true | |
| - name: Setup MSYS2 (Windows) | |
| if: runner.os == 'Windows' | |
| uses: msys2/setup-msys2@v2 | |
| with: | |
| msystem: MINGW64 | |
| update: true | |
| path-type: inherit | |
| install: >- | |
| base-devel | |
| mingw-w64-x86_64-gcc | |
| mingw-w64-x86_64-cmake | |
| mingw-w64-x86_64-make | |
| git | |
| - name: Install build dependencies (Linux) | |
| if: runner.os == 'Linux' | |
| run: | | |
| sudo apt-get update | |
| sudo apt-get install -y git make gcc binutils cmake | |
| shell: bash | |
| - name: Build (Windows) | |
| if: runner.os == 'Windows' | |
| shell: msys2 {0} | |
| run: | | |
| make clean | |
| make | |
| env: | |
| TAG: ${{ needs.check-updates.outputs.tag_name }} | |
| - name: Build (Unix) | |
| if: runner.os != 'Windows' | |
| run: | | |
| make clean | |
| make | |
| env: | |
| TAG: ${{ needs.check-updates.outputs.tag_name }} | |
| shell: bash | |
| - name: Upload artifacts | |
| if: env.ACT != 'true' | |
| uses: actions/upload-artifact@v6 | |
| continue-on-error: true | |
| with: | |
| name: libstorage-${{ matrix.os }}-${{ matrix.cpu }} | |
| path: dist/*/ | |
| retention-days: 90 | |
| - name: Verify artifacts (Windows) | |
| if: runner.os == 'Windows' | |
| shell: msys2 {0} | |
| run: | | |
| echo "Verifying artifacts..." | |
| if [ -d "dist" ]; then | |
| find dist -name "*.a" -type f | while read lib; do | |
| echo "Found: $lib" | |
| ls -lh "$lib" | |
| done | |
| find dist -name "SHA256SUMS.txt" -type f | while read checksum_file; do | |
| echo "Verifying checksums in: $checksum_file" | |
| checksum_dir=$(dirname "$checksum_file") | |
| (cd "$checksum_dir" && sha256sum -c "SHA256SUMS.txt") | |
| done | |
| echo "" | |
| echo "Artifacts are available in: $(pwd)/dist/" | |
| else | |
| echo "No dist directory found" | |
| fi | |
| - name: Verify artifacts (Unix) | |
| if: runner.os != 'Windows' | |
| shell: bash | |
| run: | | |
| echo "Verifying artifacts..." | |
| if [ -d "dist" ]; then | |
| find dist -name "*.a" -type f | while read lib; do | |
| echo "Found: $lib" | |
| ls -lh "$lib" | |
| done | |
| find dist -name "SHA256SUMS.txt" -type f | while read checksum_file; do | |
| echo "Verifying checksums in: $checksum_file" | |
| checksum_dir=$(dirname "$checksum_file") | |
| (cd "$checksum_dir" && sha256sum -c "SHA256SUMS.txt") | |
| done | |
| echo "" | |
| echo "Artifacts are available in: $(pwd)/dist/" | |
| else | |
| echo "No dist directory found" | |
| fi | |
| # Fetch upstream release notes | |
| fetch-notes: | |
| runs-on: ubuntu-latest | |
| needs: [test, check-updates] | |
| if: needs.check-updates.outputs.should_build == 'true' | |
| outputs: | |
| release_notes: ${{ steps.fetch.outputs.notes }} | |
| steps: | |
| - name: Fetch upstream release notes | |
| id: fetch | |
| run: | | |
| TAG_NAME="${{ needs.check-updates.outputs.tag_name }}" | |
| # Try GitHub API first | |
| API_URL="https://api.github.com/repos/logos-storage/logos-storage-nim/releases/tags/$TAG_NAME" | |
| NOTES=$(curl -s "$API_URL" | python3 -c "import sys, json; data=json.load(sys.stdin); print(data.get('body', ''))" 2>/dev/null) | |
| # Fallback to git tag annotation | |
| if [ -z "$NOTES" ] || [ "$NOTES" == "null" ]; then | |
| NOTES=$(git ls-remote --tags https://github.com/logos-storage/logos-storage-nim.git "refs/tags/$TAG_NAME^{}" | awk '{print $1}' | xargs -I{} sh -c 'git fetch https://github.com/logos-storage/logos-storage-nim.git {} 2>/dev/null && git show -s --format=%B {}' 2>/dev/null || echo "") | |
| fi | |
| # Escape for YAML - only escape quotes, keep newlines as-is | |
| NOTES_ESCAPED=$(echo "$NOTES" | sed 's/"/\\"/g') | |
| # Use a delimiter to preserve newlines in the output | |
| echo "notes<<EOF" >> $GITHUB_OUTPUT | |
| echo "$NOTES_ESCAPED" >> $GITHUB_OUTPUT | |
| echo "EOF" >> $GITHUB_OUTPUT | |
| # Create release | |
| release: | |
| needs: [check-updates, build, fetch-notes] | |
| if: needs.check-updates.outputs.should_build == 'true' | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Download artifacts | |
| if: env.ACT != 'true' | |
| uses: actions/download-artifact@v7 | |
| continue-on-error: true | |
| with: | |
| pattern: libstorage-* | |
| merge-multiple: true | |
| path: /tmp/release | |
| - name: Compress and checksum | |
| if: env.ACT != 'true' | |
| run: | | |
| cd /tmp/release | |
| # Find all directories and create archives | |
| for dir in */; do | |
| dir_name=$(basename "$dir") | |
| # Directory name is already in the correct format: {tag}-{platform} | |
| # Use it directly to create the archive name | |
| archive="logos-storage-nim-${dir_name}.tar.gz" | |
| cd "$dir" | |
| tar cfz "/tmp/release/${archive}" *.a libstorage.h SHA256SUMS.txt | |
| cd /tmp/release | |
| done | |
| # Create SHA256SUMS.txt for all archives | |
| sha256sum *.tar.gz > SHA256SUMS.txt | |
| - name: Create release | |
| if: env.ACT != 'true' | |
| uses: softprops/action-gh-release@v2 | |
| continue-on-error: true | |
| with: | |
| tag_name: ${{ needs.check-updates.outputs.tag_name }} | |
| name: ${{ needs.check-updates.outputs.tag_name }} | |
| body: | | |
| ## Pre-built static libraries for logos-storage-nim ${{ needs.check-updates.outputs.tag_name }} | |
| Tag: ${{ needs.check-updates.outputs.tag_name }} | |
| Commit: ${{ needs.check-updates.outputs.tag_commit }} | |
| Date: ${{ needs.check-updates.outputs.tag_date }} | |
| Artifacts: | |
| - Linux x86_64: logos-storage-nim-${{ needs.check-updates.outputs.tag_name }}-linux-amd64.tar.gz | |
| - Linux AArch64: logos-storage-nim-${{ needs.check-updates.outputs.tag_name }}-linux-arm64.tar.gz | |
| - macOS Apple Silicon: logos-storage-nim-${{ needs.check-updates.outputs.tag_name }}-darwin-arm64.tar.gz | |
| - Windows x86_64: logos-storage-nim-${{ needs.check-updates.outputs.tag_name }}-windows-amd64.tar.gz | |
| Each archive contains: | |
| - libstorage.a: Main storage library | |
| - libnatpmp.a: NAT-PMP library | |
| - libminiupnpc.a: MiniUPnP library | |
| - libbacktrace.a: Backtrace library | |
| - libstorage.h: C header file | |
| - SHA256SUMS.txt: Checksums for all files | |
| Archive checksums: SHA256SUMS.txt | |
| --- | |
| ### Upstream Release Notes: | |
| ${{ needs.fetch-notes.outputs.release_notes }} | |
| files: | | |
| /tmp/release/*.tar.gz | |
| /tmp/release/SHA256SUMS.txt | |
| draft: false | |
| prerelease: false | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GH_PAT }} | |
| # Refresh stable release if older than 89 days | |
| refresh-stable: | |
| runs-on: ubuntu-latest | |
| if: github.event_name == 'schedule' | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v6 | |
| - name: Check if stable release needs refresh | |
| id: check_refresh | |
| run: | | |
| # Get latest stable release | |
| LATEST_STABLE=$(gh release list --limit 1 --exclude-pre-releases --json tagName,publishedAt --jq '.[0]') | |
| TAG_NAME=$(echo "$LATEST_STABLE" | jq -r '.tagName') | |
| PUBLISHED_AT=$(echo "$LATEST_STABLE" | jq -r '.publishedAt') | |
| # Calculate age in days | |
| PUBLISHED_TIMESTAMP=$(date -d "$PUBLISHED_AT" +%s) | |
| CURRENT_TIMESTAMP=$(date +%s) | |
| AGE_DAYS=$(( (CURRENT_TIMESTAMP - PUBLISHED_TIMESTAMP) / 86400 )) | |
| if [ $AGE_DAYS -ge 89 ]; then | |
| echo "should_refresh=true" >> $GITHUB_OUTPUT | |
| echo "tag_name=$TAG_NAME" >> $GITHUB_OUTPUT | |
| echo "age_days=$AGE_DAYS" >> $GITHUB_OUTPUT | |
| echo "Stable release is $AGE_DAYS days old, needs refresh" | |
| else | |
| echo "should_refresh=false" >> $GITHUB_OUTPUT | |
| echo "Stable release is $AGE_DAYS days old, no refresh needed" | |
| fi | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Refresh stable release | |
| if: steps.check_refresh.outputs.should_refresh == 'true' | |
| run: | | |
| TAG_NAME="${{ steps.check_refresh.outputs.tag_name }}" | |
| echo "Deleting release and tag: $TAG_NAME" | |
| # Delete release | |
| gh release delete "$TAG_NAME" --yes | |
| # Delete tag | |
| git push origin ":refs/tags/$TAG_NAME" | |
| echo "Triggering rebuild for tag: $TAG_NAME" | |
| # Trigger rebuild | |
| gh workflow run stable-release.yml | |
| env: | |
| GH_TOKEN: ${{ secrets.GH_PAT }} |