feat(guide): Processes section + bcos-only audience tier (BCOS_ADMIN)

New 3-tier gating: customer < internal (MSP/BCOS) < bcos (BCOS_ADMIN only).
GuideController.isBcos + service visible() honour audience: bcos. New 'processes'
app with Develop / Main / Release / Gitea-Issue process docs (audience bcos),
rendered as a Processes nav section only BCOS_ADMIN can see. Verified: BCOS 200,
MSP/customer 404.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-01 16:09:33 -04:00
parent 2488408ee9
commit 4d56db455e
7 changed files with 203 additions and 9 deletions
@@ -23,12 +23,12 @@ public class GuideController {
@GetMapping("/nav")
public List<NavApp> nav(Authentication auth) {
return service.nav(isInternal(auth));
return service.nav(isInternal(auth), isBcos(auth));
}
@GetMapping("/modules/{module}")
public ResponseEntity<ModuleView> module(@PathVariable String module, Authentication auth) {
return service.module(module, isInternal(auth))
return service.module(module, isInternal(auth), isBcos(auth))
.map(ResponseEntity::ok)
.orElseGet(() -> ResponseEntity.notFound().build());
}
@@ -39,4 +39,12 @@ public class GuideController {
.map(a -> a.getAuthority())
.anyMatch(a -> a.equals("ROLE_MSP_ADMIN") || a.equals("ROLE_BCOS_ADMIN"));
}
/** BCOS_ADMIN only — gates the strictest tier (audience: bcos), e.g. the Processes section. */
private boolean isBcos(Authentication auth) {
if (auth == null) return false;
return auth.getAuthorities().stream()
.map(a -> a.getAuthority())
.anyMatch(a -> a.equals("ROLE_BCOS_ADMIN"));
}
}
@@ -79,15 +79,19 @@ public class MarkdownGuideService {
return new GuideDoc(app, module, title, tab, order, audience, body);
}
private boolean visible(GuideDoc d, boolean includeInternal) {
return includeInternal || !"internal".equals(d.audience());
/** audience tiers: customer (all) < internal (MSP/BCOS admin) < bcos (BCOS admin only). */
private boolean visible(GuideDoc d, boolean includeInternal, boolean includeBcos) {
String a = d.audience();
if ("bcos".equals(a)) return includeBcos;
if ("internal".equals(a)) return includeInternal;
return true;
}
/** apps → modules → tabs, filtered by audience. */
public List<NavApp> nav(boolean includeInternal) {
public List<NavApp> nav(boolean includeInternal, boolean includeBcos) {
Map<String, Map<String, List<GuideDoc>>> byApp = new TreeMap<>();
for (GuideDoc d : docs) {
if (!visible(d, includeInternal)) continue;
if (!visible(d, includeInternal, includeBcos)) continue;
byApp.computeIfAbsent(d.app(), k -> new TreeMap<>())
.computeIfAbsent(d.module(), k -> new ArrayList<>()).add(d);
}
@@ -107,9 +111,9 @@ public class MarkdownGuideService {
}
/** one module with its tabs (markdown bodies), filtered by audience. */
public Optional<ModuleView> module(String moduleKey, boolean includeInternal) {
public Optional<ModuleView> module(String moduleKey, boolean includeInternal, boolean includeBcos) {
List<GuideDoc> mdocs = docs.stream()
.filter(d -> d.module().equals(moduleKey) && visible(d, includeInternal))
.filter(d -> d.module().equals(moduleKey) && visible(d, includeInternal, includeBcos))
.sorted(Comparator.comparingInt(GuideDoc::order))
.toList();
if (mdocs.isEmpty()) return Optional.empty();
@@ -0,0 +1,36 @@
---
module: processes.develop
title: Develop Process
tab: Overview
order: 10
audience: bcos
---
How day-to-day work flows on `develop`. **BCOS admin only.**
## The rule
**Always work on `develop`, never `main` directly.** Every sub-project repo has branch protection
on `main`. Before any change, `cd` into the sub-project and confirm you're on `develop` (or a
feature branch off it).
**Repos that intentionally stay on `main`** (no `develop`): `hiveops-src` (monorepo root),
`hiveops-docs`, `hiveops-release`, `hiveops-devops`.
## Flow
1. **`git pull` first** — remotes move; skipping this causes rebase/merge conflicts on push.
2. `cd /source/hiveops-src/hiveops-<name>` → `git checkout develop`.
3. Make the change. For a new feature, branch off `develop`; for small/content changes, commit
straight to `develop` (established pattern).
4. Push → open a **PR to `develop`** for feature branches (CI build + tests must pass), then merge.
5. **Deploy to bcos.dev first and smoke-test** the real path before calling it done — a green
container is not a smoke test. Only then does it move toward `main` (see [[processes.main]]).
## Conventions
- Conventional commits: `feat(scope): …`, `fix`, `chore`, `docs`, `content`. End commit messages
with the `Co-Authored-By: Claude …` line.
- **Commit before stopping** — never leave uncommitted work in any repo.
- **Issue first** — no code without a Gitea issue (see [[processes.gitea-issues]]).
## Do NOT
- Do NOT commit directly to `main` or bypass branch protection.
- Do NOT skip the bcos.dev smoke test before promoting.
@@ -0,0 +1,44 @@
---
module: processes.gitea-issues
title: Gitea Issue Process
tab: Overview
order: 10
audience: bcos
---
How we track work in Gitea. **BCOS admin only.** Git hosting is **Gitea** (not GitHub/GitLab).
## Issue first — always
**Any work, however phrased ("just do X", "develop Y"), gets a Gitea issue created BEFORE code.**
No issue = don't start. Before fixing a bug, **search the repo's open `Bug`s first** — one may
already exist (avoid duplicates).
## Labels (org-level, shared across all `hiveiq-src` repos)
- **Type:** `Bug`, `Feature`, `Enhancement`, `Breaking`, `Documentation`, `Security`, `Testing`
- **Priority:** `Priority/Critical` · `High` · `Medium` · `Low`
- **Review/Triage:** `Reviewed/Confirmed` · `Duplicate` · `Invalid` · `Won't Fix`
- **Status:** `Status/Blocked` · `Need More Info` · `Abandoned`
- **Workflow:** `Verify Fix` (fix deployed, verify before closing) · `Reopen` (regressed) ·
`Tested/Not Fixed`
## Assignment
- **New issue** → assign `hiveiq` (triage / fix owner).
- **Fix deployed** → reassign to the verifier and apply **`Verify Fix`**.
- **Frontend** fixes → **tristan** (QA). **Backend** fixes → **johannes** (tristan can't test backend).
- `erin` has no Gitea account yet — assign only `tristan` until she does.
## Closing — always manual
**Never close an issue via the API.** Claude applies labels and comments only. The team closes
issues in the Gitea UI after verification. Never PATCH `state: closed`.
## A fix isn't done until its issue is reconciled
After deploying a fix: **comment the fix on the matching issue + apply `Verify Fix` + assign the
verifier.** Never leave a deployed fix with a stale/untouched issue.
## PR approval (ties to [[processes.main]])
Johannes approves the PR in the Gitea UI (SOC2 gate) — never Claude. After approval, Claude merges
via API.
## API note
Create/label/assign via the Gitea REST API with the `hiveiq` token. `directlx` = human reviewer
(Johannes). Use `GIT_SSL_NO_VERIFY=true` for Gitea calls (self-signed internal cert).
@@ -0,0 +1,42 @@
---
module: processes.main
title: Main Process
tab: Overview
order: 10
audience: bcos
---
How code reaches `main`. **BCOS admin only.** `main` is branch-protected — **no direct commits, ever.**
## Two — and only two — ways onto `main`
### 1. Routine release: `develop → main`
- Open a PR `develop → main`. CI must pass; **1 approval required**.
- **Johannes approves in the Gitea UI** (SOC2 gate) — Claude never approves.
- After approval, **Claude merges via API**.
### 2. Production hotfix: `hotfix/* → main`
- **Never `develop → main` for a production fix.** A prod fix goes on a `hotfix/*` branch → PR
targeting `main` → Johannes approval → merge.
- This keeps unrelated in-flight `develop` work out of a targeted production fix.
## The SOC2 gate (non-negotiable)
1. Johannes **approves the PR in the Gitea UI** — never Claude, never programmatically.
2. Claude **merges via API** only after approval.
3. **PAUSE.** Merging is *not* deploying. After merge, **wait for an explicit "deploy" go-ahead.**
Never chain merge → build → deploy automatically — deploy is its own approval.
## Deploy (once explicitly authorized) — production is blue/green
- Blue/green services: `auth · incident · devices · fleet · transactions · agent-proxy`.
Everything else is single-instance.
- **Deploy to the STANDBY slot only** (check the active colour first). Verify, then switch.
- **The switch is not just the nginx line** — internal service-to-service callers go by container
name directly (`*_SERVICE_URL=http://hiveiq-<svc>-<colour>:port` in `compose/*.yml`), bypassing
nginx. Move **every** caller too, or the real code path stays on the old slot.
- **Keep the old slot running** until the new one is verified — it's the rollback. Stop it only
after verification passes.
## Do NOT
- Do NOT merge or deploy before Johannes approves the PR.
- Do NOT bundle multiple actions under one "go" — one decision per approval gate.
- Do NOT deploy to the active blue/green slot.
@@ -0,0 +1,43 @@
---
module: processes.release
title: Release Process
tab: Overview
order: 10
audience: bcos
---
How we cut and ship releases. **BCOS admin only.** Golden order: **bcos.dev first → verify → PR to
`main` → production.** Never skip staging.
## Microservice release
1. Merge to `main` via the [[processes.main]] gate.
2. Build + push: `./build.sh --registry registry.bcos.cloud --push` (image `hiveiq-<svc>`).
3. Deploy on the production services VM (`173.231.195.250`): `docker compose pull hiveiq-<svc>`
→ `docker compose up -d hiveiq-<svc>` — blue/green rules apply (see [[processes.main]]).
4. Update **VERSIONS.md** and add a **What's New** entry (below).
## Agent release (`hiveops-agent`)
1. **Bump first** (current version in repo = already in prod): `mvn versions:set -DnewVersion=X.Y.Z
-DgenerateBackupPoms=false`.
2. `mvn clean package -DskipTests` → build fat JAR.
3. Build the 4 dist packages (`deployment/build-dist.sh`, run sequentially — it clears `dist/`
each run: standard+patch × windows+linux). SCP each to the CDN VM (`173.231.195.252`)
`downloads/agent/`.
4. On the CDN VM: `./scripts/release-agent.sh X.Y.Z <all 5 args>` — must pass all 5 so patches
appear in Fleet Artifacts.
5. Commit the version bump + push; update **VERSIONS.md**.
## Browser release (`hiveops-browser`, CDN — not Docker)
Build installers (`npm run build:win|linux`) → SCP to CDN `downloads/` → on the CDN VM
`./scripts/release-browser.sh X.X.XX <exe> <AppImage> <deb>` → commit `downloads/` → `./deploy.sh`.
## What's New (every release)
Edit `hiveops-release/releases.json` — **prepend** the entry under the app's key — then
`./deploy.sh` in `hiveops-release`. Section types: `added / fixed / improved / changed / removed`.
Verify at `https://cdn.bcos.cloud/whats-new?app=<key>`.
## Do NOT
- Do NOT ship an unverified build to a customer-facing channel (a fleet-wide `latest.json` flip,
an ungated CDN publish). Stage on bcos.dev → QA → prod, no skips.
- Do NOT frame a customer-facing release as routine plumbing when asking for approval — name it as
exactly what it is.
+18 -1
View File
@@ -51,9 +51,11 @@
// group nav: product-app "Modules" section (all except platform/technical) + platform + technical
$: searching = !!search.trim();
$: productApps = filtered.filter(a => a.app !== 'platform' && a.app !== 'technical');
const SPECIAL = ['platform', 'technical', 'processes'];
$: productApps = filtered.filter(a => !SPECIAL.includes(a.app));
$: platformApp = filtered.find(a => a.app === 'platform') ?? null;
$: technicalApp = filtered.find(a => a.app === 'technical') ?? null;
$: processesApp = filtered.find(a => a.app === 'processes') ?? null;
async function loadNav() {
loadingNav = true;
@@ -160,6 +162,21 @@
{/if}
</div>
{/if}
<!-- PROCESSES (BCOS admin only — API-gated) -->
{#if processesApp}
<div class="nav-sec">
<button class="sec-head" on:click={() => toggle('processes')}>
<span class="chev">{(searching || open['processes']) ? '▾' : '▸'}</span> Processes
</button>
{#if (searching || open['processes'])}
{#each processesApp.modules as m (m.module)}
<button class="nav-item" class:active={m.module === activeModuleKey}
on:click={() => selectModule(m.module)} title={m.module}>{m.title}</button>
{/each}
{/if}
</div>
{/if}
{/if}
</nav>