ci: replace the Azure DevOps pipeline with GitHub Actions publishing to GHCR - #1
Merged
Conversation
Ten install scripts hardcoded amd64/x86_64/x64, which made an arm64 build impossible. Add a sourced _arch.sh helper exporting the three spellings upstreams actually use, and switch every affected script to it. All ten tools publish arm64 Linux artefacts. PowerShell is the exception: Microsoft's Ubuntu package repository has no arm64 'powershell' deb, so arm64 installs from the upstream tarball into /opt/microsoft/powershell/7, resolving the runtime dependencies the deb would have pulled in. Also verify Helm's checksum, which was downloaded and then checked by a commented-out line, and drop an unused variable that shellcheck flags.
Replaces azure-pipelines.yml, which needed an Azure DevOps agent pool, an ACR service connection and a Dependency-Track instance to run at all - none of which a public repository can offer a contributor. ci.yml runs on pull requests and pushes to main; release.yml cuts a weekly calendar-versioned release from a --no-cache rebuild. Both call the reusable build.yml, so the two paths cannot drift. Each architecture builds on its own native runner, loads the image, runs tests/run-all-tests.sh inside it, and only then pushes by digest; a merge job assembles the manifest list. Docker's own github-builder workflow was the shorter route but supports neither local loading nor per-platform tests. Published images carry a BuildKit SBOM and provenance plus a Sigstore-signed SLSA attestation from actions/attest, which is why there is no separate cosign step. Trivy reports to the Security tab without gating: this image always carries some upstream HIGHs, and blocking on them would stall the weekly rebuild and leave users on a strictly more vulnerable image. Auth is the repository's own GITHUB_TOKEN, so there are no secrets to set up. Actions are pinned to commit SHAs. Dockerfile lint rules move to .hadolint.yaml with a reason against each, so CI and the pre-commit hook agree by construction.
devcontainer.json now points at ghcr.io/grinidx/devcontainer-devops:latest, with the local Dockerfile build as the commented-out contributor path. New users get the tools in a pull rather than a half-hour build; CONTRIBUTING covers switching back when changing the image itself. Document CalVer honestly: most of versions.json installs latest, so a tag is a point-in-time snapshot and the digest is the only stable identifier. ARCHITECTURE gains a CI/CD section covering why the matrix is hand-rolled, why the runners are native rather than QEMU, and why scans do not gate. Its code fence was also never closed, so everything after line 3 was rendering as one block. Fixes several stale or placeholder references found on the way through: - README's licence section said '[Add your license information here]' over a real MIT LICENSE file, and Support was a placeholder - 'git clone <repository-url>' in README and QUICKSTART - VERSION_MANAGEMENT pointed at .devcontainer/build/ and .devcontainer/local/, neither of which exists - QUICKSTART linked to .devcontainer/variants/README.md, which does not exist - .pre-commit/README claimed pre-commit ran in the Azure DevOps pipeline; it did not, and now there is no such pipeline - two Unreleased CHANGELOG entries contradicted the changes above
The repo held a mix with nothing declaring which was intended: the root docs and configs were CRLF, every shell script under .devcontainer/files/ was LF. Editing a CRLF file with an LF-writing tool left it mixed, and the mixed-line-ending pre-commit hook then rewrote the whole file. Settle on LF, which is what VS Code was already configured for here (devcontainer.json sets files.eol to \n) and what three quarters of the tracked files already used. Shell scripts especially need it - a CRLF shebang makes the kernel look for an interpreter with a trailing carriage return and fails with a confusing 'bad interpreter'. Apart from .gitattributes itself this commit changes no content; a whitespace-blind diff over it is empty.
The pinned branch could never have worked. It pointed at
azcopyvnext.azureedge.net, a retired CDN, through a path containing a shell
glob ('release-${VERSION}-20*') that curl has no way to expand. Nothing
noticed because versions.json leaves azcopy empty, so only the 'latest'
branch is ever taken.
Use the GitHub release asset instead, which is where the aka.ms aliases
redirect to anyway. Verified that both architectures resolve 200 at
v10.32.6.
'pre-commit run --all-files' failed before reaching half its hooks. Four
separate causes:
- .secrets.baseline did not exist, so the detect-secrets hook aborted
- check-json parsed .devcontainer/devcontainer.json, which is JSONC and
legitimately contains comments
- 21 tracked scripts carried a shebang without an executable bit. The
Dockerfile chmods them at build time, which is why nobody noticed
- the ansible-lint hook pinned v6.22.1, incompatible with current
ansible-core, and its 'files: \.(yaml|yml)$' pattern aimed it at the
GitHub workflow files. This repo has no playbooks or roles, so the hook
and .ansible-lint are removed rather than repaired
markdownlint and yamllint were failing on their defaults against content
that had never satisfied them - MD013 at 80 columns, a document-start
marker GitHub workflows do not use, and the perennial yamllint complaint
that the 'on:' trigger key is a YAML 1.1 boolean. Both now have config
files, matching how .hadolint.yaml already works, and the findings that
were real are fixed at source: code fences given languages, a literal
<tool> backticked, and two over-long lines in build.yml shortened.
Those two lines shrank by collapsing the repeated
'${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}' pair, which appeared eight
times, into a single env.IMAGE.
Verified: pre-commit run --all-files now exits 0, with only hadolint-docker
skipped for want of a Docker daemon in this environment.
The [Unreleased] section had two '### Added' and two '### Changed' blocks, and my earlier commit inserted '### Removed' partway down the first Added list. The result read 'Removed: Installation scripts ... Terraform, Terragrunt, Azure CLI', which is the opposite of what happened. Each subsection now appears once, in Keep a Changelog order, with the original entries preserved and this branch's entries appended. ARCHITECTURE's external-connections diagram listed 'Azure ACR (Images)' and 'DevOps (CI/CD)' directly above the build flow, reading as though the image were still built in Azure DevOps and stored in ACR. The box describes what the tooling inside the container talks to, so it keeps ACR as a registry the user works with, swaps the stale DevOps entry for Entra ID, which is what az and kubelogin actually authenticate against, and says plainly that this is not where the image comes from.
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.
Replaces the Azure DevOps pipeline with GitHub Actions building and publishing to GHCR, and makes the image multi-architecture.
azure-pipelines.ymlneeded an ADO agent pool, an ACR service connection and a Dependency-Track instance before it would run at all. None of that is available to someone who clones a public repo. This runs on the repository's ownGITHUB_TOKENwith no secrets to configure.What changed
Three workflows.
ci.yml(pull requests, pushes tomain) andrelease.yml(Sundays 03:00 UTC, or manual) are thin entry points; both call the reusablebuild.yml, which holds every step that touches the image, so the two paths cannot drift.Native multi-arch.
linux/amd64onubuntu-24.04,linux/arm64onubuntu-24.04-arm, both free and unlimited on public repos. Ten install scripts hardcodedamd64/x86_64/x64; they now source a shared_arch.sh. Every affected tool publishes arm64 Linux artefacts. PowerShell is the one divergence: Microsoft's Ubuntu repo publishes thepowershelldeb for amd64 only (verified against bothbinary-amd64andbinary-arm64indexes), so arm64 installs from the upstream tarball.Build, test, then push. Each runner builds its platform, loads it, runs
tests/run-all-tests.shinside the real image, and only then pushes by digest. A merge job assembles the manifest list. This is why Docker's owndocker/github-builderreusable workflow was not used - it supports neither local loading nor per-platform tests.CalVer.
v2026.08.09, cut by the weekly no-cache rebuild, which also moves:latest. Most ofversions.jsoninstalls "latest", so SemVer would imply a compatibility contract this image cannot honour. The README says plainly that a tag is a snapshot and the digest is the only stable identifier.Supply chain. BuildKit SBOM +
provenance: mode=max, andactions/attestsigns the merged manifest with a short-lived Sigstore certificate. That is why there is no cosign step -actions/attestis Sigstore-backed already, and a second signature over the same digest with the same trust root adds nothing. Trivy reports to the Security tab without gating; this image always carries some upstream HIGHs and blocking would stall the weekly rebuild, leaving users on a strictly more vulnerable image.devcontainer.jsonpulls the image by default. New users get the tools in a pull instead of a half-hour build.CONTRIBUTING.mdcovers switching to the local build when changing the image itself.Actions are pinned to commit SHAs with the version in a trailing comment.
Verified locally
shellcheck -x -S warning,bash -n, JSON parse over every file,hadolint --config .hadolint.yaml, andactionlintall pass.devcontainer.jsonstill parses as JSONC. No broken relative links in any markdown file.Not verified: the build itself has not run. The likely first failures are disk exhaustion (runners have ~14 GB free, hence the reclaim step) and an arm64 gap in a tool I could only check by release-asset name.
Incidental fixes
Found while working through the files, all in the docs commit:
[Add your license information here]above a real MITLICENSE; Support was a placeholder toogit clone <repository-url>in README and QUICKSTARTVERSION_MANAGEMENT.mdtold you to edit.devcontainer/build/devcontainer.jsonor.devcontainer/local/devcontainer.json; neither exists.devcontainer/variants/README.md, which does not exist.pre-commit/README.mdclaimed pre-commit ran in the ADO pipeline; it did not[Unreleased]CHANGELOG entries contradicted the changes in this PRARCHITECTURE.mdopened a code fence at line 3 and never closed itWorth a look before merge
.secrets.baselineis missing, sopre-commit run --all-filesfails on thedetect-secretshook. CI runs the linters directly rather than through pre-commit, so this does not block, but the hook is broken for contributors. Left alone as out of scope.install-azcopy.sh's pinned-version path is broken independently of this change: the URL contains a shell glob that curl cannot expand, andazcopyvnext.azureedge.netis a retired CDN. Only the "latest" path is exercised today, so nothing fails. Arch handling was fixed on both paths.ARCHITECTURE.mdstill lists "Azure ACR (Images)" and "DevOps (CI/CD)" in its external-connections diagram. Left in place because it reads as Azure services the container talks to, which is fair for a DevOps toolbox, but you may want it gone now.release.yml.Follow-up: the deferred items are now resolved
The "worth a look before merge" list above has been dealt with, plus a few things found on the way.
pre-commit run --all-filesnow exits 0. It previously failed before reaching half its hooks, for four separate reasons:.secrets.baselinedid not exist, sodetect-secretsaborted the runcheck-jsonwas parsingdevcontainer.json, which is JSONCansible-linthook pinned v6.22.1, which no longer imports against currentansible-core, and itsfiles: \.(yaml|yml)$pattern aimed it at the GitHub workflow files. This repo has no playbooks or roles, so the hook and.ansible-lintare removed rather than repaired.markdownlintandyamllintwere failing on their defaults against content that had never satisfied them - MD013 at 80 columns, a---document start that GitHub workflows do not use, and the perennial complaint that theon:trigger key is a YAML 1.1 boolean. Both now have config files, matching how.hadolint.yamlalready works. The findings that were real are fixed at source rather than configured away: code fences given languages, a literal<tool>backticked, two over-long lines inbuild.ymlshortened.install-azcopy.sh's pinned path is fixed. It pointed at a retired CDN through a URL containing a shell glob curl cannot expand. It now uses the GitHub release asset, verified 200 on both architectures at v10.32.6.Line endings are declared. The repo mixed CRLF root docs with LF scripts and declared neither, so
mixed-line-endingkept rewriting whole files..gitattributesnow sets LF throughout, which is whatdevcontainer.jsonalready configured VS Code for. That commit changes no content - a whitespace-blind diff over it is empty apart from.gitattributesitself.The ACR/DevOps diagram entries are gone, replaced with Entra ID and a line saying plainly that the box is what the tooling talks to, not where the image comes from.
One thing I got wrong
My earlier
### Removedentry landed partway down the original### Addedlist, so the changelog read "Removed: Installation scripts ... Terraform, Terragrunt, Azure CLI" - the opposite of what happened.[Unreleased]also had two### Addedand two### Changedblocks. Each subsection now appears once, original entries preserved.Build status
The previous CI run went green end to end:
lint,build (amd64)andbuild (arm64)all passed, withmerge and attestcorrectly skipped on a pull request. So the arm64 script rewrite, the PowerShell tarball path and the disk headroom all hold up in practice - those were the three unknowns flagged above.hadolint-dockeris the one hook I could not run here; it needs a Docker daemon, which is a prerequisite of this repo anyway. CI runs hadolint directly against the same.hadolint.yaml.