sign-sideinstaller #154
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: sign-sideinstaller | |
| # Re-signs the SideInstaller IPA with every certificate in the cert pool and | |
| # publishes a tap-to-install page. The IPA source is whatever URL you put in | |
| # ipa-url.txt (editable any time) — or a one-off override passed below. | |
| # | |
| # The cert pool is the merge of every source in cert-url.txt. That list starts | |
| # with the repo's own certs/ folder, so dropping a certificate in there (see | |
| # certs/README.md for the layout) adds it to the pool and triggers a run. | |
| # | |
| # Every run starts with a precheck (scripts/check_for_changes.sh) that compares | |
| # the certificate pool, the unsigned IPA and the page template against the state | |
| # the last run recorded in <output>/sign-state.tsv. If a re-run would rebuild | |
| # the same page, the job stops there: nothing is signed, nothing is committed | |
| # and the published page is left as it is. Tick "force" to override that. | |
| on: | |
| workflow_dispatch: | |
| inputs: | |
| channel: | |
| description: "Build channel: stable -> index.html (public), beta -> beta.html (test)" | |
| required: true | |
| type: choice | |
| default: stable | |
| options: | |
| - stable | |
| - beta | |
| ipa_url: | |
| description: "Override unsigned IPA URL (leave blank to use ipa-url.txt)" | |
| required: false | |
| default: "" | |
| cert_zip_url: | |
| description: "Override cert pool source(s): space-separated .zip URLs, github.com/owner/repo URLs and/or repo folder paths (e.g. certs), merged and de-duped (blank = use cert-url.txt)" | |
| required: false | |
| default: "" | |
| force: | |
| description: "Re-sign even when nothing has changed (skips the no-change check)" | |
| required: false | |
| type: boolean | |
| default: false | |
| push: | |
| paths: | |
| - "ipa-url.txt" # re-run automatically when you point at a new IPA | |
| - "certs/**" # ...and when you upload/remove your own certificates | |
| schedule: | |
| - cron: "0 23 * * 0" # weekly refresh (Sunday) so the page tracks cert expiry | |
| concurrency: | |
| group: sign-sideinstaller | |
| cancel-in-progress: false | |
| permissions: | |
| contents: write # commit generated files back to the repo | |
| pages: write # publish the install page to GitHub Pages | |
| id-token: write # required by actions/deploy-pages | |
| jobs: | |
| sign: | |
| runs-on: macos-latest | |
| outputs: | |
| # "true" when the precheck found nothing to rebuild, which also holds the | |
| # Pages deploy below so the live page is left exactly as it is. | |
| skipped: ${{ steps.precheck.outputs.skip }} | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v7 | |
| with: | |
| fetch-depth: 0 | |
| # Tag/release triggers check out a detached HEAD, which can't be pushed | |
| # to. Force a real branch (the triggering branch, or the default branch | |
| # when a tag fired the run) so the commit step has somewhere to push. | |
| ref: ${{ github.ref_type == 'tag' && (github.event.repository.default_branch || 'main') || github.ref_name }} | |
| - name: Make scripts executable | |
| run: chmod +x scripts/sign_with_all_certs.sh scripts/generate_plist.sh scripts/generate_index.sh scripts/check_for_changes.sh | |
| # Resolve the channel once and export the paths every later step + script | |
| # reads from env. Beta gets its own output dir and page so a test build is | |
| # never mixed into the public index.html / output/ artifacts. Automatic | |
| # triggers (push, schedule) have no channel input and fall back to stable. | |
| - name: Resolve release channel | |
| shell: bash | |
| env: | |
| CHANNEL_INPUT: ${{ github.event.inputs.channel }} | |
| DEFAULT_BRANCH: ${{ github.event.repository.default_branch }} | |
| run: | | |
| channel="${CHANNEL_INPUT:-stable}" | |
| # Tag/release triggers run with GITHUB_REF_NAME set to the tag and a | |
| # detached HEAD. Commit to — and build manifest URLs against — the | |
| # default branch in that case, so links point at the branch that | |
| # actually receives the new files instead of an immutable tag snapshot. | |
| if [[ "${GITHUB_REF_TYPE}" == "tag" ]]; then | |
| target_branch="${DEFAULT_BRANCH:-main}" | |
| else | |
| target_branch="${GITHUB_REF_NAME}" | |
| fi | |
| base="https://raw.githubusercontent.com/${GITHUB_REPOSITORY}/${target_branch}" | |
| { | |
| echo "TARGET_BRANCH=$target_branch" | |
| echo "CHANNEL=$channel" | |
| if [[ "$channel" == "beta" ]]; then | |
| echo "OUTPUT_HTML=beta.html" | |
| echo "OUTPUT_DIR=output-beta" | |
| echo "OUTPUT_BASE_URL=$base/output-beta" | |
| echo "PAGE_TITLE=SideInstaller — Beta" | |
| echo "APP_TAGLINE=Beta test build, not the public release. Follow the three steps below to get set up." | |
| else | |
| echo "OUTPUT_HTML=index.html" | |
| echo "OUTPUT_DIR=output" | |
| echo "OUTPUT_BASE_URL=$base/output" | |
| fi | |
| } >> "$GITHUB_ENV" | |
| echo "Resolved channel: $channel (target branch: $target_branch)" | |
| # Cheap gate in front of the expensive job: resolve the certificate pool | |
| # and the IPA, and compare them with what the last run published. When | |
| # nothing that feeds the page has moved, every step below is skipped and | |
| # the committed index.html — and the live site — stay untouched. | |
| - name: Check whether anything changed | |
| id: precheck | |
| env: | |
| UNSIGNED_IPA_URL: ${{ github.event.inputs.ipa_url }} | |
| CERT_ZIP_URL: ${{ github.event.inputs.cert_zip_url }} | |
| FORCE_RUN: ${{ github.event.inputs.force }} | |
| GITHUB_TOKEN: ${{ github.token }} | |
| run: ./scripts/check_for_changes.sh | |
| - name: Sign IPA with all certificates | |
| if: steps.precheck.outputs.skip != 'true' | |
| env: | |
| # Dispatch overrides win; otherwise the scripts fall back to the | |
| # ipa-url.txt / cert-url.txt files in the repo, then the latest release. | |
| UNSIGNED_IPA_URL: ${{ github.event.inputs.ipa_url }} | |
| CERT_ZIP_URL: ${{ github.event.inputs.cert_zip_url }} | |
| # Authenticates the Releases API lookup for the unsigned IPA. | |
| GITHUB_TOKEN: ${{ github.token }} | |
| run: ./scripts/sign_with_all_certs.sh | |
| - name: Ensure signed IPAs exist | |
| if: steps.precheck.outputs.skip != 'true' | |
| shell: bash | |
| run: | | |
| shopt -s nullglob | |
| signed=("${OUTPUT_DIR}"/"${OUTPUT_PREFIX:-sideinstaller}"-*.ipa) | |
| shopt -u nullglob | |
| if [[ ${#signed[@]} -eq 0 ]]; then | |
| echo "No signed IPAs were produced in ${OUTPUT_DIR}/." >&2 | |
| exit 1 | |
| fi | |
| printf '%s\n' "${signed[@]}" | |
| - name: Generate OTA manifests | |
| if: steps.precheck.outputs.skip != 'true' | |
| run: ./scripts/generate_plist.sh | |
| - name: Generate install page | |
| if: steps.precheck.outputs.skip != 'true' | |
| env: | |
| # Points the page's "Download IPA" button at the overridden build when | |
| # this run was dispatched with one; blank leaves it on the latest release. | |
| UNSIGNED_IPA_URL: ${{ github.event.inputs.ipa_url }} | |
| run: ./scripts/generate_index.sh | |
| # Commit the state the precheck computed, not a fresh one, so next week's | |
| # comparison is against exactly the inputs this page was built from. | |
| - name: Record signing state | |
| if: steps.precheck.outputs.skip != 'true' | |
| shell: bash | |
| run: | | |
| # The precheck leaves the state it computed at $NEW_STATE_FILE. If it | |
| # could not compute one (pool inspection failed, and the run went ahead | |
| # anyway), drop the recorded state rather than keep a stale one — the | |
| # next run then rebuilds from scratch instead of trusting it. | |
| if [[ -f "${NEW_STATE_FILE:-}" ]]; then | |
| cp "$NEW_STATE_FILE" "$OUTPUT_DIR/sign-state.tsv" | |
| else | |
| echo "No precheck state to record; clearing the previous one." >&2 | |
| rm -f "$OUTPUT_DIR/sign-state.tsv" | |
| fi | |
| - name: Commit and push generated files | |
| if: steps.precheck.outputs.skip != 'true' | |
| run: | | |
| git config user.name "github-actions[bot]" | |
| git config user.email "github-actions[bot]@users.noreply.github.com" | |
| git add -A "$OUTPUT_DIR" "$OUTPUT_HTML" | |
| if git diff --cached --quiet; then | |
| echo "Nothing to commit" | |
| exit 0 | |
| fi | |
| git commit -m "sign-sideinstaller ($CHANNEL): refresh signed IPAs, manifests, and install page" | |
| for attempt in 1 2 3; do | |
| git pull --rebase origin "${TARGET_BRANCH}" || true | |
| if git push origin "HEAD:${TARGET_BRANCH}"; then exit 0; fi | |
| echo "Push failed; retrying (attempt $attempt)" >&2 | |
| sleep 5 | |
| done | |
| echo "Failed to push after 3 attempts" >&2 | |
| exit 1 | |
| - name: Assemble Pages site | |
| if: steps.precheck.outputs.skip != 'true' | |
| run: | | |
| rm -rf _site | |
| mkdir -p _site | |
| # Publish whichever channel pages exist so a beta deploy never drops the | |
| # public index.html (and vice versa). Install links inside each page point | |
| # at committed raw-githubusercontent manifests, so the pages work | |
| # standalone; Pages just needs the HTML itself. | |
| if [[ -f index.html ]]; then cp index.html _site/index.html; fi | |
| if [[ -f beta.html ]]; then cp beta.html _site/beta.html; fi | |
| # DNS profile linked from the page's Setup step — must ship with the site. | |
| if [[ -f SideInstallerDNS.mobileconfig ]]; then cp SideInstallerDNS.mobileconfig _site/SideInstallerDNS.mobileconfig; fi | |
| # Terms of Service linked from the page footer. | |
| if [[ -f terms.html ]]; then cp terms.html _site/terms.html; fi | |
| - name: Upload Pages artifact | |
| if: steps.precheck.outputs.skip != 'true' | |
| uses: actions/upload-pages-artifact@v5 | |
| with: | |
| path: _site | |
| # Publishes the install page. Requires Settings -> Pages -> Source = "GitHub Actions" | |
| # (one-time). This job is what shows up in the Actions tab as the Pages deploy. | |
| # It is skipped entirely when the precheck found nothing to rebuild, which | |
| # leaves the current deployment serving as-is. | |
| deploy-pages: | |
| needs: sign | |
| if: needs.sign.outputs.skipped != 'true' | |
| runs-on: ubuntu-latest | |
| environment: | |
| name: github-pages | |
| url: ${{ steps.deployment.outputs.page_url }} | |
| steps: | |
| - name: Deploy to GitHub Pages | |
| id: deployment | |
| uses: actions/deploy-pages@v5 |