惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

T
Threat Research - Cisco Blogs
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
The Register - Security
The Register - Security
A
About on SuperTechFans
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
L
LangChain Blog
N
Netflix TechBlog - Medium
量子位
博客园 - 三生石上(FineUI控件)
宝玉的分享
宝玉的分享
H
Help Net Security
D
Docker
D
DataBreaches.Net
T
Tailwind CSS Blog
阮一峰的网络日志
阮一峰的网络日志
B
Blog
博客园 - 聂微东
Apple Machine Learning Research
Apple Machine Learning Research
Google DeepMind News
Google DeepMind News
The Cloudflare Blog
F
Full Disclosure
GbyAI
GbyAI
F
Fortinet All Blogs
Last Week in AI
Last Week in AI
Y
Y Combinator Blog
人人都是产品经理
人人都是产品经理
Recent Announcements
Recent Announcements
博客园 - Franky
MongoDB | Blog
MongoDB | Blog
有赞技术团队
有赞技术团队
博客园 - 叶小钗
小众软件
小众软件
V
Visual Studio Blog
月光博客
月光博客
Stack Overflow Blog
Stack Overflow Blog
The GitHub Blog
The GitHub Blog
Recorded Future
Recorded Future
J
Java Code Geeks
雷峰网
雷峰网
P
Privacy & Cybersecurity Law Blog
C
Cisco Blogs
C
Cyber Attacks, Cyber Crime and Cyber Security
AWS News Blog
AWS News Blog
Webroot Blog
Webroot Blog
美团技术团队
N
News | PayPal Newsroom
G
Google Developers Blog
Security Archives - TechRepublic
Security Archives - TechRepublic
博客园_首页
V
Vulnerabilities – Threatpost

DEV Community

Authentication Security Deep Dive: From Brute Force to Salted Hashing (With Java Examples) Why AI Systems Don’t Fail — They Drift Spilling beans for how i learn for exam😁"Reinforcement Learning Cheat Sheet" I Replaced Chrome with Safari for AI Browser Automation. Here's What Broke (and What Finally Worked) How Python Borrows Other People's Work The $40 Architecture: Processing 1 Billion API Requests with 99.99% Uptime Vibe Coding: A Workflow Guide (From Zero to SaaS) Most webhook security guides protect the wrong side. The scary part is delivery. Headless CMS for TanStack Start: Build a Blog with Cosmic EU Age Verification App "Hacked in 2 Minutes" — What Actually Happened Comfy Cloud’s delete function does not actually remove files Running AI Models on GPU Cloud Servers: A Beginner Guide Event-driven media intelligence with AWS Step Functions and Bedrock I scored 500 AI prompts across 8 quality dimensions — here's what broke How to Call Google Gemini API from Next.js (Free Tier, No Backend Needed) The Portal Protocol: Reclaiming Human Connection in the Age of AI How to Fix Your Team's Scattered Knowledge Problem With a Self-Hosted Forum Intro to tc Cloud Functors: A Graph-First Mental Model for the Modern Cloud Designing Multi-Tenant Backends With Both Ownership and Team Access I Built a Neumorphic CSS Library with 77+ Components — Here's What I Learned PostgreSQL Performance Optimization: Why Connection Pooling Is Critical at Scale Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 🚀 I Built an Ethical Hacking Scanner Tool – Open Source Project I Replaced /usage and /context in Claude Code With a Single Statusline A Pythonic Way to Handle Emails (IMAP/SMTP) with Auto-Discovery and AI-Ready Design I Collected 8.9 Million Polymarket Price Points — Here's What I Found About How Markets Really Move EcoTrack AI — Carbon Footprint Tracker & Dashboard Everyone's Using AI. No One Agrees How. 5 self-hosted ebook managers worth trying in 2026 Building Your First AI Agent with LangChain: From Chatbot to Autonomous Assistant Common SOC 2 Failures (Real World) Stop Vibe-Checking Your AI App: A Practical Guide to Evals How to Use SonarQube and SonarScanner Locally to Level Up Your Code Quality Your Next To-Do App Is Dead — I Replaced Mine with an OpenClaw AI Sign a Nostr event in 60 lines of Python using coincurve — no nostr-sdk, no nbxplorer, no rust toolchain ITGC Audit Explained Like You’re in Big 4 Patch Tuesday abril 2026: Microsoft parcha 163 vulnerabilidades y un zero-day en SharePoint Stop scraping everything: a better way to track competitor price changes Listing on MCPize + the Official MCP Registry while routing payments OUTSIDE the marketplace — how I kept 100% of my x402 revenue Building an AI-Powered Risk Intelligence System Using Serverless Architecture Why We Ripped Function Overloading Out of Our AI Toolchain Testing AI-Generated Code: How to Actually Know If It Works SaaS Churn Is Killing Your Business. Here Is What to Do About It (Without a Support Team) The Speed of AI Is No Longer Linear - And Self-Improving Models Are Why How to Implement RBAC for MCP Tools: A Practical Guide for Engineering Teams From Standard Quote to Persuasive Proposal: AI Automation for Arborists I built a CLI that scaffolds complete multi-tenant SaaS apps Axios CVE-2025–62718: The Silent SSRF Bug That Could Be Hiding in Your Node.js App Right Now The dashboard that ended our friendship Data Pipelines Explained Simply (and How to Build Them with Python) The Hidden Cost of AI Systems Nobody Talks About. undefined vs undeclared, and how typeof behaves Switching from file-based jobs to NATS/Kafka in Rust without changing code io_uring Adventures: Rust Servers That Love Syscalls Why Agentic AI is Killing the Traditional Database The POUR principles of web accessibility for developers and designers Quantum Neural Network 3D — A Deep Dive into Interactive WebGL Visualization How To Install Caveman In Codex On macOS And Windows Automation Pipeline Reliability: Why Your Workflow Breaks When Nobody Is Watching I Built an 'Open World' AI Coding Agent — It Works From ANY Folder From Freelancing to Product: A Tech Service Company's SaaS Transformation China's AI Giants: Adding Tencent Hunyuan & ByteDance Doubao to AI University (74 Providers) On the Vibe Coders and Their Lies clerk: Auto-Summarize Your Claude Code Sessions AI Weekly — 2026/04/10–04/17 | The Model Lockdown Is Here, but the Toolchain Is the Real Battleground AI 週報 — 2026/04/10–2026/04/17 模型封鎖潮來了,但工具鏈才是真戰場 Maybe this is how Open-Source apps are born... 🚀 Fine-Tune LLMs with LoRA and QLoRA: 2026 Guide tRPC v11 + Next.js App Router: End-to-End Type Safety Without the Boilerplate ShadCN UI in 2026: Why I Stopped Installing Component Libraries and Started Owning My Components SaaS Billing in React Server Components: Stripe + Supabase Without a Single `useEffect` Join our DEV Weekend Challenge — $1,000 in Prizes Across TEN winners! Submissions Due April 20 at 6:59 AM UTC. Implementing FSRS Spaced Repetition in Flutter + Supabase — Adding Memory Science to an AI Learning App "I Texted My Localhost From the Train — Claude Code Fixed the Bug Before I Got Home" I Built a Sales Prep AI and It Went Deeper Than Expected Design to Code #2: One JSON, Eleven Outputs Solving the 100M-Row Problem: A Summary Table Pattern for High-Volume Push Notification Logs Flutter Web With Wasm: What Actually Changes For Developers I Built 50 Royalty-Free Soundtracks for My Side Project in a Weekend Using AI Music Generation The Vibe Coding Security Checklist: 7 Things to Check Before You Ship Stop Letting Googlebot Guess Fix Your React App's SEO Right Desconstruindo o Streaming do LinkedIn: Como Criar um Engine de Extração de Vídeo de Alta Performance com HLS e FFmpeg (EDA Part-1) EDA (Exploratory Data Analysis) Explained With Real Life — Why Looking at Your Data Is the Most Important Step in Machine Learning Brand Relationship Management at Scale: Our 4-Touch Outreach System for 200+ Brands Why String.fromEnvironment() Might Return an Empty String in Dart JGuardrails 1.0.0 — Hardening Java LLM Apps Against Jailbreaks, Toxicity, and Prompt Injection Plan and Schedule a Full Week of Threads Content From One Claude Conversation Coding Cat Oran Ep3, Five Tables Changed Everything Updated: BFF Pattern I'm done watching freelancers get buried by 200 proposals. So I'm building the alternative. This is my first post BFS Algorithm in Java Step by Step Tutorial with Examples Tracking LLM Pricing Monthly: An Open Dataset for 22 AI Models How We Measure Content ROI on a Comparison Site: Revenue Attribution Without Perfect Data Introducing Nova AI Ops: The AI-Native Operating System for SRE Teams I built a free desktop video downloader for Windows — Grabbit How Talkie OCR Helps Vision-Impaired & Dyslexic Users Read the World Around Them VRCFaceTracking安装和iPhone面捕配置教程,有bug Even CrowdStrike Can't See Your Agents The Automation Gold Rush: What n8n Workflows and Claude Are Opening Up for Developers Right Now
We Shipped to 3 Platforms With One Merge. Here's Exactly How.
Ashish Patel · 2026-04-23 · via DEV Community

We were maintaining 3 Git branches for the same white-label SaaS app one per platform. Every feature meant 3 PRs, every bug fix meant 3 cherry-picks. Here's how we collapsed it into a single branch with runtime identity detection and a build-once CI pipeline.

It started as a practical decision. We white-label our SaaS dashboard for multiple enterprise clients same core product, different branding, domain, and feature set per customer. The obvious solution? Keep a branch per platform, merge features into each one when they're ready.

Simple. Reasonable. And quietly, over months, one of the most painful parts of our workflow.


The Ritual Nobody Wanted to Own

Picture this: you've just finished a feature. Tests pass. Code review done. You're ready to ship.

Now do it three times.

merge to launchpad → CI runs → deployed ✓
cherry-pick to nexus → resolve conflicts → deployed ✓
cherry-pick to orion → resolve conflicts → deployed ✓

Enter fullscreen mode Exit fullscreen mode

Every single release. Every bug fix. Every dependency bump. Three PRs, three CI runs, three opportunities for the branches to quietly drift apart. And they always do because entropy is patient and developers are busy.

The worst part wasn't the volume of work. It was the invisible divergence. Orion misses a commit. Nobody notices until a customer reports a bug that was fixed two weeks ago on Launchpad. Now you're doing archaeology in three git histories trying to figure out what's in sync and what isn't.


The 11pm Problem

Normal feature work is just annoying overhead. Emergency fixes are where this setup becomes genuinely dangerous.

A production bug doesn't care that it's late. It doesn't care that you're tired, that the cherry-pick has a conflict, or that you're not sure if that related refactor from last week landed on all three branches.

You fix it. You cherry-pick it. You hope.

We've all been in that Slack thread: "Nexus is fixed, Orion still looks wrong, checking..."

That's not a process problem. That's a system problem and you can't discipline your way out of a bad system.


The Real Antipattern: Using Git as a Config File

Here's the thing we eventually understood: the branches weren't the problem. The reason we needed the branches was.

Every platform difference a feature that only two of three clients get, a logo, a product name in the nav lived inside a Git diff between branches. The branch was the configuration. And the moment you use Git history as a config layer, you've committed yourself to manual synchronization forever.

No config file. No feature flag. Just three versions of the truth, slowly walking away from each other.


The Fix: Make the App Know Where It Is

The solution wasn't a better branching strategy. It was making the platform identity a runtime concern instead of a deploy-time one.

Step 1 Read the hostname, derive everything else

const HOSTNAME = window.location.hostname;

const isLaunchpad = HOSTNAME.includes("launchpad.io");
const isNexus     = HOSTNAME.includes("nexus.io");
const isOrion     = HOSTNAME.includes("orion.io");

Enter fullscreen mode Exit fullscreen mode

One build artifact. The app figures out where it's running the moment it boots. No environment variables, no separate bundles, no deploy-time injection.

Step 2 Feature flags that know your identity

Instead of hiding features by deleting them from a branch, we gate them with flags derived from the detected platform:

export const FEATURE_FLAGS = {
    CONTENT_INSPIRATION: !isOrion,
    DIRECTORY:           !isOrion,
    PLATFORM_CUSTOMIZATION: !isOrion,
};

Enter fullscreen mode Exit fullscreen mode

These flags control both route registration and sidebar visibility a platform that doesn't support a feature never mounts the route and never renders the nav item. It's as if the feature doesn't exist on that platform, without it actually being absent from the codebase.

Want to enable a feature for Orion next quarter? One line. No branch merge, no conflict, no archaeology.

Step 3 Structured assets instead of overwritten paths

Each platform's branding assets used to live at the same path across branches a logo.png that meant different things depending on which branch you were on. We replaced that with an explicit identity directory:

public/identities/
  launchpad/big-logo.png
  launchpad/small-logo.png
  nexus/big-logo.png
  nexus/small-logo.png
  orion/big-logo.png
  orion/small-logo.png

Enter fullscreen mode Exit fullscreen mode

A PLATFORM_CONFIG lookup table maps the detected hostname to the right asset paths at runtime. All logos coexist in one repo no collisions, no branch-specific overrides.

Step 4 Build once, deploy everywhere in parallel

This is where it all comes together. The CI pipeline now has one job that builds the app and three jobs that deploy the same artifact simultaneously:

push to main
  └── detect-changes
       └── build  ──────────── (one artifact, uploaded once)
            ├── deploy → Launchpad (S3 + CDN invalidation + health check)
            ├── deploy → Nexus     (S3 + CDN invalidation + health check)
            └── deploy → Orion     (S3 + CDN invalidation + health check)

Enter fullscreen mode Exit fullscreen mode

Here's the actual GitHub Actions structure:

jobs:
  detect-changes:
    outputs:
      dashboard: ${{ steps.filter.outputs.dashboard }}

  build:
    needs: detect-changes
    if: needs.detect-changes.outputs.dashboard == 'true'
    steps:
      - run: pnpm build
      - uses: actions/upload-artifact@v4
        with:
          name: dashboard-build
          path: apps/dashboard/build

  deploy-launchpad:
    needs: build
    steps:
      - uses: actions/download-artifact@v4
        with:
          name: dashboard-build
      - name: Sync S3
        # sync to launchpad bucket
      - name: Invalidate CloudFront
        # invalidate /index.html only
      - name: Health check
        run: |
          for i in $(seq 1 10); do
            STATUS=$(curl -s -o /dev/null -w "%{http_code}" --max-time 15 https://app.launchpad.io)
            [ "$STATUS" = "200" ] && { echo "Health check passed"; exit 0; }
            echo "Attempt $i: HTTP $STATUS retrying in 15s"
            sleep 15
          done
          exit 1

  deploy-nexus:
    needs: build
    # same pattern, different bucket + distribution

  deploy-orion:
    needs: build
    # same pattern, different bucket + distribution

Enter fullscreen mode Exit fullscreen mode

A few details worth calling out:

Path-based change detection. The build only triggers when something in apps/ or packages/ actually changed. Updating a doc or a workflow comment doesn't burn CI minutes rebuilding the app.

Pinned action versions. Every third-party GitHub Action is pinned to a commit SHA not @v2, not @master. Floating tags are someone else's broken deploy waiting to become yours.

Surgical CDN invalidation. Only /index.html gets invalidated after each deploy. Every other asset is content-hashed by Vite, so it auto-invalidates when its content changes. Invalidating /* on every deploy is wasteful and slower for users still in-flight.

Health checks that actually mean something. After each deploy, CI curls the platform's own URL and retries for up to 2.5 minutes. If the app doesn't respond healthy, the job fails loudly on the right platform's job not silently 20 minutes later in a support ticket.


The GitHub Workflow: Step by Step

This wasn't a weekend rewrite we moved incrementally. Here's the exact sequence we followed so nothing broke in production while the migration was in progress.

Step 1 Audit what was actually different across branches

Before touching any code, we listed every platform-specific difference:

  • Logo and favicon paths
  • Product name in the nav, page titles, and meta tags
  • Features enabled/disabled per platform
  • Sidebar items and routes that shouldn't exist on certain platforms

This became the spec for what PLATFORM_CONFIG and FEATURE_FLAGS needed to cover.

Step 2 Build the platform detection layer

Added a single platform-config.js module that derives everything from window.location.hostname. All downstream code reads from here nothing else checks the hostname directly.

const HOSTNAME = window.location.hostname;

export const isLaunchpad = HOSTNAME.includes("launchpad.io");
export const isNexus     = HOSTNAME.includes("nexus.io");
export const isOrion     = HOSTNAME.includes("orion.io");

export const PLATFORM_CONFIG = {
    name:     isOrion ? "Orion" : isNexus ? "Nexus" : "Launchpad",
    bigLogo:  `/identities/${isOrion ? "orion" : isNexus ? "nexus" : "launchpad"}/big-logo.png`,
    smallLogo:`/identities/${isOrion ? "orion" : isNexus ? "nexus" : "launchpad"}/small-logo.png`,
};

export const FEATURE_FLAGS = {
    CONTENT_INSPIRATION:    !isOrion,
    DIRECTORY:              !isOrion,
    PLATFORM_CUSTOMIZATION: !isOrion,
};

Enter fullscreen mode Exit fullscreen mode

Step 3 Replace all hardcoded identity values

Went through every file that referenced a platform name, logo path, or brand color and replaced them with PLATFORM_CONFIG lookups. Components that rendered conditionally based on the branch now read from FEATURE_FLAGS.

Step 4 Migrate assets into public/identities/

Moved all per-platform logos into the structured directory. Deleted the old shared logo.png that was being overwritten per branch. Verified each platform rendered the correct logo locally by temporarily overriding window.location.hostname in dev.

Step 5 Gate routes and sidebar items behind feature flags

Routes that shouldn't exist on Orion were wrapped:

// Before hard-deleted from the branch
{ path: "content-inspiration", element: <ContentInspiration /> }

// After present in all branches, conditionally registered
...(FEATURE_FLAGS.CONTENT_INSPIRATION
    ? [{ path: "content-inspiration", element: <ContentInspiration /> }]
    : [])

Enter fullscreen mode Exit fullscreen mode

Same pattern applied to sidebar data flags guard both the nav item and the route, so there's no way to reach a disabled page via URL either.

Step 6 Rewrite the workflow from scratch

The original workflow was a single job: build → deploy to one bucket. We replaced it with:

detect-changes uses dorny/paths-filter to check if anything in apps/ or packages/ actually changed. If not, the entire pipeline skips. This stops CI from rebuilding the app on every doc or config commit.

One gotcha: GitHub Actions outputs are always strings. Comparing outputs.dashboard == true silently fails it needs to be == 'true'. We hit this and added a comment in the workflow so the next person doesn't spend an hour debugging it.

build runs once, uploads the artifact with actions/upload-artifact@v4. Retention set to 1 day it only needs to survive the deploy jobs that run immediately after.

deploy-* (×3, in parallel) each job downloads the same artifact and runs:

  1. S3 sync with --delete flag to remove stale files
  2. CloudFront invalidation on /index.html only not /*
  3. Health check with 10 retries and 15s intervals

Step 7 Pin every action to a commit SHA

Replaced all @v2 / @master action references with pinned SHAs:

# Before dangerous
uses: jakejarvis/s3-sync-action@master
uses: chetan/invalidate-cloudfront-action@v2

# After pinned
uses: jakejarvis/s3-sync-action@be0c4ab89158cac4278689ebedd8407dd5f35a83
uses: chetan/invalidate-cloudfront-action@cacab256f2bd90d1c04447a7d6afdaf6f346e7b3

Enter fullscreen mode Exit fullscreen mode

Floating tags mean a third-party maintainer can silently change what your workflow runs. Pinning to a SHA is the only way to guarantee reproducibility.

Step 8 Change CDN invalidation from /* to /index.html

The old workflow invalidated everything on every deploy. With Vite, every asset (JS, CSS, images) gets a content hash in its filename so they auto-invalidate when they change. The only file that keeps a stable name is index.html. Invalidating /* was burning CDN quota and slowing down cache warm-up for no reason.

Step 9 Set up isolated test infrastructure

Before merging any of this to main, we validated the entire pipeline end-to-end using:

  • Separate S3 buckets per platform (workflow-testing-1/2/3)
  • Separate CloudFront distributions pointing to those buckets
  • A separate trigger branch so the test workflow fires independently of production

Health checks in the test workflow point to the test CloudFront domains not the production URLs. This was a critical detail: if health checks hit production, a passing check tells you nothing about whether your test deploy actually worked.

Step 10 Merge, delete the platform branches, never look back

Once the workflow passed end-to-end on test infrastructure all three platforms deployed correctly from a single artifact, health checks green we merged to main and retired the per-platform branches.


What Shipping Looks Like Now

Before shipping a feature:

Platform Work
Launchpad Open PR → merge → wait for CI
Nexus Open PR → cherry-pick → resolve conflicts → merge
Orion Open PR → cherry-pick → resolve conflicts → merge
Total 3 PRs, ~30–45 min overhead, hope nothing diverged

After:

Platform Work
All three Open 1 PR → merge
Total CI handles the rest

Before a fix at 11pm:

  • Fix it on the main branch
  • Cherry-pick to Nexus (conflicts likely)
  • Cherry-pick to Orion (more conflicts)
  • Deploy all three
  • Manually verify all three are healthy
  • Update the Slack thread with "ok they're all good I think"

After:

  • Fix it
  • Merge
  • Watch CI confirm all three are healthy
  • Go to sleep

What This Unlocked

For developers: CI/CD went from something nobody wanted to touch to something you can actually iterate on. We built an isolated test path separate infrastructure, separate trigger branch, dedicated CDN distributions so pipeline changes can be validated without touching production. The mental model shifted from "three codebases that happen to look similar" to "one codebase that knows where it's deployed."

For the team: New engineers can onboard to the entire deployment model by reading one workflow file and one config module. No tribal knowledge about branch state, no institutional memory required, no "ask someone who knows which cherry-picks landed where."

For the business: Every platform ships the same tested artifact at the same time. No platform running a version that's two fixes behind. No inconsistent feature availability that support has to explain to enterprise clients. Adding a fourth platform is a config entry and a deploy job not a new branch to maintain forever.


The Takeaway

Three branches for three platforms is one of those decisions that feels obviously correct when you make it and obviously wrong in hindsight. It's not a bad call it's one that defers complexity into a form that's invisible until it's actively hurting you.

The underlying question is: where does your platform identity live? If the answer is "in Git history," you've made humans responsible for synchronization that machines should own. And humans are bad at synchronization not because they're careless, but because they have better things to do.

Move the identity into runtime config. Build the artifact once. Let the pipeline own distribution. The complexity doesn't disappear it moves somewhere that doesn't require three PRs to manage.

One branch. One build. One merge to ship everywhere.