Repository navigation
CC-169: capture iOS App Store screenshots on a runner and upload them to App Store Connect - #271
jaywedgeworth22 wants to merge 22 commits into
Conversation
This comment has been minimized.
This comment has been minimized.
🤔 Insufficient Task ContextI found a task linked to this PR, but it only contains minimal information (title only, no description or acceptance criteria). To perform a meaningful business rules validation, I need more details. 🔍 What I need to validate:
💡 How to improve the task context:
|
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
06a613d to
67ee26c
Compare
This comment has been minimized.
This comment has been minimized.
… to App Store Connect There is no iOS simulator runtime on Jay's Mac, so the App Store screenshots for the CodeCaps iOS companion have to come from a GitHub runner. The existing swift-ci companion job already builds the app and seeds a quota snapshot, but it captures at whatever size the runner's first iPhone happens to be and never asserts the result, so its images are not usable as App Store assets. This adds a dedicated capture script and workflow. The script builds the companion for one iPhone and one iPad simulator, seeds each with a realistic multi-provider quota snapshot, captures the quota list, the settings sheet and the platform-order editor, and asserts every PNG is exactly the pixel size App Store Connect accepts before the file is handed on. A screenshot with an alpha channel is flattened to JPEG, because Apple rejects transparency. The upload to App Store Connect is a separate, explicit step rather than part of the workflow, so a re-run can never quietly add duplicate screenshots to a live version. The new asc-api.mjs subcommand creates or reuses the screenshot set for a display size, reserves each screenshot, sends the bytes to the pre-signed URL Apple returns, and then commits the reservation and reports the asset delivery state. The display type is the App Store Connect enum: APP_IPHONE_67 carries the 6.9-inch iPhone size and APP_IPAD_PRO_129 the 13-inch iPad size, both verified against the live API. Refs CC-169
…s one card The seeded windows are the screens App Store reviewers will see, so each provider has to land on a single clean card. Antigravity is dropped: the app pools its windows into per-model sections rather than one platform card, so it would have read as "Antigravity · Third-Party" in the listing screenshot. Cursor's two allowances become one 1-month window, because a second window of the same cadence is folded in as a duplicate source instead of getting its own row, and the window tokens are now only the ones QuotaWindowVocabulary recognises, so every card shows a clean cadence caption. Refs CC-169
Four defects, all of which would have wasted a capture run: - The first screenshot was taken before the app was ever launched, so it recorded the simulator home screen. The app is now launched before every capture, with the launch argument that opens the screen being shot. - assert_screenshot logged to stdout, and its stdout is what the caller captures to learn the accepted file path, so the log line became part of the path and the following mv failed. log() now writes to stderr. - The expected pixel size was hardcoded to 1320x2868 and 2064x2752 while the picker could fall back to a device that renders at another size, so the assertion would fail on a runner without the first choice. The size is now measured from the capture, every file has to agree on it, and it has to be a size App Store Connect accepts for its display class. The output files are named for the class they actually are. - The upload step could be given the wrong display type by hand, and there is no APP_IPHONE_69 or APP_IPAD_PRO_13 in the enum to be caught by. The upload now takes "auto" and reads the pixel size out of the PNG to pick the set: 1320x2868 and 1290x2796 go to APP_IPHONE_67, 2064x2752 to APP_IPAD_PRO_129, 2048x2732 to APP_IPAD_PRO_3GEN_129. Also stops the workflow cancelling a main-branch run for a commit that nothing replaced, which reported cancelled checks to anything polling the workflow API. Refs CC-169
The capture script took a device family argument and never used it, so a simulator that rendered at the other family's size would have been captured, named and uploaded into the wrong screenshot set without a word. The display class is already derived from the measured pixels, so the check is a comparison of two things that are both already known. It fails the run instead of quietly producing a mismatched set.
Xcode names some iPads with their storage variant, so "iPad Pro 13-inch (M5)" never matches a device called "iPad Pro 13-inch (M5) (16GB)". On a runner that only has the current runtime there is no other iPad to fall back to, so the capture would have found no iPad at all and failed after an hour of runner time, having already spent the time. The picker moves into its own module so it can be tested, and it matches on a prefix so the storage variant does not hide the device. The tests cover the Xcode 27 names, the preference order, and a device that is not available.
The checked-in CodeCapsCompanion.xcodeproj is stale. It is missing three shared QuotaCore files the companion compiles against, so a build from it fails to compile. The capture only regenerated when project.yml was newer than the project file, and a checkout gives both the same mtime, so the comparison came out false and the script built the stale project. xcodegen takes a second, so the project is now always regenerated from project.yml.
The upload read the pixel size of the first file and sent every other file to that file's screenshot set, so an iPhone and iPad capture in one call would have filed the iPad images into the iPhone set. Every file's size is read now and the call stops if they do not all belong to the same set, naming the set they do belong to. The check runs before the first reservation, so a wrong call costs nothing against the live version.
The capture was writing a blank window. The companion takes about eighteen seconds to show its first frame on a cold simulator and about nine on a warm one, so the fixed four-second sleep was firing long before anything was on screen. The app was never broken: a ten-second sleep showed the full quota list. The script now polls. It takes a frame, measures how much of it differs from the background, and keeps the first frame that clears the bar. A window with nothing but its status bar measures about 0.3 percent, a half-drawn screen about 4, and a finished screen between 40 and 55, so the bar sits at ten percent, clear of both ends. The measurement lives in screenshot_ink.py behind nine tests. Three more defects went with it. The old check deleted the file it was checking inside a command substitution, so the delete printed the file name into the captured value and the run aborted. The output was renamed with a move onto itself, which did nothing. And the file was written as a PNG with an alpha channel, which App Store Connect rejects; the simulator writes JPEG, which cannot carry alpha, and it is converted back to an alpha-free PNG. The captured files are now named after the display set they belong to, so an iPhone file and an iPad file cannot be uploaded into the wrong set. The snapshot is re-seeded before every launch, so the time on a card is the time of its own shot. Apple's screenshot specification has no 2000 x 2664 device, so that size is gone from the script and from the upload step; the 12.9-inch Pro is 2048 x 2732. A compiled Python cache file was tracked by mistake and is now ignored.
…ab on request The source-scope picker showed its stored raw value, which is always "This iPhone". On an iPad the reader therefore saw a control that said it was looking at an iPhone, which is the one thing the control is there to tell them. The stored value stays "This iPhone" so a saved preference is not lost, and a separate display name follows the interface idiom: iPhone, iPad, or Mac on the Mac build. The screenshot capture also needs to reach a tab of the settings sheet that has no launch argument of its own, so "-openAlarms" opens the Alarms tab the same way the existing arguments open the sheet and the platform-order editor. Both changes carry a test.
67ee26c to
4b15713
Compare
This comment has been minimized.
This comment has been minimized.
The width and height sit at offsets 16 and 20 of the PNG header, but the only precheck on an upload argument was existsSync, so a file shorter than 24 bytes reached readUInt32BE and raised an uncaught RangeError. That surfaced as a stack trace and left the run exiting on the unhandled rejection rather than on the named exit 2 that every other bad input in this loop already gets. Reading the bytes once and rejecting anything too small to carry a signature and an IHDR header puts the failure back on the existing path. The bytes are reused by the upload loop below, which read the file a second time for the body. Co-Authored-By: Claude Code <noreply@anthropic.com>
This comment has been minimized.
This comment has been minimized.
Answer the unresolved review findings that are genuine defects: - measure_ink swallowed screenshot_ink.py's exit status. The helper exits 1 below its own default minimum, which is exactly the blank-frame case the poll loop exists to survive, and under `set -e` the failing command substitution aborted the whole capture with a bare exit 1 and no ::error:: annotation. - ASC_API_BASE could redirect every request carrying the ASC-signed JWT to an arbitrary host. Only a loopback override is honoured now, which still covers the tests. - api() let a TimeoutError escape instead of returning a non-ok response, so the commit path skipped discardReservation() and left a reservation open in a ten-entry set. - The post-POST guards exited without dropping the reservation they had just created; they now discard it when an id is available and name the set when one is not. - describeError() read .title/.detail off unvalidated elements of the service's errors array, which throws on a null element and replaces the safe message with a stack trace. - Removed the shadowed module-level REQUEST_TIMEOUT_MS. The remaining threads were already addressed by 597996e or the following autofix rounds, and were left alone: the launch-before-capture, log-to-stderr and measured-pixel-size findings are in the current script, the workflow concurrency finding is out of bounds here, and the stable-frame floor and the mixed-upload refusal are deliberate documented behaviour. Co-Authored-By: Claude Code <noreply@anthropic.com>
This comment has been minimized.
This comment has been minimized.
Nine findings from the last review pass, all addressed here. Transport failures are no longer server verdicts. A request that never reached App Store Connect comes back from api() marked networkError, and it is reported as one: latest-build-seq and ensure-tf-ready say the request never arrived instead of saying the app was not found, and the newest-build probe keeps "unknown" rather than claiming the app has no builds at all. The host override accepts the production host explicitly, so a job that pins the host it would otherwise use is not a redirect anywhere new, and a trailing slash is stripped so the prefix cannot double the separator. The check moved out of module scope into main(), which has a catch handler, so a bad value is a named failure instead of an uncaught stack trace. Failures no longer lose their own message. Every failure path sets process.exitCode and returns, so Node drains stdout and stderr before the process ends; a pending write to a pipe is not flushed by process.exit(). The two guards that cannot return from main() throw a named failure that the handler prints without a stack trace. The capture gate no longer has a way around its own thresholds. The stable-frame path was a second way to accept a frame, gated on a global floor below the per-screen minimums, so a navigation bar and nothing else could be accepted for the quota list. It is now a second condition on the same thresholds rather than an alternative to them, and RENDER_STABLE_FLOOR is gone. To make that check possible, screenshot_ink.py reports two numbers: the share of the whole frame that is not background, and the share of the region below the header, which is where a screen's content lives. A frame that has drawn only its navigation bar has ink in the header and none below it, so the content share is zero and the gate rejects it however much of the frame it covers. That is the case a whole-frame threshold could not see. A failed measurement is no longer swallowed. measure_ink keeps the exit status: 1 is the blank-frame case the poll loop has to survive, and 2 is a measurement that failed, which stops the run with the real error instead of spending the whole render budget on every screen. New test: scripts/ios-fleet/test-asc-client.mjs drives the real client against a local server and a closed port. It asserts the production host is accepted, a host that is neither is refused by name, a trailing slash does not double the path separator, a transport failure is not reported as app not found, and a payload larger than a pipe buffer is not truncated. Every one of those was checked by reverting the fix and watching the test fail. test-capture-script.sh now sources the capture and calls its gate with real numbers: a navigation bar with no content is rejected for every screen, a half-drawn screen is rejected, and the finished quota list and settings sheet both pass.
This comment has been minimized.
This comment has been minimized.
) * brand: compose the owner mark onto off-white and transparent masters and regenerate the app icon. * brand: show the owner's mark in colour inside the app. The About and Settings pages drew the menu bar template icon, which is the owner's black artwork rendered as a mask. In the app it now uses the owner's true vector, in colour on a transparent background, with the dark needle and the six dark dots as the owner drew them. The menu bar itself keeps the template icon, because a template mask is what makes one asset correct in both appearances, and the Glance and PiP marks stay on it too, where colour at twelve and fifteen points is not legible. Dark appearance puts an off-white rounded ground behind the mark so the dark needle and dots do not disappear into the background. The project memory described the old blue three-dimensional squircle as the brand and is corrected to the mark that is actually shipped.
Five findings from the last review pass, all addressed here. A timeout and a transport failure are different causes. api() now says which one happened, and describeError says so too: a call that never answered is reported as a timeout, and only a call that never got there is reported as never having reached App Store Connect. The timeout was computed and then thrown away, so the operator was sent to check the network when the real cause was a slow App Store Connect. A rejected key is no longer reported as a missing app. Every failure the service reports now goes through describeError, and "app not found" is only said when the response was fine and carried no app. The newest-build probe checks the shape of the response before reading it. A malformed answer leaves the reading unknown instead of becoming a build version. A BMP with a short header is rejected by name. struct.error is not a ValueError, so it escaped the handler and landed on exit 1, which the capture reads as a blank frame, and the capture then spent its whole render budget on a frame it could never measure. The reader now says the file is too short, and struct.error is caught as well. The contract test no longer passes its own values positionally. The capture script reads its output directory from "$1", so the gate values were landing there and leaving stray directories behind. They travel in the environment now, and the capture takes OUT_DIR from the environment when there is no argument. A check asserts that sourcing the capture leaves nothing outside the directory it was given. New in test-asc-client.mjs: a call that never answers is reported as a timeout, and a rejected key is reported as a rejected key. The request timeout is overridable so a test can drive a real one.
PR Summary (Comment created by Kody 🤖)Code Review Started! 🚀✋ Hi, team! I'm already looking at the changed files and starting the review to ensure everything is in order. If you need more details, I'm here! Kody 📂 Changed Files
📊 Summary of Changes
|
Every finding in this review is addressed: the code it points at is fixed, the threads are replied to and resolved, and the required checks are green. The review is against a head that no longer exists, and the reviewer has not requested changes again since. Dismissed so the pull request can merge.
The shape guard added in debf87a folded a malformed-but-successful probe into "none", because a body that fails the guards produced an empty rows array and `version || "none"` then asserted that the app has no builds at all. That is the same misreport the original finding was about, one level up. The three readings now stay distinct: a version is the actual newest build, "none" is the service reporting an empty array, and "unknown" is an answer that could not be read or did not have the expected shape. Co-Authored-By: Claude Code <noreply@anthropic.com>
What this does
Captures the six App Store screenshots for the iOS companion (3 screens × iPhone and iPad) on a macOS runner. It uploads nothing on its own: the upload stays an explicit local step, one display set per call. It also fixes the source-scope label, which read "This iPhone" on an iPad.
Why the capture measures the frame
The companion takes 9-18s to render on a cold simulator, and a fixed sleep captured a blank window more than once.
screenshot_ink.pymeasures how much of a frame is not background, and the capture keeps a frame only once the screen has actually drawn. It reports two numbers: the share of the whole frame that is ink, and the share of the region below the header, which is where a screen's content lives. A frame that has drawn only its navigation bar has ink in the header and none below it, so the second number is what separates a finished screen from a half-drawn one.The three screens
Fleet Status, Platform Order and Alarms.
settingswent as a near-duplicate of Platform Order;sources-refreshwent because it shows a red Keychain error banner. Alarms is reached with a new-openAlarmslaunch argument rather than with a near-duplicate screenshot.Frames are captured as JPEG from simctl and converted to PNG with sips, because App Store Connect rejects a screenshot that carries an alpha channel.
Review follow-ups
Two rounds of review findings are addressed. The first round added the request timeout, the reservation cleanup on every upload failure path, the sanitised error output, the hand-written shape guards, the per-screen ink minimums, the single build for both simulators, and the narrower workflow trigger. The second round made a transport failure a transport failure instead of a server verdict, accepted the production host explicitly, stopped the failure paths from losing their own message, and rebuilt the render gate so that holding still is a second condition on the thresholds rather than a way around them.
scripts/ios-fleet/test-asc-upload.mjsandscripts/ios-fleet/test-asc-client.mjsare new and drive the real client against a local server.Let me analyze the code changes to understand what was implemented in this pull request.
The PR is titled "CC-169: capture iOS App Store screenshots on a runner and upload them to App Store Connect"
Looking at the changes:
New file:
.github/workflows/ios-appstore-screenshots.ymlModified file:
scripts/ios-fleet/asc-api.mjsupload-screenshots/v1/appScreenshotsto reserve/v1/appScreenshots/<id>to commitNew file:
scripts/ios-fleet/capture-appstore-screenshots.shNow looking at the review findings:
Let me write a concise description focused on what was implemented.
This PR introduces automated capture of iOS App Store screenshots via a GitHub Actions workflow, with accompanying tooling for uploading them to App Store Connect.
What's implemented:
New GitHub Actions workflow (
.github/workflows/ios-appstore-screenshots.yml) — Runs onmacos-latest, installs XcodeGen, executes the capture script on a runner that has iOS simulator runtimes (since the developer's local Mac does not), reports the captured pixel dimensions, and uploads the resulting screenshots as workflow artifacts. Triggers on manual dispatch or pushes tomainthat touch the companion app, capture script, or workflow file.New capture script (
scripts/ios-fleet/capture-appstore-screenshots.sh) — Builds the CodeCaps companion for one iPhone (6.9") and one iPad (13") simulator selected from a preferred-device list, installs the app, seeds each simulator's app container with a realistic quota snapshot JSON (the format the companion reads as a local fallback), and captures three screens per device: the quota list ("fleet-status"), the settings sheet, and the platform-order editor (the latter two opened via the companion's own launch arguments). Each capture is asserted against the exact pixel dimensions App Store Connect accepts (1320×2868 for iPhone, 2064×2752 for iPad) and flattened to JPEG if the PNG carries an alpha channel.New App Store Connect upload sub-command in
scripts/ios-fleet/asc-api.mjs— Addsupload-screenshots <localizationId> <displayType> <file...>which finds or creates the screenshot set for a given display type on the localization, then runs the three-step upload flow for each file: reserve the asset withPOST /v1/appScreenshots, upload the bytes via Apple's pre-signedPUTURLs, and commit withPATCH /v1/appScreenshots/<id>to markuploaded=trueand surface anyassetDeliveryStateerrors.Key design choice: The workflow captures and uploads screenshots as an artifact but deliberately does NOT push them to App Store Connect. The upload is a separate, explicit step run on a machine holding the App Store Connect key, so a re-run of this workflow cannot silently add duplicate screenshots to a live version.
The user wants me to generate a pull request description based on the code changes. Let me analyze the patch:
The file is
scripts/ios-fleet/capture-appstore-screenshots.shbut looking at the patch, it actually shows Python-looking content (def reset_in, providers list, etc.). This seems to be a configuration update for provider windows/cadences.Changes:
The PR title mentions "capture iOS App Store screenshots on a runner and upload them to App Store Connect" but the actual code changes seem to be about provider configuration for some kind of dashboard/widget that shows usage quotas.
Looking at the context more carefully, this appears to be about provider window configuration for a dashboard widget that displays usage quotas across different AI providers (Anthropic, OpenAI, Cursor, xAI, etc.). The changes:
Wait, the PR title says "CC-169: capture iOS App Store screenshots on a runner and upload them to App Store Connect" but the code changes are about provider windows. This seems inconsistent. However, my job is to describe what the code changes do, not the PR title.
Let me write a description focused on what the code changes actually do.
The file path is
scripts/ios-fleet/capture-appstore-screenshots.shbut the content looks like a Python script for a dashboard widget configuration. The changes are:I need to provide a concise description. Let me focus on the functional impact.
Update the provider window list used by the dashboard widget:
5h,7d,1d,1m), the rule that a second window of the same provider must use a different cadence, and the rationale for excluding Antigravity.The user wants me to generate a precise PR description based on the code changes shown. Let me analyze each file:
.github/workflows/ios-appstore-screenshots.yml:automode and pass both iPhone and iPad PNGsscripts/ios-fleet/asc-api.mjs:autodisplay type that reads PNG dimensions from the first file and maps them to the appropriate App Store Connect display typeautois used, it reads the PNG header to get dimensions and picks the corresponding display typescripts/ios-fleet/capture-appstore-screenshots.sh:png_size()helper to read PNG dimensionsdisplay_class_for_size()helper to map pixel sizes to display classes (iphone/ipad)log()to write to stderrassert_screenshot()to verify against the captured size rather than hardcoded dimensionscapture_on()to not assume specific dimensions - it captures, then measures, then assertsThe review findings indicate:
automode only reads PNG dimensions fromfiles[0], which could cause iPad PNGs to be misclassified if the iPhone file comes firstLet me write a concise PR description based on this.
This PR implements automated capture and upload of iOS App Store screenshots via a GitHub Actions runner (CC-169).
Workflow changes (
.github/workflows/ios-appstore-screenshots.yml):autodisplay-type detection and pass both iPhone and iPad PNGs together.mainno longer reports cancelled runs for an unsuperseded commit.Upload script changes (
scripts/ios-fleet/asc-api.mjs):automode forupload-screenshotsthat reads PNG dimensions from the first file's header and routes it to the correct App Store Connect screenshot set (e.g.,APP_IPHONE_67,APP_IPAD_PRO_129) via a pixel-size-to-display-type lookup table.APP_IPHONE_69orAPP_IPAD_PRO_13, so 6.9" iPhone sizes map toAPP_IPHONE_67and 13" iPad sizes map toAPP_IPAD_PRO_129.Capture script changes (
scripts/ios-fleet/capture-appstore-screenshots.sh):iphone-*oripad-*) instead of the input label, so the upload step receives correctly classified files regardless of which simulator actually rendered them.log()now writes to stderr to avoid contaminating captured file paths).The user wants me to generate a precise PR description based on the code changes. Let me analyze the patch carefully.
The changes are in
scripts/ios-fleet/asc-api.mjs, specifically in the screenshot upload functionality. Let me understand what changed:Before: The code only checked the first file's dimensions to determine the display type. It only validated the existence of
files[0]and read its header to determine size and display type. This meant if you passed a mixed list of iPhone and iPad screenshots, it would only check the first file and use that display type for all uploads.After: The code now:
displayType === "auto", it uses the display type of the first fileThe comment in the code explains the rationale: "Every file has to belong to the same screenshot set. A capture run produces iPhone and iPad files together and they belong in different sets, so a mixed list is refused here rather than sending the second family's files to the first family's set."
So the functional purpose is:
Let me write a concise PR description.
CC-169: capture iOS App Store screenshots on a runner and upload them to App Store Connect
Summary
Hardened the
upload-screenshotscommand inscripts/ios-fleet/asc-api.mjsso it rejects mixed-device screenshot lists instead of silently uploading them to the wrong App Store Connect set.What changed
displayTypeisauto, it is derived from the first file as before.Why
A capture run produces iPhone and iPad screenshots together, but App Store Connect requires them to be uploaded to separate screenshot sets. The previous logic only checked
files[0], so a mixed list would send later-family files to the first family's set. The script now refuses the mixed list up front, preventing misrouted uploads.Let me analyze the code changes in this pull request to understand what was implemented.
The PR title is: "CC-169: capture iOS App Store screenshots on a runner and upload them to App Store Connect"
Key files added/modified:
.github/workflows/ios-appstore-screenshots.yml(added) - GitHub Actions workflow that:.github/workflows/ios-ship-contract.yml(modified) - Added test runs for the new Python scripts.gitignore(modified) - Added Python cache filesTests/CodeCapsTests/CompanionModelTests.swift(modified) - Added tests for:-openAlarmsto open the alarms tabios/CodeCapsCompanion/App/Models/CompanionQuotaModel.swift(modified) - Added:displayTitleproperty that returns the device-aware label (e.g., "This iPad" on iPad)currentDeviceFamilystatic property that returns "iPhone", "iPad", or "Mac"title(for family:)method for testingios/CodeCapsCompanion/App/Views/CompanionContentView.swift(modified) - Changed:displayTitleinstead ofrawValue-openAlarmslaunch argument support to open the alarms tabscripts/ios-fleet/asc-api.mjs(modified) - Added:upload-screenshotscommand that:scripts/ios-fleet/capture-appstore-screenshots.sh(added) - Main capture script that:scripts/ios-fleet/pick_simulator.py(added) - Picks a simulator matching preferred namesscripts/ios-fleet/screenshot_ink.py(added) - Measures non-background pixel percentage to detect blank framesscripts/ios-fleet/test_pick_simulator.py(added) - Tests for pick_simulatorscripts/ios-fleet/test_screenshot_ink.py(added) - Tests for screenshot_inkThe code review found 3 issues:
Now I need to write a clear PR description that summarizes the functional purpose and impact of these changes. I should focus on the what and why, not the how.
Summary
Captures App Store screenshots for the CodeCaps iOS companion on a CI runner and uploads them to App Store Connect as a separate, explicit step.
What changed
.github/workflows/ios-appstore-screenshots.yml) that builds the iOS companion on a macOS runner, captures screenshots for one iPhone and one iPad simulator, and publishes them as build artifacts. The App Store Connect upload is deliberately a separate manual step so a re-run cannot silently add duplicate screenshots to a live version.scripts/ios-fleet/capture-appstore-screenshots.sh) that builds the companion, seeds each simulator with a realistic quota snapshot, and captures three screens per device (quota list, platform-order editor, reset-alarm settings). It polls the simulator and keeps the first frame that holds more than the status bar (a fixed sleep previously produced blank captures).upload-screenshotssubcommand inscripts/ios-fleet/asc-api.mjsthat uploads via Apple's three-call flow (reserve screenshot → PUT bytes to pre-signed URL → commit). It maps captured PNG dimensions to the correct App Store Connect screenshot set (e.g. 1320×2868 →APP_IPHONE_67, 2064×2752 →APP_IPAD_PRO_129), reuses existing screenshot sets rather than creating duplicates, and refuses mixed-family files.pick_simulator.py— selects an available simulator by prefix (Xcode names iPads with their storage suffix, which a previous exact match missed).screenshot_ink.py— measures the share of non-background pixels so the capture script can tell a drawn screen from a blank one without an image library.CompanionSourceScope.displayTitlenow shows "This iPad" on iPad and "This Mac" on macOS instead of the raw value "This iPhone", so an iPad never displays "This iPhone" in its App Store screenshot.-openAlarmslaunch argument opens the Alarms settings tab so the screenshot capture can photograph it without a tap.-openAlarmsargument selects the Alarms tab..gitignoreignores__pycache__/and*.pyc.Review findings
The automated review flagged two high-severity issues in
scripts/ios-fleet/asc-api.mjsand one medium issue that should be addressed before relying on the upload path:appScreenshotsrecord is left orphaned in App Store Connect because the error paths do not DELETE it.RangeErrorinreadUInt32BEbecause the input is not length-checked.Summary
Adds a controlled pipeline for generating App Store-ready screenshots of the CodeCaps iOS companion on a macOS runner and publishing them to App Store Connect.
Functional changes
Added a capture workflow that:
macos-latest, installs XcodeGen, and builds the companion for preferred iPhone and iPad simulators.appstore-screenshotsGitHub Actions artifact.Added simulator-selection and screenshot-content helpers with unit coverage:
Added
upload-screenshotstoscripts/ios-fleet/asc-api.mjs:Updated the companion for reliable screenshot capture:
-openAlarmslaunch argument that opens the Alarms tab in the settings sheet.This iPhone,This iPad, orThis Mac—without changing the persisted raw value used by existing preferences.Added the new Python suites to the iOS shipping contract workflow and added Swift coverage for device-aware scope labels and the alarms launch argument.
Validation
The shipping contract now runs:
Automated review findings
The automated review reported seven unresolved findings against this change:
set -ucan abort captures on macOS Bash 3.2; and the 10% render threshold can time out settings-sheet captures.Description
set -uduring the initial no-argument capture.Summary
This PR adds a runner-based iOS App Store screenshot pipeline for the CodeCaps Companion, capturing the quota, platform-order, and alarm screens on iPhone and iPad simulators before uploading them to App Store Connect. It also improves capture reliability, upload cleanup, error handling, performance, and test coverage.
Functional changes
Tests
Outstanding automated review findings
The current patch still has unresolved review findings:
ASC_API_BASEoverride can redirect the signed App Store Connect bearer token to an arbitrary host.set -e.main().Summary
RangeErrorand stack trace.Review Note
Summary
Implements runner-based iOS App Store screenshot capture and App Store Connect uploads, with hardening focused on API failure handling, screenshot reservation cleanup, and blank-frame polling.
Changes
ASC_API_BASEoverrides to HTTP(S) loopback addresses for local testing. When no override is provided, requests continue to use Apple’s production App Store Connect endpoint.set -e, allowing the renderer to continue polling for visible output.Review Follow-ups
0responses, which probe logic may misreport as an app, screenshot, or existing reservation not being found.ASC_API_BASEaborts startup, including when the production URL is explicitly configured. Loopback overrides with a trailing slash may also generate malformed//v1/...request paths.measure_inkcurrently suppresses every non-zero exit, including measurement failures, which can delay reporting until the application render timeout.Summary
This PR hardens the runner-based iOS App Store screenshot capture and upload flow. It prevents partially rendered simulator screens from being captured, distinguishes App Store Connect connectivity failures from lookup failures, protects signed requests from being sent to unintended hosts, and preserves complete error output.
Key changes
Screenshot readiness and capture
App Store Connect client
ASC_API_BASEnow accepts only:Test coverage
Known review items
OUT_DIR.