How the artifacts in this repository are produced, versioned, and released.
| Path | What it is |
|---|---|
src/plcc-ng/ |
The devcontainer feature (canonical source) |
test/plcc-ng/ |
Feature tests (devcontainer features test layout) |
images/plcc-ng/, images/plcc-ng-full/ |
Prebuilt image definitions (devcontainer.json + label Dockerfile) |
test/smoke-test.sh |
In-container smoke test shared by both images |
scripts/sync-features.sh |
Copies src/plcc-ng into each image workspace (copies are gitignored) |
scripts/build-and-test.sh |
Builds one image variant and smoke-tests it — same entry point locally and in CI |
The images consume the local feature copy (synced by
scripts/sync-features.sh), never the published one — so a PR that changes
the feature is tested end-to-end before anything is published.
-
A PR merges to
mainwith a Conventional Commit message:fix:→ patch,feat:→ minor,feat:with aBREAKING CHANGE:footer → major, andci:/docs:/chore:/test:→ no release.Use the footer, not
feat!:. The Angular preset does not parse the!shorthand, sofeat!:alone yields no release while CI stays green. For the same reason, prefer a merge commit over squash on a major: squash can drop the footer and silently downgrade the release to a minor. -
The Release workflow's
planjob asks semantic-release, in dry-run, whether this commit will release anything. Onmainthe only reason to build is to feed a release — PR CI has already verified the code. -
The
buildjob builds both image variants on native amd64 and arm64 runners, smoke-tests each, and pushes per-arch candidate tags{sha}-amd64/{sha}-arm64— only whenplansaid a release is coming. Otherwise its steps are skipped and the job finishes in seconds. The job itself always runs, so its check is always reported. -
The
releasejob runs semantic-release for real: computes the next version, tags the repo, updatesCHANGELOG.md, creates the GitHub release. -
The
publish-imagesjob stitches the candidate tags into multi-arch manifests taggedX.Y.Z,X.Y,X, andlatestfor both images. It is skipped when nothing was released. -
The
publish-featurejob runsdevcontainer features publishforsrc/; it publishes only ifdevcontainer-feature.json'sversionhas not been published before, so it is a no-op on most releases.
Image versions come from semantic-release; the feature is versioned
independently — bump the version field in
src/plcc-ng/devcontainer-feature.json in the same PR as any feature change.
check-plcc-ng-release.yml runs nightly at 09:00 UTC (and via Run
workflow). It compares the latest PyPI release of plcc-ng against the
version pinned in images/plcc-ng/.devcontainer/devcontainer.json and opens
a PR when they differ. Review CI and merge if green.
The release type is derived, not fixed. A new plcc-ng major opens a
feat: PR carrying a BREAKING CHANGE: footer, so the images release as a
major too; anything else opens a fix:. This matters because
docs/choosing-your-image.md tells courses to pin the major tag for a term —
shipping a new major of the tool under the same :N would break that promise
mid-semester.
One PR updates every place the version appears:
| File | What changes |
|---|---|
images/*/.devcontainer/devcontainer.json |
the pinned feature version, in both variants |
test/plcc-ng/scenarios.json |
the pinned-version test scenario |
test/plcc-ng/pinned_version.sh |
EXPECTED_VERSION, so the assertion matches its own scenario |
src/plcc-ng/devcontainer-feature.json |
proposals, the description example, and a patch bump of the feature's own version so the edit publishes |
docs/choosing-your-image.md |
the { "version": "X.Y.Z" } example |
On a major bump it additionally retags the fully qualified image
references in README.md, devcontainer.json and
docs/choosing-your-image.md from :N to :M, and the PR body asks a human
to update the tag-guidance prose — that wording names both the new major and
the one being left behind, so it cannot be rewritten mechanically.
ghcr.io/ourplcc/features/plcc-ng:1 is deliberately never retagged: it
tracks the feature's own version, not plcc-ng's.
CI pushes each PR's images as
ghcr.io/ourplcc/devcontainers/{plcc-ng,plcc-ng-full}:pr-{N} (amd64 only)
and comments the tags on the PR. Point any devcontainer.json image at one
of those tags to try it.
# Feature tests (fast-ish; builds small test images)
devcontainer features test --project-folder . --features plcc-ng \
--base-image mcr.microsoft.com/devcontainers/base:ubuntu
# Build + smoke test an image variant (plcc-ng-full takes 20–40 min)
bash scripts/build-and-test.sh plcc-ng
bash scripts/build-and-test.sh plcc-ng-full
# REUSE/license lint
bash scripts/reuse.sh lintThe repo's own devcontainer (.devcontainer/devcontainer.json) includes
Node, Docker-in-Docker, and the devcontainer CLI, so all of the above works
inside it. Open it in VS Code on your own machine, not a Codespace; see
CONTRIBUTING.md.
One-time steps when this repository is first published to GitHub:
- Create
ourPLCC/plcc-ng-devcontainerand pushmain(no tags — the first release must compute as 1.0.0). - Add two repository secrets for the release bot's GitHub App:
APP_ID(the App's numeric ID, from its General settings page) andAPP_PRIVATE_KEY(a private key generated on that same page — the full contents of the downloaded.pemfile).release.ymlandcheck-plcc-ng-release.ymleach mint a fresh ~1-hour installation token from these at the start of their run (actions/create-github-app-token), rather than storing a long-lived token as a secret — installation tokens can't be pasted once and kept, since they expire in about an hour. Required becauseGITHUB_TOKENcannot push semantic-release's changelog commit to the protectedmainbranch. The App must be installed with access to this repo (org Settings → Installations → the App → Configure → Repository access) and grantedContents: Read and writeandPull requests: Read and writepermissions. - Protect
mainwith a ruleset (Settings → Rules → Rulesets): require a pull request before merging, and require exactly one status check:ci-gate. Do not require job names such asbuild (plcc-ng)— Actions posts one check per job, so a matrix rename or a new job leaves the ruleset waiting on a name nothing reports, and the PR hangs on "Expected" forever with every real check green.ci-gatedepends on the other jobs and its name never changes. Add the release bot's GitHub App to the ruleset's bypass list, or semantic-release's push will be rejected same as any other direct push — the App only appears as a selectable bypass actor once its installation has been granted access to this specific repo (step 2). - Merge any PR with a
feat:commit → first release publishes everything. - In the ourPLCC org package settings, make these GHCR packages public:
devcontainers/plcc-ng,devcontainers/plcc-ng-full,features/plcc-ng. (New GHCR packages default to private.)
-
Never delete a
{sha}-amd64/{sha}-arm64version that belongs to a release. They look like disposable build leftovers and are not:publish-imagesassembles the multi-arch manifest forX.Y.Z,X.Y,Xandlatestby referencing those per-arch manifests by digest. Deleting one breaksdocker pullfor every tag pointing at it — including:1, whichdocs/choosing-your-image.mdtells courses to pin for a term.Since the
planjob landed, a candidate is only produced by a commit that actually releases, so in practice every new candidate is load-bearing. -
Safe to delete:
pr-{N}tags, and candidates left over from beforeplanexisted — commits that built but published nothing. Verify rather than guess: a candidate is safe only if no release index references its digest.REPO=ourplcc/devcontainers/plcc-ng # or plcc-ng-full A='application/vnd.oci.image.index.v1+json,application/vnd.docker.distribution.manifest.list.v2+json' TOK=$(curl -s "https://ghcr.io/token?scope=repository:$REPO:pull&service=ghcr.io" | jq -r .token) # digests that releases depend on for t in $(curl -s -H "Authorization: Bearer $TOK" "https://ghcr.io/v2/$REPO/tags/list" \ | jq -r '.tags[]' | grep -Ev -- '-amd64$|-arm64$|^pr-'); do curl -s -H "Authorization: Bearer $TOK" -H "Accept: $A" \ "https://ghcr.io/v2/$REPO/manifests/$t" | jq -r '.manifests[]?.digest' done | sort -u
Anything whose digest appears in that list must stay.
-
Renovate/Dependabot are not configured; the base image and language feature versions float by design (
base:ubuntu, Nodelts), pinned only where reproducibility matters (plcc-ng, Java major, Python minor).