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:
@@ -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.
|
||||
@@ -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>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user