Install
$ agentstack add skill-bendaamerahmed-backstage-idp-plugin-backstage-techdocs ✓ scanned · ✓ verified, works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ✓ Network access No
- ✓ Filesystem access No
- ✓ Shell / process execution No
- ✓ Environment & secrets No
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
Verified badge
Passed review? Show it. Paste this badge into your README, it links to the public security report.
Reliability & compatibility
Declared compatibility
Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.
We're building live execution health for every listing: tool-call success rate, median latency, uptime, and last-checked timestamps, measured, not self-reported. It isn't live yet, so we don't show numbers we can't stand behind.
How agent discovery & health will work →About
Backstage TechDocs
Wire the docs-like-code pipeline correctly, and tell apart the four places it breaks: the entity annotation, the mkdocs source, the generator, and the publisher.
Preconditions
- Release line from
backstage.json; TechDocs packages resolved withyarn why @backstage/plugin-techdocs-backendandyarn why @techdocs/cli. - Backend generation:
backend.add(import('@backstage/plugin-techdocs-backend'))inpackages/backend/src/index.tsis the new backend system; acreateRouterinpackages/backend/src/plugins/techdocs.tsis legacy — migrate it (backstage-plugin-migrate) before adding a build-strategy module. - Frontend generation: NFS (default since v1.49) registers TechDocs and its addon modules as
featuresfrom/alphaexports; legacy hasTechDocsIndexPage/TechDocsReaderPageinside `and anEntityTechdocsContenttab inEntityPage.tsx`. - The effective merged config, not
app-config.yamlalone:yarn backstage-cli config:print --lax. Production almost always overridestechdocs.*inapp-config.production.yaml. - Whether the runtime has Docker (for
runIn: docker) or Python withmkdocs-techdocs-core(forrunIn: local).
Procedure
- Read the triple before anything else.
techdocs.builder,techdocs.generator.runIn,techdocs.publisher.typedetermine which failures are even possible.
builder: 'local'— the backend generates and publishes on demand when a user opens the docs tab. Out-of-the-box default.builder: 'external'— the backend only reads pre-built docs from storage. It will never build, no matter what is wrong with the source. This is the recommended production architecture.publisher.type:'local'|'awsS3'|'googleGcs'|'azureBlobStorage'.localwrites totechdocs.publisher.local.publishDirectory, defaulting to astaticdir at the backend root — pod-local, lost on restart, and invisible to sibling replicas.
- Check the entity side second. In
catalog-info.yaml,metadata.annotations['backstage.io/techdocs-ref']is the only thing that points TechDocs at source:
dir:.— docs live besidecatalog-info.yaml;dir:./sub-folderfor a subdirectory.url:https://github.com/org/repo/tree/(GitLaburl:https://host/org/repo, Bitbucket.../src/, Azure.../_git/). Suffix a subdirectory path with/so relative paths resolve.- An entity with no
techdocs-refgets no docs tab content. A monorepo child that should show a parent's docs usesbackstage.io/techdocs-entity: :/(plusbackstage.io/techdocs-entity-pathto deep-link), not a duplicatetechdocs-ref. - A
url:target must be reachable by anintegrations.*credential in the backend's config, not just yours (backstage-repo-discovery).
- Check the source layout third. At the root of whatever
techdocs-refresolves to:mkdocs.yml(ormkdocs.yaml) plus adocs/directory containing at minimumindex.md. Minimum config:
```yaml site_name: 'example-docs' nav:
- Home: index.md
plugins:
- techdocs-core
```
techdocs-core is injected automatically when absent unless techdocs.generator.mkdocs.omitTechdocsCorePlugin: true. Rename docs/ only via mkdocs' own docs_dir key. nav is optional — omitting it makes MkDocs infer navigation from the file tree.
- Reproduce locally before changing any config. From the directory holding
mkdocs.yml:npx @techdocs/cli serve(Docker, full Backstage-like reader on :3000) ornpx @techdocs/cli serve --no-docker.serve:mkdocsgives a bare MkDocs server, which isolates whether a problem is MkDocs or the TechDocs reader. Add--mkdocs-parameter-strictto make warnings — dead links, files absent fromnav— fail the build instead of silently producing empty pages. - Choose
runIndeliberately; it is a deployment constraint, not a preference.
dockerpullsspotify/techdocsand needs a usable Docker socket. Inside a containerised Backstage this means Docker-in-Docker or a mounted host socket — usually unavailable in Kubernetes, and a privilege escalation where it is.localrunsmkdocsfrom the backend image'sPATH. You must bake it in: installpython3/python3-pip/python3-venvbefore theUSER nodeline inpackages/backend/Dockerfile, create a venv, thenpip3 install mkdocs-techdocs-coreafter all other Python packages so it wins dependency resolution. Python 3.11+.
Pin the version (mkdocs-techdocs-core==) so a backend rebuild cannot silently change rendering.
- For production, move generation out of the backend. Set
techdocs.builder: 'external', configure a cloud publisher, and generate in each entity repository's CI. Do not do the config flip and the CI rollout in the same change — anexternalbackend with nothing yet in the bucket serves 404s for every entity. - Write the CI job as generate-then-publish, two distinct steps.
``bash npx @techdocs/cli generate --no-docker --source-dir . --output-dir ./site --etag "$COMMIT_SHA" npx @techdocs/cli publish --publisher-type awsS3 --storage-name "$TECHDOCS_S3_BUCKET_NAME" \ --entity default/Component/my-component --directory ./site ``
--entityis the//triplet and must match the catalog entity exactly; it determines the storage path the backend later reads.--etag(commit SHA) lands intechdocs_metadata.jsonand is what the backend compares to decide freshness. Omitting it disables staleness detection.--no-dockerrequiresmkdocs-techdocs-corein the CI runner. Without it, use the default Docker path and drop the flag.- Credentials:
AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY/AWS_REGION,GOOGLE_APPLICATION_CREDENTIALS, or AzureDefaultAzureCredentialenv vars.publishalso accepts--awsRoleArn,--awsBucketRootPath,--gcsBucketRootPath,--azureAccountName/--azureAccountKey. - Trigger on merges to the default branch only, and only when docs paths changed, unless you want every commit rewriting the bucket.
- Split the credentials by direction. CI needs write: S3
s3:ListBucket,s3:PutObject,s3:DeleteObject,s3:DeleteObjectVersion; GCS object+bucket create; AzureStorage Blob Data Owner. The Backstage backend withbuilder: 'external'needs read only: S3s3:ListBucket+s3:GetObject; AzureStorage Blob Data Reader. Give the backend a read-only principal — it is the cheapest guarantee that a misconfigured backend cannot wipe published docs. - Use a hybrid build strategy instead of an all-or-nothing flip when migrating. Keep
builder: 'local', point the publisher at cloud storage, and implementDocsBuildStrategyfrom@backstage/plugin-techdocs-nodein a backend module registered againsttechdocsBuildsExtensionPoint(techdocs.setBuildStrategy(...)), gated on an entity annotation. Read the extension point's method names from the installed package's types before writing the call. - Add non-core mkdocs plugins in the place that actually executes MkDocs. CI/
--no-docker: install via pip in the runner image.runIn: docker: build aFROM spotify/techdocs:image with the plugin pip-installed, publish it, and settechdocs.generator.dockerImage. Then declare it in eachmkdocs.yml, or fleet-wide viatechdocs.generator.mkdocs.defaultPlugins/ the CLI's--defaultPlugin. Air-gapped runners also needtechdocs.generator.mkdocs.disableExternalFonts: true(CLI--disableExternalFonts) or MkDocs blocks on Google Fonts. - Install addons as modules, matching the app's generation. Under NFS,
yarn --cwd packages/app add @backstage/plugin-techdocs-module-addons-contrib, then register the module's/alphaexport (e.g.techDocsReportIssueAddonModule) in the app'sfeatures. Addons render in registration order. Legacy apps wrap the reader page in `inApp.tsx` instead.
Verification
- Source builds standalone:
npx @techdocs/cli generate --no-docker --source-dir --output-dir ./site --verboseexits 0 and producessite/index.htmlandsite/techdocs_metadata.json. No metadata file means no publishable site regardless of exit code. - Storage contains the entity: list the bucket under the lowercased
///prefix and confirmindex.htmlandtechdocs_metadata.jsonare present with a recent timestamp. - Backend can read it: hit the techdocs-backend metadata and static endpoints for the entity (
/api/techdocs/..., with a token if auth is enforced) — confirm the exact route shapes against the installed@backstage/plugin-techdocs-backendrouter rather than assuming them. Metadata 200 + staticindex.html200 means the publisher half is correct and any remaining problem is frontend. - With
builder: 'local', the sync endpoint streams build events; a non-cached/updatedterminal event is the real generator error, which the UI usually swallows. yarn backstage-cli config:check --laxafter editingtechdocs.*, and re-print the merged config to confirm the production override took.- Nav integrity:
--mkdocs-parameter-strictonserveturns orphaned files and dead links into failures.
Failure modes
- "No documentation found" / empty docs tab for one entity. In order: missing or malformed
backstage.io/techdocs-ref; the ref points at a directory with nomkdocs.yml; the entity was never published under that exact triplet (a rename inmetadata.nameorphans the old storage path);builder: 'external'and CI has simply never run for that repo. Distinguish by listing the bucket prefix — present in storage means a backend/annotation problem, absent means a pipeline problem. - Docs missing only for repos with a
.gitattributes.export-ignorestrips markdown, assets, ormkdocs.ymlfrom the archive the backend downloads, so the preparer sees a directory with no docs. The source looks perfect in the browser. Fix the.gitattributesor move that repo to CI generation. - Docs render but are permanently stale. With
builder: 'external', the backend never rebuilds — the bucket is the only source of truth, so a stale page means CI did not run or published to a different triplet. Withbuilder: 'local'and a cloud publisher, staleness is decided by theetagintechdocs_metadata.json; a pipeline that omits--etag, or that always writes the same value, makes every subsequent check report "up to date". Also checktechdocs.cache.ttlbefore blaming the pipeline. - Docs stale or flapping across page loads with
publisher.type: 'local'and >1 replica. Each pod built its own copy into its own filesystem; the load balancer decides which vintage you see.localpublishing is single-replica-only. Move to object storage. Config file '/content/mkdocs.yml' does not exist.The generator mounted a directory that has no mkdocs config at its root — usuallydir:.on a monorepo where docs live in a subfolder, ormkdocs.yamlvsmkdocs.ymlmismatched against a hardcoded name. Fix the ref, not the generator.The "" plugin is not installedfrom MkDocs. The plugin is declared inmkdocs.ymlbut absent from whatever runs MkDocs. It builds on a laptop with the plugin pip-installed and fails in the generator image. Add it to the custom image (runIn: docker) or the CI/backend image (--no-docker/runIn: local) — never by removing the declaration, which silently drops the feature.- Nav renders but pages 404, or pages exist but are unreachable.
naventries are paths relative todocs_dir; a typo yields a nav link to nothing, and a file absent fromnavis built but unlinked. Both are MkDocs warnings, so the build succeeds and the site is quietly broken. Run with--mkdocs-parameter-strictin CI. Links between markdown files must target the.mdfile, not the generated URL. AccessDenied/403from the publisher duringpublish. A write failure: the CI principal lackss3:PutObject/DeleteObject(deletes are needed to clear stale files, so a put-only policy fails only on updates), or the bucket policy/KMS key rejects it, ortechdocs.publisher.awsS3.ssedisagrees with the bucket's enforced encryption. The same error from the running backend is a read failure and means the backend's principal is missings3:GetObject/ListBucket— the CI job is fine and the docs are in the bucket.- Build failure vs publish failure — they need opposite fixes.
generatefailing is a source problem: MkDocs config, a missing plugin, bad markdown. Nothing reaches storage and the previously published site keeps serving, so users see stale docs and no error.publishfailing after a successfulgenerateis an infrastructure problem: credentials, bucket, network. Never wire them as one shell command with&&hidden behind a single step name; keep them separate so the CI log says which half broke. - Everything works in
yarn startand nothing works in production. Local dev defaults tobuilder: 'local'+runIn: docker+publisher: 'local'and quietly builds on demand. The deployed backend has no Docker socket, orbuilder: 'external'. Always reproduce against the merged production config, not the local default (backstage-incident-debug). - PlantUML diagrams blank.
svg_objectoutput is stripped as untrusted HTML. Usesvg_inline.
Do not
- Do not flip
techdocs.builderto'external'before docs are actually in the bucket for the entities users open. - Do not run
techdocs-cli publishormigrate --removeOriginalagainst a shared or production bucket without an explicit stop-and-get-authorization step;migrate --removeOriginalmoves rather than copies. - Do not give the Backstage backend write credentials to the docs bucket when
builder: 'external'. - Do not enable
runIn: dockerfor a containerised backend without confirming a usable Docker socket; prefer baking mkdocs into the image. - Do not remove a plugin from
mkdocs.ymlto make a generator error go away. - Do not hand-edit files in the storage bucket; the next CI run overwrites them and the
etagwill lie. - Do not change
metadata.nameormetadata.namespaceon a documented entity without republishing — the old storage triplet becomes unreachable (backstage-catalog). - Do not rely on default (non-strict) MkDocs builds in CI; broken nav and dead links exit 0.
Source & license
This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.
- Author: bendaamerahmed
- Source: bendaamerahmed/backstage-idp-plugin
- License: MIT
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet, be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.