Conventions for AI agents and human contributors working in this repo. This is the canonical
instructions file (plain Markdown, read natively by GitHub Copilot — including Copilot code
review — Cursor, Codex, … and by Claude Code). It defers to the monorepo's root
AGENTS.md for the shared
engineering contract (PR/commit conventions, trust gate, guardrails, draft-PR discipline); the
rules below are what's specific to this repo.
Three things share one repo:
- The org's public profile —
profile/README.md(rendered on the org page), plus the org-wide community health files in.github/(CONTRIBUTING.md,CODE_OF_CONDUCT.md,SECURITY.md, issue/PR templates) and the sharedworkflow-templates/. - The org's declarative GitHub configuration —
deploy/, the source of truth for the devantler-tech org's GitHub state, expressed as Crossplane managed resources via provider-upjet-github. - The suite's shared CI/CD building blocks — composite actions and reusable workflows every product repository calls. See Actions catalogue.
On every v* tag, cd.yaml publishes deploy/ as a cosign-signed
OCI artifact to ghcr.io/devantler-tech/github-config/manifests. The
platform cluster onboards it as the github-config
tenant, which verifies the signature and then has Flux + Crossplane reconcile the live GitHub org
to match these manifests — including reverting out-of-band changes made in the GitHub UI or via
gh api. See platform's
docs/github-management.md
for the architecture, the GitHub App credential setup, and the Observe-first adoption flow.
- Manage GitHub declaratively — never imperatively. Org / repo / team / label configuration is
changed only by editing
deploy/and shipping a PR. Do not usegh apiwrites or the GitHub UI to change managed config: thegithub-configtenant reverts out-of-band drift, so an imperative change is at best a no-op and at worst churns against the reconciler. (Reading viagh apiis fine; writing managed config is not.) Applying an existing label to an issue is triage (content, allowed); creating or editing a label definition is config (declarative-only). Unsupported provider surfaces use reviewed-main reconcilers: Actions execution policies live inworkflow-execution-policies/, and organization custom-property schemas incustom-properties/. Their repository-owned apply workflows consume these declarations; change the files, never the live configuration. Repository property values remain namespaced Crossplane resources underdeploy/repository-properties/. - Ownership goes to a team, not an individual. The canonical owner across the suite is the
maintainersteam — model access onTeam/TeamRepository, never on individual logins. - Observe-first when adopting an existing resource. A new
Repository/IssueLabels/team CR for an already-live object must adopt it without risk of recreate/delete: set thecrossplane.io/external-nameannotation to the live name and use a management policy that excludesDelete(observe/late-initialize), per platform'sdocs/github-management.md. Once adopted, an activeRepositorynormally runs onObserve/Create/UpdatewithoutLateInitialize: the values late-initialized during adoption stay inforProvideras provider-owned state, and onceLateInitializeis gone no newly observed field is ever copied in again — so a resource adopted without a given field never acquires it, and nothing but the config can supply it. A field the org already enforces, such aswebCommitSignoffRequired, must be declared inforProviderat the value the org enforces — never left unconfigured and never seeded through the create-onlyinitProvider. upjet builds the Terraform configuration fromforProvider, an absent optional bool takes the provider's zero value offalse, andfalseagainst a livetrueis a permanent diff, so every update PATCH carriesweb_commit_signoff_required: falseand GitHub rejects the whole request with 422 "Commit signoff is enforced ... and cannot be disabled". Declaring the live value removes that field's own diff. The deployed provider-upjet-github v0.20.0 embeds terraform-provider-github v6.13.0, whose fix for upstream #2077 omits the unchanged field when another setting needs an update. Active repositories can therefore useUpdatewhile keeping the enforced live value explicit. The adopted privateclient-ordersrepository uses onlyObserve/Update: recreation is forbidden, privacy and Issues are explicit, and init-only public-safe seeds preserve its externally owned description, homepage and security settings. Never add those fields toforProvider, enableCreateorLateInitialize, or copy private metadata into this repository. TheplatformandksailRepository resources host workflow-built Pages sites that the GitHub App cannot write, so each declares the deprecatedpagesfield exactly as GitHub reports it. Declaring the whole list replaces stale provider state and leaves no Pages diff for an update to send (issue #232). KHoist's newly bootstrapped site has no observed Pages block, so an enforced declaration instead retries a forbidden Pages PUT. Keep only its Pages bootstrap expectation ininitProvider.pages, retain the homepage and repository settings inforProvider, and verify the Pages expectation through the read-only repository drift check (#614). Do not expand the managed App's Pages permissions. After a repository's one-timearchived: trueupdate lands, move it to exactlyObserve: GitHub makes archived repositories read-only, and retainingUpdateorLateInitializelets newly exposed provider fields create a permanent update loop.tests/repository-update-policy.shpins both invariants. Verify the provider kind/field schema against the authoritative source (crossplane-contrib/provider-upjet-githubpackage/crds/examples-generated/namespaced/) — the CRs cannot be schema-validated locally (no cluster; CI only runskubectl kustomize).
IssueLabelsis authoritative (Terraformgithub_issue_labels): it declares a repo's complete label set and removes any undeclared label. Before applying it to a repo with live labels, enumerate that repo's existing labels (including Dependabot'sdependencies/github_actions, Renovate extras) so the declared set is a superset — or you will delete labels in use. The canonical taxonomy lives once indeploy/labels/kustomization.yaml(a shared patch appended to every repo); eachdeploy/labels/<repo>.yamladds only that repo's automation-specific extras, including dependency/ecosystem labels and lifecycle labels that an automation requires to retain state.
| Path | Contents |
|---|---|
deploy/repositories/<repo>.yaml |
one Repository per managed repo (settings, merge/signoff, metadata) |
deploy/teams/ |
one Team per file; includes the active Maintainers and Admins teams |
deploy/team-memberships/ |
one explicit user-to-team membership per file |
deploy/team-repositories/ |
one team-to-repository permission grant per file |
deploy/labels/<repo>.yaml |
one IssueLabels per repo; canonical taxonomy in labels/kustomization.yaml |
deploy/repository-permissions/<repo>.yaml |
one RepositoryPermissions per active (non-archived) repo; requires actions pinned to a full commit SHA |
deploy/repository-properties/ |
generated explicit values for each active repository; source and ownership in custom-properties/ |
deploy/provider-config.yaml |
the provider-upjet-github ProviderConfig (App credentials) |
deploy/external-secret.yaml |
the ExternalSecret sourcing the GitHub App credentials |
deploy/kustomization.yaml |
top-level kustomization wiring the above + the shared repo-settings patch |
The roadmap of record is GitHub Issues — epic #56
("declarative GitHub-org-as-code") with native issue types and sub-issue links. Triage incoming issues into that
structure; implementing PRs use Fixes #N.
Organization settings not represented by the provider are audited against
organization-settings/expected.json. The check is read-only:
0 means all declared settings match, 1 means measured drift, and 2 means invalid or incomplete
evidence. Its manual workflow runs only on reviewed main after explicit opt-in, requests read
authority and never changes settings. Keep both observation/failure controls and the workflow
credential-boundary mutations in CI. Provider adoption remains in #85; scheduling and flag
retirement remain in #404.
Routine repository/admin-team monitoring is described in
.github/governance-audits.md. Its daily schedule is enabled;
manual execution is input-free and requires reviewed main. Keep admission, private-diagnostic suppression,
complete-outcome reporting and source/credential mutations in required CI. Routine admission has no experimental switches. Natural scheduled observation and
main dispatch prove both full checks and the reporter. Legacy maintenance retirement is complete
(#84). Actions has also been deleted (#240); its temporary coverage exemption is removed, so
an undeclared recreation must fail the coverage audit. Historical signer identities remain data.
The existing organization CodeQL finding rule is imported Observe-only. Run
bash tests/code-scanning-ruleset.sh after changing its identity, read-only
lifecycle or CI integration. Full observed selectors, thresholds and bypasses
are required before any managed adoption; see
deploy/organization-rulesets/README.md
and the remaining observation and coverage gates in #636. Run
bash tests/metadata-code-scanning-ruleset.sh after changing the separate
organization rule for .github and monorepo; preserve its exact verified scope,
all finding thresholds, no bypasses and non-deleting lifecycle. Native creation
and positive/refusal evidence remain separate delivery gates.
The adopted dependency-review, auto-merge, workflow-scanning and language-selected
.NET rules permit only Observe and Update after complete native observation (#674).
Run bash tests/legacy-workflow-observation.sh after changing these or the two
template workflow rules: it binds all six complete policies to their captured
native state, permitting only the maintained source, required provider encodings,
disabled Go-template enforcement after native proof (#698), and disabled .NET
policies with one inert rule and no workflow association after the complete
effective-consumer census (#703).
Keep both replacement repository rules active, default-branch-only,
without bypasses and bound to their exact GitHub Actions checks. The Go aggregate and all
three distinct .NET platform checks were proven with positive and intentional-failure controls.
Native application and ordinary consumer execution remain separate gates under #237.
Validate before every PR (the sole required check, CI - Required Checks, gates on this):
kubectl kustomize deploy/ > /dev/null # must build clean
bash tests/admin-team-policy.sh # Admins policy invariants
bash tests/declarative-coverage.sh # every repo declared in every rendered dimension
bash tests/custom-properties.sh # explicit values, schema ownership and refusal controls
bash tests/declarative-coverage-fail-closed.sh # rendered-label reads fail closed
bash tests/actions-retirement.sh # retired catalogue has no rendered management
bash tests/repository-update-policy.sh # active Repository update invariants
bash tests/signing-rule-retirement.sh # retained signing-rule identity and safe lifecycle
bash tests/release-contract.sh # deploy/ changes must trigger a release
bash tests/deploy-deletions.sh # removed deploy/ resources must be acknowledged per resource
bash tests/repository-drift.sh # declared-vs-live comparison logic
bash tests/repository-coverage.sh # live-org-vs-deploy/ comparison logic
bash tests/workflow-execution-inventory.sh # who and what can start each workflow
bash tests/workflow-execution-actors.sh # live actor IDs/types observed per workflow and event
bash tests/workflow-execution-policies.sh # reviewed execution policies are valid desired state
bash tests/apply-workflow-execution-policies.sh # the policy reconciler, against an offline API stand-inThose fifteen commands are the baseline checks that ci.yaml's validate-manifests job runs; the
same workflow also runs the catalogue's tests (see Actions catalogue). Pull requests additionally pass
their changed paths and title through scripts/validate-release-contract.sh and their base/head
renders plus the pull-request body through scripts/validate-deploy-deletions.sh (every managed
resource that leaves the render needs its own Deletion-Acknowledged: <Kind>.<group>/<name> body line, spelled the way the
failure prints it); merge groups skip those event-specific checks. The deletion check also renders
deploy/ at the base, so a pull request whose base main does not build fails there with kubectl's
exit status.
Both validators are owned by .github/actions/pr-admission. The shared required
repository-pr-admission.yaml runner executes that implementation from the pull request's immutable
reviewed base and treats candidate manifests, history and metadata only as data. Editing candidate
CI or validators cannot replace it. Keep tests/deploy-guards-ruleset.sh and
tests/repository-admission-ruleset.sh passing after changing this boundary. The former singleton
organization associations are disabled, inert, non-deleting records after native positive and
tampering proof; the repository-local deploy-guards.yaml wrapper remains independently tested.
Shared admission for this catalogue and monorepo requires both organization workflow injection and a repository-bound executed result; the ordinary source-only skipped result cannot satisfy it. A broken reviewed source can block its own repair. Only a maintainer may coordinate a temporary recovery with reconciliation and restore all affected bindings afterward. That procedure must account for both active bindings, not the retired singleton rule. Agents never automate an emergency bypass or fall back to a candidate-controlled validator.
Organization-required workflows ignore pull request title/body edits even when their trigger lists
edited. pr-metadata-guards.yaml provides manually dispatched read-only feedback from reviewed main.
It reads the current open catalogue PR through GitHub's API, binds validators to the workflow's
immutable source revision, and treats the candidate as data only. It refuses fork PRs and other
bases. It has no required-status binding; the existing full CI protection still applies.
Keep .github/tests/test-ci-metadata-events.sh passing. Retain catalogue CI's edited trigger
until trusted producer, current-head attribution and live enforcement are proven under #250.
kubectl (with built-in kustomize) is preinstalled on CI runners. A clean build proves the manifests
are well-formed; the Crossplane CRDs themselves are applied/validated on-cluster (the
github-config tenant), not in CI — so a green build is necessary but not sufficient, and any new CR
must be schema-checked against the provider's published CRDs (above).
Repo-specific watch-list for the daily engineer:
- Drift / coverage. New repos in the org, or org/repo/team settings changed in the UI, mean
deploy/is now behind reality. Bringing them under management (Observe-first) isroadmap/enhancementwork — never a UI fix.repository-coverage-check.yamlrunsscripts/check-repository-coverage.shdaily at 05:29 UTC and onworkflow_dispatch, and fails on anUNDECLAREDlive, non-archived repository. Its App token reads metadata across every installed repository, because scoping it to the declared ones would hide an undeclared private repository; it fails closed when the App installation is limited to selected repositories or the listing misses a declared one. A failing run onmainopens the issue Scheduled repository coverage check is failing on main, and the next passing run closes it. - Declared settings that never landed.
repository-drift-check.yamlrunsscripts/check-repository-drift.shand fails when aforProviderfield disagrees with the live repository. Cluster state cannot answer this alone: aRepositoryonly PATCHes when it has a pending diff, so anObserve-only resource — or one whose writes GitHub rejects — reportsSynced=ReconcileSuccesswhile its declaration is never applied. ADRIFTline is a real defect in one of those two shapes; fix the resource, never the live repo. It runs daily at 05:17 UTC and onworkflow_dispatch. The scoped App token reads repository settings through REST and fills missing merge-policy fields through GraphQL. Both reads must identify the same repository and visibility; missing fields or partial responses fail the check. A failing scheduled or dispatched run onmainopens or updates the issue Scheduled repository drift check is failing on main, and the next passing run closes it; triage that issue like any other breakage. Runs are queued rather than overlapping, so an older result never overwrites a newer one.tests/repository-drift.shpins that wiring. - Workflow execution policies are applied by a workflow, not Crossplane.
apply-workflow-execution-policies.yamlruns when a change toworkflow-execution-policies/or its reconciler lands onmain, daily at 05:43 UTC, and onworkflow_dispatch. It mints a token fromAPP_CLIENT_ID/APP_PRIVATE_KEYwith organization administration only, makes the live policies match the files by name, reads each back, and never deletes one. A red run names the policy and printsFAILED,MISMATCHorUNKNOWN; fix the file or the credential, never the live policy. Moving these intodeploy/once the provider supports them is #226. cd.yamlis the publish path, triggered onv*tags only;ci.yamlproduces the PR-time required check. A redcd.yamlmeans the org-config OCI artifact didn't republish — investigate before assuming the live org is in sync.- Agent-file freshness. Keep this
AGENTS.mdin sync with the actualdeploy/layout and the shared contract. This repo does not carry a separate.github/copilot-instructions.md— Copilot readsAGENTS.mddirectly, so a parallel review-only file would be redundant (per the root contract, if one ever appears, delete it and fold anything unique here).
The shared actions and reusable workflows are governed by this core and the topic guides below. Read each guide before doing the work its row names.
| Guide | Read before |
|---|---|
| Catalogue layout and references | Adding or moving components, self-references, release conventions |
| Workflow and CI contracts | Any catalogue implementation, test or workflow change, including feature flags |
| Validation and publication contracts | Any catalogue validation, to-do forwarding, publication or release-recovery change |
| Tested invariants | Any catalogue implementation or test change |
The catalogue layout lists component directories and all repository-owned workflows. Catalogue workflows declare workflow_call; the others operate this repository.
| File | Purpose |
|---|---|
.releaserc |
This repository's semantic-release configuration. The create-release self-test dry-run reads it too. |
.mega-linter.yml |
MegaLinter config (disables SPELL_CSPELL) |
.yamllint.yml |
YAML linting rules |
.cspell.json |
Spell-checker config and custom words |
.markdownlint.json |
Markdown linting rules |
zizmor.yml |
Zizmor security scanner pinning policies |
Full detail in actions/CONTRIBUTING.md. Key rules:
- Action directory naming:
actions/<active-verb>-<purpose>(e.g.,setup-go-toolchain, notgo-setup) - Inputs/outputs: kebab-case only (e.g.,
app-id,github-token) - Action type: Prefer composite over JavaScript/Docker
- External action pinning: Pin third-party actions (non-
actions/*, non-github/*, non-devantler-tech/*) to commit SHAs with a# v<version>comment — enforced byzizmor.yml. For a main-tracked dep with no releases, use# <branch> (no upstream releases). action.yaml: Always setauthor: devantler-tech- Distribution / Marketplace: The remaining portfolio-first catalogue is directly reusable and unlisted while it contains multiple subdirectory actions and reusable workflows. Skill maintenance actions live in the standalone
setup-agent-skills-action,update-agent-skills-action, andguard-installed-skill-edits-actionrepositories. Marketplace does not automatically list nested action metadata, so do not addbranding:blocks here as a proxy for publication.
Read the workflow and CI contracts before adding or changing a test. Keep the required reducer, credential boundaries and native behavior proofs intact.
Failure-mode coverage for gating workflows (the convention): every gating reusable workflow — one whose job is to fail a PR on bad input — carries both a passes-on-good-input and a blocks-on-bad-input self-test, because a happy-path test alone cannot catch a gate that silently stopped biting. The pattern (per test-govulncheck-strict-blocks and test-zizmor-blocks): point the gate's own action — pinned to the same SHA, guarded by a *-action-lockstep check — at a deliberately-bad fixture under .github/tests/, continue-on-error, then assert the run failed and reported the expected finding (so an operational error can't false-pass). The fixture lives outside the gate's own scan scope (e.g. .github/tests/zizmor-fixture/ is outside .github/workflows/) so it never trips the real gate. Non-gating workflows (release/publish/deploy dry-runs, enable-auto-merge, template-sync, update-agent-skills, scan-for-todo-comments) have no "bad input" to reject, so a happy-path [Test] job is complete coverage. Where a clean failure-mode fixture is genuinely impractical, record the reasoned gap rather than forcing a fragile test: the dependency-review workflow needs a PR diff introducing a bad dependency, so its blocking path is replayed at the action level by the offline on-failure-vulnerable scenario (see Dependency-review comment coverage) rather than through the workflow.
GitHub App tokens (not GITHUB_TOKEN) are used for operations that must trigger other workflows or bypass branch protection:
- name: 🔑 Generate GitHub App Token
uses: actions/create-github-app-token@<sha> # <version>
id: app-token
with:
client-id: ${{ vars.APP_CLIENT_ID }}
private-key: ${{ secrets.APP_PRIVATE_KEY }}Two App identities exist:
- botantler-1 —
APP_CLIENT_ID(var) +APP_PRIVATE_KEY(secret): the primary release and automation identity (cuts releases, commits signed fixes, arms auto-merge). - botantler-2 —
APP_CLIENT_ID_2(var) +APP_PRIVATE_KEY_2(secret): the approver identity for workflows that approve a pull request another identity opened (a pull request cannot be approved by its opener).
# Run yamllint
yamllint .github/workflows/
# Check action/workflow pinning with zizmor
zizmor --config zizmor.yml .github/workflows/
# Lint workflows (if installed)
actionlintThe auto-merge workflow's own allow-lists (TRUSTED_BOT_AUTHORS, TRUSTED_TRIGGER_ACTORS and TRUSTED_REVIEW_ACTORS in .github/workflows/enable-auto-merge.yaml) decide what that workflow acts on and are maintained only there; they are separate from the agents' trust gate in the monorepo AGENTS.md.
Blast radius first: a change to a composite action / reusable workflow affects every consumer repo. Prefer additive, backward-compatible changes; call out any breaking input/output change prominently in the PR body and treat it as a deliberate decision (keep an alias where feasible).
Validate before any PR: actionlint on every changed workflow/action (else a thorough YAML parse); confirm uses: refs resolve and are pinned/aligned; check inputs/outputs/shell: are declared; for reusable workflows keep on: workflow_call inputs/secrets backward-compatible. No app build here — YAML correctness + pinning is the gate. Keep all remote actions pinned to full-length commit SHAs. Native $/ self-references bind to the defining file's repository and exact commit; same-source actions and reusable workflows use this form. Retain source checkouts only where scripts or fixture data still need their files. Deliberately checked-out product-base validators must retain that different source identity. Follow the catalogue guide's self-reference rules; never weaken a security control to pass a check.
Read the tested invariants before changing their implementation or self-tests.
The root instruction budget reserves 500 bytes below 32 KiB. Run the required layout check after changing this file, a guide or repository-owned workflow:
bash .github/tests/test-agent-instructions-layout.sh