ANTIGRAVITY LABJP
Articles/App Development
App Development/2026-07-03Advanced

When CI Passes but App Review Rejects Your Screenshots — Field Notes on Measuring the Freshness of Your Validation Rules

Store asset validation can pass in CI and still get rejected in review, because the rules themselves go stale. Move store specs into a freshness-dated contract file, then add locale overflow checks and perceptual diffs.

CI/CD16GitHub Actions5store assetsApp Store7Google Play6Antigravity304quality validationscreenshots

Premium Article

The night before submission, every store asset check in CI was green. The next morning, App Store Connect returned a metadata rejection caused by screenshots. I read the logs three times. The validation script kept insisting that every file had passed.

The generation pipeline wasn't the problem. The validation rules themselves had gone stale.

Screenshot size requirements change quietly a few times a year. Yet most validation scripts bake the store spec of the day they were written into the code as constants like 1170x2532. From that moment on, a green build no longer means "this meets the store's requirements." It means "this meets the requirements as they existed when someone wrote this script." The gap between the two is invisible until you actually submit.

As an indie developer shipping apps in multiple locales for years, I hit exactly this rejection right before a release deadline. I suspected the generation pipeline immediately — it never occurred to me to suspect the validator. Since then I structure store asset validation in two tiers: one layer that validates the assets, and one layer that validates the freshness of the validation rules themselves.

This article walks through that two-tier setup on GitHub Actions, with code you can run.

Why a Green Build Stops Being Trustworthy

It helps to see why this failure mode is so hard to catch.

LayerWho changes itHow oftenDetectable by plain CI?
Asset generation (capture and processing)Your own codePer commitYes — classic validation
Validation rules (sizes, formats, count limits)Apple / GoogleA few times a year, unannouncedNo, not as-is
Per-locale captionsTranslation updatesIrregularNo, not without rendering

The first layer is what conventional CI protects. The second and third change outside your repository, which fundamentally clashes with commit-triggered CI: when the outside world moves, no build runs — and when a build does run, it passes against outdated rules.

Closing the gap takes three small tools rather than a big platform:

  1. Pull store specs out of the code into a contract file that carries freshness metadata
  2. Have CI check that freshness on every run, and fail once the contract is past its shelf life
  3. Compare rendered per-locale output against golden images with a perceptual diff

Move Store Specs into a Freshness-Dated Contract File

Evict the rules from code constants into JSON, and record when and against what they were last verified.

{
  "specVersion": "2026-06-20",
  "verifiedAt": "2026-06-20",
  "verifiedAgainst": "App Store Connect Help, screenshot specifications page",
  "staleAfterDays": 45,
  "ios": {
    "screenshots": {
      "requiredSizes": [
        { "label": "6.9inch", "width": 1320, "height": 2868 },
        { "label": "6.5inch", "width": 1284, "height": 2778 }
      ],
      "maxCount": 10,
      "maxBytes": 5242880,
      "formats": ["png", "jpg"]
    }
  },
  "android": {
    "screenshots": {
      "minWidth": 1080,
      "minHeight": 1920,
      "maxCount": 8,
      "maxBytes": 8388608,
      "formats": ["png", "jpg"]
    }
  }
}

The key field is staleAfterDays. A machine cannot judge whether the spec contents are still correct — but it can absolutely judge how many days have passed since a human last reconciled them against the official page. So the validator checks the contract before it checks a single image.

// scripts/check-spec-freshness.js
const spec = require('../store-spec.json');
 
const verifiedAt = new Date(spec.verifiedAt);
const ageDays = Math.floor((Date.now() - verifiedAt.getTime()) / 86400000);
 
if (ageDays > spec.staleAfterDays) {
  console.error(
    `❌ store-spec.json was last verified ${ageDays} days ago (limit: ${spec.staleAfterDays}). ` +
    `Reconcile it against the official spec pages and update verifiedAt.`
  );
  process.exit(1);
}
console.log(`✅ Spec freshness OK (${ageDays} days since verification)`);

Why fail the build instead of warning? Because warnings get read for about a week, and then they become scenery. I use a 45-day limit: Apple's size requirement changes tend to land roughly on a quarterly cadence, so I set the shelf life slightly shorter than a quarter. The renewal itself is a ten-minute job — open the official pages, reconcile, bump verifiedAt. Ten minutes on a schedule is a cheap trade for structurally eliminating deadline-night rejections.

One caution: the size numbers in this article are examples. The entire point of a contract file is that you build the first version by reconciling against the official specs yourself, not by copying numbers from a blog post — including this one.

Thank you for reading this far.

Continue Reading

What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.

WHAT YOU'LL LEARN
A CI design that treats store specs as a freshness-dated contract file instead of hardcoded constants, with working validation code
How to wire three defense layers into GitHub Actions — locale text overflow, perceptual diffs against golden images, and spec staleness
A manifest that binds each asset set to a build number, so a rejection can be traced to a single commit in minutes
Secure payment via Stripe · Cancel anytime

Unlock This Article

Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.

or
Unlock all articles with Membership →
Share

Thank You for Reading

Antigravity Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • Copy-paste ready implementation code
  • New advanced guides published daily
  • $5/mo or $10 for lifetime access
View Membership →

Related Articles

App Dev2026-06-21
Feeding Store Review Guidelines as a PDF to the Agent to Build a Per-App Pre-Release Checklist
Using Antigravity v2.1.4's PDF attachment, this walks through reading the App Store and Play review guidelines into the agent as context and turning them into a pre-release checklist tied to your app's actual features.
App Dev2026-05-06
Automate Unity CI/CD with Antigravity and GitHub Actions: A Practical Guide
Set up a complete Unity CI/CD pipeline using GameCI, GitHub Actions, and Antigravity — from automated testing to TestFlight uploads. A practical guide for indie developers who want to stop building manually.
App Dev2026-03-22
Build an Auto-Monetization CI/CD Pipeline with Antigravity AI Agents — From Code Generation to Billing
Integrate Antigravity's AI agents into a CI/CD pipeline that automates content generation, deployment to Cloudflare Workers, Stripe billing monitoring, and performance optimization end-to-end.
📚RECOMMENDED BOOKS
Build a Large Language Model (From Scratch)
Sebastian Raschka
LLM Dev
Prompt Engineering for LLMs
Berryman & Ziegler
Prompting
AI Engineering
Chip Huyen
AI Eng
* Contains affiliate links
See all →