Make the entire bnc-cpt-doc Obsidian vault browsable in a web browser at a stable internal URL, while keeping the same files unmodified for use inside Obsidian. The site is auth-gated via Google Identity-Aware Proxy (IAP) — only authenticated members of the team's Google Workspace / IAM group can read it. Editing remains in Obsidian; the published site is read-only.
The pipeline is intentionally minimal: run a build command inside the vault, sync the output to a GCS bucket, and let an existing HTTPS Load Balancer + IAP enforce auth.
wikilinks, backlinks, graph view, callouts, transclusions, embeds. Other tools require plugins (e.g. mkdocs-roamlinks-plugin) and still lose graph/backlinks.public/ — fits any static host (GCS, S3, Pages, etc.).bnc-cpt-doc/:bnc-cpt-doc/quartz.config.ts — site title, base URL, theme, plugin pipeline.bnc-cpt-doc/quartz.layout.ts — page layout (header, sidebars, graph view).bnc-cpt-doc/content/ — symlink or contentRoot config pointing at bnc-cpt-doc/doc/md/. The vault is not moved.bnc-cpt-doc/public/ — build artifact (gitignored).bnc-cpt-doc/ as the vault root; Quartz reads the same doc/md/ tree at build time.${var.org}-${var.app}-doc-site (single, non-per-env — docs are environment-agnostic). Provisioned via the existing module bnc-cpt-inf/src/terraform/modules/gcp-bucket-for-web-site-static-v01/.allUsers is not granted roles/storage.objectViewer (this is the module's current default for static sites; an enable_iap flag is added to switch the binding off).05-ssl-load-balancer.tf (already in place for other static sites). Backend bucket points to the docs bucket.A record (docs.${ROOT_DOMAIN} or similar) pointing at the LB's static IP, provisioned in step 007-dns.do_build_and_publish_docs (in bnc-cpt-utl/src/bash/run/build-and-publish-docs.func.sh). Runs locally as ysg. Use cases: post-spec-edits, pre-review, ad-hoc.bnc-cpt-doc push to master — runs the same shell action from a runner. Allows the site to stay in sync without anyone running anything locally.gsutil -m rsync -d -r is safe to run repeatedly; only changed files are uploaded.npx quartz create initializes scaffolding, then npm install in bnc-cpt-doc/.bash
sudo -u ysg bash -c 'cd /opt/bnc/bnc-cpt/bnc-cpt-doc && npx quartz build'
Produces bnc-cpt-doc/public/ (gitignored).bash
sudo -u ysg bash -c 'cd /opt/bnc/bnc-cpt/bnc-cpt-doc && npx quartz build --serve'
Serves on http://localhost:8080.sudo -u ysg bash -c 'cd /opt/bnc/bnc-cpt/bnc-cpt-utl && ./run -a do_build_and_publish_docs'
The shell action:
1. Resolves ${ORG}, ${APP} from the existing do_set_vars_v205.
2. cd into bnc-cpt-doc/ and runs npx quartz build.
3. Activates the deployer service account from ~/.gcp/.${ORG}/key-${ORG_APP}-inf.json.
4. gsutil -m rsync -d -r public/ gs://${ORG}-${APP}-doc-site/ — -d removes objects no longer in source, -r recurses, -m parallelizes.
5. (If Cloud CDN is enabled in front of the LB) issues gcloud compute url-maps invalidate-cdn-cache for /*.
bnc-cpt-doc/.github/workflows/publish-docs.yml.push: branches: [master] and workflow_dispatch.npx quartz build → google-github-actions/auth (Workload Identity Federation, no JSON keys) → gsutil rsync → CDN invalidate.roles/storage.objectAdmin on ${ORG}-${APP}-doc-site (and CDN-invalidate role if applicable) — least privilege.08-Project-Health/, ad-hoc tracking, security analyses) that must not leak.roles/iap.httpsResourceAccessor to:csitea-developers@csitea.net) — preferred, additions/removals are managed at the group level.publish: false or exclude folders from the build. The full vault is published, but only authorized humans can browse it.publish: true/false) is available.gitleaks-style) before publishing — added as a pre-publish step in the workflow.302 to Google Sign-in for unauthenticated bots; pages are not crawled. As an extra safety net, the published site emits a <meta name="robots" content="noindex"> from the Quartz config.bnc-cpt-doc/quartz.config.ts)pageTitle: e.g. "BNC-CPT — Engineering Docs" (env-driven via env var so the morph template story keeps working).baseUrl: the IAP-protected hostname, e.g. docs.carpulsetracker.com.enableSPA: true (smooth nav).enablePopovers: true (link previews on hover).FrontMatter, CreatedModifiedDate, Latex, SyntaxHighlighting, ObsidianFlavoredMarkdown (wikilinks, callouts), GitHubFlavoredMarkdown, TableOfContents, CrawlLinks, Description.RemoveDrafts.ContentPage, FolderPage, TagPage, ContentIndex, Assets, Static, NotFoundPage, Favicon — plus AliasRedirects so any inbound legacy links from the pre-reorg paths resolve.theme.colors: align with WUI brand palette (loaded from bnc-cpt-wui design tokens — manual mirror at v1.0, automated lookup later).bnc-cpt-doc/quartz.layout.ts)Component.Explorer({ folderClickBehavior: "collapse" })).bnc-cpt-utl/src/bash/run/build-and-publish-docs.func.sh)do_build_and_publish_docs.DOC_DIR — defaults to ${APP_PATH}/${ORG_APP}-doc.BUCKET_NAME — defaults to ${ORG}-${APP}-doc-site.GCP_KEY — defaults to ~/.gcp/.${ORG}/key-${ORG_APP}-inf.json.INVALIDATE_CDN — defaults to true if a Cloud CDN URL map exists.npx quartz build → gcloud auth activate-service-account → gsutil -m rsync -d -r → optional CDN invalidate → emit success log with bucket URL.${LOG_DIR}/${RUN_UNIT}.${YYYYMMDD}.log.A. Reusable static-site module enhancement (bnc-cpt-inf/src/terraform/modules/gcp-bucket-for-web-site-static-v01/):
enable_iap (default false — preserves backward compat for existing public sites).enable_iap = true:google_storage_bucket_iam_member.public_read (allUsers) binding.google_compute_backend_bucket_iam_member granting roles/storage.objectViewer on the bucket to the LB's backend-bucket service account (or simpler: keep using the existing service account already used by the LB).iap { enabled = true, oauth2_client_id = ..., oauth2_client_secret = ... } on the google_compute_backend_service (note: backend buckets do NOT support IAP directly today on all GCP product versions — if backend-bucket IAP is unavailable in ~> 5.0 provider, route via a backend-service with a CDN-only origin pointing at the bucket; documented as an implementation note in the module).google_iap_brand (one per GCP project, idempotent) and google_iap_client.iap_member_emails and iap_member_groups variables; produce google_iap_web_backend_service_iam_member bindings for each.B. New step bnc-cpt-inf/src/terraform/017-gcp-doc-site/:
enable_iap = true and the iap_member_groups = [group:csitea-developers@csitea.net].wui_fqdn / dns_zone_name patterns from step 015.C. DNS (extension to step 007-dns):
docs.${root_domain} A record pointing at the new LB IP.D. New Secret Manager entries (step 029-create-gcp-secrets):
${ORG}-${APP}-iap-oauth-client-id${ORG}-${APP}-iap-oauth-client-secretE. Service Account (step 003-gcp-iam-users or alongside step 017):
${ORG}-${APP}-doc-deployer@…iam.gserviceaccount.com with:roles/storage.objectAdmin scoped to gs://${ORG}-${APP}-doc-site.roles/compute.urlMapsAdmin (CDN cache invalidate) — optional.~/.gcp/.${ORG}/key-${ORG_APP}-doc-deployer.json (or use Workload Identity Federation in CI to avoid keys altogether)..github/workflows/publish-docs.yml in bnc-cpt-doc)name: Publish docs
on:
push: { branches: [master] }
workflow_dispatch:
jobs:
publish:
runs-on: ubuntu-latest
permissions: { contents: read, id-token: write }
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: '22' }
- run: npm ci && npx quartz build
- id: leak-scan
run: gitleaks detect --no-banner --source .
- uses: google-github-actions/auth@v2
with:
workload_identity_provider: ${{ secrets.WIF_PROVIDER }}
service_account: ${{ secrets.DOC_DEPLOYER_SA }}
- uses: google-github-actions/setup-gcloud@v2
- run: gsutil -m rsync -d -r public/ gs://${{ secrets.DOC_BUCKET }}/
- run: gcloud compute url-maps invalidate-cdn-cache ${{ secrets.DOC_URL_MAP }} --path "/*" --async
bnc-cpt-doc/.gitignore:public/ (Quartz build output).node_modules/..quartz-cache/.content/ as the default content root; configure contentRoot: "doc/md" in quartz.config.ts so the existing tree is reused without symlinks.enable_https_redirect flag in the module).| # | Question | Decision |
|---|---|---|
| 1 | Static site generator | Quartz 4 — Obsidian-native (wikilinks, backlinks, graph, callouts, transclusions). MkDocs/Docusaurus rejected on Obsidian-feature parity grounds |
| 2 | Hosting | Private GCS bucket behind HTTPS LB — reuses existing gcp-bucket-for-web-site-static-v01 module; cheaper than Cloud Run, no cold start |
| 3 | Authentication | Auth-gated via Google IAP (Option 2) — public bucket rejected because vault contains internal-only material (project-health notes, security analyses, ad-hoc tracking) |
| 4 | Per-doc privacy filter | Not needed — full vault is published behind IAP; per-doc filtering deferred until a need to share a subset publicly arises |
| 5 | Build trigger | Manual shell action (do_build_and_publish_docs) + GitHub Actions on push to master — same code path; CI uses Workload Identity Federation (no key files) |
| 6 | Bucket naming / per-env | Single env-agnostic bucket ${ORG}-${APP}-doc-site — docs are not environment-specific |
| 7 | Vault layout disturbance | Zero — Quartz contentRoot points at doc/md/; no symlinks, no moves, Obsidian opens the same vault unchanged |
| 8 | Allow-list management | Google Workspace group (e.g. csitea-developers@csitea.net) — never per-user IAM bindings |
| 9 | DNS hostname | docs.${root_domain} via extension to existing step 007-dns |
| 10 | Search engine exposure | Belt-and-suspenders: IAP redirects unauthenticated bots to sign-in; Quartz also emits <meta name="robots" content="noindex"> |
| 11 | Pre-publish secret scan | Required — gitleaks in CI before gsutil rsync; prevents accidental leakage even though access is gated |
| 12 | CI auth method | Workload Identity Federation — no JSON keys checked into Actions secrets |
Operational Specification v1.0.0 — Quartz 4 · Private GCS bucket behind HTTPS LB · IAP auth-gated · Workspace-group allow list · Zero vault disturbance · Manual + CI publish