Portable DevContainer bootstrap
New DevContainers use an explicitly selected Praetor source snapshot. Adopted
repositories do not need Praetor's cmd/standardsctl sources or
docker/dev/Dockerfile in their own project tree.
praetorctl devcontainer generate \
--source-root /path/to/reviewed/praetor \
--config .standards.yaml \
--output .devcontainer/devcontainer.json
praetorctl devcontainer verify
Generation prepares the configuration and exact companion files. It does not
build an image, execute the selected source, or certify application tests.
The selected source must be a Git checkout declaring the Praetor module. From its
tracked and nonignored untracked files, preparation captures what
go build ./cmd/standardsctl reads: the Go sources of every module package
cmd/standardsctl imports, directly or transitively, plus go.mod, go.sum,
LICENSE, and the assets of each reached go:embed family — every managed
asset family in the registry (managedasset.Families,
internal/managedasset/family.go; today the five declared tools/markdownlint
assets) and the templates/*/*.tmpl bodies flavor apply scaffolds. The import
closure is read with Go's parser, not the go command, so preparation needs no Go
toolchain (captureBootstrapClosure in
internal/devcontainer/bootstrap_closure.go). Build constraints are not
evaluated: a reached package contributes every non-test file and its imports,
for every platform. Packages only other commands import, such as
cmd/standards-mcp, stay out, and an import of a module package with no
captured Go source fails preparation, because the image could not build it.
The set is captured twice; a changed snapshot fails preparation. Go's test surface,
_test.go files and anything under a testdata directory, is not captured:
the generated Dockerfile only runs go build ./cmd/standardsctl, which reads
neither. The Git pathspec excludes it and the name check refuses it first, for
declared assets too, through one rule, util.IsGoTestSurface
(internal/util/gosource.go), so a test file cannot enter a captured set, and
an archive carrying one fails verification
(internal/devcontainer/bootstrap_source.go). A captured set holding a family's
embedding source, such as tools/markdownlint/assets.go or templates/embed.go,
must hold every asset that source embeds, and each source may carry only its
declared directive (bootstrapAssetFamilies). Undeclared
go:embed inputs and unsupported native build inputs fail explicitly rather
than being omitted. Capture is bounded to 4,096 files, 16 MiB total, and 1 MiB
per file.
The recorded customizations.praetor.bootstrap specification identifies the
source snapshot, compressed archive, Dockerfile, and immutable builder/base
images. The compiled CLI retains the version declared by the selected source;
its displayed version is not the bundle digest. Read the source identity from
the retained bootstrap specification and verified companions. The compressed
source is carried in at most twelve 512 KiB base64 files (maxBootstrapParts
in internal/devcontainer/bootstrap.go), alongside
Dockerfile.praetor. Keep these files together with the JSON. They are
source-bearing artifacts: select and review the source before copying a bundle
to another repository. The hashes establish integrity, not source authenticity
or a release signature.
TestRepositoryBootstrapSourceKeepsHeadroom
(internal/devcontainer/bootstrap_source_test.go) measures Praetor's own
capture against the frame capacity, the 16 MiB total and the file count. It
fails above 80% of any of them, so growth is reported before a bootstrap stops
fitting; run it with go test -v to print the current usage. With Praetor's
source compressing to about a third, the frame capacity (twelve 512 KiB base64
files) binds before the 16 MiB total does. The cap rose from eight to twelve on
2026-10-18, when Praetor's own capture passed 80% of eight frames.
The Dockerfile verifies the archive, checks module integrity without changing
go.mod or go.sum, and builds the selected CLI. The runtime installs it at
/usr/local/bin/praetorctl, plus a standardsctl compatibility name. Startup
uses that absolute CLI to compile the agent context, then invokes
/usr/bin/make verify-all. Native application tools must still be supplied by
the selected DevContainer features or a reviewed custom container. The image
carries praetorctl only, not the other binaries a
workstation install places:
tribunusctl syncs the operator's model data for the
Tribunus router, which an adopter DevContainer does
not run (renderBootstrapDockerfile in internal/devcontainer/bootstrap.go).
No RUN line of Dockerfile.praetor holds a pipe. A pipeline's status is its
last command's, so cat ... | base64 -d would hide a failed cat: the archive
frames are joined into a file, decoded from it, and checked against a digest
file, so every command's status counts and Hadolint's
DL4006 has nothing to
report. The Dockerfile sets no pipefail SHELL instead, because the builder
image is selectable (--builder-image) and the two shells Hadolint accepts for
pipefail, /bin/ash and /bin/bash, each exist in only one image family:
Alpine has no bash, the Debian golang images no ash (bootstrapArchiveSteps
in internal/devcontainer/bootstrap.go).
TestBootstrapDockerfilePipesFollowPipefailShell requires any piped RUN to
follow a pipefail SHELL in its stage, and
TestRenderedBootstrapDockerfilePassesHadolintDL4006 runs hadolint on the
rendering where it is installed and skips, saying so, where it is not
(internal/devcontainer/bootstrap_pipefail_test.go).
To exercise the prepared Dockerfile independently of an IDE:
Run the generated postCreateCommand against a disposable project checkout
when testing startup. A successful configuration verification checks the exact
recorded companions; a successful image build and successful startup are
separate evidence. Verification uses the retained specification, so it does
not require the original workstation source path or a hardcoded generator
commit to remain available.
Verification compares devcontainer.json itself against the render of the
expected configuration, after normalising CRLF line endings to LF so a Windows
checkout with core.autocrlf=true does not report that file as drift (HISS-21).
JSON forbids an unescaped carriage return inside a string, so every CRLF in the
file is whitespace between tokens and the normalisation cannot hide an edit.
The normalisation covers devcontainer.json only. Companions such as
Dockerfile.praetor are checked against their recorded hashes as raw bytes, so
a CRLF checkout of a ready bootstrap fails verification on Dockerfile.praetor;
checkout line endings describes the .gitattributes
rule adoption writes to prevent that.
Verification does not compare a re-render of what decoded, because
Go's JSON decoder matches member names case-insensitively and keeps the last of a
duplicate pair: POSTCREATECOMMAND, RemoteUser and a repeated
postCreateCommand all decode into the managed struct and re-marshal to the
spec spelling, while the DevContainer runtime reads object keys case-sensitively
and would run none of them. A key outside the managed schema, such as a
hand-added initializeCommand or runArgs, is named in the rejection; every
other edit, including whitespace and content after the configuration object, is
reported as drift. Adoption preserves an existing custom DevContainer and
reports it as execution-unverified, but standardsctl audit verifies any
.devcontainer/devcontainer.json against the declared standards
(cmd/standardsctl/audit.go, auditAgentContextAndDevcontainer). A file
carrying keys outside the managed schema therefore fails audit; no
configuration keeps such keys and passes it.
Without --source-root, or with an explicitly selected config-only catalog,
generation writes an unavailable configuration and returns an error. Its
startup fails with an actionable message; it cannot report a missing CLI as
ready. To remediate, rerun the same command with --source-root. It replaces
the placeholder without --force while the file is exactly what Praetor
rendered for the same profiles and features, CRLF line endings aside; any edit
keeps the file behind --force (admitReplacement in
internal/devcontainer/bootstrap_io.go, tests in
internal/devcontainer/bootstrap_replace_test.go). --force without
--source-root never replaces a ready bootstrap with a placeholder: the
configuration would stop starting and Dockerfile.praetor and the source parts
would be left orphaned. Select a source root, or remove the bundle files first
to drop the bootstrap deliberately. Invalid explicit source paths, mutable image references, incomplete
bundles, symlinks, and altered companions are errors. Optional --builder-image
and --base-image overrides must name a lowercase SHA-256 digest, as
repository@sha256:<digest> or repository:tag@sha256:<digest>; a tag alone or
a malformed digest is refused (validateBootstrapImages in
internal/devcontainer/bootstrap.go, tests in
internal/devcontainer/bootstrap_digest_test.go).
The reviewed defaults, DefaultBuilderImage and DefaultBaseImage in
internal/devcontainer/bootstrap.go, are digest-only references. The
// Reviewed at line directly above each keeps the full
repository:tag@sha256:<digest> reference it was reviewed at, and
TestReviewedDefaultCommentsNameTheirDigest holds that line to the constant's
digest. The line is the image's one pin
(Moving a reviewed default image). @devcontainers/cli 0.89.0 refuses
a repository:tag@sha256:<digest> reference while it inspects the registry
(devcontainers/cli#1307), so it cannot build a bundle that records one
(#333). Praetor still
accepts a tagged override; give the digest-only form to a bundle that CLI builds.
Regeneration keeps the images the output file records. Without --base-image
or --builder-image, generation reads the bootstrap specification already
recorded there. A recorded image that is an earlier reviewed default, such as
the ubuntu-24.04 base Praetor shipped before the 26.04 move, is refreshed to
the current reviewed pin. So is the current default recorded with a tag, such as
the ubuntu26.04 form generation recorded before the defaults became
digest-only. Earlier defaults are listed as repository@digest, oldest first,
under base and builder in internal/devcontainer/prior-images.json, which
is compiled into the binary, and a recorded image matches the current or
an earlier default when it names the same repository and digest, under any tag
or none (isReviewedPin in internal/devcontainer/bootstrap_recorded.go). Any
other recorded image is the adopter's choice and is kept, so --force does not
swap it for the reviewed default. That includes another digest of the reviewed
default repository, such as a debian-12 base or a newer golang builder, and
the reviewed digest under another repository, such as a mirror. An explicit flag
always wins.
Each image kept, refreshed or replaced against its recorded value is printed as a [RECORDED IMAGE KEPT], [RECORDED IMAGE REFRESHED] or
[RECORDED IMAGE REPLACED] line naming the recorded image, the selected one and
the flag that changes it. A missing file, a custom DevContainer or an invalid
specification records no choice, so the reviewed defaults apply
(InheritRecordedImages in internal/devcontainer/bootstrap_recorded.go,
tests in internal/devcontainer/bootstrap_recorded_test.go and
TestDevContainerCLIForceKeepsRecordedBaseImage in
cmd/standardsctl/devcontainer_bootstrap_test.go).
Adoption uses its explicit --lock-source-root as the bootstrap source and the
same planned or preserved manifest that audit consumes. A config-only catalog
can supply governance policy while bootstrap remains visibly unavailable.
Dry-run lists planned companion paths without writing them. Existing custom
DevContainers are preserved and reported as execution-unverified. Forced
adoption keeps recorded images by the same rule and lists each note as a
warning.
Feature options and editor settings
The selected catalog entries decide which DevContainer features a bundle installs (archetype authoring). Two values are Praetor's own:
common-utilsreceivesinstallZsh: falseandupgradePackages: true, so the image upgrades the OS packages of its digest-pinned base instead of keeping their fixable vulnerabilities. Options a catalog sets forcommon-utilsmerge over these defaults key by key: a key the catalog sets wins, and every default it leaves unset is kept. The shipped archetypes selectcommon-utilswith no options and keep both defaults; only an explicitupgradePackages: falseturns the upgrade off. The rule matches the feature under any registry, mirror, tag or digest (selectedFeatureOptionsininternal/devcontainer/devcontainer.go).- A container with Go tooling sets
go.toolsManagement.autoUpdatetofalse, the Go extension's own default, so the extension does not update its tools after creation behind a pinned Go feature and golangci-lint version. The generated DevContainer installsgolangci-lintat the version pinned intools/lint/go.modoutside the Go feature (ghcr.io/devcontainers/features/go:1). Because the Go feature installs linters only inside itsinstallGoToolsblock, configuringinstallGoTools: falseon the Go feature (or leaving options default) retains the pinnedgolangci-lintexecutable in/usr/local/bin/golangci-lint. Editor settings configurego.lintTool: golangci-lintandgo.lintOnSave: package; a generated container configuringgo.lintTool: golangci-lintwithout installing the tool or using an unpinned version (latest) is refused bydevcontainer verify.
Tests: internal/devcontainer/feature_defaults_test.go, internal/devcontainer/golangci_test.go.
Moving a reviewed default image
The bundle files are generated, so an image never moves by an edit to
.devcontainer/Dockerfile.praetor or devcontainer.json: verification rejects
the edit and names the two commands that repair it. In a Praetor checkout, one
command moves a default
(#323):
--base-image moves the base the same way, and both may be given. The command
(Bump in internal/devcontainer/bootstrap_bump.go):
- Rewrites the pin in
internal/devcontainer/bootstrap.go, the// Reviewed atline and the digest-only constant below it, and theFROMline ofdocker/dev/Dockerfile, which builds from the builder pin. - Appends the replaced image to
internal/devcontainer/prior-images.json, so a regeneration refreshes an adopter's bundle that recorded it instead of keeping it as the adopter's choice. - Regenerates the bundle at
--outputfrom the checkout with the new pins and verifies it.
A failure at any step restores every file the command wrote. The new reference
must carry a tag and a digest and stay in the pin's repository; a tagless
reference is refused. --source-root selects the checkout and defaults to the
directory of --config; --force and --verify are not accepted. Commit the
pin, the prior list, the Dockerfile and the bundle together.
The tag moves with the same command. Give the new tag with its digest, and the
command rewrites the tag in the // Reviewed at line and in the FROM line,
also when the new tag names the digest already pinned (a floating tag and a
patch tag share one). The tests read the tag and the digest from the
// Reviewed at line (ReviewedReferences in
internal/devcontainer/reviewed_images.go) and spell neither, so the tree
passes after a move to another tag, by this command or by a Renovate tag
update. Prose that explains a tag, such as the comment above DefaultBaseImage,
is not rewritten: review it in the same change. A tag that also occurs in the
image's repository or digest is refused, because Renovate replaces the tag's
text inside the pin and could not move such a tag alone.
The prior list only grows, and nothing prunes it: a bundle that recorded a
removed image would be kept as the adopter's choice instead of refreshed. Each
role holds at most 256 images (maxPriorImages in
internal/devcontainer/reviewed_images.go). A bump that would pass that bound
is refused before it writes anything, and the refusal names the remedy: raise
maxPriorImages in a reviewed change, then run the bump again.
Without an image option the command finishes a move already made to the pin: it
records the image the bundle still holds as replaced and regenerates. That is
how a Renovate update lands, of the digest or of the tag with it.
renovate.json points Renovate at the pin, never at the bundle:
- A regex custom manager reads the
// Reviewed atline and its constant withReviewedPinPattern(internal/devcontainer/reviewed_images.go). Renovate looks the digest up under the reviewed tag and rewrites every occurrence of the old digest in the match, so the line and the constant move together. - A
packageRulesentry disables Renovate for.devcontainer/devcontainer.jsonand.devcontainer/Dockerfile.praetor. The Dockerfile pins its images digest-only, and Renovate looks a reference without a tag up aslatest: it proposedgolang:latestanddevcontainers/base:latestthat way. - A group keeps the pin and the
FROMline ofdocker/dev/Dockerfilein one branch. - DevContainer features move through the catalog, not through
devcontainer.json: two custom managers read the feature references and the Node.js version under.config/archetypes/(archetype authoring).
The Renovate branch moves the pin and the Dockerfile only, so
TestPinnedImagesMatchTheDockerfilesThatUseThem fails on its takeover until
praetorctl devcontainer bump has run there
(Renovate pull requests).
Tests: internal/devcontainer/bootstrap_bump_test.go, which also bumps to
another tag and to the bound of the prior list,
internal/devcontainer/reviewed_images_test.go,
cmd/standardsctl/devcontainer_bump_test.go, which replays a Renovate builder
update and a tag move through the command and requires devcontainer verify to
pass, and internal/devcontainer/renovate_test.go, which applies
renovate.json to the tree: the pins are read with their tags, the bundle is
disabled, and no file Renovate reads pins a digest without a tag. The assertions
the committed tree is held to are in
internal/devcontainer/reviewed_tree_test.go, and the bump tests run them on
the checkout a bump leaves.
An adopted repository gets the same protection from adoption. When its bundle
is one Praetor generated, the renovate-ignore step lists
.devcontainer/devcontainer.json and .devcontainer/Dockerfile.praetor in the
entry that disables the adopter's Renovate for Praetor-managed files
(documentation governance). A DevContainer of
the adopter's own is preserved and stays with their Renovate. Image updates
reach an adopter with a regeneration from a reviewed Praetor checkout.
Checkout line endings
praetorctl adopt pins the bundle's Dockerfile to LF. It writes
.devcontainer/Dockerfile.praetor text eol=lf (Attributes in
internal/devcontainer/checkout.go) as the first rule of the managed block at
the end of .gitattributes, the block that also carries the figure engine's
rules while docs:seo-portal is enabled (ManagedAttributes in
internal/adopt/gitattributes.go). Without the rule, a clone with
core.autocrlf=true, git's default on Windows, checks Dockerfile.praetor out
with CRLF and fails audit on a file nobody edited
(#313).
The rule names that one file and no wildcard. Dockerfile.praetor is the only
bundle file verification compares as raw bytes: devcontainer.json is compared
after normalisation and the base64 source parts carry no line terminator
(TestCheckoutAttributes_Positive in internal/devcontainer/checkout_test.go).
The block sits at the end of the file, where git lets it win, so a rule on
.devcontainer/* would also claim the repository's own files beside the
bundle: an image there would become text in spite of a *.png binary rule, and
the next git add would rewrite the CRLF byte pairs every PNG signature holds.
With the rule on the one file, an image, a CRLF script and every other file the
repository keeps in .devcontainer/ stay under the repository's own rules
(TestCheckoutAttributes_Negative_NoRuleUsesAWildcard,
TestDevContainerAttribute_Negative_OperatorFilesBesideTheBundleKeepTheirBytes
in internal/adopt/gitattributes_devcontainer_test.go).
- Fresh and existing files. A repository without
.gitattributesgets one holding the block. An existing file keeps every line outside the block, the repository's own rules on.devcontainer/included, and gains the block once; a rerun changes nothing (TestDevContainerAttributeAdoptionGolden). - Earlier adoptions. A repository adopted before the rule existed receives
it on its next plain
praetorctl adopt: the documentation-only block is refreshed without--force, and a repository without the documentation facet gains the block (TestReconcileGitAttributes_Positive_EarlierBlockRefreshedWithoutForce). Until thenpraetorctl auditstill passes on an LF checkout, because audit accepts the block with and without the rule (GitAttributesCanonical). - Conflicting rules. A rule outside the block conflicts when its pattern
starts with
.devcontainer/, can matchDockerfile.praetor, and givestextoreola state that contradictstext eol=lf:-text,!text,binary,-eol,!eolor aneolother thanlf. Examples are.devcontainer/* -text,.devcontainer/Dockerfile.praetor eol=crlfand.devcontainer/** binary. Such a rule stops adoption before its first write, in a dry run too, and the message quotes it, because the block would silently override it for that file. Remove or change the rule, or decline the step. Everything else is kept and merges: a rule on another file, such as.devcontainer/*.json eol=crlf, or on a subdirectory of the directory; a compatible rule, such as.devcontainer/* text=auto eol=lf; and a rule on a wider pattern, such as* -text, which is the repository's default and which the block overrides for the one file adoption writes (TestManagedAttributes_Negative_ConflictingOperatorRuleRefused,TestManagedAttributes_Boundary_OnlyRulesOnThePinnedFileConflict,TestAttributePatternMatches,TestAdopt_Negative_ConflictingAttributeRuleStopsBeforeAnyWrite). - Edited block. A block whose lines were edited stops a plain run before
its first write and names the forced rerun;
--forcerestores it as a replace with a backup, whether or not the documentation facet is enabled (TestAdopt_EditedAttributeBlockWithoutDocumentationFacet,TestPreflightAttributes_Boundary_EditedBlock). - Declined step. With
dev-containerinadoption.declinethe bundle is the repository's own, so the block carries no DevContainer rule, and no block remains once the documentation facet is disabled as well (TestReconcileManagedAttributes_Boundary_DeclineDropsTheRule).
When the bundle differs from its recorded inputs only by line endings,
praetorctl audit and praetorctl devcontainer verify say so and name the
remedy (devContainerCheckoutRemedy in
cmd/standardsctl/devcontainer_checkout.go). A .gitattributes without the
rule is told to run praetorctl adopt and commit .gitattributes; where the
manifest declines dev-container, adoption writes no rule, so the message says
to add it by hand
(TestDevContainerCheckoutRemedy_Negative_DeclinedStepDoesNotNameAdoption). A
.gitattributes that carries the rule means the working tree was checked out
before the rule existed, or another attribute source overrides it;
git check-attr text eol -- .devcontainer/Dockerfile.praetor shows which.
Either way the file in the working tree is still CRLF, and git checkout does
not rewrite it: git skips a file whose index entry is unchanged, and a
converted file is unchanged to git. The message therefore names the two
commands that do (RecheckoutCommands in internal/devcontainer/checkout.go):
git rm --cached --quiet -- .devcontainer/Dockerfile.praetor
git checkout HEAD -- .devcontainer/Dockerfile.praetor
The first drops the index entry and leaves the file in place; the second
restores the entry from HEAD and writes the file under the attributes now in
force. Verification fails until then: a CRLF Dockerfile.praetor is not the
file the specification records (TestCheckoutAttributes_Negative,
TestAuditDevContainer_ConvertedCheckoutNamesTheRule in
cmd/standardsctl/devcontainer_checkout_test.go).
TestDevContainerAttribute_Positive_CRLFCheckoutVerifiesOnceTheRuleExists
commits a bundle in a repository with core.autocrlf=true and shows each
step: the clone fails verification without the rule, the two commands bring a
working tree that predates the rule to LF without deleting a file by hand, and
a clone under the rule verifies as it is.
Bundle freshness
praetorctl devcontainer verify checks a bundle against its own records, so a
bundle cut a hundred commits ago verifies as cleanly as one cut a minute ago
(#338). The freshness check
compares it with the tree instead:
It recaptures the build source from the working tree beside --config with the
capture generation uses, and compares its digest with the recorded
sourceSHA256. Equal digests print [FRESH]. Otherwise the check finds the
newest commit reachable from HEAD that changed a bundle file
(devcontainer.json, Dockerfile.praetor or a praetor-source.*.b64 part),
counts the commits from it to HEAD, and measures its committer age. Inside the
bounds it prints both numbers under [DRIFT WITHIN BOUNDS] and passes; past
either bound it fails with them and the regeneration command. A bundle exactly
at a bound passes (CheckFreshness in
internal/devcontainer/bootstrap_freshness.go).
The bounds are declared in .standards.yaml; an unset key keeps its default:
The commit default is about three weeks of this repository's landing rate in
September 2026, and the age default is three runs of the weekly refresh below.
Each value must be an integer from 1 to 100,000 commits or 3,650 days
(internal/config/devcontainer_policy.go).
A missing configuration, one without a bootstrap, an unavailable bundle, a
source root that does not declare the Praetor module, a shallow clone, a drifted
bundle no commit records, and any git failure are errors, never a pass. The
check therefore needs the full history: CI checks out with fetch-depth: 0. It
measures a Praetor source checkout's own bundle; an adopted repository whose
bundle came from a separate Praetor checkout gets an error rather than a
measurement. Tests: internal/devcontainer/bootstrap_freshness_test.go,
internal/config/devcontainer_policy_test.go and
cmd/standardsctl/devcontainer_freshness_test.go.
make devcontainer-freshness runs the check inside make verify-all, and the
DevContainer Bundle Freshness step of .github/workflows/ci.yml runs it on
the light CI runs that skip verify-all. It is not part of verify on purpose:
validateReadyBootstrap in internal/devcontainer/bootstrap.go stays the
self-consistency check that freshness backstops, so verification needs no Git
history.
.github/workflows/devcontainer-refresh.yml keeps the bundle inside the bounds.
Every Monday at 05:00 UTC, and on dispatch, in the canonical repository only, it
records the freshness report, regenerates the bundle with
praetorctl devcontainer generate --source-root . --force, verifies it, and
commits the .devcontainer changes on chore/devcontainer-bundle-refresh. The
commit is signed off under the PRAETOR_BOT_NAME and PRAETOR_BOT_EMAIL
identity adopt.yml uses. What it publishes depends on the PRAETOR_PR_TOKEN
secret, a token with contents and pull-requests write on the repository:
PRAETOR_PR_TOKEN |
Refresh pull request | Result |
|---|---|---|
| set | none open | force-pushes the branch and opens the pull request, with the freshness report in its body |
| set | open | force-pushes the branch and updates that pull request |
| unset | none open | pushes the branch with GITHUB_TOKEN, stays green, and warns with the link that opens the pull request |
| unset | open | pushes nothing and warns that the open pull request should land |
GITHUB_TOKEN cannot stand in for the secret: this repository does not let it
open pull requests, and GitHub starts no workflow for a push it makes. A pull
request a person opens from the warning's link runs its checks; a later push
with GITHUB_TOKEN would move it to a head no check ran on, so the job leaves an
open one alone. Until the secret is configured, someone has to open and land the
refresh pull request before the bundle passes a bound, or the freshness gate
fails in verify-all and CI. With the secret set, a refused push or pull request
fails the run. The warnings also appear in the run's step summary.
scripts/devcontainer_refresh_publish.sh implements this step and
scripts/test_devcontainer_refresh_publish.py (make devcontainer-refresh-test,
part of verify-all) covers each row and the workflow wiring.
The pull request carries no receipt, so the landing pipeline takes it over like
a Renovate pull request. To refresh by
hand, run the same generate command from a clean checkout and commit the
.devcontainer changes on their own.
Migration
This release is breaking for existing adopter files and API callers.
Verify and standardsctl audit now report drift on a devcontainer.json
they previously accepted when it carries a key outside the managed schema, an
aliased or duplicated key, or content after the configuration object.
LoadDevContainer now refuses a key outside the managed schema. The
Synthesize* functions refuse more than MaxLoopLimit profiles or facets
instead of truncating them. The rendered features and postCreateCommand follow
the selected feature set, so an unedited non-bootstrap file can report drift
after the upgrade. To remediate, review the file and regenerate the bundle:
praetorctl devcontainer generate \
--source-root /path/to/reviewed/praetor \
--config .standards.yaml \
--output .devcontainer/devcontainer.json \
--force
praetorctl devcontainer verify
--force replaces only the named bundle files. Move hand-added keys such as
initializeCommand or runArgs out of the file first: audit cannot pass while
they remain.
Existing working custom or Praetor self-host DevContainers remain untouched.
Legacy generated files that refer to missing Praetor paths now fail verification.
Review those files, then run the generation command above with --force to
replace only the named bundle files. Do not use broad forced adoption solely to
repair a container. A new config-only invocation no longer produces an implied
runnable container: select a complete reviewed source checkout to activate the
bootstrap. API callers should use PrepareBundle and WriteBundle; the legacy
JSON writer cannot publish a recorded specification without its companions.
Runtime/profile selection is sourced from the selected pinned catalog entries.
Only the selected profile and facets contribute DevContainer features; duplicate
references must agree on options. Without a pinned catalog the profile still
decides: a native-gpu-systems repository receives the C/C++ toolchain
extensions and no Go feature, even when framework is declared alongside it.
The generated postCreateCommand follows the feature set the container actually
receives, not the profile list. go run ./cmd/standardsctl compile-context is
emitted only for a framework repository whose features install a Go toolchain:
the selected catalog decides that whenever one was selected, and the profile
decides it only on the legacy path with no selection. A framework repository
whose catalog carries no Go feature therefore starts with make verify-all
alone, which changes what standardsctl audit expects from an existing
non-bootstrap .devcontainer/devcontainer.json for that combination. Profiles
choose which IDE tooling is installed; they do not overrule the catalog about
what is present. More than MaxLoopLimit declared profiles or facets is refused
rather than truncated. This does not establish IDE feature-installation or
application-tool execution proof.
A native-gpu-systems DevContainer no longer passes clangd
--compile-commands-dir=core/build. Without that flag clangd searches each
edited file's ancestor directories and their build/ subdirectories for
compile_commands.json (clangd project setup),
which finds a root, build/ or core/build/ database alike; the fixed directory
pointed every other layout at a missing path. The DevContainer settings and the
.vscode/settings.json adoption writes share one definition,
util.ClangdArguments in internal/util/clangd.go; the regression tests are in
internal/devcontainer/adopter_paths_test.go. An unedited native bundle generated
before this change reports drift in praetorctl devcontainer verify and
standardsctl audit; regenerate it with the --source-root ... --force command
above. Adoption keeps an existing .vscode/settings.json unless it runs with
--force.
Digest-only reviewed defaults
A ready bootstrap generated before #333 records the reviewed defaults as
repository:tag@sha256:<digest>. It still passes praetorctl devcontainer
verify and standardsctl audit, but @devcontainers/cli 0.89.0 cannot build
it. Regenerate it with the generation command above and --force: each image
is reported as [RECORDED IMAGE REFRESHED] and recorded digest-only, which
changes Dockerfile.praetor and the recorded dockerfileSHA256. The images
pulled are unchanged, because the digests are. To keep a tagged reference, pass
it with --base-image or --builder-image
(TestDevContainerCLIDigestOnlyImages in
cmd/standardsctl/devcontainer_bootstrap_test.go).
Test sources leave the bootstrap archive
A ready bootstrap recorded before this change carries _test.go and testdata
files. Verification now refuses such an archive with bootstrap source <path> is
test-only, so praetorctl devcontainer verify and standardsctl audit report
it until the bundle is regenerated with the generation command above and
--force. Regeneration rewrites devcontainer.json, Dockerfile.praetor and
the praetor-source.*.b64 parts; the recorded sourceSHA256, archiveSHA256
and dockerfileSHA256 change even when no build source changed. The compiled
CLI does not change, because go build never read the removed files.
The archive carries only the build closure
Before #501 the archive held every non-test Go file of the module, so packages
the CLI never imports filled the four-frame cap. Preparation now captures the
import closure of cmd/standardsctl, and the cap is twelve frames. A ready
bootstrap recorded earlier still verifies: its extra packages pass the same
name rule, and its part count is within the new cap. Regenerating it with
--force shrinks the archive and changes the recorded digests. A bundle that
needs more than four parts is refused by a praetorctl released before this
change, so verify such a bundle with a current CLI. This command prints the
current usage of each bound:
Pinned Go tools, common-utils defaults and an unpiped Dockerfile
A bundle generated before this change reports drift in
praetorctl devcontainer verify and praetorctl audit: its
devcontainer.json sets go.toolsManagement.autoUpdate to true and passes
common-utils without the merged defaults, and its recorded dockerfileSHA256
names the piped Dockerfile rendering, reported as bootstrap Dockerfile
identity differs from its recorded inputs
(feature options). Regenerate it with
the generation command above and --force. Regeneration still reads what the
earlier bundle records: an adopter's base or builder image is kept, and
--force without a source root still refuses to replace the ready bootstrap
(decodeRecordedBootstrap in internal/devcontainer/bootstrap_io.go, tests in
internal/devcontainer/bootstrap_rendering_test.go).
Infrastructure test environments
The current GitOps profile selects tool features; it does not provision a Kubernetes cluster, Ansible target, or VM. Keep editor tooling and task execution requirements distinct. The infrastructure environment plan extends the matrix with configurable container, cluster and guest requirements, backend admission and cleanup evidence. The sandbox comparison explains where Firecracker and other VM backends fit. These runtime adapters are planned, and are not enabled by generating a DevContainer.