Skip to content

Scan IPSW

Scan IPSW #235

Workflow file for this run

name: Scan IPSW
on:
workflow_dispatch:
inputs:
ipsw_url:
description: "IPSW download URL"
required: true
type: string
release_date:
description: "Release date (ISO 8601, e.g. 2025-04-02)"
required: true
type: string
beta:
description: "Is this a beta release?"
required: false
type: boolean
default: false
beta_number:
description: "Beta number (e.g. 3)"
required: false
type: string
rc:
description: "Is this a Release Candidate?"
required: false
type: boolean
default: false
rc_number:
description: "RC number (e.g. 2). Omit for just RC"
required: false
type: string
device_specific:
description: "Device-specific build (e.g. M3 launch build)"
required: false
type: boolean
default: false
defaults:
run:
shell: bash -xeuo pipefail {0}
concurrency:
group: scan
cancel-in-progress: false
permissions: {}
jobs:
scan:
name: Scan
runs-on: scanner
environment:
name: starhaven
deployment: false
permissions:
contents: read
steps:
- name: Add Homebrew to PATH
run: echo "/opt/homebrew/bin" >> "${GITHUB_PATH}"
- name: Mint bot token
id: app-token
uses: actions/create-github-app-token@f8d387b68d61c58ab83c6c016672934102569859 # v3.0.0
with:
app-id: ${{ secrets.APP_ID }}
private-key: ${{ secrets.APP_PRIVATE_KEY }}
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
fetch-depth: 0
- name: Clean workspace
run: git clean -fdx data/macos/releases/
- name: Resolve IPSW path
env:
IPSW_URL: ${{ inputs.ipsw_url }}
run: |
ORIG_NAME=$(basename "${IPSW_URL%%\?*}")
VERSION=$(echo "${ORIG_NAME}" | sed -E 's/UniversalMac_([0-9.]+)_.*/\1/')
BUILD=$(echo "${ORIG_NAME}" | sed -E 's/UniversalMac_[0-9.]+_([^_]+)_.*/\1/')
MAJOR=${VERSION%%.*}
IPSW_DIR="/Volumes/macOS-Archive/macOS/${MAJOR}"
mkdir -p "${IPSW_DIR}"
IPSW_FILE="${IPSW_DIR}/macOS-${VERSION}-${BUILD}.ipsw"
echo "IPSW_FILE=${IPSW_FILE}" >> "${GITHUB_ENV}"
- name: Download IPSW
env:
IPSW_URL: ${{ inputs.ipsw_url }}
run: |
set +x
if [[ -f "${IPSW_FILE}" ]]; then
echo "IPSW already cached at ${IPSW_FILE}, skipping download"
else
TOTAL=$(curl -sSLI --retry 5 --retry-connrefused "${IPSW_URL}" | grep -i content-length | tail -1 | tr -dc '0-9')
TOTAL_MB=$(( TOTAL / 1048576 ))
TOTAL_GB=$(( TOTAL_MB * 10 / 1024 ))
echo "Downloading IPSW ($(( TOTAL_GB / 10 )).$(( TOTAL_GB % 10 )) GB)..."
curl -sSL --retry 10 --retry-delay 10 --retry-connrefused --retry-all-errors -C - -o "${IPSW_FILE}.part" "${IPSW_URL}" &
CURL_PID=$!
while kill -0 "${CURL_PID}" 2>/dev/null; do
if [[ -f "${IPSW_FILE}.part" ]]; then
SIZE=$(stat -f%z "${IPSW_FILE}.part" 2>/dev/null || echo 0)
SIZE_MB=$(( SIZE / 1048576 ))
PCT=$(( SIZE_MB * 100 / TOTAL_MB ))
echo "${PCT}% (${SIZE_MB} MB / ${TOTAL_MB} MB)"
fi
sleep 30
done
wait "${CURL_PID}"
mv "${IPSW_FILE}.part" "${IPSW_FILE}"
fi
- name: Set IPSW modification time to release date
env:
RELEASE_DATE: ${{ inputs.release_date }}
run: |
TOUCH_DATE=$(echo "${RELEASE_DATE}" | tr -d '-')0000
touch -t "${TOUCH_DATE}" "${IPSW_FILE}"
- name: Restore CLI build cache
id: cache-cli
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
with:
path: .build/release/macosdb
key: macosdb-cli-${{ hashFiles('Sources/**/*.swift', 'Package.swift', 'Package.resolved') }}
- name: Build CLI
if: steps.cache-cli.outputs.cache-hit != 'true'
run: swift build -c release --product macosdb
- name: Save CLI build cache
if: steps.cache-cli.outputs.cache-hit != 'true'
uses: actions/cache/save@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
with:
path: .build/release/macosdb
key: macosdb-cli-${{ hashFiles('Sources/**/*.swift', 'Package.swift', 'Package.resolved') }}
- name: Sync working tree to latest main
run: |
git fetch origin main
git reset --hard origin/main
echo "BASE_SHA=$(git rev-parse HEAD)" >> "${GITHUB_ENV}"
- name: Scan IPSW
env:
IPSW_URL: ${{ inputs.ipsw_url }}
RELEASE_DATE: ${{ inputs.release_date }}
IS_BETA: ${{ inputs.beta }}
BETA_NUMBER: ${{ inputs.beta_number }}
IS_RC: ${{ inputs.rc }}
RC_NUMBER: ${{ inputs.rc_number }}
IS_DEVICE_SPECIFIC: ${{ inputs.device_specific }}
run: |
SCAN_ARGS=(
"${IPSW_FILE}"
--output data/macos/releases
--release-date "${RELEASE_DATE}"
--update-index
--verbose
--save-aea-key
--ipsw-url "${IPSW_URL}"
)
if [[ "${IS_BETA}" == "true" ]]; then
SCAN_ARGS+=(--beta)
fi
if [[ -n "${BETA_NUMBER}" ]]; then
SCAN_ARGS+=(--beta-number "${BETA_NUMBER}")
fi
if [[ "${IS_RC}" == "true" ]]; then
SCAN_ARGS+=(--rc)
fi
if [[ -n "${RC_NUMBER}" ]]; then
SCAN_ARGS+=(--rc-number "${RC_NUMBER}")
fi
if [[ "${IS_DEVICE_SPECIFIC}" == "true" ]]; then
SCAN_ARGS+=(--device-specific)
fi
.build/release/macosdb scan "${SCAN_ARGS[@]}"
- name: Generate SHA-256 sidecar
run: .build/release/macosdb validate "${IPSW_FILE}"
- name: Create signed commit and open PR
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
APP_SLUG: ${{ steps.app-token.outputs.app-slug }}
REPOSITORY: ${{ github.repository }}
IPSW_URL: ${{ inputs.ipsw_url }}
RELEASE_DATE: ${{ inputs.release_date }}
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
run: |
BOT_USER="${APP_SLUG}[bot]"
BOT_ID=$(gh api "/users/${BOT_USER}" --jq .id)
NEW_RELEASE_FILE=$(git ls-files --others --exclude-standard 'data/macos/releases/')
BASENAME=$(basename "${NEW_RELEASE_FILE}" .json)
BRANCH="feat/data-${BASENAME}"
TITLE="feat(data): add ${BASENAME}"
printf -v PR_BODY '## Summary\n- Scanned from IPSW: `%s`\n- Release date: %s\n\nAuto-generated by the [scan workflow](%s).' \
"${IPSW_URL}" "${RELEASE_DATE}" "${RUN_URL}"
COMMIT_BODY="Signed-off-by: ${BOT_USER} <${BOT_ID}+${BOT_USER}@users.noreply.github.com>"
# Stage untracked files as intent-to-add so git diff sees them.
git add -N data/
ADDITIONS=$(
git diff --name-only HEAD -- data/ |
while IFS= read -r file; do
CONTENT=$(base64 < "$file" | tr -d '\n')
jq -n --arg path "$file" --arg contents "$CONTENT" \
'{path: $path, contents: $contents}'
done | jq -s .
)
gh api "repos/${REPOSITORY}/git/refs" -X POST \
-f "ref=refs/heads/${BRANCH}" \
-f "sha=${BASE_SHA}"
jq -n \
--arg repo "${REPOSITORY}" \
--arg branch "${BRANCH}" \
--arg title "${TITLE}" \
--arg body "${COMMIT_BODY}" \
--arg head "${BASE_SHA}" \
--argjson additions "${ADDITIONS}" \
'{
query: "mutation($input: CreateCommitOnBranchInput!) { createCommitOnBranch(input: $input) { commit { oid url } } }",
variables: {
input: {
branch: { repositoryNameWithOwner: $repo, branchName: $branch },
message: { headline: $title, body: $body },
fileChanges: { additions: $additions },
expectedHeadOid: $head
}
}
}' | gh api graphql --input -
gh pr create \
--base main \
--head "${BRANCH}" \
--title "${TITLE}" \
--body "${PR_BODY}"
echo "SCAN_BRANCH=${BRANCH}" >> "${GITHUB_ENV}"
- name: Auto-merge pull request
timeout-minutes: 30
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
run: |
gh pr merge "${SCAN_BRANCH}" --squash --auto --delete-branch
echo "Waiting for PR to merge..."
while true; do
STATE=$(gh pr view "${SCAN_BRANCH}" --json state -q .state)
if [[ "${STATE}" == "MERGED" ]]; then
echo "PR merged successfully"
break
elif [[ "${STATE}" == "CLOSED" ]]; then
echo "::error::PR was closed without merging"
exit 1
fi
sleep 30
done