Scan IPSW #237
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: 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). Defaults to today." | |
| required: false | |
| 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@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1 | |
| with: | |
| client-id: ${{ vars.APP_CLIENT_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 release date | |
| env: | |
| INPUT_DATE: ${{ inputs.release_date }} | |
| run: | | |
| if [[ -z "${INPUT_DATE}" ]]; then | |
| RELEASE_DATE=$(date -u +%Y-%m-%d) | |
| echo "No release_date provided; defaulting to today (${RELEASE_DATE})" | |
| else | |
| if ! [[ "${INPUT_DATE}" =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}$ ]]; then | |
| echo "::error::release_date '${INPUT_DATE}' is not ISO 8601 (YYYY-MM-DD)" | |
| exit 1 | |
| fi | |
| TODAY=$(date -u +%Y-%m-%d) | |
| if [[ "${INPUT_DATE}" > "${TODAY}" ]]; then | |
| echo "::error::release_date '${INPUT_DATE}' is in the future (today is ${TODAY})" | |
| exit 1 | |
| fi | |
| RELEASE_DATE="${INPUT_DATE}" | |
| fi | |
| echo "RELEASE_DATE=${RELEASE_DATE}" >> "${GITHUB_ENV}" | |
| - 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 | |
| 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@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 | |
| 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@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 | |
| 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 }} | |
| 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: Lock archive files | |
| run: | | |
| touch -r "${IPSW_FILE}" "${IPSW_FILE}.sha256" | |
| for f in "${IPSW_FILE}" "${IPSW_FILE}.pem" "${IPSW_FILE}.sha256"; do | |
| [[ -f "$f" ]] && chflags uchg "$f" | |
| done | |
| - 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 }} | |
| RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} | |
| TRIGGERING_ACTOR: ${{ github.triggering_actor }} | |
| 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>" | |
| if [[ -n "${TRIGGERING_ACTOR}" ]]; then | |
| ACTOR_NAME=$(gh api "/users/${TRIGGERING_ACTOR}" --jq '.name // .login') | |
| ACTOR_EMAIL=$(gh api "/users/${TRIGGERING_ACTOR}" --jq '.email // "\(.id)+\(.login)@users.noreply.github.com"') | |
| COAUTHOR="Co-authored-by: ${ACTOR_NAME} <${ACTOR_EMAIL}>" | |
| COMMIT_BODY="${COMMIT_BODY}"$'\n'"${COAUTHOR}" | |
| fi | |
| # 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}" | |
| { | |
| echo "COMMIT_BODY<<EOF" | |
| echo "${COMMIT_BODY}" | |
| echo "EOF" | |
| } >> "${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 --body "${COMMIT_BODY}" | |
| 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 |