M1-02: Woodpecker CI — lint and simulator build #2

Closed
opened 2026-08-06 10:53:39 -04:00 by agent · 3 comments
Member

CI on every branch: SwiftLint, then xcodebuild against the iOS simulator.

Scope

  • .woodpecker.yml running SwiftLint and a simulator build.
  • Runs on all branches and on PRs into main.
  • No deploy step. This repo never deploys anything; the relay's production deploy lands
    in stash-relay at M3.
  • XcodeGen runs as a CI step, since the .xcodeproj is not committed.

Why it's this early

The repos were split precisely so CI stays simple — a monorepo would have meant path filters
to stop an iOS change from redeploying stash.relay.wes.today. Keep this config boring
enough that it never becomes a thing to reason about.

Done when

A pushed branch goes green, and a deliberately-broken lint rule goes red.

Milestone 1 of the Stash v3 rebuild, scoped 2026-08-06. M1 is a functioning iOS app on Wes's
phone with no sync at all — the entire goal is recording voice memos reliably and getting
them off the device by hand. Sync arrives in M3.

Working agreement for every issue in this repo: feature branch, tested before the PR opens
(on-device where the label says so), then Wes reviews the PR and we walk the code together.
Docs update in the same commit. A PR that takes more than 20 minutes to review is too big —
say so and split it.

CI on every branch: SwiftLint, then `xcodebuild` against the iOS simulator. ## Scope - `.woodpecker.yml` running SwiftLint and a simulator build. - Runs on all branches and on PRs into `main`. - **No deploy step.** This repo never deploys anything; the relay's production deploy lands in `stash-relay` at M3. - XcodeGen runs as a CI step, since the `.xcodeproj` is not committed. ## Why it's this early The repos were split precisely so CI stays simple — a monorepo would have meant path filters to stop an iOS change from redeploying `stash.relay.wes.today`. Keep this config boring enough that it never becomes a thing to reason about. ## Done when A pushed branch goes green, and a deliberately-broken lint rule goes red. --- *Milestone 1 of the Stash v3 rebuild, scoped 2026-08-06. M1 is a functioning iOS app on Wes's phone with **no sync at all** — the entire goal is recording voice memos reliably and getting them off the device by hand. Sync arrives in M3.* *Working agreement for every issue in this repo: feature branch, tested before the PR opens (on-device where the label says so), then Wes reviews the PR and we walk the code together. Docs update in the same commit. A PR that takes more than 20 minutes to review is too big — say so and split it.*
Author
Member

Branch m1-02-woodpecker-ci, one commit. Stacked on m1-01-repo-scaffold — CI needs an app to build, so it branches off #1 rather than off main, and will show that commit until #1 merges.

What's in it

.woodpecker.yml, three steps, on every branch and on PRs targeting main:

  1. swiftlint lint --strict — first, because it is seconds where a build is minutes.
  2. xcodegen generate — the .xcodeproj isn't committed, so CI generates it the way a fresh clone does. Separate step so "project.yml is wrong" reads differently from "the code doesn't compile".
  3. xcodebuild against generic/platform=iOS Simulator with CODE_SIGNING_ALLOWED=NO.

No deploy step. The generic destination is deliberate over a named device — pinning to iPhone 17 Pro Max means CI breaks the day Xcode retires that simulator, which has nothing to do with the change under test.

What I validated

Locally, by parsing the YAML and executing each step's commands in order against a clean tree:

  • Green path: all three steps exit 0, BUILD SUCCEEDED.
  • Red path: added a force-unwrap, and the lint step exits 2 with Force Unwrapping Violation, stopping the pipeline before the build. So a deliberately-broken lint rule does go red.

What I did not validate — and this is the blocker

This pipeline has never run in Woodpecker, and cannot yet. xcodebuild is macOS-only, and Woodpecker has exactly one agent: the docker backend container sitting next to the server on the Linux core-infra node (roles/woodpecker/templates/compose.yml.j2 in homeprod). There is no macOS runner, so there is nowhere for step 3 to execute.

I've labelled the workflow platform: darwin/arm64 so it routes to a macOS agent the moment one exists. Until then the pipeline sits pending, not failing. Pending means nothing ran, which is worth knowing, because a pending pipeline next to a PR reads like a passing one at a glance.

Closing the gap is a homeprod change, not a stash-ios one, and it is bigger than this issue: register a macOS Woodpecker agent on the local backend with Xcode, xcodegen and swiftlint on PATH, which also means exposing the server's gRPC port beyond the compose network it is currently confined to, with the firewall rule that implies. That is your call on infrastructure, so I stopped here rather than reaching into homeprod for it.

The "done when" on this issue — a pushed branch goes green — is therefore not met. The config is written and its commands are proven; the runner is not there. Leaving this open.

Branch `m1-02-woodpecker-ci`, one commit. **Stacked on `m1-01-repo-scaffold`** — CI needs an app to build, so it branches off #1 rather than off `main`, and will show that commit until #1 merges. **What's in it** `.woodpecker.yml`, three steps, on every branch and on PRs targeting `main`: 1. `swiftlint lint --strict` — first, because it is seconds where a build is minutes. 2. `xcodegen generate` — the `.xcodeproj` isn't committed, so CI generates it the way a fresh clone does. Separate step so "project.yml is wrong" reads differently from "the code doesn't compile". 3. `xcodebuild` against `generic/platform=iOS Simulator` with `CODE_SIGNING_ALLOWED=NO`. No deploy step. The generic destination is deliberate over a named device — pinning to `iPhone 17 Pro Max` means CI breaks the day Xcode retires that simulator, which has nothing to do with the change under test. **What I validated** Locally, by parsing the YAML and executing each step's commands in order against a clean tree: - Green path: all three steps exit 0, **BUILD SUCCEEDED**. - Red path: added a force-unwrap, and the `lint` step exits 2 with `Force Unwrapping Violation`, stopping the pipeline before the build. So a deliberately-broken lint rule does go red. **What I did not validate — and this is the blocker** **This pipeline has never run in Woodpecker, and cannot yet.** `xcodebuild` is macOS-only, and Woodpecker has exactly one agent: the `docker` backend container sitting next to the server on the Linux core-infra node (`roles/woodpecker/templates/compose.yml.j2` in `homeprod`). There is no macOS runner, so there is nowhere for step 3 to execute. I've labelled the workflow `platform: darwin/arm64` so it routes to a macOS agent the moment one exists. Until then the pipeline sits **pending**, not failing. Pending means nothing ran, which is worth knowing, because a pending pipeline next to a PR reads like a passing one at a glance. Closing the gap is a `homeprod` change, not a `stash-ios` one, and it is bigger than this issue: register a macOS Woodpecker agent on the `local` backend with Xcode, `xcodegen` and `swiftlint` on PATH, which also means exposing the server's gRPC port beyond the compose network it is currently confined to, with the firewall rule that implies. That is your call on infrastructure, so I stopped here rather than reaching into `homeprod` for it. The "done when" on this issue — a pushed branch goes green — is therefore **not met**. The config is written and its commands are proven; the runner is not there. Leaving this open.
Author
Member

Updated on Wes's call: split into two workflows so the half that doesn't need a Mac actually runs.

.woodpecker.yml is gone, replaced by:

  • .woodpecker/lint.ymlswiftlint lint --strict in the official Linux container. Labelled backend: docker, so it lands on the agent that already exists. This runs today. Labelled by backend rather than by platform on purpose: the SwiftLint image is published for amd64 and arm64 both, so this workflow has no reason to hold an opinion about the agent's CPU.
  • .woodpecker/build.ymlxcodegen generate then the simulator xcodebuild, labelled platform: darwin/arm64. Unchanged otherwise, and still pending until a macOS agent exists.

Still no deploy step in either.

What I validated on the split

Ran SwiftLint in the actual ghcr.io/realm/swiftlint Linux container, with the entrypoint overridden by a shell — which is how Woodpecker invokes a step, and it matters here: the image's own entrypoint is swiftlint, so a naive docker run ... swiftlint lint --strict passes "swiftlint" and "lint" as paths and silently lints the wrong thing. Under a shell entrypoint it behaves correctly:

  • Green: 2 files, 0 violations, exit 0 — identical to the local run.
  • Red: added a force-unwrap, error: Force Unwrapping Violation, exit 2.

Still not validated

Neither workflow has run in Woodpecker. lint.yml now can, but I have no Woodpecker token and didn't go poking at the server, so I haven't confirmed stash-ios is activated in Woodpecker at all — if the repo was never enabled there, nothing fires regardless of what's in these files. Worth a glance at ci.wes.today when you review.

build.yml still cannot run, for the reason in my previous comment. Building by hand locally in the meantime, which the tested-before-PR rule already required; AGENTS.md and README.md both say so now.

Leaving this open.

Updated on Wes's call: split into two workflows so the half that doesn't need a Mac actually runs. `.woodpecker.yml` is gone, replaced by: - **`.woodpecker/lint.yml`** — `swiftlint lint --strict` in the official Linux container. Labelled `backend: docker`, so it lands on the agent that already exists. **This runs today.** Labelled by backend rather than by platform on purpose: the SwiftLint image is published for amd64 and arm64 both, so this workflow has no reason to hold an opinion about the agent's CPU. - **`.woodpecker/build.yml`** — `xcodegen generate` then the simulator `xcodebuild`, labelled `platform: darwin/arm64`. Unchanged otherwise, and still pending until a macOS agent exists. Still no deploy step in either. **What I validated on the split** Ran SwiftLint in the actual `ghcr.io/realm/swiftlint` Linux container, with the entrypoint overridden by a shell — which is how Woodpecker invokes a step, and it matters here: the image's own entrypoint is `swiftlint`, so a naive `docker run ... swiftlint lint --strict` passes "swiftlint" and "lint" as *paths* and silently lints the wrong thing. Under a shell entrypoint it behaves correctly: - Green: 2 files, 0 violations, exit 0 — identical to the local run. - Red: added a force-unwrap, `error: Force Unwrapping Violation`, exit 2. **Still not validated** Neither workflow has run in Woodpecker. `lint.yml` now *can*, but I have no Woodpecker token and didn't go poking at the server, so I haven't confirmed `stash-ios` is activated in Woodpecker at all — if the repo was never enabled there, nothing fires regardless of what's in these files. Worth a glance at ci.wes.today when you review. `build.yml` still cannot run, for the reason in my previous comment. Building by hand locally in the meantime, which the tested-before-PR rule already required; `AGENTS.md` and `README.md` both say so now. Leaving this open.
Owner

This has been resolved.

This has been resolved.
wk closed this issue 2026-08-06 14:30:50 -04:00
Sign in to join this conversation.
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
Stash/stash-ios#2
No description provided.