feat: upload ISOs to R2 bucket with S3-compat fallback #2
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 Community Editions | ||
| on: | ||
| schedule: | ||
| - cron: '0 6 * * 1' # weekly Monday 6am UTC | ||
| workflow_dispatch: | ||
| permissions: | ||
| contents: read | ||
| concurrency: | ||
| group: build-community-${{ github.sha }} | ||
| cancel-in-progress: false | ||
| jobs: | ||
| build: | ||
| strategy: | ||
| fail-fast: false | ||
| max-parallel: 2 | ||
| matrix: | ||
| edition: | ||
| - slug: gnome | ||
| repo: spivanatalie64/acreetionos-gnome | ||
| desktop: GNOME | ||
| display: Wayland | ||
| arch: x86_64 | ||
| - slug: plasma | ||
| repo: spivanatalie64/acreetionos-plasma | ||
| desktop: KDE Plasma | ||
| display: Wayland | ||
| arch: x86_64 | ||
| - slug: xfce | ||
| repo: spivanatalie64/acreetionos-xfce | ||
| desktop: XFCE | ||
| display: XLibre | ||
| arch: x86_64 | ||
| - slug: i3 | ||
| repo: spivanatalie64/acreetionos-i3 | ||
| desktop: i3 | ||
| display: X11 | ||
| arch: x86_64 | ||
| - slug: sway | ||
| repo: spivanatalie64/acreetionos-sway | ||
| desktop: Sway | ||
| display: Wayland | ||
| arch: x86_64 | ||
| - slug: mate | ||
| repo: spivanatalie64/acreetionos-mate | ||
| desktop: MATE | ||
| display: XLibre | ||
| arch: x86_64 | ||
| - slug: openbox | ||
| repo: spivanatalie64/acreetionos-openbox | ||
| desktop: Openbox | ||
| display: XLibre | ||
| arch: x86_64 | ||
| - slug: hyprland | ||
| repo: spivanatalie64/acreetionos-hyprland | ||
| desktop: Hyprland | ||
| display: Wayland | ||
| arch: x86_64 | ||
| - slug: 32bit | ||
| repo: spivanatalie64/acreetionos32 | ||
| desktop: Cinnamon | ||
| display: XLibre | ||
| arch: i686 | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| - uses: actions/checkout@v4 | ||
| - name: Checkout edition repo | ||
| uses: actions/checkout@v4 | ||
| with: | ||
| repository: ${{ matrix.edition.repo }} | ||
| path: src | ||
| - name: Apply branding | ||
| run: | | ||
| sed -i 's/sudo //g' src/build.sh 2>/dev/null || true | ||
| sed -i 's/iso_label=.*/iso_label="AcreetionOS ${{ matrix.edition.desktop }}"/' src/profiledef.sh 2>/dev/null || true | ||
| sed -i 's/iso_publisher=.*/iso_publisher="AcreetionOS Project <https:\/\/acreetionos.org>"/' src/profiledef.sh 2>/dev/null || true | ||
| sed -i 's/iso_application=.*/iso_application="AcreetionOS Linux Distribution"/' src/profiledef.sh 2>/dev/null || true | ||
| - name: Emit build metadata | ||
| id: meta | ||
| run: | | ||
| echo "slug=${{ matrix.edition.slug }}" >> $GITHUB_OUTPUT | ||
| echo "date_tag=$(date +%Y%m%d)" >> $GITHUB_OUTPUT | ||
| echo "iso_name=${{ matrix.edition.slug }}-$(date +%Y%m%d).iso" >> $GITHUB_OUTPUT | ||
| - name: Write building status to R2 | ||
| env: | ||
| CF_ACCOUNT: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} | ||
| CF_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} | ||
| run: | | ||
| STATUS='{"edition":"${{ matrix.edition.desktop }}","slug":"${{ steps.meta.outputs.slug }}","status":"building","started":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'","run_url":"https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}","iso_name":"${{ steps.meta.outputs.iso_name }}","desktop":"${{ matrix.edition.desktop }}","display":"${{ matrix.edition.display }}","arch":"${{ matrix.edition.arch }}"}' | ||
| echo "$STATUS" > /tmp/status.json | ||
| curl -s -X PUT "https://api.cloudflare.com/client/v4/accounts/$CF_ACCOUNT/r2/buckets/acreetionos-isos/objects/${{ steps.meta.outputs.slug }}-status.json" \ | ||
| -H "Authorization: Bearer $CF_TOKEN" \ | ||
| --data-binary @/tmp/status.json \ | ||
| -H "Content-Type: application/json" || echo "Status write skipped" | ||
| - name: Build ISO in Docker | ||
| id: build | ||
| run: | | ||
| mkdir -p /tmp/iso | ||
| docker run --privileged --rm \ | ||
| -v $PWD/src:/workspace \ | ||
| archlinux@sha256:fb47d15e0fd28369c700edcfa46dd44e234d8ab6fee9627ee5f1ead5f1fc6c21 \ | ||
| bash -c ' | ||
| pacman -Syu --noconfirm archiso grub && \ | ||
| cd /workspace && \ | ||
| chmod +x *.sh 2>/dev/null; \ | ||
| bash build.sh 2>&1 || { echo "BUILD_FAILED"; exit 1; } | ||
| ' 2>&1 | tee /tmp/build.log | ||
| ls -la src/out/ 2>/dev/null && cp src/out/*.iso /tmp/iso/ 2>/dev/null || true | ||
| if [ -z "$(ls /tmp/iso/*.iso 2>/dev/null)" ]; then | ||
| echo "build_ok=false" >> $GITHUB_OUTPUT | ||
| else | ||
| echo "build_ok=true" >> $GITHUB_OUTPUT | ||
| fi | ||
| - name: Upload build log | ||
| if: always() | ||
| env: | ||
| CF_ACCOUNT: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} | ||
| CF_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} | ||
| run: | | ||
| if [ -f /tmp/build.log ]; then | ||
| curl -s -X PUT "https://api.cloudflare.com/client/v4/accounts/$CF_ACCOUNT/r2/buckets/acreetionos-isos/objects/${{ steps.meta.outputs.slug }}-build.log" \ | ||
| -H "Authorization: Bearer $CF_TOKEN" \ | ||
| --data-binary @/tmp/build.log \ | ||
| -H "Content-Type: text/plain" || echo "Log upload skipped" | ||
| fi | ||
| - name: Prepare ISO for upload | ||
| id: iso | ||
| if: steps.build.outputs.build_ok == 'true' | ||
| run: | | ||
| ISO=$(ls /tmp/iso/*.iso 2>/dev/null | head -1) | ||
| if [ -n "$ISO" ]; then | ||
| cp "$ISO" "/tmp/${{ steps.meta.outputs.iso_name }}" | ||
| echo "iso_path=/tmp/${{ steps.meta.outputs.iso_name }}" >> $GITHUB_OUTPUT | ||
| echo "iso_name=${{ steps.meta.outputs.iso_name }}" >> $GITHUB_OUTPUT | ||
| echo "ok=true" >> $GITHUB_OUTPUT | ||
| else | ||
| echo "ok=false" >> $GITHUB_OUTPUT | ||
| fi | ||
| - name: Upload ISO to R2 | ||
| if: steps.iso.outputs.ok == 'true' | ||
| env: | ||
| CF_ACCOUNT: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} | ||
| CF_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} | ||
| run: | | ||
| # Upload to R2 using Cloudflare API | ||
| echo "Uploading ${{ steps.meta.outputs.iso_name }} to R2..." | ||
| curl -s -X PUT \ | ||
| "https://api.cloudflare.com/client/v4/accounts/$CF_ACCOUNT/r2/buckets/acreetionos-isos/objects/${{ steps.meta.outputs.iso_name }}" \ | ||
| -H "Authorization: Bearer $CF_TOKEN" \ | ||
| --data-binary @"${{ steps.iso.outputs.iso_path }}" \ | ||
| -H "Content-Type: application/octet-stream" && echo "Uploaded: ${{ steps.meta.outputs.iso_name }}" || echo "R2 upload failed" | ||
| # Upload as latest too | ||
| curl -s -X PUT \ | ||
| "https://api.cloudflare.com/client/v4/accounts/$CF_ACCOUNT/r2/buckets/acreetionos-isos/objects/${{ steps.meta.outputs.slug }}-latest.iso" \ | ||
| -H "Authorization: Bearer $CF_TOKEN" \ | ||
| --data-binary @"${{ steps.iso.outputs.iso_path }}" \ | ||
| -H "Content-Type: application/octet-stream" && echo "Uploaded: ${{ steps.meta.outputs.slug }}-latest.iso" || echo "Latest upload failed" | ||
| # Also try S3-compatible upload as fallback | ||
| echo "Also trying S3-compatible upload..." | ||
| pip3 install -q boto3 2>/dev/null || true | ||
| python3 -c " | ||
| import boto3, os | ||
| s3 = boto3.client('s3', | ||
| endpoint_url=f'https://{os.environ[\"CF_ACCOUNT\"]}.r2.cloudflarestorage.com', | ||
| aws_access_key_id=os.environ['CF_ACCOUNT'], | ||
| aws_secret_access_key=os.environ['CF_TOKEN'], | ||
| region_name='auto') | ||
| try: | ||
| s3.upload_file('${{ steps.iso.outputs.iso_path }}', 'acreetionos-isos', '${{ steps.meta.outputs.iso_name }}') | ||
| print('S3 upload OK') | ||
| s3.copy_object(Bucket='acreetionos-isos', CopySource=f'/acreetionos-isos/${{ steps.meta.outputs.iso_name }}', Key='${{ steps.meta.outputs.slug }}-latest.iso') | ||
| print('Latest copy OK') | ||
| except Exception as e: | ||
| print(f'S3 upload failed: {e}') | ||
| " 2>&1 || echo "S3 upload skipped" | ||
| - name: Update build status | ||
| if: always() | ||
| env: | ||
| CF_ACCOUNT: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} | ||
| CF_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} | ||
| run: | | ||
| if [ "${{ steps.iso.outputs.ok }}" = "true" ]; then | ||
| STATUS='{"edition":"${{ matrix.edition.desktop }}","slug":"${{ steps.meta.outputs.slug }}","status":"success","finished":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'","run_url":"https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}","iso_name":"${{ steps.meta.outputs.iso_name }}","download_url":"https://pub-173a1f638a3b4c95b5f58b09c0b968aa.r2.dev/acreetionos-isos/${{ steps.meta.outputs.iso_name }}"}' | ||
| else | ||
| STATUS='{"edition":"${{ matrix.edition.desktop }}","slug":"${{ steps.meta.outputs.slug }}","status":"failed","finished":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'","run_url":"https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}","desktop":"${{ matrix.edition.desktop }}","display":"${{ matrix.edition.display }}","arch":"${{ matrix.edition.arch }}"}' | ||
| fi | ||
| echo "$STATUS" > /tmp/status.json | ||
| curl -s -X PUT "https://api.cloudflare.com/client/v4/accounts/$CF_ACCOUNT/r2/buckets/acreetionos-isos/objects/${{ steps.meta.outputs.slug }}-status.json" \ | ||
| -H "Authorization: Bearer $CF_TOKEN" \ | ||
| --data-binary @/tmp/status.json \ | ||
| -H "Content-Type: application/json" || echo "Status update skipped" | ||
| cleanup: | ||
| needs: build | ||
| runs-on: ubuntu-latest | ||
| if: always() | ||
| steps: | ||
| - name: Delete old ISOs | ||
| env: | ||
| CF_ACCOUNT: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} | ||
| CF_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} | ||
| run: | | ||
| echo "Cleaning up old ISOs..." | ||
| OBJECTS=$(curl -s "https://api.cloudflare.com/client/v4/accounts/$CF_ACCOUNT/r2/buckets/acreetionos-isos/objects" \ | ||
| -H "Authorization: Bearer $CF_TOKEN") | ||
| echo "$OBJECTS" | python3 -c " | ||
| import sys, json, os | ||
| from datetime import datetime, timezone | ||
| cutoff = datetime.now(timezone.utc).timestamp() - (30 * 86400) | ||
| data = json.load(sys.stdin) | ||
| objects = data.get('result', {}).get('objects', []) | ||
| for obj in objects: | ||
| key = obj.get('key', '') | ||
| if 'latest' in key or 'status' in key or 'build.log' in key: | ||
| continue # keep latest/status/log files | ||
| uploaded = obj.get('uploaded', '') | ||
| try: | ||
| ts = datetime.fromisoformat(uploaded.replace('Z', '+00:00')).timestamp() | ||
| if ts < cutoff: | ||
| os.system(f'curl -s -X DELETE \"https://api.cloudflare.com/client/v4/accounts/$CF_ACCOUNT/r2/buckets/acreetionos-isos/objects/{key}\" -H \"Authorization: Bearer $CF_TOKEN\" && echo \"Deleted {key}\"') | ||
| except: pass | ||
| " 2>/dev/null || echo "Cleanup skipped" | ||