ci: rolling alpha prerelease on default/develop, v* for tagged releases #4
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 | |
| # Two publication paths share one IPA/deb build: | |
| # push tag v* -> the `release` job: a real, permanent GitHub release | |
| # push to default branch/develop -> the `alpha-release` job: the rolling `alpha` prerelease | |
| # The rolling tag is deliberately NOT named v-anything so a CI-pushed tag can | |
| # never re-enter the release path. | |
| # | |
| # workflow_dispatch exists to re-publish the rolling alpha; pick the default | |
| # branch (or develop) in the UI. A dispatch from any other ref is refused. | |
| on: | |
| push: | |
| branches: [master, develop, feature/icube-testflight] | |
| tags: ['v*'] | |
| pull_request: | |
| workflow_dispatch: | |
| permissions: | |
| contents: read | |
| concurrency: | |
| group: ${{ github.workflow }}-${{ github.ref }} | |
| cancel-in-progress: ${{ github.ref == format('refs/heads/{0}', github.event.repository.default_branch) || github.ref == 'refs/heads/develop' }} | |
| env: | |
| XCODE_COMMON_BUILD_ARGS: -project Source/iOS/App/DolphiniOS.xcodeproj -derivedDataPath "${{ github.workspace }}/build-Xcode" -sdk iphoneos -destination generic/platform=iOS DOL_PBID_ORGANIZATION_IDENTIFIER="me.oatmealdome" CODE_SIGNING_ALLOWED="NO" CODE_SIGNING_REQUIRED="NO" | |
| jobs: | |
| guard: | |
| name: Check dispatch ref | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Refuse a dispatch that would publish nothing | |
| if: ${{ github.event_name == 'workflow_dispatch' && github.ref != format('refs/heads/{0}', github.event.repository.default_branch) && github.ref != 'refs/heads/develop' && !startsWith(github.ref, 'refs/tags/v') }} | |
| run: | | |
| MSG="Run this workflow with the branch set to the default branch or 'develop'." | |
| MSG="$MSG Alpha publishing is ref-gated (default/develop -> alpha," | |
| MSG="$MSG tags v* -> release), so a dispatch from '${GITHUB_REF_NAME}'" | |
| MSG="$MSG would build and publish nothing." | |
| echo "::error::$MSG" | |
| exit 1 | |
| build: | |
| name: Build IPAs | |
| needs: [guard] | |
| runs-on: [self-hosted, macOS] | |
| timeout-minutes: 180 | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| submodules: 'recursive' | |
| fetch-depth: 0 | |
| - name: Set schema for tagged release | |
| if: startsWith(github.ref, 'refs/tags/v') | |
| run: | | |
| echo "DOL_BUILD_SOURCE=official" >> $GITHUB_ENV | |
| echo "XCODE_CONFIGURATION_NJB=Release (Non-Jailbroken)" >> $GITHUB_ENV | |
| echo "XCODE_CONFIGURATION_JB=Release (Jailbroken)" >> $GITHUB_ENV | |
| echo "XCODE_CONFIGURATION_TS=Release (TrollStore)" >> $GITHUB_ENV | |
| echo "DEB_CONTROL_NAME=control-normal" >> $GITHUB_ENV | |
| - name: Set schema for branch / PR / dispatch | |
| if: ${{ !startsWith(github.ref, 'refs/tags/v') }} | |
| run: | | |
| echo "DOL_BUILD_SOURCE=development" >> $GITHUB_ENV | |
| echo "XCODE_CONFIGURATION_NJB=Release (Non-Jailbroken)" >> $GITHUB_ENV | |
| echo "XCODE_CONFIGURATION_JB=Release (Jailbroken)" >> $GITHUB_ENV | |
| echo "XCODE_CONFIGURATION_TS=Release (TrollStore)" >> $GITHUB_ENV | |
| echo "DEB_CONTROL_NAME=control-normal" >> $GITHUB_ENV | |
| - name: Build Application | |
| run: | | |
| rm -rf "${{ github.workspace }}/archives" || true | |
| mkdir "${{ github.workspace }}/archives" | |
| xcodebuild archive -archivePath "${{ github.workspace }}/archives/NonJailbroken.xcarchive" -configuration "${{ env.XCODE_CONFIGURATION_NJB }}" -scheme "DiOS (NJB)" ${{ env.XCODE_COMMON_BUILD_ARGS }} DOL_BUILD_SOURCE="${{ env.DOL_BUILD_SOURCE }}" CURRENT_PROJECT_VERSION=${{ github.run_number }} | |
| xcodebuild archive -archivePath "${{ github.workspace }}/archives/Jailbroken.xcarchive" -configuration "${{ env.XCODE_CONFIGURATION_JB }}" -scheme "DiOS (JB)" ${{ env.XCODE_COMMON_BUILD_ARGS }} DOL_BUILD_SOURCE="${{ env.DOL_BUILD_SOURCE }}" CURRENT_PROJECT_VERSION=${{ github.run_number }} | |
| xcodebuild archive -archivePath "${{ github.workspace }}/archives/TrollStore.xcarchive" -configuration "${{ env.XCODE_CONFIGURATION_TS }}" -scheme "DiOS (JB)" ${{ env.XCODE_COMMON_BUILD_ARGS }} DOL_BUILD_SOURCE="${{ env.DOL_BUILD_SOURCE }}" CURRENT_PROJECT_VERSION=${{ github.run_number }} | |
| - name: Build IPA Files | |
| run: | | |
| mkdir -p "${{ github.workspace }}/products" | |
| "${{ github.workspace }}/Source/iOS/App/Project/Scripts/CreateIpa.sh" "${{ github.workspace }}/archives/NonJailbroken.xcarchive/Products/Applications/DolphiniOS.app" "OatmealDome Software" "${{ github.workspace }}/Source/iOS/App/Project/Entitlements/Public.entitlements" "${{ github.workspace }}/products/Non-Jailbroken.ipa" | |
| "${{ github.workspace }}/Source/iOS/App/Project/Scripts/CreateIpa.sh" "${{ github.workspace }}/archives/TrollStore.xcarchive/Products/Applications/DolphiniOS.app" "OatmealDome Software" "${{ github.workspace }}/Source/iOS/App/Project/Entitlements/Private.entitlements" "${{ github.workspace }}/products/TrollStore.tipa" | |
| - name: Build DEB File | |
| run: | | |
| "${{ github.workspace }}/Source/iOS/App/Project/Scripts/CreateDeb.sh" "${{ github.workspace }}/archives/Jailbroken.xcarchive/Products/Applications/DolphiniOS.app" "OatmealDome Software" "${{ github.workspace }}/Source/iOS/App/Project/Entitlements/Private.entitlements" "${{ github.workspace }}/Source/iOS/App/Project/Package/Deb/${{ env.DEB_CONTROL_NAME }}.in" "${{ github.workspace }}/Source/iOS/App/Project/Package/Deb/postinst.sh" "${{ github.workspace }}/Source/iOS/App/Project/Package/Deb/postrm.sh" "Applications" "${{ github.workspace }}/products/Jailbroken-Rootful.deb" | |
| "${{ github.workspace }}/Source/iOS/App/Project/Scripts/CreateDeb.sh" "${{ github.workspace }}/archives/Jailbroken.xcarchive/Products/Applications/DolphiniOS.app" "OatmealDome Software" "${{ github.workspace }}/Source/iOS/App/Project/Entitlements/Private.entitlements" "${{ github.workspace }}/Source/iOS/App/Project/Package/Deb/${{ env.DEB_CONTROL_NAME }}-rootless.in" "${{ github.workspace }}/Source/iOS/App/Project/Package/Deb/postinst.sh" "${{ github.workspace }}/Source/iOS/App/Project/Package/Deb/postrm.sh" "var/jb/Applications" "${{ github.workspace }}/products/Jailbroken-Rootless.deb" | |
| - name: Upload products | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: icube-ipas | |
| path: products/* | |
| if-no-files-found: error | |
| retention-days: 14 | |
| alpha-release: | |
| name: Update Alpha Release | |
| needs: build | |
| if: | | |
| always() && | |
| needs.build.result == 'success' && | |
| ( | |
| github.event_name == 'workflow_dispatch' || | |
| github.event_name == 'push' | |
| ) && | |
| !startsWith(github.ref, 'refs/tags/') && | |
| ( | |
| github.ref == format('refs/heads/{0}', github.event.repository.default_branch) || | |
| github.ref == 'refs/heads/develop' | |
| ) | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: write | |
| issues: write | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| REPO: ${{ github.repository }} | |
| TAG: alpha | |
| ISSUE_TITLE: 'Alpha builds — download, install, and what they are' | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| - name: Download artifacts | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: icube-ipas | |
| path: products | |
| - name: Prepare alpha body | |
| id: prep_body | |
| run: | | |
| set -euo pipefail | |
| SHA="${GITHUB_SHA}" | |
| SHORT_SHA="${SHA:0:7}" | |
| DATE=$(date -u +"%Y-%m-%d %H:%M UTC") | |
| RUN_URL="${GITHUB_SERVER_URL}/${REPO}/actions/runs/${GITHUB_RUN_ID}" | |
| BODY_FILE="$RUNNER_TEMP/alpha_body.md" | |
| PREV_SHA=$(gh api "repos/${REPO}/git/refs/tags/${TAG}" --jq '.object.sha' 2>/dev/null || echo "") | |
| CHANGES="" | |
| if [ -n "$PREV_SHA" ] && [ "$PREV_SHA" != "$SHA" ] \ | |
| && git cat-file -e "${PREV_SHA}^{commit}" 2>/dev/null; then | |
| CHANGES=$(git log --oneline --no-merges "${PREV_SHA}..${SHA}" | head -40) | |
| fi | |
| [ -n "$CHANGES" ] || CHANGES=$(git log --oneline --no-merges -10) | |
| { | |
| echo "## Alpha Build \`${SHORT_SHA}\`" | |
| echo | |
| echo "**Automated alpha build from \`${GITHUB_REF_NAME}\`**" | |
| echo | |
| echo "| | |" | |
| echo "|---|---|" | |
| echo "| **Commit** | [\`${SHORT_SHA}\`](${GITHUB_SERVER_URL}/${REPO}/commit/${SHA}) |" | |
| echo "| **Built** | ${DATE} |" | |
| echo "| **CI Run** | [View build log](${RUN_URL}) |" | |
| echo | |
| echo "> Unsigned sideload builds. Replaced on every successful push to the default branch or \`develop\`. For a stable build use the [latest release](${GITHUB_SERVER_URL}/${REPO}/releases/latest)." | |
| echo | |
| echo "### Commits since last alpha" | |
| echo | |
| echo '```' | |
| echo "${CHANGES}" | |
| echo '```' | |
| echo | |
| echo "### Artifacts" | |
| echo | |
| echo "- \`Non-Jailbroken.ipa\` — SideStore / AltStore / Sideloadly" | |
| echo "- \`TrollStore.tipa\` — TrollStore" | |
| echo "- \`Jailbroken-Rootful.deb\` / \`Jailbroken-Rootless.deb\` — jailbroken devices" | |
| echo | |
| echo "> This release is automatically updated on every successful default-branch / \`develop\` CI run." | |
| } > "$BODY_FILE" | |
| echo "body_file=$BODY_FILE" >> "$GITHUB_OUTPUT" | |
| echo "short_sha=$SHORT_SHA" >> "$GITHUB_OUTPUT" | |
| - name: Move alpha tag | |
| run: | | |
| set -euo pipefail | |
| # -F (typed) for force so it serialises as a JSON boolean; -f would | |
| # send the string "true", which the refs API rejects with 422. | |
| gh api "repos/${REPO}/git/refs/tags/${TAG}" \ | |
| -X PATCH -f sha="${GITHUB_SHA}" -F force=true >/dev/null 2>&1 || \ | |
| gh api "repos/${REPO}/git/refs" \ | |
| -X POST -f ref="refs/tags/${TAG}" -f sha="${GITHUB_SHA}" >/dev/null | |
| - name: Publish alpha prerelease | |
| run: | | |
| set -euo pipefail | |
| TITLE="iCube Alpha (${{ steps.prep_body.outputs.short_sha }})" | |
| shopt -s nullglob | |
| FILES=(products/*) | |
| if [ ${#FILES[@]} -eq 0 ]; then | |
| echo "::warning::No IPA/deb files to attach" | |
| FILES=() | |
| fi | |
| if gh release view "${TAG}" --repo "${REPO}" >/dev/null 2>&1; then | |
| gh release edit "${TAG}" --repo "${REPO}" \ | |
| --title "${TITLE}" \ | |
| --notes-file "${{ steps.prep_body.outputs.body_file }}" \ | |
| --target "${GITHUB_SHA}" \ | |
| --prerelease --draft=false | |
| if [ ${#FILES[@]} -gt 0 ]; then | |
| gh release upload "${TAG}" --repo "${REPO}" "${FILES[@]}" --clobber | |
| fi | |
| REL_ID=$(gh api "repos/${REPO}/releases/tags/${TAG}" --jq .id) | |
| gh api -X PATCH "repos/${REPO}/releases/${REL_ID}" -F draft=true >/dev/null | |
| gh api -X PATCH "repos/${REPO}/releases/${REL_ID}" \ | |
| -F draft=false -F prerelease=true >/dev/null | |
| else | |
| gh release create "${TAG}" --repo "${REPO}" \ | |
| --title "${TITLE}" \ | |
| --notes-file "${{ steps.prep_body.outputs.body_file }}" \ | |
| --prerelease \ | |
| --target "${GITHUB_SHA}" \ | |
| "${FILES[@]}" | |
| fi | |
| - name: Update pinned tracking issue | |
| continue-on-error: true | |
| run: | | |
| set -euo pipefail | |
| SHORT_SHA="${{ steps.prep_body.outputs.short_sha }}" | |
| DATE=$(date -u +"%Y-%m-%d %H:%M UTC") | |
| RUN_URL="${GITHUB_SERVER_URL}/${REPO}/actions/runs/${GITHUB_RUN_ID}" | |
| BODY_FILE="$RUNNER_TEMP/issue_body.md" | |
| { | |
| echo "# Alpha Builds — Download & Install Guide" | |
| echo | |
| echo "## Latest Alpha Build" | |
| echo | |
| echo "| | |" | |
| echo "|---|---|" | |
| echo "| **Status** | :white_check_mark: Build Passing |" | |
| echo "| **Commit** | [\`${SHORT_SHA}\`](${GITHUB_SERVER_URL}/${REPO}/commit/${GITHUB_SHA}) |" | |
| echo "| **Built** | ${DATE} |" | |
| echo "| **CI Run** | [View build log](${RUN_URL}) |" | |
| echo "| **Download** | [\`alpha\` release](${GITHUB_SERVER_URL}/${REPO}/releases/tag/alpha) |" | |
| echo | |
| echo "---" | |
| echo | |
| echo "### What these are" | |
| echo | |
| echo "Every successful push to the **default branch** or \`develop\` rebuilds unsigned IPAs/debs and replaces the single [\`alpha\`](${GITHUB_SERVER_URL}/${REPO}/releases/tag/alpha) prerelease. There is only ever one alpha — the current tip." | |
| echo | |
| echo "| | Stable | Alpha |" | |
| echo "|---|---|---|" | |
| echo "| **Source** | tagged \`v*\` releases | automatic builds from the default branch / \`develop\` |" | |
| echo "| **Updates** | periodic | every successful CI run |" | |
| echo "| **Use it for** | daily play | testing a fix before it ships |" | |
| echo | |
| echo "### Installing" | |
| echo | |
| echo "1. Download from the [\`alpha\` release](${GITHUB_SERVER_URL}/${REPO}/releases/tag/alpha):" | |
| echo " - **SideStore / AltStore / Sideloadly:** \`Non-Jailbroken.ipa\`" | |
| echo " - **TrollStore:** \`TrollStore.tipa\`" | |
| echo " - **Jailbroken:** \`Jailbroken-Rootful.deb\` or \`Jailbroken-Rootless.deb\`" | |
| echo "2. Sideload with your preferred installer. Website: [icube-emu.com/downloads](https://icube-emu.com/downloads/)." | |
| echo | |
| echo "### Reporting a bug in an alpha" | |
| echo | |
| echo "Please [open an issue](${GITHUB_SERVER_URL}/${REPO}/issues/new) and include:" | |
| echo "- The commit SHA (\`${SHORT_SHA}\` or newer — Settings > About if shown)" | |
| echo "- Steps to reproduce" | |
| echo "- Device model and iOS/tvOS version" | |
| echo | |
| echo "---" | |
| echo | |
| echo "> Auto-updated by CI on every alpha build. Edits here are overwritten." | |
| } > "$BODY_FILE" | |
| LABEL=alpha-tracker | |
| gh label create "$LABEL" --repo "${REPO}" --color 0E8A16 \ | |
| --description "Auto-updated alpha build tracker" >/dev/null 2>&1 || true | |
| NUM=$(gh issue list --repo "${REPO}" --state all --label "$LABEL" \ | |
| --limit 1 --json number --jq '.[0].number // empty' 2>/dev/null || echo "") | |
| if [ -z "$NUM" ]; then | |
| NUM=$(gh issue list --repo "${REPO}" --state all --limit 500 \ | |
| --json number,title \ | |
| --jq "map(select(.title == \"${ISSUE_TITLE}\")) | .[0].number // empty") | |
| fi | |
| if [ -n "$NUM" ]; then | |
| gh issue edit "$NUM" --repo "${REPO}" --body-file "$BODY_FILE" \ | |
| --add-label "$LABEL" >/dev/null | |
| else | |
| NUM=$(gh issue create --repo "${REPO}" \ | |
| --title "${ISSUE_TITLE}" \ | |
| --body-file "$BODY_FILE" \ | |
| | grep -oE '[0-9]+$') | |
| gh issue edit "$NUM" --repo "${REPO}" --add-label "$LABEL" >/dev/null 2>&1 || \ | |
| echo "::warning::could not label tracking issue #${NUM}; title match is now the only lookup" | |
| echo "==> Created tracking issue #${NUM}" | |
| fi | |
| ID=$(gh api "repos/${REPO}/issues/${NUM}" --jq '.node_id') | |
| gh api graphql -f query='mutation($id:ID!){pinIssue(input:{issueId:$id}){issue{number}}}' \ | |
| -f id="$ID" >/dev/null 2>&1 || \ | |
| echo "::warning::could not pin issue #${NUM} (already pinned, 3-pin cap, or permission)" | |
| echo "==> Tracking issue: ${GITHUB_SERVER_URL}/${REPO}/issues/${NUM}" | |
| release: | |
| name: Publish tagged release | |
| needs: build | |
| if: | | |
| always() && | |
| needs.build.result == 'success' && | |
| startsWith(github.ref, 'refs/tags/v') | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: write | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| REPO: ${{ github.repository }} | |
| steps: | |
| - name: Download artifacts | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: icube-ipas | |
| path: products | |
| - name: Publish GitHub release | |
| run: | | |
| set -euo pipefail | |
| TAG="${GITHUB_REF_NAME}" | |
| SHORT_SHA="${GITHUB_SHA:0:7}" | |
| TITLE="iCube ${TAG} (${SHORT_SHA})" | |
| shopt -s nullglob | |
| FILES=(products/*) | |
| if [ ${#FILES[@]} -eq 0 ]; then | |
| echo "::error::No IPA/deb files to attach" | |
| exit 1 | |
| fi | |
| if gh release view "$TAG" --repo "$REPO" >/dev/null 2>&1; then | |
| gh release upload "$TAG" --repo "$REPO" "${FILES[@]}" --clobber | |
| else | |
| gh release create "$TAG" --repo "$REPO" \ | |
| --title "$TITLE" \ | |
| --generate-notes \ | |
| --target "${GITHUB_SHA}" \ | |
| "${FILES[@]}" | |
| fi |