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..7c4c278 --- /dev/null +++ b/src/cf-cli/NOTES.md @@ -0,0 +1,8 @@ +## 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. + +`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/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..271d5f2 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 @@ -101,13 +101,32 @@ 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..." -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/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/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..4b97a80 100644 --- a/test/cf-cli/scenarios.json +++ b/test/cf-cli/scenarios.json @@ -1,4 +1,18 @@ { + "latest_release": { + "image": "mcr.microsoft.com/devcontainers/base:resolute", + "features": { + "cf-cli": {} + } + }, + "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