← All work
DevOps, 2025 – 2026

Agency CI/CD and OpenProject sync

One set of reusable GitHub Actions for twenty client repos, three deploy shapes for web, and a mobile release line on Codemagic: PR events and preview URLs land in OpenProject, builds land in Firebase App Distribution, TestFlight and Play, and Discord hears about all of it.

Role
Own, rolled out across the agency
Context
Ottr Technology, all client repos
Scale
4 reusable workflows, ~20 consuming repos, 3 web deploy shapes, 1 mobile release line

Background

Ottr runs its client work in OpenProject and its code on GitHub. Before this, the two never met: a PR was opened, reviewed and merged on GitHub, and someone typed a comment into the work package later, or did not. Deploys were a mix of hand-run PM2 restarts and Vercel clicks, and nobody was told when one happened.

I built a small set of reusable workflows and a standard deploy shape per stack, then rolled them out repo by repo. Twenty repos now share the same four workflow files and the same secrets layout.

The problem

Project managers lived in OpenProject and could not see engineering state. Engineers lived in GitHub and did not update work packages. Preview URLs were pasted into chat and lost. Deploys to VPS servers were done by whoever had SSH access that day, with no record.

Every fix had to work for a team that spins up a new client repo every few weeks, so it had to be a copy of one file, not a setup procedure.

Approach

  1. Convention first: the branch name carries the OpenProject work-package ID. Every workflow extracts it with one regex, so there is nothing to configure per repo beyond two secrets.
  2. sync-pr-events-to-open-project: on PR opened, reopened, ready for review, review requested, reviewed and closed or merged, post a GitHub-styled activity to the work package, with the PR link, reviewer and review body.
  3. actions-status: mirror the CI result of each run into the work package, so a red build is visible to the PM without opening GitHub.
  4. trigger-preview: vercel pull, vercel build and vercel deploy --prebuilt on every PR, then post the preview URL and PR link as an activity on the work package. PMs review the preview from the ticket.
  5. nodejs-test: fetch the work-package subject and write it into the PR summary, so the PR itself says what it is for.
  6. Three deploy shapes, one per stack. Frontends: Vercel preview on PR, production on main. Node and Strapi backends on VPS: SSH with appleboy/ssh-action, git pull, install, PM2 reload from ecosystem.config.js, then a Discord deploy notification. Containerised backends: a shared _deploy.yml called by deploy-staging and deploy-production with GitHub environments, docker compose -p project -f file up over SSH, images from GHCR.
  7. Mobile release line for the Flutter apps (Baccpacc, GC LAL). GitHub Actions runs flutter analyze and flutter test with coverage on every PR. Codemagic builds on Mac Mini M2: the staging branch produces a signed staging-flavour APK and an ad-hoc IPA and ships them to Firebase App Distribution for testers; main produces a release AAB and IPA, uploads to the Play production track as a draft and to TestFlight, and holds for manual submit. Signing keys, keystore properties and .env files are written from secrets at build time. A tiny GitHub workflow starts the Codemagic build through its API so the trigger is visible next to the PR.
  8. Supply-chain gates on the release path: Trivy scans the Android build and fails on fixed CRITICAL and HIGH findings, CodeQL SARIF goes to the Security tab on the Go backend and the dashboard, Dependabot PRs are validated by the cheap analyze-and-test job only, so no paid build minutes are spent on dependency bumps.
  9. A Strapi self-hosting template with PM2 config and a seed script, so a new CMS backend starts from a known-good deploy.
  10. Scheduled database backups as a workflow (Kiln), Discord alerts for deploys, and a Prometheus, Grafana and Loki stack where the project justified it.

Architecture

ci-pipeline/The four reusable workflow files, copied into each repo's .github/workflows.
open-project-testThrowaway repo used to develop and test the OpenProject sync against a real board.
strapi-hosting-sampleReference Strapi deploy: PM2 ecosystem, seed script, Postgres.
gc-backend, ottr-kilnCompose deploy shape: _deploy.yml + staging/production environments + GHCR.
concrete-vision, achieve-expeditionsPM2 deploy shape with Discord notify.
baccpacc-app codemagic.yamlMobile release line: staging-qa → Firebase App Distribution, production → Play draft + TestFlight, Discord notify.
baccpacc-app / gc-mobile ci.ymlflutter analyze + test with coverage on PRs; old GitHub build workflows kept as manual fallbacks.

Outcome

  • Twenty repos consume the workflows: dashboards, marketing sites, CMS backends, the component kit, the server-alert tool.
  • Every PR on a client project shows up in the right OpenProject work package with its preview URL, without anyone touching the ticket.
  • New client repos get CI, previews and deploy notifications by copying four files and setting two secrets.
  • Every push to the app's staging branch reaches testers' phones through Firebase App Distribution without anyone building locally; production builds wait in Play and TestFlight for a human to press submit.

Lessons

  • Put the integration key in the branch name. It is the one thing every engineer already types, and it removes all per-repo configuration.
  • Reusable workflows beat a shared action for a small agency: the file is visible in each repo, and a junior can read it top to bottom.

Stack

GitHub Actionsreusable workflowsOpenProject APIVercel CLIPM2Docker ComposeGHCRCodemagicFirebase App DistributionTestFlightGoogle PlayTrivyCodeQLDiscord webhooksPrometheusGrafanaLoki