From cb7ee2d567d58a312284856441dd5f62986a2b74 Mon Sep 17 00:00:00 2001 From: patrickcrocker Date: Sun, 1 Nov 2026 13:09:46 +0000 Subject: [PATCH 1/2] Support native ARM64 CF, CredHub, and Fly CLIs --- .github/workflows/test.yaml | 21 ++++++++++++ src/cf-cli/NOTES.md | 5 +++ src/cf-cli/devcontainer-feature.json | 2 +- src/cf-cli/install.sh | 10 ++++-- src/credhub-cli/NOTES.md | 23 +++++++++++++ src/credhub-cli/devcontainer-feature.json | 5 +-- src/credhub-cli/install.sh | 40 ++++++++++++++++++----- src/fly-cli/NOTES.md | 16 +++++++++ src/fly-cli/devcontainer-feature.json | 13 ++++++-- src/fly-cli/install.sh | 12 +++++-- test/cf-cli/native_architecture.sh | 6 ++++ test/cf-cli/scenarios.json | 8 +++++ test/cf-cli/test.sh | 9 +++++ test/credhub-cli/native_architecture.sh | 6 ++++ test/credhub-cli/scenarios.json | 16 +++++++++ test/credhub-cli/test.sh | 9 +++++ test/fly-cli/native_architecture.sh | 6 ++++ test/fly-cli/scenarios.json | 9 +++++ test/fly-cli/test.sh | 9 +++++ 19 files changed, 207 insertions(+), 18 deletions(-) create mode 100644 src/cf-cli/NOTES.md create mode 100644 src/credhub-cli/NOTES.md create mode 100644 src/fly-cli/NOTES.md create mode 100644 test/cf-cli/native_architecture.sh create mode 100644 test/credhub-cli/native_architecture.sh create mode 100644 test/fly-cli/native_architecture.sh diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 81b5ca4..030bd5c 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -37,6 +37,27 @@ jobs: - name: "Generating tests for '${{ matrix.features }}' against '${{ matrix.baseImage }}'" run: devcontainer features test -f ${{ matrix.features }} -i ${{ matrix.baseImage }} . + test-native-cli: + runs-on: ${{ matrix.runner }} + strategy: + fail-fast: false + matrix: + runner: + - ubuntu-latest + - ubuntu-24.04-arm + feature: + - cf-cli + - credhub-cli + - fly-cli + steps: + - uses: actions/checkout@v3 + + - name: "Install latest devcontainer CLI" + run: npm install -g @devcontainers/cli + + - name: "Test native CLI scenarios" + run: devcontainer features test -f ${{ matrix.feature }} --skip-autogenerated --skip-duplicated . + test-global: runs-on: ubuntu-latest continue-on-error: true diff --git a/src/cf-cli/NOTES.md b/src/cf-cli/NOTES.md new file mode 100644 index 0000000..7c0b605 --- /dev/null +++ b/src/cf-cli/NOTES.md @@ -0,0 +1,5 @@ +## Architectures + +AMD64 uses the Cloud Foundry package download endpoint. ARM64 uses the native +Linux ARM64 archive from the selected GitHub release, so the selected version +must publish that archive. The installer verifies the CLI runs after installation. diff --git a/src/cf-cli/devcontainer-feature.json b/src/cf-cli/devcontainer-feature.json index 3f2fb04..f4f4060 100644 --- a/src/cf-cli/devcontainer-feature.json +++ b/src/cf-cli/devcontainer-feature.json @@ -1,7 +1,7 @@ { "name": "Cloud Foundry CLI", "id": "cf-cli", - "version": "1.0.0", + "version": "1.0.1", "description": "Installs the Cloud Foundry CLI.", "options": { "version": { diff --git a/src/cf-cli/install.sh b/src/cf-cli/install.sh index 4d7d521..dca8a47 100644 --- a/src/cf-cli/install.sh +++ b/src/cf-cli/install.sh @@ -4,7 +4,7 @@ USERNAME=${USERNAME:-${_REMOTE_USER:-"automatic"}} -set -e +set -eo pipefail export TMPDIR=$(mktemp -d /tmp/feature.XXXXXX) trap "rm -rf $TMPDIR" EXIT @@ -106,8 +106,14 @@ find_version_from_git_tags VERSION "https://github.com/cloudfoundry/cli" check_packages curl ca-certificates echo "Downloading cf..." -curl -SL "https://packages.cloudfoundry.org/stable?release=linux64-binary&version=${VERSION}" | tar -zxC "$TMPDIR" +if [ "${architecture}" = "arm64" ]; then + download_url="https://github.com/cloudfoundry/cli/releases/download/v${VERSION}/cf${VERSION%%.*}-cli_${VERSION}_linux_arm64.tgz" +else + download_url="https://packages.cloudfoundry.org/stable?release=linux64-binary&version=${VERSION}" +fi +curl -fsSL --retry 3 "${download_url}" | tar -zxC "$TMPDIR" install "$TMPDIR/cf" /usr/local/bin/cf +cf version ### END install diff --git a/src/credhub-cli/NOTES.md b/src/credhub-cli/NOTES.md new file mode 100644 index 0000000..f26723f --- /dev/null +++ b/src/credhub-cli/NOTES.md @@ -0,0 +1,23 @@ +## Native ARM64 installation + +The feature downloads a release archive matching the container architecture. +When a release has no Linux ARM64 archive, it builds the selected source tag with +vendored dependencies instead. Unqualified legacy Linux archives are used only +on AMD64. + +For ARM64 source builds, include the official Go feature. Select a Go version +that satisfies the selected CredHub tag's `go.mod`; CredHub 2.9.62 requires Go +1.26.0 or newer. The feature orders itself after Go and uses the installed +toolchain without automatically downloading another version. + +```json +"features": { + "ghcr.io/devcontainers/features/go:1": { + "version": "1.27.2", + "installGoTools": false + }, + "ghcr.io/nulldriver/devcontainer-features/credhub-cli:1": { + "version": "2.9.62" + } +} +``` diff --git a/src/credhub-cli/devcontainer-feature.json b/src/credhub-cli/devcontainer-feature.json index 2a718bb..0470709 100644 --- a/src/credhub-cli/devcontainer-feature.json +++ b/src/credhub-cli/devcontainer-feature.json @@ -1,7 +1,7 @@ { "name": "CredHub CLI", "id": "credhub-cli", - "version": "1.0.2", + "version": "1.0.3", "description": "Installs the CredHub CLI.", "options": { "version": { @@ -16,6 +16,7 @@ } }, "installsAfter": [ - "ghcr.io/devcontainers/features/common-utils" + "ghcr.io/devcontainers/features/common-utils", + "ghcr.io/devcontainers/features/go" ] } diff --git a/src/credhub-cli/install.sh b/src/credhub-cli/install.sh index 4a69015..b5e9939 100644 --- a/src/credhub-cli/install.sh +++ b/src/credhub-cli/install.sh @@ -102,7 +102,6 @@ export DEBIAN_FRONTEND=noninteractive ### BEGIN install REPO="cloudfoundry/credhub-cli" -ARCH="amd64" # Soft version matching find_version_from_git_tags VERSION "https://github.com/$REPO" "tags/" @@ -110,13 +109,38 @@ find_version_from_git_tags VERSION "https://github.com/$REPO" "tags/" check_packages curl ca-certificates echo "Downloading credhub..." -# Try to download the release with the architecture type -if curl -sL -o /dev/null -w "%{http_code}" "https://github.com/$REPO/releases/download/$VERSION/credhub-linux-$ARCH-$VERSION.tgz" | grep -q "200"; then - curl -sL "https://github.com/$REPO/releases/download/$VERSION/credhub-linux-$ARCH-$VERSION.tgz" | tar -zxC "$TMPDIR" -else - # Fall back to the older naming convention - curl -sL "https://github.com/$REPO/releases/download/$VERSION/credhub-linux-$VERSION.tgz" | tar -zxC "$TMPDIR" -fi +archive="${TMPDIR}/credhub.tgz" +download_url="https://github.com/${REPO}/releases/download/${VERSION}/credhub-linux-${architecture}-${VERSION}.tgz" +http_status="$(curl -sSL --retry 3 -o "${archive}" -w '%{http_code}' "${download_url}")" + +case "${http_status}" in + 200) + tar -xzf "${archive}" -C "${TMPDIR}" + ;; + 404) + if [ "${architecture}" = "amd64" ]; then + # Older unqualified Linux archives contain AMD64 binaries only. + curl -fsSL --retry 3 "https://github.com/${REPO}/releases/download/${VERSION}/credhub-linux-${VERSION}.tgz" | tar -zxC "${TMPDIR}" + else + if ! command -v go >/dev/null 2>&1; then + echo "ARM64 source builds require Go. Add ghcr.io/devcontainers/features/go:1 to your devcontainer features." >&2 + exit 1 + fi + echo "No ARM64 release archive; building CredHub ${VERSION} from its source tag." + mkdir -p "${TMPDIR}/source" + curl -fsSL --retry 3 "https://github.com/${REPO}/archive/refs/tags/${VERSION}.tar.gz" | tar -xz --strip-components=1 -C "${TMPDIR}/source" + ( + cd "${TMPDIR}/source" + CGO_ENABLED=0 GOTOOLCHAIN=local GOOS=linux GOARCH="${architecture}" \ + go build -mod=vendor -ldflags "-X code.cloudfoundry.org/credhub-cli/version.Version=${VERSION}" -o "${TMPDIR}/credhub" . + ) + fi + ;; + *) + echo "Failed to download CredHub: HTTP ${http_status} from ${download_url}" >&2 + exit 1 + ;; +esac install "$TMPDIR/credhub" /usr/local/bin/credhub credhub --version diff --git a/src/fly-cli/NOTES.md b/src/fly-cli/NOTES.md new file mode 100644 index 0000000..273a604 --- /dev/null +++ b/src/fly-cli/NOTES.md @@ -0,0 +1,16 @@ +## Architectures and version pinning + +Downloads match the container architecture on AMD64 and ARM64. By default, Fly is +downloaded from the configured Concourse `url` and matches that server's version. + +Set `version` to an exact Concourse release to download its GitHub release archive +instead. This allows building a bootstrap container before your Concourse server +exists and makes the installed CLI version reproducible. + +```json +"features": { + "ghcr.io/nulldriver/devcontainer-features/fly-cli:1": { + "version": "8.3.2" + } +} +``` diff --git a/src/fly-cli/devcontainer-feature.json b/src/fly-cli/devcontainer-feature.json index 4b51a5e..1cc5f1d 100644 --- a/src/fly-cli/devcontainer-feature.json +++ b/src/fly-cli/devcontainer-feature.json @@ -1,17 +1,26 @@ { "name": "Concourse Fly CLI", "id": "fly-cli", - "version": "1.0.0", + "version": "1.1.0", "description": "Installs the Concourse Fly CLI.", "options": { "url": { "type": "string", "default": "https://ci.concourse-ci.org", - "description": "Concourse URL to download the Fly CLI from", + "description": "Concourse URL to download the Fly CLI from when version is empty", "proposals": [ "", "https://ci.concourse-ci.org" ] + }, + "version": { + "type": "string", + "default": "", + "description": "Optional exact Concourse release version to download from GitHub instead of the configured URL.", + "proposals": [ + "", + "8.3.2" + ] } }, "installsAfter": [ diff --git a/src/fly-cli/install.sh b/src/fly-cli/install.sh index 8c7bbea..60eb77c 100644 --- a/src/fly-cli/install.sh +++ b/src/fly-cli/install.sh @@ -1,10 +1,11 @@ #!/usr/bin/env bash : ${URL:=https://ci.concourse-ci.org} +: ${VERSION:=""} USERNAME=${USERNAME:-${_REMOTE_USER:-"automatic"}} -set -e +set -eo pipefail export TMPDIR=$(mktemp -d /tmp/feature.XXXXXX) trap "rm -rf $TMPDIR" EXIT @@ -59,11 +60,16 @@ export DEBIAN_FRONTEND=noninteractive ### BEGIN install -# Soft version matching check_packages curl ca-certificates echo "Downloading fly..." -curl -SL "$URL/api/v1/cli?arch=amd64&platform=linux" -o "$TMPDIR/fly" +if [ -n "${VERSION}" ]; then + VERSION="${VERSION#v}" + curl -fsSL --retry 3 "https://github.com/concourse/concourse/releases/download/v${VERSION}/fly-${VERSION}-linux-${architecture}.tgz" | tar -zxC "$TMPDIR" +else + curl -fsSL --retry 3 "${URL%/}/api/v1/cli?arch=${architecture}&platform=linux" -o "$TMPDIR/fly" +fi install "$TMPDIR/fly" /usr/local/bin/fly +fly --version ### END install diff --git a/test/cf-cli/native_architecture.sh b/test/cf-cli/native_architecture.sh new file mode 100644 index 0000000..f96c31a --- /dev/null +++ b/test/cf-cli/native_architecture.sh @@ -0,0 +1,6 @@ +#!/usr/bin/env bash + +set -e + +export EXPECTED_VERSION="8.19.0" +source "$(dirname "$0")/test.sh" diff --git a/test/cf-cli/scenarios.json b/test/cf-cli/scenarios.json index 8b1a2d5..994dd08 100644 --- a/test/cf-cli/scenarios.json +++ b/test/cf-cli/scenarios.json @@ -1,4 +1,12 @@ { + "native_architecture": { + "image": "mcr.microsoft.com/devcontainers/base:resolute", + "features": { + "cf-cli": { + "version": "8.19.0" + } + } + }, "install_cf_cli_specific_version": { "image": "mcr.microsoft.com/devcontainers/base:debian", "features": { diff --git a/test/cf-cli/test.sh b/test/cf-cli/test.sh index 3e5c1fe..b1159b2 100644 --- a/test/cf-cli/test.sh +++ b/test/cf-cli/test.sh @@ -5,5 +5,14 @@ set -e source dev-container-features-test-lib check "cf version" cf version +if [ -n "${EXPECTED_VERSION:-}" ]; then + check "pinned cf version" bash -c 'cf version | grep -F "$EXPECTED_VERSION"' +fi + +case "$(dpkg --print-architecture)" in + amd64) expected_machine=62 ;; + arm64) expected_machine=183 ;; +esac +check "native cf ELF architecture" test "$(od -An -tu2 -j18 -N2 "$(command -v cf)" | tr -d ' ')" = "${expected_machine}" reportResults diff --git a/test/credhub-cli/native_architecture.sh b/test/credhub-cli/native_architecture.sh new file mode 100644 index 0000000..0a91607 --- /dev/null +++ b/test/credhub-cli/native_architecture.sh @@ -0,0 +1,6 @@ +#!/usr/bin/env bash + +set -e + +export EXPECTED_VERSION="2.9.62" +source "$(dirname "$0")/test.sh" diff --git a/test/credhub-cli/scenarios.json b/test/credhub-cli/scenarios.json index 55297e5..ff688b8 100644 --- a/test/credhub-cli/scenarios.json +++ b/test/credhub-cli/scenarios.json @@ -1,7 +1,23 @@ { + "native_architecture": { + "image": "mcr.microsoft.com/devcontainers/base:resolute", + "features": { + "ghcr.io/devcontainers/features/go:1": { + "version": "1.27.2", + "installGoTools": false + }, + "credhub-cli": { + "version": "2.9.62" + } + } + }, "install_credhub_cli_specific_version": { "image": "mcr.microsoft.com/devcontainers/base:debian", "features": { + "ghcr.io/devcontainers/features/go:1": { + "version": "1.27.2", + "installGoTools": false + }, "credhub-cli": { "version": "2.9.50" } diff --git a/test/credhub-cli/test.sh b/test/credhub-cli/test.sh index 20c982f..5a5530c 100644 --- a/test/credhub-cli/test.sh +++ b/test/credhub-cli/test.sh @@ -5,5 +5,14 @@ set -e source dev-container-features-test-lib check "credhub version" credhub --version +if [ -n "${EXPECTED_VERSION:-}" ]; then + check "pinned credhub version" bash -c 'credhub --version | grep -F "$EXPECTED_VERSION"' +fi + +case "$(dpkg --print-architecture)" in + amd64) expected_machine=62 ;; + arm64) expected_machine=183 ;; +esac +check "native credhub ELF architecture" test "$(od -An -tu2 -j18 -N2 "$(command -v credhub)" | tr -d ' ')" = "${expected_machine}" reportResults diff --git a/test/fly-cli/native_architecture.sh b/test/fly-cli/native_architecture.sh new file mode 100644 index 0000000..7930780 --- /dev/null +++ b/test/fly-cli/native_architecture.sh @@ -0,0 +1,6 @@ +#!/usr/bin/env bash + +set -e + +export EXPECTED_VERSION="8.3.2" +source "$(dirname "$0")/test.sh" diff --git a/test/fly-cli/scenarios.json b/test/fly-cli/scenarios.json index 989c635..cfd0b78 100644 --- a/test/fly-cli/scenarios.json +++ b/test/fly-cli/scenarios.json @@ -1,4 +1,13 @@ { + "native_architecture": { + "image": "mcr.microsoft.com/devcontainers/base:resolute", + "features": { + "fly-cli": { + "version": "8.3.2", + "url": "https://unused.invalid" + } + } + }, "install_fly_cli_specific_url": { "image": "mcr.microsoft.com/devcontainers/base:debian", "features": { diff --git a/test/fly-cli/test.sh b/test/fly-cli/test.sh index f8b94ae..63a9f58 100644 --- a/test/fly-cli/test.sh +++ b/test/fly-cli/test.sh @@ -5,5 +5,14 @@ set -e source dev-container-features-test-lib check "fly version" fly --version +if [ -n "${EXPECTED_VERSION:-}" ]; then + check "pinned fly version" bash -c 'fly --version | grep -F "$EXPECTED_VERSION"' +fi + +case "$(dpkg --print-architecture)" in + amd64) expected_machine=62 ;; + arm64) expected_machine=183 ;; +esac +check "native fly ELF architecture" test "$(od -An -tu2 -j18 -N2 "$(command -v fly)" | tr -d ' ')" = "${expected_machine}" reportResults From 65ee8136d092b380aecb734fd67dfba99cfbf697 Mon Sep 17 00:00:00 2001 From: patrickcrocker Date: Sun, 1 Nov 2026 13:19:07 +0000 Subject: [PATCH 2/2] Resolve CF latest from published releases instead of unbuilt tags --- src/cf-cli/NOTES.md | 3 +++ src/cf-cli/install.sh | 19 ++++++++++++++++--- test/cf-cli/latest_release.sh | 5 +++++ test/cf-cli/scenarios.json | 6 ++++++ 4 files changed, 30 insertions(+), 3 deletions(-) create mode 100644 test/cf-cli/latest_release.sh diff --git a/src/cf-cli/NOTES.md b/src/cf-cli/NOTES.md index 7c0b605..7c4c278 100644 --- a/src/cf-cli/NOTES.md +++ b/src/cf-cli/NOTES.md @@ -3,3 +3,6 @@ AMD64 uses the Cloud Foundry package download endpoint. ARM64 uses the native Linux ARM64 archive from the selected GitHub release, so the selected version must publish that archive. The installer verifies the CLI runs after installation. + +`latest`, `current`, and `lts` resolve the latest published GitHub release rather +than the highest Git tag, because tags can exist before their release binaries. diff --git a/src/cf-cli/install.sh b/src/cf-cli/install.sh index dca8a47..271d5f2 100644 --- a/src/cf-cli/install.sh +++ b/src/cf-cli/install.sh @@ -101,10 +101,23 @@ export DEBIAN_FRONTEND=noninteractive ### BEGIN install -# Soft version matching -find_version_from_git_tags VERSION "https://github.com/cloudfoundry/cli" - check_packages curl ca-certificates +# Tags can precede release assets. Default installs must select a published release. +case "${VERSION}" in + latest|current|lts) + release_url="$(curl -fsSL --retry 3 -o /dev/null -w '%{url_effective}' https://github.com/cloudfoundry/cli/releases/latest)" + VERSION="${release_url##*/}" + VERSION="${VERSION#v}" + if [[ ! "${VERSION}" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + echo "Could not resolve a stable CF CLI release from ${release_url}" >&2 + exit 1 + fi + echo "VERSION=${VERSION}" + ;; + *) + find_version_from_git_tags VERSION "https://github.com/cloudfoundry/cli" + ;; +esac echo "Downloading cf..." if [ "${architecture}" = "arm64" ]; then download_url="https://github.com/cloudfoundry/cli/releases/download/v${VERSION}/cf${VERSION%%.*}-cli_${VERSION}_linux_arm64.tgz" diff --git a/test/cf-cli/latest_release.sh b/test/cf-cli/latest_release.sh new file mode 100644 index 0000000..e550eac --- /dev/null +++ b/test/cf-cli/latest_release.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash + +set -e + +source "$(dirname "$0")/test.sh" diff --git a/test/cf-cli/scenarios.json b/test/cf-cli/scenarios.json index 994dd08..4b97a80 100644 --- a/test/cf-cli/scenarios.json +++ b/test/cf-cli/scenarios.json @@ -1,4 +1,10 @@ { + "latest_release": { + "image": "mcr.microsoft.com/devcontainers/base:resolute", + "features": { + "cf-cli": {} + } + }, "native_architecture": { "image": "mcr.microsoft.com/devcontainers/base:resolute", "features": {