writing-skills
writing-skills
Use when creating new skills, editing existing skills, or verifying skills work before deployment
multi-environment-delivery-and-releaseAI Workspace Infra multi-environment delivery, Vault OIDC, release and secret-isolation rules. Use when changing GitHub Actions environment routing, Vault roles/policies, CI secret reads, workflow_dispatch inputs, release tags, or deployment paths in platform-ops-toolkit and related delivery repositories. Covers exact workflow allowlists, KV tiers, transiti…
Independent GitHub snapshot · Automated classification · Original documentation governs
A reusable template for repos that route deployments through GitHub Actions and authenticate to Vault via OIDC. Replace the placeholder workflow/role names below with the target repo's actual ones; keep the routing shape and secret-handling rules.
For AI Workspace Infra, read AI Workspace Infra Repository Map first. Treat the checked-in workflow plus its external scripts as execution truth; reconcile conflicting README prose rather than routing a deployment from documentation alone.
A single delivery workflow (e.g. <delivery-workflow>.yaml) should route traffic to specific environments based on Git events. Never hardcode environments outside of these bounds:
pull_request -> routes to sit environment.main or release/* push -> routes to uat environment.vMAJOR.MINOR.PATCH tag -> routes to prod environment.For platform-ops-toolkit/platform-ops.yaml, preserve the mapped resource file,
workspace, backend key, domain base, and Vault role as one atomic profile. Changing
only one of them can make Terraform manage one host while Ansible deploys another.
resources/<project>/<env>/<provider>/*.yaml; workflow inputs, Terraform
workspaces, backend keys, Vault roles, CMDB output, and deployment targets MUST
resolve to that same tuple before credentials are read.main, silently rewrite image
tags, or infer a production path from a missing environment value.An immutable cross-repository snapshot is a build/release candidate, not an implicit deployment request. Keep these stages separate:
Never use main, latest, or a partially successful snapshot as a deployment
version. Failed retries retain their tags and use a new -rN suffix. The
snapshot summary must preserve the workflow URL, per-repository SHA/build
status, artifact evidence, and any retry reason.
Client and edge-node provisioning scripts (such as setup-proxy.sh) that consume daily builds must not assume rigid, unversioned asset filenames:
artifact-${goarch}-<hash>.tar.gz) or latest daily build tags.agent-svc-plus -> xconnect-edge-agent), installation logic must:
agent-proxy, agent-svc-plus)./usr/local/bin/.http://127.0.0.1:<port> 或 http://localhost:<port>);Production eligibility is a hard allowlist, not a naming convention. A production-capable
workflow MUST fail closed unless github.ref matches one of:
refs/tags/vMAJOR.MINOR.PATCH (the immutable stable release tag); orrefs/heads/release/vMAJOR.MINOR (the protected release line, only when an explicit
production action and approval are also present).main, feature/bugfix branches, pull-request refs, arbitrary dispatch refs, and all
daily-build-* / uat-daily-build-* snapshots are non-production inputs. A workflow may
use a release branch to validate or stage a release, but it MUST NOT infer production
promotion from a branch name alone.
Stable and daily artifacts MAY share one tagging script. The shared tagging script MUST require or derive an explicit tag kind and validate the complete tag/ref/environment matrix before creating anything:
| Tag kind | Example | Allowed use |
|---|---|---|
| Stable | v1.2.3 | Production promotion only after release gates |
| Daily/UAT | uat-daily-build-YYYY.MM.DD-rN | UAT/SIT validation and deployment only |
| Daily snapshot | daily-build-YYYY.MM.DD-rN | Non-production integration only |
Published stable tags MUST never be moved, overwritten, force-updated, or deleted. A failed
stable release gets a new version; a failed daily attempt gets a new -rN snapshot suffix.
The preflight MUST verify the event, ref, source SHA, tag immutability, artifact matrix,
artifact digest/provenance, GitOps desired version, Vault role/KV path, target route, and
required test conclusions before any production credential is read or deployment mutation
starts.
When promoting an immutable build from UAT to PROD via daily-main-snapshot.yaml:
snapshot_source_ref must explicitly point to an existing, verified non-production snapshot tag (uat-daily-build-YYYY.MM.DD-rN or daily-build-YYYY.MM.DD-rN). Direct promotion from moving main is strictly prohibited.snapshot_tag must be a valid, immutable SemVer or calendar-versioned release tag (vYYYY.MM.DD[-rN] or vX.Y.Z).daily-snapshot-builds.json must have their container images or release binaries built and verified for that exact tag.serverless-orchestrator.yml) must gate the frontend Cloudflare deployment on a 100% successful Cloud Run backend rollout. Never publish frontend assets against a failed or partial backend.github-actions-<repo>-sit / -uat / -prod.A JWT role is only as strong as its bound_claims. Binding on repository alone is not isolation — it lets any branch, any workflow, and any contributor with push access mint that role's token. Bind on repository plus a trigger-scope claim:
{
"role_type": "jwt",
"user_claim": "sub",
"bound_audiences": ["vault"],
"bound_claims_type": "string",
"bound_claims": {
"repository": "<org>/<repo>",
"job_workflow_ref": "<org>/<repo>/.github/workflows/<delivery>.yaml@refs/heads/main",
"environment": "prod"
},
"token_policies": ["github-actions-<repo>-prod"],
"token_no_default_policy": true,
"token_type": "batch",
"token_ttl": "20m",
"token_max_ttl": "20m"
}
user_claim: use sub (stable, encodes repo + ref + workflow). Avoid actor — it keys the Vault identity to a human username rather than the workload.bound_claims_type: default to string (exact match). Use glob only where a wildcard is genuinely required — a * matches / too, so refs/heads/release/* is satisfiable by any branch a repo writer can create.job_workflow_ref: pin it. Without it, a newly added or modified workflow file in the same repo can assume the role.environment claim over ref for prod. A ref-based binding inherits the strength of branch protection; a GitHub Environment with required reviewers gates the token behind an approval.token_ttl + token_max_ttl (a CI job needs minutes, not hours), token_no_default_policy: true, and token_type: batch for non-renewable CI tokens.Per-environment isolation is defeated if every role can read one shared path holding cloud API keys, Terraform state credentials, or a global SSH private key. The loosest role defines the effective privilege of the whole system: if the sit role is bindable from any branch and can read that path, anyone who can push a branch owns the infrastructure.
Split the KV tree by whether a secret has an environment dimension at all:
| Tier | Path | sit | uat | prod | Permission |
|---|---|---|---|---|---|
| ① Common services | kv/data/<shared> (registry pull creds, etc.) | ✅ | ✅ | ✅ | read only, never writable |
| ② Base credentials | kv/data/<shared>/<env> (cloud API key, TF state creds, SSH deploy key) | own only | own only | own only | read only |
| ③ Environment secrets | kv/data/<env>/* | own only | own only | own only | read/write (prod: no delete) |
Classification test — ask in order, first yes wins:
Permissions:
prod must not hold delete on kv/data/<env>/*, and especially not on kv/metadata/<env>/* — metadata delete permanently destroys every version of a secret.Why ① and ② can coexist under one prefix: in KV v2, kv/data/<shared> and kv/data/<shared>/<env> are separate secrets — a path can be both a secret and a prefix. A policy path of kv/data/<shared> matches the root exactly and does not match subpaths (that needs kv/data/<shared>/*). So "shared root + only your own subpath" holds strictly.
Enforce it, don't just document it. These are assertions, not conventions — verify them in CI:
kv/data/<shared>/<other-env>;kv/data/<shared>/* wildcard — that single glob collapses tier ② in one step, and it is the premise the whole layout rests on;create/update/delete/patch/sudo;prod carries no delete on either kv/data or kv/metadata.Path isolation is not credential isolation. Copying one credential set into three per-environment paths isolates the paths while the credentials stay shared — a sit compromise still yields prod. The structure is a precondition; the security benefit only lands once each environment holds genuinely distinct keys. Say which of the two you have actually achieved.
workflow_dispatch input. Dispatch inputs are stored unmasked in run metadata and bypass every binding above. A "fallback for when JWT breaks" is a standing credential-injection path — fix the JWT instead.refs/heads/* fails auth at best, and silently routes to the wrong environment at worst. Keep the routing table (§1), the VAULT_ROLE expression, and the role definitions reviewed together — they drift independently.platform-ops-toolkit manages roles through docs/tasks/vault_auth_split.sh. Adding a workflow that calls Vault requires adding that exact workflow filename to job_workflow_ref, merging the configuration change, and having an authorized operator apply it. Do not widen the claim to an unrestricted wildcard.main or release/*.feature/* and bugfix/* MUST target main.hotfix/* MUST target release/*.v*). Stable tags are immutable release identities: never move, overwrite, or delete them after publication.main and release/* permanently. Protect them with required reviews/checks and prohibit force-push, ref replacement, and deletion; normal reviewed commits remain allowed.v* as a stable release tag and retain it permanently, including date-based and legacy version forms. Apply repository tag protection or a ruleset so only the release automation identity can create such tags and nobody can update or delete them.RECENT_RETENTION_DAYS (default: 7 calendar days). This includes prefixes such as daily-build-*, uat-daily-build-*, sit-*, and equivalent repository-specific environment tags.If a secret is exposed in the repository:
git filter-repo)—do not merely "delete" the file in a new commit.A real incident: an account database export, an SMTP password, and an OAuth token manual all
ended up committed as literal values in markdown/YAML — live MFA TOTP secrets included — sitting
in a public repository's history for months before a gitleaks gate caught them. Regenerating
credentials fixed the accounts; the git history purge is the part that doesn't fix itself and
needs git filter-repo (§4) plus a force-push everyone with a clone must re-pull.
kv/<path> <KEY_NAME> — never the
secret's actual value. A setup guide that shows a real SMTP password or a real API key is
itself a leak, indistinguishable in git history from one that was never meant to be read.
Write password: "{{ vault kv/CICD SMTP_PASSWORD }}" or the equivalent for the pipeline in
question, not the string it resolves to.git clone.gitleaks failing on a PR that didn't introduce the secret is not a false positive to
suppress. It scans full history; a red Sec QA Gate on an unrelated PR means a prior commit
leaked something and the fix is the purge above, not an allowlist entry.For a multi-repository UAT release, completion is a controlled sequence, not a single workflow result:
main. Confirm
the merge commit and required checks; do not tag a feature branch or a local
worktree that was not merged.uat-daily-build-YYYY.MM.DD-rN) from the resolved main SHAs. The first
snapshot for a UTC date is r1; retries and later same-day snapshots use
the next available rN. A failed attempt keeps its tag; retry with the
next suffix rather than moving or reusing it..env.<env> or equivalent.docs/tasks/ or the consuming repository's equivalent.If any checkpoint is unknown, pending, or only inferred from a downstream UI,
the release remains incomplete. Use the evidence table in
ci-cd-workflow-spec to classify the fault before changing code or data.