Skip to content

sign-sideinstaller #154

sign-sideinstaller

sign-sideinstaller #154

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