This document covers the design of DevSecNinja/.github and how consuming
repositories use it.
.
├── .github/
│ ├── workflows/ # Reusable workflow definitions (on: workflow_call)
│ ├── skills/ # Copilot skill definitions
│ ├── CODEOWNERS
│ ├── ISSUE_TEMPLATE/
│ ├── PULL_REQUEST_TEMPLATE.md
│ ├── SECURITY.md
│ └── copilot-instructions.md # Org-wide Copilot coding standards
├── .renovate/ # Renovate shared preset fragments
├── actions/ # Composite actions (run inside the caller's job)
├── config-sync/
│ ├── files/ # Files auto-synced to every repo (read-only)
│ │ └── .github/labels-base.yaml # Base label set synced to all repos
│ └── templates/ # Starting-point files — copy & adapt per repo
├── docs/
│ └── design-decisions/ # Architecture Decision Records (ADRs)
├── profile/
│ └── README.md # Organisation profile shown on github.com/DevSecNinja
└── workflow-templates/ # Starter workflows for the GitHub Actions UI picker
All reusable workflows live in .github/workflows/ and are called with
on: workflow_call. Pin the uses: ref to a full commit SHA with a version
comment and supply the required inputs.
Version inputs are always required. The calling repo owns the tool version and annotates it with a
# renovate:comment so Renovate opens a PR (which runs the caller's CI) when a new version is released. See ADR 0001.
Runs up to eleven linters. By default, linter failures are reported without
failing the workflow; set lint-fail-on-error to true to make lint failures
fail the workflow. Toggle individual linters on/off with boolean inputs; all
linter toggles default to true.
| Input | Description |
|---|---|
mise-version |
Required. mise version to install. |
golangci-lint-version |
Required. golangci-lint version to install. |
go-version-file |
Go version file for setup-go. Default: go.mod. |
lint-config-dir |
Optional linter config directory. Default: "". |
lint-fail-on-error |
Fail when a linter fails. Default: false. |
lint-dprint |
Markdown formatting (dprint). Default: true. |
lint-yamlfmt |
YAML formatting (yamlfmt). Default: true. |
lint-yamllint |
YAML linting (yamllint). Default: true. |
lint-actionlint |
GitHub Actions linting. Default: true. |
lint-gitleaks |
Secret scanning (gitleaks). Default: true. |
lint-go |
Go linting (golangci-lint). Default: true. |
lint-shellcheck |
Shell linting (shellcheck). Default: true. |
lint-shfmt |
Shell formatting (shfmt). Default: true. |
lint-checkov |
IaC security scan (checkov). Default: true. |
lint-trivy |
Filesystem scan (trivy). Default: true. |
lint-zizmor |
Actions security scan (zizmor). Default: true. |
lint-config-drift |
Config-drift check. Default: false. |
Example caller:
jobs:
lint:
uses: DevSecNinja/.github/.github/workflows/lint.yml@<sha> # v1.0.0
permissions:
contents: read
security-events: write
with:
# renovate: datasource=github-releases depName=jdx/mise
mise-version: "2026.4.5"
# renovate: datasource=github-releases depName=golangci/golangci-lint
golangci-lint-version: "v2.11.4"
# lint-config-dir: config-sync/files
# lint-fail-on-error: trueRuns dprint, yamlfmt, and shfmt in write mode, then commits and pushes any formatting changes.
| Input | Description |
|---|---|
mise-version |
Required. mise version to install. |
autofix-config-dir |
Optional formatter config directory. Default: "". |
autofix-dprint |
Markdown formatting (dprint). Default: true. |
autofix-yamlfmt |
YAML formatting (yamlfmt). Default: true. |
autofix-shfmt |
Shell formatting (shfmt). Default: true. |
commit-message |
Commit message. Default: style: auto-fix formatting. |
Creates a GitHub Release from a version tag using git-cliff for release
notes.
| Input | Description |
|---|---|
mise-version |
Required. mise version to install. |
tag |
Required. Tag name, e.g. v1.2.3. |
Example caller:
on:
push:
tags: ["v*"]
jobs:
release:
uses: DevSecNinja/.github/.github/workflows/release.yml@<sha> # v1.0.0
permissions:
contents: write
with:
# renovate: datasource=github-releases depName=jdx/mise
mise-version: "2026.4.5"
tag: ${{ github.ref_name }}Opens or updates release PRs and creates version tags from Conventional
Commits. GitHub App authentication is mandatory so release PR and tag
events trigger downstream workflows; the reusable fails closed for
empty or whitespace-only credentials and never uses GITHUB_TOKEN.
| Input / secret | Description |
|---|---|
app-id |
Required. Release Please GitHub App ID. |
app-private-key |
Required secret. Private key paired with app-id. |
target-branch |
Branch to track. Default: main. |
config-file |
Release Please configuration path. |
manifest-file |
Release Please manifest path. |
The App installation needs only Contents and Pull requests write permissions (Metadata read is automatic). Every caller must also enable Allow GitHub Actions to create and approve pull requests.
Callers choose one publication pattern:
- Set
skip-github-release: falsefor a direct Release Please GitHub Release, with no competing tag publisher. This is the default for simple consumers. - Set
skip-github-release: truewith a tag-triggeredrelease.ymlthat owns assets, attestations, notes, and publication. Release Please still creates the tag in this pattern; use it only when those custom publication steps are needed.
See the onboarding guide and ADR 0007.
Runs configurable site validation commands, deploys the production artifact to GitHub Pages and/or Cloudflare Pages from the configured production branch, and optionally deploys same-repository pull request previews to Cloudflare Pages. Cloudflare jobs detect missing Cloudflare secrets before any deploy work. Missing secrets fail production Cloudflare deploys and skip preview-only deploys. An optional production custom domain can be registered after deployment. Callers may also opt into safe DNS CNAME creation; DNS remains unmanaged by default.
| Input | Description |
|---|---|
node-version |
Required. Node.js version to install. |
node-cache |
Package manager cache for test and production jobs. Default: empty (disabled). |
go-version |
Go version to install before the build. Required for Hugo Modules. Default: empty (no Go setup). |
go-version-file |
File to read the Go version from, used only when go-version is empty. |
wrangler-version |
Required. Wrangler version to install for previews; inputs cannot be conditional. |
production-branch |
Branch that deploys to production. Default: main. |
artifact-path |
Directory uploaded to Pages. Default: .. |
artifact-name |
Deploy a prebuilt artifact from an earlier caller job instead of building. Default: empty. |
install-command |
Dependency install command. Default: npm ci. |
test-command |
Validation command block. Default: empty. |
test-setup-command |
Optional command after install and before tests. |
build-command |
Optional build command before deployment. |
pre-deploy-command |
Optional production-only pre-upload command. |
pre-preview-command |
Optional preview-only pre-deploy command. |
update-sitemap-lastmod |
Update sitemap <lastmod> dates. Default: false. |
sitemap-path |
Sitemap file path. Default: sitemap.xml. |
github-pages |
Deploy production to GitHub Pages. Default: true. |
cloudflare-preview |
Enable Cloudflare pull request previews. Default: true. |
cloudflare-production |
Deploy production to Cloudflare Pages. Default: false. |
cloudflare-production-on-release |
Deploy Cloudflare production only on release events, not every main commit. Default: false. |
cloudflare-custom-domain |
Register one lowercase FQDN after a production deploy. Default: empty. |
cloudflare-manage-dns |
Create the custom-domain CNAME when absent; never replace conflicts. Default: false. |
cloudflare-dns-zone |
Lowercase zone apex containing the custom domain. Required when DNS management is enabled. |
cloudflare-dns-proxied |
Proxy a workflow-created CNAME through Cloudflare. Default: true. |
cloudflare-acceptance |
Deploy an acceptance build to Cloudflare on main commits. Default: false. |
cloudflare-acceptance-branch |
Cloudflare branch name for acceptance deploys. Default: acceptance. |
cloudflare-project-name |
Cloudflare Pages project; lowercase letters, numbers, and hyphens only. |
cloudflare-production-branch |
Cloudflare production branch. Default: main. |
preview-comment-marker |
Marker used to update the preview PR comment. |
Build, pre-deploy, and pre-preview commands run with an APP_COMMIT_SHA
environment variable set to ${{ github.event.pull_request.head.sha || github.sha }}.
On pull_request events github.sha is the ephemeral merge commit, so builds
should read APP_COMMIT_SHA to stamp the real PR head commit in previews.
This workflow provides a fixed build environment: Node, Go, an install command
and a build command. A build that needs anything else — a GITHUB_TOKEN to
read the API, a cloud credential, a toolchain that isn't here — should not try
to smuggle it through more inputs. Build it in your own job and hand over the
result:
jobs:
build:
runs-on: ubuntu-24.04
permissions:
contents: read
issues: read # whatever *your* build needs
steps:
- uses: actions/checkout@<sha>
- uses: actions/setup-node@<sha>
with:
node-version: "24"
- run: npm ci && npm test && npm run build
env:
GITHUB_TOKEN: ${{ github.token }}
- uses: actions/upload-artifact@<sha>
with:
name: site
path: dist
# v4+ omits dotfiles; needed for .well-known, .nojekyll, …
include-hidden-files: true
pages:
needs: build
uses: DevSecNinja/.github/.github/workflows/pages.yml@<sha> # vX.Y.Z
permissions:
contents: read
deployments: write
id-token: write
pages: write
with:
artifact-name: site
artifact-path: dist
cloudflare-project-name: my-siteA repository may call this workflow more than once — once per site. Cloudflare
deploy and preview concurrency is scoped per project name, so parallel calls do
not cancel or serialise each other. Give each call its own
preview-comment-marker so the preview comments don't overwrite one another.
artifact-name and build-command are mutually exclusive — the artifact is
deployed exactly as uploaded. With artifact-name set the deploy jobs skip
Node/Go setup, install and build entirely.
Set cloudflare-custom-domain only with cloudflare-production: true, an
explicit cloudflare-project-name, the required Wrangler version, and
CLOUDFLARE_ACCOUNT_ID / CLOUDFLARE_API_TOKEN secrets. Registration needs
Account Cloudflare Pages Write permission. The workflow validates the domain as
a lowercase FQDN, deploys production, then reads or creates that domain through
the official Pages API. Preview, acceptance, pull request cleanup, and GitHub
Pages deploys never register it.
The registration job reports three reusable-workflow outputs:
cloudflare-custom-domain-status and
cloudflare-custom-domain-dns-target, plus
cloudflare-custom-domain-dns-action (unmanaged, created, or no-op). The
DNS target comes from the Pages project API's subdomain field, not from the
requested project name. Cloudflare can assign a collision-safe value such as
net-worth-calculator-xn8.pages.dev, even when
cloudflare-project-name is net-worth-calculator.
Wrangler remains responsible for Pages project creation and deployment. It does
not provide robust generic DNS-record lifecycle management, so the optional
cutover uses Cloudflare's official REST API with the same token. To enable it,
set cloudflare-manage-dns: true, provide the containing zone apex through
cloudflare-dns-zone, and optionally set cloudflare-dns-proxied (default
true). The token then needs Account Cloudflare Pages Write, Zone Read, and DNS
Edit permissions.
The workflow resolves exactly one active zone in the configured account, reads all exact-name records, and creates a TTL-auto CNAME only when no record exists. An exact CNAME to the resolved target with the requested proxy state is a no-op. Any other type, target, proxy state, duplicate, or ambiguous zone fails without modification. It never replaces or deletes a record.
For a workflow-managed cutover:
- Deploy and verify the generated
pages.devsite. - Set
cloudflare-custom-domain,cloudflare-manage-dns: true, andcloudflare-dns-zone. The custom domain must equal the zone or be its subdomain. - Review the summary for the resolved project, CNAME target, DNS action, proxy
state, and Pages domain status. A bounded activation wait may end in
initializingorpendingwhile Cloudflare finishes propagation; failed or unexpected states fail the job. - Keep the previous host or DNS value available as a rollback path until the custom domain and certificate are stable. Disabling DNS management does not delete records. To roll back a newly created record, restore the prior service first if applicable, then explicitly remove the CNAME in Cloudflare.
Leave cloudflare-manage-dns at its default false for manual or externally
managed DNS. Registration retains the v3.1 behavior and reports the target
without calling the DNS API.
Because artifacts are scoped to the workflow run and a reusable workflow's jobs
run inside the caller's run, no extra permissions and no credential handoff are
involved. Note that the same artifact is deployed to previews and to
production; if previews must be built without credentials, branch on
github.event_name in your own build job. See
ADR 0006.
Example caller:
on:
push:
branches: ["main"]
pull_request:
types: [opened, edited, synchronize, reopened, closed]
workflow_dispatch:
jobs:
pages:
uses: DevSecNinja/.github/.github/workflows/pages.yml@<sha> # v3.2.0
permissions:
contents: read
deployments: write
id-token: write
issues: write
pages: write
pull-requests: write
with:
# renovate: datasource=node-version depName=node
node-version: "24"
# renovate: datasource=npm depName=wrangler
wrangler-version: "3"
# Required when the site resolves a theme through Hugo Modules, which
# shells out to Go. Without it the runner's preinstalled Go is used.
# renovate: datasource=golang-version depName=go
go-version: "1.26.5"
test-setup-command: npx playwright install webkit --with-deps
test-command: |
npm run test:unit
npm run test:html
npm run test:a11y
npm run test:e2e
update-sitemap-lastmod: true
# Optional Cloudflare production instead of GitHub Pages:
# github-pages: false
# cloudflare-production: true
# cloudflare-project-name: "my-site"
# cloudflare-custom-domain: "www.example.com"
# cloudflare-manage-dns: true
# cloudflare-dns-zone: "example.com"
# cloudflare-dns-proxied: true
secrets:
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}Syncs files from config-sync/files/ to the calling repository. Runs on a
weekly schedule and opens a PR when files drift.
Source layout mirrors target layout. A file at
config-sync/files/<path> is synced to <repo>/<path>, recursively. For
example, config-sync/files/.github/CODEOWNERS lands at
<repo>/.github/CODEOWNERS.
Per-repo opt-out. List paths (one per line, repo-relative) in
.github/.config-sync-ignore to skip them during sync. # comments and
blank lines are allowed. The unmanaged repo topic remains the
all-or-nothing escape hatch.
| Input | Description |
|---|---|
sync-templates |
Also bootstrap files from config-sync/templates/. Default: false. |
central-ref |
Git ref of DevSecNinja/.github to sync from. Default: main. |
Example caller:
jobs:
sync:
uses: DevSecNinja/.github/.github/workflows/config-sync.yml@<sha> # v1.0.0
permissions:
contents: write
pull-requests: write
# with:
# sync-templates: true # also bootstrap template files for new reposRuns a caller-provided refresher command for repo-specific vendored files,
checks the configured paths for drift, and optionally opens or updates a PR
using actions/open-pr/.
The caller controls token permissions: grant contents: read for validation,
or contents: write and pull-requests: write when create-pr: true.
Use the same refresher command in two places:
- Scheduled/manual maintenance: call this workflow with
create-pr: true(the default) so refreshed vendored files land through a normal PR. - Pull-request validation: call this workflow with
create-pr: falseso a PR that directly introduces vendored drift fails until the refreshed files are committed.
| Input | Description |
|---|---|
refresh-command |
Required. Command that refreshes vendored files in the caller repo. |
paths |
Required. Newline-delimited pathspecs to watch and stage. |
create-pr |
Open/update a PR on drift. Set to false for validation-only PR lint. |
branch |
Working branch for the maintenance PR. Default: chore/vendored-file-sync. |
base |
Base branch for the maintenance PR. Default: main. |
pr-title |
PR title and default commit message. Default: chore: sync vendored files. |
pr-body |
PR body (Markdown). Default: "". |
labels |
Newline-delimited labels to apply on PR creation. |
commit-message |
Override the commit message. |
draft |
Open the PR as a draft. Default: false. |
signoff |
Add Signed-off-by trailer. Default: false. |
Outputs: changed, pr-number, pr-url.
Example caller — scheduled/manual auto-remediation plus PR validation:
name: Vendored File Sync
on:
pull_request:
paths:
- scripts/lib/**
- scripts/update-log-sh.sh
schedule:
- cron: "0 6 * * 1"
workflow_dispatch:
permissions:
contents: read
jobs:
validate:
if: github.event_name == 'pull_request'
uses: DevSecNinja/.github/.github/workflows/vendored-file-sync.yml@<sha> # v1.4.0
permissions:
contents: read
with:
refresh-command: bash scripts/update-log-sh.sh
paths: scripts/lib/
create-pr: false
sync:
if: github.event_name != 'pull_request'
uses: DevSecNinja/.github/.github/workflows/vendored-file-sync.yml@<sha> # v1.4.0
permissions:
contents: write
pull-requests: write
with:
refresh-command: bash scripts/update-log-sh.sh
paths: scripts/lib/
branch: chore/sync-vendored-log-sh
pr-title: 'chore: sync vendored log.sh'
pr-body: |
Refreshes the vendored `scripts/lib/log.sh` files from upstream.
labels: |
automated
vendored-file-syncSyncs the repository's labels to the actual GitHub labels on the repo. By
default it merges the org-wide base label set
(.github/labels-base.yaml, kept current by config-sync) with the
repository's own .github/labels.yaml. Either file is optional —
whichever exist are merged, and with delete-other-labels (default
true) any label not present in the union is removed.
| Input | Description |
|---|---|
base-labels-file |
Shared base labels file. Default: .github/labels-base.yaml. |
labels-file |
Repo-specific labels file. Default: .github/labels.yaml. |
delete-other-labels |
Delete labels not present in the config file(s). Default: true. |
Example caller:
jobs:
label-sync:
uses: DevSecNinja/.github/.github/workflows/label-sync.yml@<sha> # v1.0.0
permissions:
contents: read
issues: writeAuto-labels pull requests and issues based on path patterns
(pr-labeler.yaml) and keyword rules (issue-labeler.yaml).
Example caller:
jobs:
labeler:
uses: DevSecNinja/.github/.github/workflows/labeler.yml@<sha> # v1.0.0
permissions:
contents: read
pull-requests: write
issues: writeConverts TODO comments in code to GitHub Issues on push to the default
branch.
Example caller:
jobs:
run:
uses: DevSecNinja/.github/.github/workflows/todo-to-issue.yml@<sha> # v1.0.0
permissions:
contents: read
issues: write
with:
auto_assign: false
label: "todo-to-issue"Assigns newly-opened issues (or a specific issue when triggered manually) to the users listed in CODEOWNERS.
Example caller:
jobs:
assign:
uses: DevSecNinja/.github/.github/workflows/assign-issue-to-codeowners.yml@<sha> # v1.0.0
with:
issue-number: ${{ inputs.issue-number || 0 }}
permissions:
contents: read
issues: writeComposite actions live in actions/ and run inside the caller's job.
Use them when the action needs to operate on the caller's working tree,
OIDC subject, or pre-existing job state — things a reusable workflow
(which runs on its own runner) cannot see.
Pin the uses: ref to a full commit SHA with a version comment, just
like reusable workflows.
Opens (or force-updates) a pull request from the current working tree. Use it whenever a job has produced changes that should land via PR review rather than a direct push to a protected branch — signing, formatting, generated-content updates.
Key properties:
- Uses only the preinstalled
ghCLI; no third-party action. - Default
GITHUB_TOKENdoes not retrigger workflows on the bot's push, so it is safe inside loops like sign-on-push. - Reuses the existing PR (force-pushes its branch) when one already
exists for the same
branch -> basepair, so repeat runs do not spam duplicate PRs. - Branch protection on the base branch stays fully enforced.
| Input | Required | Default | Description |
|---|---|---|---|
branch |
yes | — | Working branch to create or force-push to. |
title |
yes | — | PR title (also default commit message). Conventional Commit subject. |
base |
no | main |
Branch to merge into. |
body |
no | "" |
PR body (Markdown). |
commit-message |
no | title |
Override the commit message. |
paths |
no | . |
Newline-delimited pathspecs to git add. |
labels |
no | "" |
Newline-delimited labels. |
draft |
no | false |
Open as draft. |
signoff |
no | false |
Add Signed-off-by trailer. |
if-no-changes |
no | skip |
skip = exit 0; fail = error when nothing to commit. |
git-user-name |
no | github-actions[bot] |
Commit author name. |
git-user-email |
no | github-actions[bot] noreply | Commit author email. |
github-token |
no | ${{ github.token }} |
Token for git push and gh. Override with PAT/App for cross-repo PRs. |
Outputs: changed, created, pr-number, pr-url.
Example caller — sign artifacts and open a PR:
name: Sign Scripts
on:
push:
branches: [main]
paths: ['**.ps1']
jobs:
sign:
runs-on: windows-latest
permissions:
contents: write
pull-requests: write
steps:
- uses: actions/checkout@v6
with: { fetch-depth: 0 }
- name: Sign scripts
shell: pwsh
run: ./sign.ps1
- uses: DevSecNinja/.github/actions/open-pr@<sha>
with:
branch: chore/sign-scripts
title: 'chore: sign scripts'
paths: '**/*.ps1'
labels: |
automated
choreSee actions/open-pr/README.md for full
documentation and more examples.
Wraps step-security/harden-runner for runtime monitoring and optional egress
blocking inside the caller's job. This is a composite action rather than a
reusable workflow because runner hardening has to execute in the same job as the
build, test, or release steps it protects.
Start in audit mode, review the observed egress, then switch high-value jobs to
block mode with explicit allowed-endpoints.
See actions/harden-runner/README.md and
ADR 0002.
Generates Conventional-Commit release notes via git-cliff and creates
an immutable GitHub Release with optional asset upload and preset notes
blocks. Composite (not reusable workflow) so any preceding
actions/attest-build-provenance step in the caller's job stays bound
to the caller's OIDC subject. See
actions/release-publish/README.md.
The workflow-templates/ directory contains starter workflows that appear in
the GitHub UI under Actions → New workflow for repositories in this
organisation. Each template has a matching .properties.json file that
provides the display name, description, and category shown in the picker.
| Template | Purpose |
|---|---|
lint.yml |
Linting pipeline |
pages.yml |
GitHub Pages deploy and PR previews |
release.yml |
GitHub Release on tag push |
release-please.yml |
App-authenticated release PRs and tags |
config-sync.yml |
Weekly config drift sync |
label-sync.yml |
Sync labels from .github/labels.yaml |
labeler.yml |
Auto-label PRs and issues |
todo-to-issue.yml |
Convert TODO comments to issues |
assign-issue-to-codeowners.yml |
Assign issues to CODEOWNERS |
These files are kept identical across all repositories. The config-sync.yml
workflow opens a PR whenever a repo's copy drifts from the source. Do not
edit these files in individual repos — submit a change here instead.
The directory tree mirrors the layout in the calling repo: a file at
config-sync/files/<path> is synced to <repo>/<path>. To opt a single
repo out of a specific path, list it in .github/.config-sync-ignore at
the repo root.
| File | Purpose |
|---|---|
.editorconfig |
Editor formatting defaults |
.github/CODEOWNERS |
Canonical CODEOWNERS (default @DevSecNinja) |
.github/labels-base.yaml |
Base label set synced to all repos |
.gitleaks.toml |
Gitleaks allow-list |
.markdownlint.yaml |
Markdown lint rules |
.shellcheckrc |
ShellCheck configuration |
.yamlfmt.yaml |
yamlfmt formatting rules |
.yamllint.yaml |
yamllint rules |
dprint.json |
dprint Markdown formatter config |
issue-labeler.yaml |
Issue auto-label rules |
pr-labeler.yaml |
PR auto-label rules |
renovate.json5 |
Renovate base config reference |
These are starting-point files. Copy them into a new repository and adapt
as needed — they are not auto-synced. See
config-sync/templates/README.md for
details.
The .renovate/ directory contains shared Renovate preset fragments imported
by individual repositories via their renovate.json5.
| Preset | Purpose |
|---|---|
base.json5 |
Core scheduling and PR settings |
autoMerge.json5 |
Auto-merge rules for patch/minor updates |
groups.json5 |
Dependency grouping |
labels.json5 |
Label assignment for Renovate PRs |
packageRules.json5 |
Per-ecosystem rules |
customManagers.json5 |
Custom regex managers (e.g. mise-version in workflow files) |
semanticCommits.json5 |
Conventional Commits for Renovate PRs |
PRs that Renovate can auto-merge are tagged with the merge: auto label.
Their titles also carry a trailing [automerge] suffix (via
commitMessageSuffix) so auto-merge PRs are easy to spot at a glance.
Code Owners are not requested as reviewers for routine Renovate PRs. Renovate
can still assign Code Owners if an automerge PR cannot merge cleanly.
What auto-merges (minor + patch, via GitHub platform automerge):
| Ecosystem | Scope | Condition |
|---|---|---|
| GitHub Actions | actions/*, docker/*, github/* |
All versions |
| Docker images | All | Current version ≥ 1.0 |
| GitHub releases | All | Current version ≥ 1.0 |
| Mise tools | All | Current version ≥ 1.0 |
| npm | All | Current version ≥ 1.0 |
| pip | All | Current version ≥ 1.0 |
| Go modules | All | Current version ≥ 1.0 |
| DevSecNinja devcontainer (digest) | ghcr.io/devsecninja/* |
Always (no wait time) |
| Dotfiles devcontainer image | ghcr.io/devsecninja/dotfiles-devcontainer |
Always (branch merge, no tests) |
| Lock file maintenance | All | Always (branch merge, no PR) |
What does NOT auto-merge:
- Major version updates (all ecosystems)
- Pre-1.0 packages (
0.x— semver minor can introduce breaking changes) - GitHub Actions from untrusted organisations
- Digest-only updates (disabled entirely for supply-chain safety, except DevSecNinja devcontainer images)
- 14-day
minimumReleaseAgeon all dependency updates — ensures community vetting before adoption. - CI must pass —
platformAutomerge: trueuses GitHub's native auto-merge, which respects required status checks. - Branch auto-merge exceptions —
ghcr.io/devsecninja/dotfiles-devcontainerand lock file maintenance updates use branch automerge withignoreTests: true. The dotfiles devcontainer rule also setsschedule: ["at any time"]so it ignores the global schedule and can merge whenever a digest update lands. - OSV vulnerability alerts bypass
minimumReleaseAge(set to0) so security fixes merge immediately. - Schedule — Renovate only runs on weekends and Fridays, except the dotfiles devcontainer image, which can merge at any time.
- Digest updates disabled — prevents merging potentially hijacked tags (except
DevSecNinja-owned devcontainer images on
ghcr.io/devsecninja/, which are trusted internal images and auto-merge immediately with no wait time).
Repositories import the shared fragments in their renovate.json5:
{
extends: [
"config:recommended",
"helpers:pinGitHubActionDigests",
":dependencyDashboard",
":semanticCommits",
"github>DevSecNinja/.github//.renovate/autoMerge.json5",
"github>DevSecNinja/.github//.renovate/base.json5",
"github>DevSecNinja/.github//.renovate/customManagers.json5",
"github>DevSecNinja/.github//.renovate/groups.json5",
"github>DevSecNinja/.github//.renovate/labels.json5",
"github>DevSecNinja/.github//.renovate/packageRules.json5",
"github>DevSecNinja/.github//.renovate/semanticCommits.json5",
],
}Architecture Decision Records are stored in
docs/design-decisions/.
| ADR | Title | Status |
|---|---|---|
| 0001 | Reusable workflows must not default package versions | Accepted |