Scan IPSW #228
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)" | |
| 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: scan | |
| deployment: false | |
| permissions: | |
| contents: write # required to push branches | |
| pull-requests: write # required to create PRs | |
| steps: | |
| - name: Add Homebrew to PATH | |
| run: echo "/opt/homebrew/bin" >> "${GITHUB_PATH}" | |
| - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| with: | |
| persist-credentials: false | |
| fetch-depth: 0 | |
| - name: Clean workspace | |
| run: git clean -fdx data/releases/ | |
| - name: Set up commit signing | |
| env: | |
| SSH_SIGNING_KEY: ${{ secrets.SSH_SIGNING_KEY }} | |
| run: | | |
| git config user.name "fuyunohoshi" | |
| git config user.email "165941061+fuyunohoshi@users.noreply.github.com" | |
| eval $(ssh-agent) | |
| echo "SSH_AUTH_SOCK=${SSH_AUTH_SOCK}" >> "${GITHUB_ENV}" | |
| echo "SSH_AGENT_PID=${SSH_AGENT_PID}" >> "${GITHUB_ENV}" | |
| pubkey=$(ssh-keygen -y -f /dev/stdin <<< "${SSH_SIGNING_KEY}") | |
| ssh-add -q - <<< "${SSH_SIGNING_KEY}" | |
| allowed_signer="$(git config get user.email) ${pubkey}" | |
| mkdir -p ~/.ssh | |
| echo "${allowed_signer}" >> ~/.ssh/allowed_signers | |
| git config gpg.format ssh | |
| git config commit.gpgsign true | |
| git config user.signingkey "${pubkey}" | |
| git config gpg.ssh.allowedSignersFile ~/.ssh/allowed_signers | |
| - name: Set up authenticated remote | |
| env: | |
| BOT_TOKEN: ${{ secrets.BOT_TOKEN }} | |
| REPOSITORY: ${{ github.repository }} | |
| run: git remote set-url origin "https://x-access-token:${BOT_TOKEN}@github.com/${REPOSITORY}.git" | |
| - 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: Create scan branch from latest main | |
| run: | | |
| git fetch origin main | |
| git checkout -f -B scan-wip origin/main | |
| - 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/releases | |
| --release-date "${RELEASE_DATE}" | |
| --update-index | |
| --verbose | |
| --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: Commit and push | |
| run: | | |
| NEW_FILE=$(git ls-files --others --exclude-standard 'data/releases/') | |
| BASENAME=$(basename "${NEW_FILE}" .json) | |
| BRANCH="feat/data-${BASENAME}" | |
| git branch -m "${BRANCH}" | |
| git add data/ | |
| git commit -m "feat(data): add ${BASENAME}" | |
| git push -u origin "${BRANCH}" | |
| echo "SCAN_BRANCH=${BRANCH}" >> "${GITHUB_ENV}" | |
| echo "SCAN_BASENAME=${BASENAME}" >> "${GITHUB_ENV}" | |
| - name: Create pull request | |
| env: | |
| GH_TOKEN: ${{ secrets.BOT_TOKEN }} | |
| IPSW_URL: ${{ inputs.ipsw_url }} | |
| RELEASE_DATE: ${{ inputs.release_date }} | |
| RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} | |
| run: | | |
| printf -v 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}" | |
| gh pr create \ | |
| --title "feat(data): add ${SCAN_BASENAME}" \ | |
| --body "${BODY}" | |
| - name: Auto-merge pull request | |
| timeout-minutes: 30 | |
| env: | |
| GH_TOKEN: ${{ secrets.BOT_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 |