Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions .github/workflows/ci-gate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -45,14 +45,16 @@ jobs:
set -euo pipefail

# The authoritative gate workflows. A PR that doesn't touch a given
# workflow's paths simply won't have a run for it (→ N/A). Lint is
# intentionally excluded (warning-only); CI Gate excludes itself.
# workflow's paths simply won't have a run for it (→ N/A). CI Gate
# excludes itself. "Lint (warning-only)" — SwiftLint/ktlint — is
# intentionally excluded and is NOT the same thing as "Python Lint",
# which is blocking and listed below.
# "Repo Hygiene" is deliberately first: unlike the others it has NO
# path filter, so it always runs and can never be N/A. That matters,
# because the guard it carries (internal documents in this PUBLIC
# repo) is most needed on exactly the docs-only PRs that trigger none
# of the path-filtered workflows below.
GATES=("Repo Hygiene" "Swift CI" "Android CI" "Helper CI" "Supabase CI")
GATES=("Repo Hygiene" "Swift CI" "Android CI" "Helper CI" "Supabase CI" "Python Lint")

# Give GitHub time to create the path-triggered runs for this SHA so
# we don't race and call a still-uncreated run "N/A".
Expand Down
28 changes: 14 additions & 14 deletions .github/workflows/helper-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -61,21 +61,21 @@ jobs:
python-version: '3.12'

- name: Install dev tooling
# ruff is PINNED. Unpinned, `--upgrade` installs whatever shipped since
# the last run, and a release that enables new rules turns a PR red for
# code it never touched. That is not hypothetical: main was green on
# 2026-07-23, and the next PR to trigger this workflow (2026-07-30, and
# only because helper-ci watches Protocol.swift) drew 380 errors across
# files nobody had edited — UP045 and friends. Ten minutes went into
# proving the feature branch was innocent.
#
# Raising this pin is a deliberate task: bump it, run `ruff check .`,
# and fix or `# noqa` what the new rules find, in a PR that does only
# that. Never bump it as a side effect of unrelated work.
run: python -m pip install --upgrade pip && python -m pip install pytest 'ruff==0.15.11' cryptography
run: python -m pip install --upgrade pip && python -m pip install pytest cryptography

- name: Ruff lint
run: ruff check .
# Ruff used to run here, and that was the bug. This job has
# `working-directory: helper`, so `ruff check .` only ever linted
# `helper/` — and this workflow's `paths:` filter meant a change to
# `scripts/*.py` did not start it at all. Python outside `helper/` was
# unlinted for the life of the repo (91 errors when first measured,
# 2026-08-17).
#
# Linting now lives in `.github/workflows/python-lint.yml`, which runs
# `ruff check .` from the REPO ROOT on every `**/*.py` change — a strict
# superset of what this step covered, so removing it leaves no gap in
# helper/ coverage. Do not re-add a ruff step here: two pinned ruff
# versions in two workflows is the copied-constant drift this repo has
# been bitten by before. The pin and its rationale live in python-lint.yml.

- name: Pytest
run: pytest -q
Expand Down
86 changes: 86 additions & 0 deletions .github/workflows/python-lint.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
name: Python Lint

# Repo-wide ruff. This is a BLOCKING gate — unlike "Lint (warning-only)", which
# is SwiftLint/ktlint and deliberately advisory. `ci-gate.yml` lists this
# workflow in GATES; a failure here fails the required check.
#
# Why this workflow exists at all, given helper-ci.yml already ran ruff:
# it did not lint what it appeared to lint. Two independent defects, either one
# of which was sufficient to hide every non-helper Python file in the repo:
#
# 1. helper-ci's `paths:` filter is helper/**, three named scripts, and one
# Swift file. A change to `scripts/foo.py` matched none of them, so the
# workflow never started.
# 2. Even when it DID start, its ruff step ran under
# `working-directory: helper`, so `ruff check .` resolved to `helper/`.
# Widening the path filter alone would have fixed nothing.
#
# Cost of the gap, measured 2026-08-17: 91 ruff errors across `scripts/`,
# `CLI Pulse Bar/scripts/` and `archive/` — none of which any CI run had ever
# reported. The proximate trigger was `scripts/acquisition_reconcile.py`, added
# in PR #433 with two F541 errors that went straight to main.
#
# The path filter below is therefore `**/*.py` and nothing narrower. If you add
# a Python file anywhere in this repo, this workflow lints it. Note `**/*.py`
# also covers paths containing spaces (`CLI Pulse Bar/scripts/*.py`) — verified
# on a scratch branch, not assumed, because that directory holds ~24 of the
# repo's Python files and a glob that silently skipped it would recreate this
# exact bug.

on:
push:
paths:
- '**/*.py'
- 'ruff.toml'
- '.github/workflows/python-lint.yml'
pull_request:
paths:
- '**/*.py'
- 'ruff.toml'
- '.github/workflows/python-lint.yml'
# Mirrors the other workflows: manual retrigger for an Actions outage
# without needing an empty commit.
workflow_dispatch: {}

concurrency:
group: python-lint-${{ github.ref }}
cancel-in-progress: true

jobs:
ruff:
name: ruff (repo-wide)
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v4

- name: Set up Python
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: '3.12'

- name: Install ruff
# ruff is PINNED. Unpinned, `--upgrade` installs whatever shipped since
# the last run, and a release that enables new rules turns a PR red for
# code it never touched. That is not hypothetical: main was green on
# 2026-07-23, and the next PR to trigger the old helper-ci workflow
# (2026-07-30, and only because helper-ci watches Protocol.swift) drew
# 380 errors across files nobody had edited — UP045 and friends. Ten
# minutes went into proving the feature branch was innocent.
#
# Raising this pin is a deliberate task: bump it, run `ruff check .`,
# and fix or `# noqa` what the new rules find, in a PR that does only
# that. Never bump it as a side effect of unrelated work.
#
# `ruff.toml` states `select` explicitly, so a bump changes rule
# BEHAVIOUR but not the rule SET — that makes the diff reviewable.
# Keep this pin and helper-ci.yml's Python version in step.
run: python -m pip install --upgrade pip && python -m pip install 'ruff==0.15.11'

- name: Ruff lint (repo root)
# NO working-directory. This runs at the repo root on purpose — see the
# header comment. `ruff.toml` at the root supplies the config and the
# single `archive/` exclusion.
run: |
ruff --version
ruff check . --output-format=github
5 changes: 3 additions & 2 deletions CLI Pulse Bar/scripts/add_for_review_ios_v192.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,11 @@
- Build 30 is bound, What's New / all screenshots are in place.
"""
from __future__ import annotations
import sys, os
import sys
import os

sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from submit_ios_v192 import get, post, patch, APP_ID, IOS_VERSION_ID, requests, H, BASE_URL
from submit_ios_v192 import get, post, APP_ID, IOS_VERSION_ID, requests, H, BASE_URL


def create_review_submission() -> str:
Expand Down
20 changes: 9 additions & 11 deletions CLI Pulse Bar/scripts/appstore_metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,8 @@
import jwt
import time
import requests
import json
import hashlib
import os
import sys

# --- Config ---
API_KEY_ID = "DMMFP6XTXX"
Expand Down Expand Up @@ -203,7 +201,7 @@ def set_localization(version_id, locale="en-US"):
}
}, raise_on_error=False)
if result is None:
print(f" (localization update skipped due to version state)")
print(" (localization update skipped due to version state)")
# whatsNew only for updates, not first version - try separately
api_patch(f"/appStoreVersionLocalizations/{loc_id}", {
"data": {
Expand All @@ -213,7 +211,7 @@ def set_localization(version_id, locale="en-US"):
}
}, raise_on_error=False)
else:
print(f" Creating new localization...")
print(" Creating new localization...")
loc_data["locale"] = locale
r = api_post("/appStoreVersionLocalizations", {
"data": {
Expand Down Expand Up @@ -272,7 +270,7 @@ def upload_screenshots(loc_id, screenshot_files, display_type):
}
}, raise_on_error=False)
if r is None:
print(f" Cannot create screenshot set (version state). Skipping.")
print(" Cannot create screenshot set (version state). Skipping.")
return
set_id = r["data"]["id"]
print(f" Created screenshot set: {set_id}")
Expand All @@ -283,7 +281,7 @@ def upload_screenshots(loc_id, screenshot_files, display_type):
if existing:
result = api_delete(f"/appScreenshots/{existing[0]['id']}")
if result >= 400:
print(f" Cannot modify screenshots (version state). Skipping.")
print(" Cannot modify screenshots (version state). Skipping.")
return
for ss in existing[1:]:
api_delete(f"/appScreenshots/{ss['id']}")
Expand Down Expand Up @@ -317,14 +315,14 @@ def upload_screenshots(loc_id, screenshot_files, display_type):
}, raise_on_error=False)

if r is None:
print(f" Failed to reserve. Skipping remaining screenshots.")
print(" Failed to reserve. Skipping remaining screenshots.")
return

screenshot_id = r["data"]["id"]
upload_ops = r["data"]["attributes"].get("uploadOperations", [])

if not upload_ops:
print(f" No upload operations returned, skipping...")
print(" No upload operations returned, skipping...")
continue

# Upload parts
Expand Down Expand Up @@ -359,7 +357,7 @@ def upload_screenshots(loc_id, screenshot_files, display_type):
# 4. Set App Info (category, etc.)
# ============================================================
def set_app_info():
print(f"\n Setting app info (category)...")
print("\n Setting app info (category)...")
r = api_get(f"/apps/{APP_ID}/appInfos")
infos = r.get("data", [])
if not infos:
Expand All @@ -381,7 +379,7 @@ def set_app_info():
},
}
})
print(f" Category set to Developer Tools")
print(" Category set to Developer Tools")
except Exception as e:
print(f" Category update note: {e}")

Expand All @@ -400,7 +398,7 @@ def set_app_info():
}
}
})
print(f" App info localization updated")
print(" App info localization updated")
break


Expand Down
3 changes: 1 addition & 2 deletions CLI Pulse Bar/scripts/compose_appstore_screenshots.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,8 @@
"""

from __future__ import annotations
import os
from pathlib import Path
from PIL import Image, ImageDraw, ImageFont, ImageChops
from PIL import Image, ImageDraw, ImageFont

CANVAS_W, CANVAS_H = 2880, 1800

Expand Down
31 changes: 18 additions & 13 deletions CLI Pulse Bar/scripts/resubmit.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@
"""
CLI Pulse - Cancel review, update screenshots, select new build, resubmit.
"""
import jwt, time, requests, os, hashlib, sys
import jwt
import time
import requests
import os
import hashlib

API_KEY_ID = "DMMFP6XTXX"
API_ISSUER = "c5671c11-49ec-47d9-bd38-5e3c1a249416"
Expand Down Expand Up @@ -37,7 +41,7 @@ def post(path, data):
try:
for e in r.json().get("errors", []):
print(f" {e.get('detail', e.get('title'))}")
except:
except Exception:
print(f" {r.text[:300]}")
return None
return r.json()
Expand All @@ -49,7 +53,7 @@ def patch(path, data):
try:
for e in r.json().get("errors", []):
print(f" {e.get('detail', e.get('title'))}")
except:
except Exception:
print(f" {r.text[:300]}")
return None
return r.json()
Expand All @@ -75,9 +79,10 @@ def cancel_reviews():
if result:
print(f" Canceled: {sub_id}")
else:
# Try legacy API
print(f" Trying legacy appStoreVersionSubmissions...")
r2 = get(f"/appStoreVersionSubmissions")
# Try legacy API. The response is deliberately not inspected —
# the call is a probe and we proceed either way.
print(" Trying legacy appStoreVersionSubmissions...")
get("/appStoreVersionSubmissions")
# Just proceed - may already be canceled


Expand Down Expand Up @@ -113,7 +118,7 @@ def select_build(version_id, platform_label):
print(f" Waiting for build processing... ({attempt+1}/12)")
time.sleep(30)

print(f" WARNING: Could not find valid build 3. Proceeding with existing build.")
print(" WARNING: Could not find valid build 3. Proceeding with existing build.")
return False


Expand Down Expand Up @@ -148,7 +153,7 @@ def upload_screenshots(loc_id, files, display_type):
}
})
if not r:
print(f" Failed to create screenshot set")
print(" Failed to create screenshot set")
return
set_id = r["data"]["id"]

Expand All @@ -174,7 +179,7 @@ def upload_screenshots(loc_id, files, display_type):
}
})
if not r:
print(f" Failed to reserve upload")
print(" Failed to reserve upload")
continue

ss_id = r["data"]["id"]
Expand Down Expand Up @@ -218,7 +223,7 @@ def submit_for_review():
vr = get(f"/apps/{APP_ID}/appStoreVersions?filter[platform]={platform}")
for v in vr.get("data", []):
if v["attributes"]["versionString"] == "1.0.0":
post(f"/reviewSubmissionItems", {
post("/reviewSubmissionItems", {
"data": {
"type": "reviewSubmissionItems",
"relationships": {
Expand All @@ -241,9 +246,9 @@ def submit_for_review():
}
})
if result:
print(f" Submitted for review!")
print(" Submitted for review!")
else:
print(f" Submit failed - may need manual submission")
print(" Submit failed - may need manual submission")
else:
print(" Could not create review submission")

Expand Down Expand Up @@ -341,7 +346,7 @@ def main():
}
}
})
print(f" Updated privacy policy URL")
print(" Updated privacy policy URL")

# Try to select latest build
if mac_vid:
Expand Down
2 changes: 0 additions & 2 deletions CLI Pulse Bar/scripts/submit_v1_10_8.py
Original file line number Diff line number Diff line change
Expand Up @@ -154,8 +154,6 @@ def bind_build(version_id, platform_label):
for b in r.get("data", []):
bv = b["attributes"].get("version")
proc = b["attributes"].get("processingState")
ver_rel = (b.get("relationships", {}).get("preReleaseVersion", {})
.get("data") or {})
states.append(f"{bv}/{proc}")
if bv == TARGET_BUILD and proc == "VALID":
found = b["id"]
Expand Down
2 changes: 0 additions & 2 deletions CLI Pulse Bar/scripts/submit_v1_11_0.py
Original file line number Diff line number Diff line change
Expand Up @@ -179,8 +179,6 @@ def bind_build(version_id, platform_label):
for b in r.get("data", []):
bv = b["attributes"].get("version")
proc = b["attributes"].get("processingState")
ver_rel = (b.get("relationships", {}).get("preReleaseVersion", {})
.get("data") or {})
states.append(f"{bv}/{proc}")
if bv == TARGET_BUILD and proc == "VALID":
found = b["id"]
Expand Down
2 changes: 0 additions & 2 deletions CLI Pulse Bar/scripts/submit_v1_11_1.py
Original file line number Diff line number Diff line change
Expand Up @@ -194,8 +194,6 @@ def bind_build(version_id, platform_label):
for b in r.get("data", []):
bv = b["attributes"].get("version")
proc = b["attributes"].get("processingState")
ver_rel = (b.get("relationships", {}).get("preReleaseVersion", {})
.get("data") or {})
states.append(f"{bv}/{proc}")
if bv == TARGET_BUILD and proc == "VALID":
found = b["id"]
Expand Down
2 changes: 0 additions & 2 deletions CLI Pulse Bar/scripts/submit_v1_12_0.py
Original file line number Diff line number Diff line change
Expand Up @@ -152,8 +152,6 @@ def bind_build(version_id, platform_label):
for b in r.get("data", []):
bv = b["attributes"].get("version")
proc = b["attributes"].get("processingState")
ver_rel = (b.get("relationships", {}).get("preReleaseVersion", {})
.get("data") or {})
states.append(f"{bv}/{proc}")
if bv == TARGET_BUILD and proc == "VALID":
found = b["id"]
Expand Down
Loading
Loading