Move to Cinc Workstation and add CI with automated releases - #27
Merged
Merged
Conversation
jrwesolo
force-pushed
the
move-to-cinc-workstation
branch
10 times, most recently
from
September 16, 2026 04:04
245d3fa to
9dec8d7
Compare
Replace Chef Workstation with Cinc Workstation throughout development and testing. Test Kitchen now selects Cinc through `product_name: cinc`, which kitchen-dokken uses to derive both the image (`cincproject/cinc`) and the client binary (`/opt/cinc/bin/cinc-client`), so neither needs to be set explicitly. This removes the Chef 19 Habitat workaround entirely. That suite needed a separate `chef/chef-hab` image plus a hardcoded path to the client inside the Habitat package directory, which had to be updated by hand on every release. Cinc 19 ships as an omnibus package, so a plain version selector works. The Chef Infra Client 17 suite is dropped because `cincproject/cinc:17` is published for amd64 only and cannot run on arm64 hosts. The minimum supported client is raised to 18 to match, which is a breaking change and the reason for the major version bump. Suites now track major versions rather than exact pins, so patch releases no longer require a commit. Platform coverage moves toward what is actually deployed: CentOS Stream 10 is replaced by Rocky Linux 9, and the interim Ubuntu 25.10 release by Ubuntu 24.04 LTS. Rocky Linux is added to the supported platform list to match. Add GitHub Actions CI with separate lint, unit, and integration stages. The integration matrix is derived from `kitchen list --json` so that kitchen.yml remains the single source of truth, and each suite and platform combination runs on its own runner in parallel. The Cinc Workstation version is pinned to the one documented in TESTING.md so that an upstream release cannot break every pull request on its own, and every job carries a timeout so a wedged container fails fast instead of holding a runner for hours. A pull request that bumps the version is also checked for two things: that the new version is not already tagged, and that it is the newest entry in the changelog with a link reference. Both are skipped when the version is unchanged, so documentation-only pull requests are unaffected. The check runs on pull requests only, because on main the premise inverts: once a release is tagged, metadata.rb keeps that version until the next bump, so every merge that follows a release would fail it. It is deliberately left out of the integration gate, since a job skipped by a conditional also skips the jobs that need it. The check lives in .github/scripts/check-version rather than inline in the workflow so that it can be run and linted directly. Merges to main release automatically. Once lint, unit and the full integration matrix are green, .github/scripts/release tags the merged commit and publishes a GitHub release whose notes are the matching section of the changelog. It is the mirror image of the pull request check: both ask whether the tag already exists, and where the check fails a pull request that reuses a released version, the release simply exits without publishing. That is what makes every merge between releases a no-op rather than an error. The two scripts share .github/scripts/lib.bash so they cannot drift on how the version is parsed, which would otherwise allow a release to tag a version that was never validated. Cancelling in-progress runs is now limited to pull requests, so a second merge cannot interrupt a release, and write permission is granted to the release job alone rather than to the workflow. A successful release then shares the cookbook to Chef Supermarket. That job sits behind a GitHub environment, so the deployment waits for a human to approve it before the job starts and before the Supermarket key becomes readable. Supermarket has no way to issue a scoped or expiring credential, so an approval gate is the available mitigation for keeping a long-lived key in Actions. The release job now reports whether it actually published, and the share is skipped when it did not, so a merge that ships no new version never asks for an approval it does not need. The share resolves the tag by name and archives that, rather than archiving whatever is checked out. In the workflow the two are the same commit, but only by implication, and the implication breaks the moment the script is re-run or used by hand. Resolving the tag also makes a publish without a release fail loudly instead of quietly shipping the wrong tree. Sharing deliberately follows tagging rather than running beside it. Supermarket evaluates its version tag quality metric at share time by asking GitHub for the repository's tags, accepting either the bare version or a v-prefixed one, and nothing re-evaluates that metric afterwards. Version 2.4.1 was shared four minutes before its tag existed and fails the metric permanently as a result. Tagging first makes that ordering a property of the pipeline instead of something to remember. The test jobs also run weekly. The suites track Cinc major versions and the dokken base images move underneath those tags, so a green main can go red with no change here. A scheduled run cannot tag or ship anything, since the version job is pull-request-only and release and publish are push-only. Pushes to main no longer share a concurrency group. The publish job holds its run open while it waits for a deployment approval and pending approvals live for thirty days, so a shared group would park every later merge behind an unapproved release. Release dates are dropped from the changelog, and the README gains a CI status badge. The tag and the GitHub release already record when a version shipped, and a date written by hand while a pull request is open is wrong whenever that pull request does not merge the same day. The Cinc Workstation install is factored into a composite action at .github/actions/setup-cinc. GitHub Actions does not support YAML anchors, so a composite action is the only way to share a step across jobs. The pinned version becomes that action's default input, which puts it in one place rather than in a workflow-level variable plus four references, and lets an individual job override it without touching the shared default. Also drop the unused `default_source :supermarket` from the Policyfile, as every cookbook in it is resolved from a local path, and apply the Cookstyle fixes that the raised client floor makes available. Correct two resource comments that still referred to a Chef 16 compatibility concern the raised floor has made moot, and ignore the undotted `kitchen.local.yml`. Correct chefignore as well. Chef matches its globs with File.fnmatch and no flags, so a bare directory name never matches the files beneath it: `.github` had to become `.github/*`. For the same reason `kitchen.yml*` did not cover an undotted `kitchen.local.yml`, which knife would otherwise pack out of a developer's working directory, so it becomes `kitchen*.yml*`. chefignore now also excludes itself. The published artifact is left as metadata.rb, LICENSE, README.md, libraries and resources.
jrwesolo
force-pushed
the
move-to-cinc-workstation
branch
from
September 16, 2026 04:17
9dec8d7 to
fda7181
Compare
jrwesolo
marked this pull request as ready for review
September 16, 2026 04:21
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Moves the cookbook off Chef Workstation and onto Cinc Workstation, and adds CI that tests every pull request, then tags, releases, and publishes on merge.
Cinc migration
Test Kitchen selects Cinc through a single provisioner key,
product_name: cinc. kitchen-dokken derives both the image (cincproject/cinc) and the client binary (/opt/cinc/bin/cinc-client) from it, so neither has to be set explicitly.This deletes the Chef 19 Habitat workaround. That suite previously needed a separate
chef/chef-habimage plus a hardcoded path into the Habitat package directory:That path embedded a build timestamp and had to be updated by hand on every release. Cinc 19 ships as an omnibus package, so a plain version selector works.
Breaking change
The minimum supported Chef Infra Client is now 18, hence the major version bump to 3.0.0. Nodes on an older client will fail the
chef_versionmetadata constraint. Users needing Chef Infra Client 12 through 17 should pin to~> 2.4.The driver for this is that
cincproject/cinc:17is published for amd64 only, with no manifest list at all, so the 17 suite cannot run on arm64 hosts. Rather than keep an untestable claim of support, the floor moves up to match what is actually exercised.Test matrix
Suites now track major versions (18, 19) instead of exact pins, so Cinc patch releases no longer require a commit.
Platform coverage shifts toward what people actually deploy:
centos-stream-10rockylinux-9ubuntu-25.10ubuntu-24.04debian-13fedora-43Both new images were confirmed to publish amd64 and arm64 manifests.
rockyis added to the supported platform list to match.CI
New GitHub Actions workflow with separate
lint,unit, andintegrationstages.The integration matrix is generated at runtime by a
discoverjob that runskitchen list --jsonand emits the instance list. This keepskitchen.ymlas the single source of truth. Adding a platform or a Cinc major picks it up with no workflow edit. Each of the eight suite/platform combinations runs on its own runner, withfail-fast: falseso one broken platform does not mask the others.The Cinc Workstation version is pinned to the one documented in
TESTING.md, so CI and the docs cannot drift and a bad upstream release cannot turn every pull request red on its own. Every job carries atimeout-minutesso a wedged container fails fast rather than holding a runner for the six hour default.A
versionjob runs on pull requests only. When a pull request bumps the version inmetadata.rb, it asserts that the new version is not already tagged and that it is the newest entry inCHANGELOG.mdwith a link reference. When the version is unchanged from the base branch both assertions are skipped, so documentation-only pull requests are unaffected.It does not run on
mainbecause the premise inverts there: once a release is tagged,metadata.rbkeeps that version until the next bump, so every merge following a release would fail the check. It is also kept out ofintegration'sneeds, since a job skipped by a conditional causes its dependents to skip as well.The check lives in
.github/scripts/check-versionrather than inline in the workflow, so it can be run and linted directly.The Cinc Workstation install is factored into a composite action at
.github/actions/setup-cinc, since GitHub Actions does not support YAML anchors and a composite action is the only way to share a step across jobs. The pinned version is that action's default input, so it lives in one place rather than in a workflow variable plus four references, and a single job can override it withwith: version:without touching the shared default.Releases
Merging to
mainnow releases. Oncelint,unit, and the whole integration matrix are green, areleasejob runs.github/scripts/release, which tags the merged commit and publishes a GitHub release whose notes are that version's section ofCHANGELOG.md.It is the mirror image of the pull request check. Both ask the same question, whether the tag already exists, and react in opposite directions:
mainThat asymmetry is what makes every merge between releases a no-op instead of an error, and it means the release needs no state of its own to know whether it has already run.
The two scripts share
.github/scripts/lib.bash, so they cannot drift on how the version is parsed. Drift there would let a release tag a version that the pull request check never validated.Two supporting changes.
cancel-in-progressis now scoped to pull requests, because a second merge landing mid-release should not cancel the first run. Andcontents: writeis granted to thereleasejob alone; the workflow default stayscontents: read.Publishing to Supermarket
A
publishjob followsreleaseand shares the cookbook to Supermarket withknife supermarket share.It is gated on a GitHub environment named
supermarket, configured with a required reviewer, self-review allowed, and a deployment branch policy limited tomain. The job therefore does not start, and the Supermarket key does not become readable, until the deployment is approved by hand. Supermarket cannot issue a scoped or expiring credential, so an approval gate is the mitigation available for keeping a long-lived key in Actions. BothSUPERMARKET_USERandSUPERMARKET_KEYlive on that environment.releasenow reports whether it actually published, andpublishis skipped when it did not:Without that, every documentation merge would queue an approval request for a release that never happened, and an approval you learn to click without reading is worse than no gate at all.
Why publishing follows tagging
Supermarket evaluates a
version_tagquality metric at share time. From its source,VersionTagWorker#evaluate:It asks GitHub for the repository's tags and accepts either spelling, so the
vprefix used here is fine. What it does not tolerate is the tag not existing yet, and nothing re-evaluates the metric afterwards.v2.4.1 currently fails this metric, and the reason is visible in the timestamps:
v2.4.1createdv2.4.1publishedIt was shared four minutes before its tag existed. That version cannot be repaired, but ordering
publishafterreleasemakes the correct sequence a property of the pipeline rather than something to remember. v3.0.0 will pass.Idempotency
publishasks Supermarket whether the version is already there and exits without uploading if so, so a re-run of a partially failed job is safe. The check distinguishes 404 from an unanswered request, and treats anything other than 200 or 404 as an error rather than as permission to publish.The tag is resolved and verified before anything else happens, so a publish without a release fails on the fundamental problem rather than on a symptom of it. Everything after that point reads its identity from the tag rather than the working tree, and the version, which has to be read first since it is what names the tag, is checked against the tag rather than trusted.
The cookbook is staged by archiving the tag, not by archiving whatever is checked out. In the workflow those are the same commit, but only by implication:
actions/checkoutputs both jobs on the run'sgithub.shaand the release job tags that same sha. Naming the tag makes the guarantee explicit, makes the script correct when re-run or used by hand, and turns a publish without a release into a loud failure rather than a quietly wrong upload.The key is written under a subshell
umask 077into a staging directory thatmktemp -dalready creates at 0700, and the whole directory is removed on exit. The umask is deliberately not global:tar -xwould then extract the cookbook at 0600, and knife stages files withFileUtils.cp, which preserves the source mode into the published tarball.Worth flagging before merge: this pull request is what creates the
releaseandpublishjobs, so merging it will tag v3.0.0, then wait for your approval before sharing it to Supermarket.Scheduled runs
The test jobs also run weekly, Mondays at 12:00 UTC. The suites track Cinc major versions and the dokken base images move underneath those tags, so a green
maincan go red with no change in this repository.A scheduled run cannot tag or ship anything:
versionis pull-request-only,releaseis push-only, andpublishneedsrelease. Worth knowing that GitHub disables scheduled workflows after 60 days of repository inactivity, and notifies the owner when it does.Concurrency
Pushes to
mainno longer share a concurrency group with each other:Pull request runs still share a group per ref, so a new push cancels the run it supersedes. Pushes to
mainget a group to themselves because thepublishjob holds its run open while it waits for a deployment approval, and pending approvals live for thirty days. A shared group would park every later merge behind an unapproved release.Also
default_source :supermarketfrom the Policyfile. Every cookbook in it resolves from a local path, so nothing was ever fetched from Supermarket.unified_mode trueunconditional now that the client floor is 18.kitchen.local.yml, matching the undottedkitchen.ymlalready in use.mainso it reports the branch rather than whatever ran most recently. The existing reference links were renumbered to keep numeric order matching document order, which is the convention the file already used.CHANGELOG.md. The tag and the GitHub release already record when a version shipped, and a date written by hand while a pull request is open is wrong whenever that pull request does not merge the same day. The headings also lose their redundant collapsed-link brackets,[v3.0.0][]becoming[v3.0.0], which resolves against the same link definition.chefignore. Chef matches its globs withFile.fnmatchand no flags, so a bare directory name never matches the files beneath it and.githubhad to become.github/*. For the same reasonkitchen.yml*did not cover an undottedkitchen.local.yml, whichknifewould otherwise pack out of a developer's working directory, so it becomeskitchen*.yml*.chefignorenow excludes itself as well. Every tracked path was enumerated against the final glob list: the published artifact ismetadata.rb,LICENSE,README.md,libraries/andresources/, and nothing else.spec/spec_helper.rbwith a note on why ChefSpec is not used here (the resources read and write the live filesystem at converge time).Testing
Cookstyle and RSpec are clean locally: 17 files no offenses, 66 examples 0 failures.
The published artifact was verified with a real
knife supermarket share --dry-run, which builds the actual tarball and lists it without uploading. Of 31 tracked files, 10 are published:metadata.rb, the generatedmetadata.json,LICENSE,README.md,libraries/, andresources/. Nospec/, notest/, nokitchen.yml, no.github/, nochefignore, no Policyfile. That is the first direct confirmation that thechefignorecorrections above do what they claim.The scripts were exercised directly against a scratch clone rather than only in CI.
check-versionwas run through seven cases: a bumped and documented version, a version unchanged from the base branch, an already-tagged version, a stale changelog heading, a missing link reference, that same case with the reference added, and an unparseablemetadata.rb.releasewas run through four against a stubbedgh: untagged with notes present, already tagged, untagged with no changelog section, and a missingGITHUB_SHA, confirming in the first two cases that it writesreleased=trueandreleased=falserespectively.publishwas run through three against a stubbedcinc: a version absent from Supermarket, one already present, and an unreachable Supermarket, checking in the first that the staged key file lands at mode 600 and that the staging directory is cleaned up. Changelog note extraction was checked for the newest entry, a middle entry, and the oldest, which is bounded by the link reference block rather than a following heading.CI ran green on the full eight instance integration matrix, covering Cinc 18 and 19 across Debian 13, Rocky Linux 9, Fedora 43, and Ubuntu 24.04. That was this platform set's first run, and its first run on amd64.