Release Architecture¶
Branching model, versioning strategy, and repository split — from feature development to customer production.
Why GitFlow¶
GitFlow was built for versioned software with explicit release cycles and a clear line between ongoing development and production. That's exactly what ACEX needs: customers pin to specific package versions, and a broken release can't be recalled once it's on PyPI.
We use a simplified GitFlow: main and develop as long-lived branches, short-lived feature/* and fix/* branches, time-bounded release/x.y branches, and hotfix/* for emergencies.
What we dropped from classic GitFlow¶
No staging branch¶
The hierarchy feature → develop → release → staging → main has a redundant layer. Staging is a Kubernetes cluster, not a code state. The release/x.y branch is the staging artifact — publish it as a pre-release to PyPI, have GitLab install that version, deploy, and test. Fixes go on the same branch; you publish rc2. The branch merges to main only after staging passes.
DAST and load tests belong in GitLab, not GitHub¶
These tests exercise a running deployment, not source code. GitHub Actions has no access to the staging cluster, and internal testing infrastructure doesn't belong in a public repository. They run in GitLab CI, triggered after GitLab deploys the release candidate.
The merge-back from main to develop should be automated¶
GitFlow requires merging main back into develop after every release so fixes applied on the release branch aren't lost. This step is automated in the GitHub Actions release workflow.
Branching strategy¶
gitGraph
commit id: "init"
branch develop
checkout develop
commit id: "ongoing work"
branch feature/50001
checkout feature/50001
commit id: "OSPF redistribution"
checkout develop
merge feature/50001 id: "merge feature"
branch release/1.4
checkout release/1.4
commit id: "fix route-leak" tag: "v1.4.0rc1"
commit id: "fix route-leak #2" tag: "v1.4.0rc2"
commit id: "stable" tag: "v1.4.0"
checkout main
merge release/1.4 id: "release to PyPI"
checkout develop
merge main id: "merge-back"
After a release merges to main, an automated step merges main back into develop to carry any release-branch fixes forward.
Branch definitions¶
| Branch | Long-lived | Based on | Merges into | Purpose |
|---|---|---|---|---|
main |
Yes | — | — | Production source. Every commit here is a released version. No direct pushes — merge only from release/* or hotfix/*. |
develop |
Yes | — | — | Integration branch. All feature work lands here. Source for the next release branch. |
feature/* |
No | develop | develop | One branch per ticket or logical change. Deleted after merge. |
fix/* |
No | develop | develop | Non-urgent bugfixes that go through the normal release cycle. |
release/x.y |
No | develop | main + develop | Release candidate. Accepts only bug fixes — no new features. Source of RC builds deployed to staging. Deleted after merging to main. |
hotfix/* |
No | main | main + develop | Emergency fixes for a production defect that can't wait for the next release. |
Feature development¶
Every new task starts from develop:
Work happens on the feature branch. A pull request targets develop. GitHub Actions runs lint and unit tests. Peer review, approval, merge. Branch deleted.
No version bumps on feature branches. Feature branches are invisible to customers.
Keep PRs small
One logical change per branch. A PR touching eight unrelated packages is a signal the branch scope drifted — split it.
Release workflow¶
When the team decides to cut a release, a release branch is created from develop:
Tag the first release candidate and push:
GitHub Actions sees the tag and publishes acex==1.4.0rc1 to PyPI as a pre-release. From this point the branch accepts only bug fixes. Fixes are applied here and cherry-picked back to develop:
Each fix gets a new RC tag (rc2, rc3) and CI publishes it automatically.
Staging: environment, not branch¶
Staging is a Kubernetes cluster running the full ACEX stack. It is not a Git branch.
When a new RC is published to PyPI, the GitLab CI pipeline for the staging environment is triggered (via a GitHub webhook). It installs the RC version and deploys it:
The staging environment mirrors production as closely as possible: same Kubernetes version, same database schema, same Vault configuration, synthetic (non-production) customer ConfigMaps and NEDs.
Pre-release versions are safe on PyPI
pip install acex does not install pre-release versions by default. A customer who runs pip install acex will never pick up 1.4.0rc1 unless they explicitly pass --pre. The RC is published early and safely.
Testing strategy¶
Tests are divided by what they exercise: source code (GitHub) vs. running system (GitLab).
- Ruff lint and format check
- Unit tests per package
- Cross-package integration tests
- Branch name policy validation
- DAST (OWASP ZAP / GitLab DAST)
- Load testing (k6 or Locust)
- Stress testing
- End-to-end tests (Playwright / pytest)
- Integration tests against a live database and real device responses
GitLab system tests run after every RC deployment to staging. If any suite fails, the release branch stays open and the team cuts a new RC with a fix.
Promoting a release candidate to production¶
When all GitLab staging tests pass:
- Open PR:
release/1.4 → main - Code review confirms the diff contains only fixes from the RC cycle — no unreviewed changes
- Merge to
main - GitHub Actions detects the tag and publishes
acex==1.4.0to PyPI as stable - Tag
v1.4.0is created onmain - Automated merge-back:
main → develop release/1.4deleted
The promotion PR is lightweight by design
The content was already validated on staging. If the diff is large, something went wrong earlier in the process.
PyPI releases¶
ACEX uses semantic versioning following PEP 440. All packages in the monorepo share a single version — released as a coordinated set via a Git tag.
| Stage | Version format | Tag | PyPI behavior |
|---|---|---|---|
| Release candidate | 1.4.0rc1 |
v1.4.0rc1 |
Published; not installed by pip install acex without --pre |
| Stable release | 1.4.0 |
v1.4.0 |
Published; installed by default |
| Patch release | 1.4.1 |
v1.4.1 |
Hotfix path: from hotfix/* directly to main |
Convention: tags are only pushed from release/* and hotfix/* branches. Never from feature branches, never directly from develop. Enforce in code review.
Repository split¶
Repository: acex-labs/acex
- All ACEX framework source (
backend/,devkit/,client/,mcp/,cli/,worker/) - Generic device drivers (
drivers/cisco_ios_cli/,drivers/juniper_junos_cli/) - Local development environment (
docker-compose.yml,mock-device/) - Public documentation
- GitHub Actions CI: lint, unit tests, PyPI publish, GHCR agent image builds
.env.examplewith generic placeholders
Repositories: internal infrastructure
- Customer
ConfigMapsandNEDs - Customer
Dockerfiles - Kubernetes manifests (staging and production clusters)
- Helm charts with customer-specific values
- DAST configurations and scan history
- Load and stress test scenarios and results
- GitLab CI pipelines for customer image builds and deployments
- Vault policies and secret references
- Internal infrastructure as code (Terraform, etc.)
Never cross-commit
No customer identifier, hostname, NED path, or internal IP should ever appear in the GitHub repository — not in comments, not in example files, not in test fixtures. Mock devices use generic hostnames. .env.example uses generic placeholders.
Customer images¶
Customer images are built in GitLab CI using released PyPI packages as their ACEX source. Customer-specific configuration is injected at build time from private GitLab repositories.
# Private GitLab repo: acex-customer-{name}/Dockerfile
ARG ACEX_VERSION=1.4.0
FROM python:3.13-slim
RUN pip install \
acex==${ACEX_VERSION} \
acex-driver-cisco-ioscli==${ACEX_VERSION} \
acex-driver-juniper-junoscli==${ACEX_VERSION}
COPY configmaps/ /app/configmaps/
COPY neds/ /app/neds/
CMD ["python", "-m", "acex"]
Built and tagged in GitLab CI:
docker build \
--build-arg ACEX_VERSION=1.4.0 \
-t registry.gitlab.com/acex-labs/acex-customer-abc:1.4.0 .
The image is pushed to the private GitLab Container Registry and deployed to the customer's Kubernetes cluster. It never touches a public registry. For staging, substitute ACEX_VERSION=1.4.0rc1 and target the staging cluster.
CI/CD pipeline division¶
| Event | Runs in | Actions |
|---|---|---|
PR opened against develop |
GitHub | Lint, format check, unit tests, integration tests, branch name policy |
Tag v*rc* pushed |
GitHub | Publish pre-release to PyPI; send webhook to GitLab staging pipeline |
| New RC detected on PyPI | GitLab | Build staging image, deploy to staging cluster, run DAST + load + E2E |
Tag v* (stable) pushed |
GitHub | Publish stable version to PyPI, build & push agent images to GHCR, merge-back to develop |
| Stable version detected on PyPI | GitLab | Build customer images, push to private registry, deploy to customer clusters |
The connection between GitHub and GitLab is a webhook: GitHub Actions posts to a GitLab pipeline trigger URL when an RC or stable version is published. GitLab handles the rest privately. No GitLab credentials or internal pipeline details exist in the public repository.
Naming conventions¶
| Entity | Format | Example |
|---|---|---|
| Feature branch | feature/{ticket-id}-{slug} |
feature/50001-ospf-redistribution |
| Fix branch | fix/{ticket-id}-{slug} |
fix/51234-vlan-reference-null |
| Release branch | release/{major}.{minor} |
release/1.4 |
| Hotfix branch | hotfix/{ticket-id}-{slug} |
hotfix/51500-critical-auth-bypass |
| Git tag (RC) | v{major}.{minor}.{patch}rc{n} |
v1.4.0rc1, v1.4.0rc2 |
| Git tag (stable) | v{major}.{minor}.{patch} |
v1.4.0 |
| PyPI version | PEP 440 | 1.4.0rc1, 1.4.0 |
| Public agent image | ghcr.io/acex-labs/{image}:{version} |
ghcr.io/acex-labs/acex-collection-agent:1.4.0 |
| Private customer image | registry.gitlab.com/{org}/acex-{customer}:{version} |
registry.gitlab.com/acex-labs/acex-customer-abc:1.4.0 |
Complete lifecycle example¶
Feature: OSPF static route redistribution (ticket #50001) — from first commit to customer production.
-
Feature branch — Developer creates
feature/50001-ospf-redistributionfromdevelop. Makes commits, writes unit tests. -
GitHub CI — PR opened to
develop. Lint and unit tests pass. Peer review approves. Merged. Branch deleted. -
Release branch cut — Team cuts
release/1.4fromdevelop. Tagsv1.4.0rc1and pushes. -
GitHub Actions — Detects tag
v1.4.0rc1. Publishesacex==1.4.0rc1as pre-release to PyPI. Sends webhook to GitLab staging pipeline. -
GitLab — Staging — Builds staging image with
acex==1.4.0rc1and synthetic ConfigMaps. Deploys to staging cluster. Runs DAST, load tests, E2E suite. -
Bug found — E2E test reveals a route-leak. Developer creates
fix/50002-route-leakfromrelease/1.4. Fix applied. Cherry-picked todevelop. Tagsv1.4.0rc2. Staging tests re-run — all pass. -
Promotion — Team approves. Tags
v1.4.0onrelease/1.4. PR opened:release/1.4 → main. -
Merged to
main— Review confirms only RC fixes in the diff. Merged. GitHub Actions publishesacex==1.4.0to PyPI (stable). Automated merge-back todevelop.release/1.4deleted. -
GitLab — Production — Detects stable
1.4.0on PyPI. Builds customer image with real ConfigMaps and NEDs. Pushesregistry.gitlab.com/acex-labs/acex-customer-abc:1.4.0. -
Customer production — Image deployed to customer Kubernetes cluster. ACEX 1.4.0 is live. The customer never touched the public GitHub repository.
Two quality gates: GitHub CI (source) and GitLab staging (system). Every change passes both before any customer sees it.