Skip to content

Adopt StationCast CI and release conventions - #131

Merged
evandhoffman merged 15 commits into
mainfrom
ci/conventions-release
Oct 12, 2026
Merged

evandhoffman merged 15 commits into
mainfrom
ci/conventions-release

Conversation

@evandhoffman

Copy link
Copy Markdown
Collaborator

Summary

This PR adopts StationCast's CI and release conventions, in one batch, so that the shared macOS runner tests them once. It is part of #126.

Releases (bump-build-number.yml)

  • Labels: skip-release skips the bump, the commit, the tag, the release and the TestFlight upload. skip-release with any semver:* label fails the job. With no label, the bump is a patch. (1.1, 1.2)
  • Commit and tag: the release commit is Release vX.Y.Z (build N), with Source merge: #N (title). git push --atomic sends the commit and the tag together. A "Detect stranded release" step tags a release commit that has no tag, and the release step skips a release that already exists. (1.3)
  • Trigger: pull_request_target, with main checked out. The job never runs code from the PR. (1.4)
  • Version checks: scripts/check_version.py and scripts/bump_version.py, with tests written first. The check fails when the version entries disagree, or when the tag does not match. (1.5)
  • Deploy key: the job pushes with RELEASE_DEPLOY_KEY, which is now set. Without the key, it falls back to GITHUB_TOKEN. (2.2)

Tests (ios-tests.yml)

  • Change filter: a "Detect iOS changes" job on Linux skips the tests when only docs or repo metadata changed. A failed filter runs the tests anyway. The check has the stable name iOS tests. (3.1)
  • Fork guard: a fork PR never reaches the macOS runner. A Linux guard job fails it with a message. (3.2)
  • One run per change:
    • A new push to a PR cancels the old run. (3.3)
    • A push to main no longer runs the suite. Each release merge dispatches release-testflight.yml, which tests the tag, so the push run only repeated the test.
  • Watchdog: ci/run_ios_tests.sh probes simctl with a 30-second watchdog. Every job has timeout-minutes. (3.4)
  • Lint: ci/lint_swift.sh runs SwiftFormat and SwiftLint --strict before the tests. The tests run after a lint failure too, so one run reports both. (3.5)
    • One-time reformat: 39 files.
    • SwiftLint findings: 101 before, 0 after, with one justified disable.
    • All 1,369 string literals have the same value as before.
    • App code changes: HKQuantityType(.x) replaces three force unwraps, and .gmt replaces one.
  • Actions: actions/checkout@v7 and actions/upload-artifact@v7 (Node 24). (3.6)

TestFlight (release-testflight.yml)

  • Keychain: the cleanup restores the keychain's lock settings and locks it, even when the job fails. (3.7)
  • Notes: after the upload, scripts/update_testflight_notes.py sets "What to Test" from ci/testflight-notes.txt, for the exact build. (4.1)
  • External testers: the promote-testflight-external.yml dispatch adds a build to External Hoffgroup. (4.3)

Tooling and hygiene

  • Python: pyproject.toml, .python-version and uv.lock. A Scripts job on Linux runs ruff, mypy --strict and pytest -rs. (4.2)
  • Secrets: a Gitleaks job runs on every PR and push. .gitleaksignore lists 4 old CocoaPods checksums. pre-commit adds the standard hooks, gitleaks, SwiftFormat, SwiftLint, ruff and mypy. (5.2)
  • Dependabot: GitHub Actions, monthly, labeled skip-release. (5.3)
  • .gitignore: Python and Xcode leftovers. (5.4)
  • Agent files:
    • AGENTS.md gets new "Verification", "PR Labels" and "Guardrails" sections, and a rewritten "CI and releases" section with no host names. (6.1)
    • GEMINI.md symlink. (6.2)
    • deploy-to-phone skill. (6.3)
    • .claude/settings.json. (6.4)

Already applied in the repo settings

  • Auto-merge and update-branch are on.
  • Secret scanning and push protection are on.
  • The skip-release and semver:patch labels exist.
  • The org and the repo hold fork PR workflows for approval (all_external_contributors).

Deferred

  • 4.4: the App Store submit workflow.
  • 4.5: the release runbook.
  • 2.1: the ruleset with required checks. It waits until the first release proves the deploy-key push.

Verification

  • actionlint: no findings on all 7 workflows.
  • uv run ruff format --check . && uv run ruff check . && uv run mypy && uv run pytest -rs: 74 passed, 0 skipped.
  • scripts/check_version.py --tag v2.5.4: passes on the real project.
  • ci/lint_swift.sh: 0 violations.
  • uvx pre-commit run --all-files: all 12 hooks pass.
  • xcodebuild build-for-testing, compile only: 0 errors and 0 Swift warnings.
  • The unit tests run in this PR's iOS tests check.

Release

skip-release. This PR changes CI, tooling and code style only. Nothing changes for a user.

evan-agent Bot added 15 commits October 12, 2026 11:32
scripts/check_version.py fails when the MARKETING_VERSION or the
CURRENT_PROJECT_VERSION entries in project.pbxproj disagree, when a
configuration lacks one, when a value is malformed, or when --tag is
not v + MARKETING_VERSION. scripts/bump_version.py replaces every
entry, so no configuration keeps an old value.

pyproject.toml holds only the dev tools (ruff, mypy --strict, pytest)
and the scripts' dependencies; it builds no package.
Ported from StationCast, with HealthExporter's bundle ID as the
default. The App Store Connect auth code moves to scripts/asc_api.py,
which both scripts share. Output goes through logging to stdout.
- .swiftformat and .swiftlint.yml come from StationCast, with the paths set to
  HealthExporter/ and HealthExporterTests/.
- ci/lint_swift.sh runs both tools and fails if either one fails.
- .pre-commit-config.yaml runs the file checks, gitleaks, SwiftFormat,
  SwiftLint, and ruff and mypy on scripts/.
- .gitignore ignores the Python caches, *.xccheckout and *.moved-aside.
SwiftFormat 0.63.0 with the new .swiftformat. Its noForceUnwrapInTests rule rewrote one check in ExportLogicTests to #expect(try #require(a == b)), which does not compile. That line is now #expect(result?.endDate == end).
- Use HKQuantityType(_:) and TimeZone.gmt, which do not return an optional.
- Write the fixed test dates as Date(timeIntervalSince1970:) values.
- Make the test helpers throw, and use #require in place of ! and try!.
- Use isEmpty in place of a comparison with 0 or "".
- Keep one force unwrap in HealthKitQueryHelpersTests, with a reason: a stored
  property initializer cannot throw.
- AGENTS.md: add the Verification, PR Labels and Guardrails sections. Say that
  the tests run in CI, not on a laptop. List GEMINI.md and the skills folder.
- GEMINI.md is a symlink to AGENTS.md.
- .agents/skills/deploy-to-phone installs a Debug build on the owner's iPhone,
  with the Xcode that matches the device iOS. .claude/skills links to it.
- .claude/settings.json allows xcodebuild and xcrun, as StationCast does.
The four findings are pod checksums in Podfile.lock and Pods/Manifest.lock from commit e0fb575. Neither file is in the tree now.
- skip-release skips the bump, commit, tag, release and TestFlight
  dispatch; skip-release with a semver:* label fails the job.
- Run on pull_request_target from main, so a fork merge can release.
- Bump with scripts/bump_version.py and check the tag with
  scripts/check_version.py before the push.
- Push the release commit and the tag with one git push --atomic, and
  tag a stranded release commit instead of bumping again.
- Check out with RELEASE_DEPLOY_KEY when it is set, else GITHUB_TOKEN.
- Add a pull request template with the release labels.
- Detect iOS changes: skip the tests when only docs, assets or repo
  metadata changed; run them if the filter fails.
- Name the test job 'iOS tests' for the ruleset, and fail fork pull
  requests in a Linux guard job instead of skipping them.
- Cancel superseded runs on pull requests only.
- Probe simctl with a 30-second watchdog, and document the recovery.
- Lint Swift before the tests, and set job timeouts.
- Move to actions/checkout@v7 and actions/upload-artifact@v7.
- After an upload, set What to Test from ci/testflight-notes.txt for the
  exact build and version, with retries.
- In the cleanup, restore the keychain lock settings and lock it.
- Add a 90-minute timeout and move to actions/checkout@v7.
- Scripts: ruff, mypy --strict, pytest (failing on a skip) and the
  version check, on the Linux runner, with a per-run uv cache.
- Gitleaks: scan new commits on every pull request and push with a
  pinned, checksummed release. Ignore old CocoaPods checksums.
- Promote a TestFlight build to external testers by dispatch.
- Dependabot: GitHub Actions, monthly, labeled skip-release.
Name the runners by label only, since the repo is public.
# Conflicts:
#	.gitleaksignore
detect-private-key flagged the literal BEGIN PRIVATE KEY marker. Also drop the stale bump-commit example from the commit guidelines.
Each release merge dispatches release-testflight.yml, which runs the suite on the tag, so a push run on main only repeated it on the shared runner. Run the tests after a lint failure too, so one run reports both.
@evandhoffman evandhoffman added the skip-release Docs, CI or tooling only: the merge cuts no release and no TestFlight upload label Oct 12, 2026
@evandhoffman
evandhoffman merged commit 5fbaf7c into main Oct 12, 2026
5 checks passed
@evandhoffman
evandhoffman deleted the ci/conventions-release branch October 12, 2026 15:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

skip-release Docs, CI or tooling only: the merge cuts no release and no TestFlight upload

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant